你的 Agent 跑着跑着就变笨?问题可能出在上下文污染
Agent 跑着跑着就开始犯蠢,很多时候不是模型不行,而是工具调用产生的上下文污染在悄悄拖垮它。
前阵子在调一个多 Agent 系统的排查故障场景。
刚开始跑得很顺:Agent 查监控、翻日志、定位慢 SQL,每一步都干净利落。跑了大概七八轮之后,它开始做奇怪的事——重复查同一个接口的日志,把已找到的根因又查了一遍,甚至开始调用八竿子打不着的工具。
我一开始以为模型不行,换了个更强的模型,好了一点,但跑了十几轮又开始犯浑。
后来把每一轮的上下文 dump 出来看,才发现问题在哪。
上下文窗口里,超过 70% 的内容是工具调用返回的原始输出。有 JSON 格式的监控数据,有截断的日志片段,有 SQL 查询结果,还有几个工具报错后重试留下的堆栈信息。
Agent 没有被喂坏,它只是被淹没了。
这件事让我意识到,工具调用本身就是一个容易被忽略的上下文污染源。不全是模型的问题,是你的上下文在慢慢变质。
工具返回的结果不会自己消失
Agent Loop 每跑一轮,工具调用的返回结果就被追加到上下文窗口里。逻辑上这没错——下一轮推理需要依赖这些 Observation 来做判断。
问题在于,这些 Observation 不是均匀分布的。一个查监控的工具返回 2000 Token 的 JSON 数据,里面真正有用的可能只有 200 Token。一个报错日志的调用返回 5000 Token,Agent 只用了其中一行错误码。
剩下的内容去哪了?它们留在上下文里,继续参与后续每一轮的注意力计算。
这就好比你在桌上摊开所有查过的资料,越堆越多,最后要在里面找一行关键信息,就得把整个桌面翻一遍。
Transformer 的注意力机制是 O(n²) 的。上下文越长,模型在无关信息上浪费的计算就越多,关键信号被稀释的概率也越大。听起来是常识,但亲手把上下文 dump 出来看之前,我从来没认真算过这个账。
越往后跑,Agent 越依赖直觉而不是推理
我注意到另一个现象:Agent 在早期轮次表现得很有条理,但跑了一段时间后,决策质量明显下降。
早期轮次里,上下文干净,System Prompt 和当前工具描述占了主导,Agent 会按规则一步步来。到了后期,上下文里塞满了各种历史观察结果,Agent 开始倾向于从大量文本中"猜"答案,而不是严格按工具链走。
这个转变很微妙。它不会直接报错,不会说我不行了,它只是越来越像一个偷懒的人——从上下文里扒拉一段看起来相关的旧信息,直接当成答案输出。
后来我查了 Anthropic 的文档,他们管这个叫 Context Rot,也就是上下文腐化。随着 Token 总量增加,模型对早前信息的回忆能力会下降。这不是幻觉问题,这是注意力机制的结构性限制。
工具出错了,Agent 会带着错误跑完后面所有轮次
还有一个更隐蔽的问题:工具调用出错后的连锁反应。
假设第二轮调用中,一个日志查询工具超时了,返回了一堆错误信息。Agent 在第三轮判断时,可能因为这个错误而修改策略,换一个工具来查。这是对的。
但坏消息是,那段错误信息还留在上下文里。
到了第十轮,Agent 已经解决了问题,但输出结论时,它可能又看到了那个错误信息,于是多加了一句"建议检查日志查询服务的稳定性"。这句话单独看没错,但放在完整的结论里,它会把用户的注意力引向一个已经解决的问题。
更糟的情况是,Agent 在后期决策中误读了早期失败的上下文,认为某个工具不可靠,于是绕开了正确的工具路径。
这种错误是积累性的。单看每一轮,Agent 的决定都合情合理。但把整个轨迹串起来看,效率在持续下降。
四个实战解法
遇到这些问题后,我试了几种方法,有些管用,有些踩了坑。
Observation 必须做摘要,不能原样追加
这是见效最快的一个改动。
工具返回原始结果后,不让它直接进上下文。而是先让 LLM 或一个轻量脚本做一次摘要:这段返回里,哪几个指标是关键的,哪个错误码是决定性的。只把摘要写回上下文。
一次调用的原始输出可能是 3000 Token,摘要后压到 200 Token。跑 20 轮,上下文就被省下了 50K+ Token 的空间。
核心信息一点没丢,但噪声少了大半。
工具描述里要写清楚"什么时候不该用"
这是写 Schema 时最容易忽略的。
很多人写工具描述时只写这个工具能干什么,从不说它不该干什么。结果 Agent 在压力下开始乱试工具——有一个查用户的工具,Agent 在查数据库连接数时也会用它,因为它的描述里写了"能返回错误信息"。
我现在每个工具的描述里都会加一条"什么时候不调用这个工具",比如 "如果用户问的是网络问题,不要调用这个工具"。模型看到这条就不太会走岔路。
对序列化结果做结构化降维
有些工具返回的是列表或嵌套 JSON,比如监控指标的时序数据。这些数据直接塞进上下文又大又没用。
我会在工具返回后,先过一道处理函数,把时序数据转成文本描述:"CPU 在 09:00-09:15 从 30% 飙升到 98%,峰值在 09:12"。
30 行 JSON 变成了一句话。Agent 读得懂,信息密度更高。
Sub-agent 隔离脏上下文
这是最后的方案,也是效果最好的。
如果任务本身就需要大量工具调用——比如同时查监控、日志、数据库、网络、配置——那么不要把它们放在同一个 Agent Loop 里。
拆成子 Agent:查监控的子 Agent 自己维护它的上下文,最后只返回一段摘要给主 Agent。查日志的子 Agent 也只返回结论。
主 Agent 的上下文始终保持干净,只处理高层的推理和决策。Sub-agent 那边上下文脏了没关系,跑完就销毁了。
这个方案成本最高,但效果也最彻底。
上下文管理应该和写 Prompt 一样重视
很多人(包括之前的我)花大量时间调 Prompt,但对上下文的管理几乎随缘。
模型每轮调用前看到的上下文,其实是一个动态组装的结果。你在 System Prompt 里写"请严格按照工具链执行",但如果上下文里塞满了无用信息,这句话的效力会被稀释得微不足道。
把上下文当作一个要持续维护的系统来对待——每轮做一次整理,把噪声挡在外面,把信号放在前面。
做 Agent 开发最需要培养的习惯不是调模型,而是时不时的 dump 一遍上下文,看看窗口里到底有什么。
答案通常会让你吓一跳。
评论
发表评论