[控场AI]
· 9 分钟阅读· 4,575 字

上下文驱逐机制如何让AI Agent空转一小时

上下文驱逐机制如何让AI Agent空转一小时

激进上下文驱逐与跨会话去重碰撞致 Agent 空转,按需驱逐与会话隔离修复后效率与质量双升。

Legion 的多 Agent 系统在开发 Go 项目时遭遇离奇故障:一个后端 Agent 一小时内执行 126 次工具调用却零代码产出。根因是两条各自正确的机制发生碰撞——上下文组装器无条件驱逐三次调用前的文件内容,而房间级去重又会命中旧会话缓存返回回执,Agent 陷入「读不到、又不被允许重读」的死循环。修复方案包括切换到按需驱逐、将内容保留绑定到会话、重写更诚实的存根措辞、以及与压缩操作同步重置已读状态。效果显著:平均缓存 token 下降 46%,Completion 输出下降 42%,模型从冗长补偿转向简洁作答。文章强调,token 节省不能等同于任务成功率,读推理日志是发现此类系统性失败的早期手段。

上下文驱逐机制如何让AI Agent空转一小时

AI Agent上下文驱逐机制失败案例

一个自主Agent花整整一小时研究代码库,执行了126次工具调用,却没有写出任何一行代码。它没有产生幻觉,也并非偷懒——它只是无法访问几秒钟前刚打开的文件内容。

这是 Legion 多Agent协作中心在开发 Go 项目时遭遇的真实基础设施故障。问题的根源不是模型能力不足,而是两个各自"正确"的机制碰撞后形成的死锁。

背景:为什么需要上下文网关

任何使用 LLM Agent 的开发者都清楚,当 Agent 在代码库上自主工作时,每次工具调用都会让上下文急剧膨胀。读文件、执行 grep、跑测试——所有历史记录都会被塞进下一次模型请求中。这种循环的成本几乎呈二次方增长:每一步的结果都在后续每一次迭代中重复计费。

Legion 的Agent团队在虚拟"房间"中协作,共享云端代码仓库。为了控制成本,开发者在7月设计了一个确定性中间层,位于工具执行循环和模型提供者之间,由三个组件构成:

Observations(观察记录): 每次工具执行的完整结果存入外部存储,上下文中仅保留轻量引用。

Receipts(回执): 请求中的过期结果被替换为简短存根,包含工具名、用途、行范围、哈希值和被驱逐的 token 量。

Read Ledger(读取账本): 系统记录Agent已接收过哪些文件范围,当重复读取未变更的片段时返回回执而非原文。

默认的 eager 策略只保留最近三次工具结果的原始文本,上限为6000 token。在29条匿名轨迹上的离线回放显示总传入流量减少了53%——数据看起来很漂亮。

Prompt Caching(提示缓存) 是主流 LLM 提供者(如 Anthropic、OpenAI)推出的一项成本优化机制:当多次请求的前缀部分完全一致时,提供者会缓存这部分内容的计算结果,后续命中缓存的 token 按远低于正常输入的价格计费。文章中的「缓存量」正是指被提供者命中缓存的输入 token 数,「未缓存量」则是每次都必须重新计算的增量部分。

理解这一机制对读懂后文数据至关重要:如果开发者只是粗暴地删减上下文,看似能降低 token 总量,但只要缓存前缀被不断破坏,未缓存部分的绝对量反而可能上升,实际账单未必下降。Legion 的修复之所以有价值,正是因为缓存量与未缓存量同时下降,说明 Agent 真正需要的信息变少了,而不是简单地把成本从一处挪到另一处。

失败现场:一小时零产出的死循环

Legion 的一个后端Agent被分配了为 Go 后端添加看板端点的任务。这已经是第三次重试。一小时后,调度器因超时强制终止了会话。

日志显示:126次工具调用(99次 bash、18次观察读取、9次 read_file),零次代码修改。Agent的内部推理暴露了它的困境:

"让我看看被驱逐的仓库映射输出"——它几乎立刻注意到了驱逐。

"这很奇怪——正常 read_file 会返回内容。回执表明工具结果被驱逐了"——意识到收到的是回执而非代码后,Agent试图通过 cat 绕过限制。

"我已经读了两次,两次都被驱逐出上下文"——经典的无限读取循环。

"我已经花了大量预算。让我果断一点"——恐慌在加剧,但它根本缺少写代码所需的新鲜签名。

根因分析:两个正确机制的致命碰撞

调查发现了一个典型死锁:两条独立逻辑流各自运作正常,但彼此不一致。

问题一:始终驱逐 vs 按需驱逐。 上下文组装器会无情地将三次调用前的任何文件读取转为回执,即使窗口中有大量空闲空间(比如10万 token 中只占了2万)。开发时追逐上下文窗口优化,忽略了离线回放计算的是节省量而非任务成功率。

问题二:房间级去重叠加会话级保留。 观察工件按 房间 + 工具 + 参数 + 文件指纹 作为键进行检索。结果第二次和第三次尝试解决任务时,命中了第一次尝试的缓存,标记为重复读取后再次返回回执。

恶性循环由此形成:上下文告诉Agent"这里没有文本,重新读取",而 Read Ledger 拦截它"你已经拿到了,这是存根"。Agent退回到 cat,激进的驱逐再次触发——文件再度消失。同时,清单被模型理解为直接禁令("不要重读")。

上下文窗口(Context Window) 指模型单次请求中能够处理的最大 token 数量,包含系统提示、历史对话、工具结果和当前输入的总和。每个模型都有硬上限(如 200K token),超过上限时要么报错,要么必须由外部的「上下文组装器」主动裁剪、摘要或驱逐旧内容。

文章中的关键矛盾正来自这个上限管理。组装器把「超过 N 次调用就驱逐」当作通用安全规则,但这条规则并不感知窗口实际占用率——哪怕 10 万 token 的窗口只用了 2 万,它照样驱逐。这种「容量焦虑」在设计时看似保守稳妥,实际上却把宝贵的、仍然新鲜的代码信息提前清空,逼迫 Agent 反复重读,形成反直觉的负优化。理解窗口模型有助于明白:驱逐策略必须是容量驱动而非步数驱动的。

修复方案:四项关键变更

事后分析推动了网关架构的四项调整:

切换到 on_demand 驱逐模式。 只有当请求总量真正超出预算限制时才驱逐数据,且严格恢复到正常范围即停止。旧行为保留为可选对比模式。

内容保留绑定会话。 跨会话缓存观察记录以节省空间的做法保留,但"返回文本还是回执"的决策严格基于当前会话是否见过该结果。新的尝试从空白状态开始。

诚实的清单文本。 存根措辞被重写:不再禁止操作,而是明确说明上下文中没有文本,允许重新读取文件。

与压缩操作同步。 组装器现在明确告知读取账本哪些文本块在溢出期间被删除,重置被擦除片段的"已读"状态。

Read Ledger(读取账本) 的设计初衷是去重:当 Agent 多次读取同一文件的同一片段时,系统判断它是重复劳动,返回一个标记为「已读过」的轻量回执即可,从而避免把同样内容反复塞进上下文浪费 token。这在单次会话、文件未变更的理想场景下完全正确。

问题出在「同一文件是否算重复」的判定跨了会话边界。缓存键包含「房间 + 工具 + 参数 + 文件指纹」,但不同任务重试属于不同的会话意图,Agent 此时并没有任何旧文本留在上下文中,账本却仍然告诉它「你已经拿到了」。修复方案中的「内容保留绑定会话」,本质上是把去重范围从房间级收窄回会话级,让缓存只服务于真正共享的知识库,而不越权决定某个 Agent 当下能否看到文件内容。

数据验证:质量与效率的双重跃升

切换到新架构后,对比九月和十月的平均指标数据:

Prompt Caching: 平均缓存量从 6.48M 降至 3.48M(-46%),平均未缓存量从 1.71M 降至 1.03M(-40%)。绝对量下降近半而比例保持不变,说明负载降低源于Agent真正需要更少上下文来完成工作,而非缓存机制的表面优化。

输出结构变化:

  • Prompt:8.01M → 5.51M(-31%)
  • Reasoning:76.1K → 58.2K(-24%)
  • Completion:62.2K → 35.8K(-42%)
  • Completion/Reasoning 比值:0.82 → 0.65(-20%)
  • Reasoning/Prompt 比值:9.28 → 11.2(+21%)

两个关键信号值得关注。其一,冗长输出崩溃:九月时最终回复量几乎等于推理量——缺乏上下文事实的模型在回复中"即兴发挥"代码来补偿记忆空缺;十月Agent开始深度思考、简洁作答。其二,绝望尖峰消失:九月样本中存在一个异常案例(15.4M prompt / 152K reasoning / 111K completion),反映深层循环性Agent崩溃,十月这类极端离群值完全消失。

Go 开发为何发生质变

最终的正确性证明体现在实际软件工程中。Go 语言极其严格、惯用化、没有动态魔法:依赖显式结构、严格的错误处理(if err != nil)、context.Context 传播和类型化配置。

九月时,回执擦除了底层细节,Agent在编写配置或服务时丢失生产文件和邻近包的结构,从零重写配置时抹掉了端口和密钥,或生成与项目格格不入的代码。

十月时,上下文中保留真实代码将**上下文学习(ICL)**推向极致。Agent切换到精确模仿模式:看到真实签名,复制架构布局(handler -> service -> repository),干净地将新微服务嵌入现有单体而不破坏依赖关系。

In-Context Learning(ICL,上下文学习) 指模型不通过微调、仅在推理时依据提示中给出的示例或真实代码来适应任务的能力。它的效果高度依赖上下文的保真度:当签名、接口、目录结构以原始形式出现时,模型能直接模仿;而一旦这些细节被摘要或存根替换,模型就只能凭训练语料的统计先验去补全。

这就是为什么 Go 这类语言对上下文质量格外敏感。Go 没有动态魔法,接口、错误传播和包依赖都靠显式声明,一个被抹掉的结构体字段或 context.Context 参数就会导致编译失败。同时,Go 社区代码风格高度趋同(if err != nil、handler -> service -> repository 分层),一旦模型看到真实的邻近代码,就能迅速对齐惯用法。这两个特性的叠加,使「保留真实代码」在 Go 项目中带来的收益远大于在宽松语言中的收益。

给Agent系统开发者的五条经验

按需驱逐,而非按计划驱逐。 "删除N步之前的一切"的固定规则在窗口仍有大量空闲空间时会破坏工作流。

隔离会话上下文。 知识库和工件缓存可以在项目间共享,但是否向Agent展示文本的决策必须严格限定在其个人会话层面。

诚实地对模型说话。 类似"不要重读"的指令会瘫痪 LLM。改写为"文本不在上下文中,如需可再次调用工具"恢复其行动自由。

Token指标不等于成功指标。 以输入输出量为目标的离线优化可能悄悄杀死Agent解决实际问题的能力。

读推理日志。 日志中出现"两次都被驱逐"这类措辞,是系统性失败的早期信号,远早于超时触发。

试图用可疑的存根节省几十万输入 token,反而导致在无尽调试和幻觉中浪费数百万 token。找到的平衡点(按需驱逐 + 诚实回执)将自主Agent从犹豫的初级工程师提升为可靠的中高级工程师。

#AI Agent#上下文管理#上下文驱逐#LLM Agent架构#Agent系统设计#Go开发#token优化#多Agent协作
分享:

相关推荐