上下文塞不下了:系统说明、历史、检索结果各该占多少

2026-07-29

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

Agent 跑到中途开始犯傻,八成不是窗口不够大,而是预算分错了地方。 大多数人遇到这个现象的第一动作是换一个窗口更大的模型,换完发现症状照旧,甚至更糟——因为窗口一变大,检索模块和历史拼装逻辑会顺势多灌一些进去,真正跟当前这一步决策相关的那部分内容,占比反而更低了。窗口是容器大小,预算分配是往容器里装什么。前者花钱就能买,后者只能自己排。

站内已经有两篇打底的:上下文窗口是什么讲的是窗口和 token 这个概念本身,Agent 的上下文怎么管讲的是上下文里装了哪几类东西、每一类的裁剪思路;这篇不重复那两件事,只回答一个具体问题:这四块内容各该占多少、失衡了怎么按顺序查出来、查到之后先动哪一刀。

一、先分清是总量不够,还是配比失衡

这两种情况的处置方向完全相反,先分清再动手,能省掉大半的无用功。

总量不够的典型表现是:任务本身就要求模型同时看到很多东西——比如让它读一份长规格文档再改十几个文件——你把无关内容全砍掉之后,剩下的硬需求还是装不下。这种情况下再怎么调配比也没用,得改任务切分方式,或者确实需要更大的窗口。

配比失衡的表现完全不同:任务本身不大,模型第一轮回答得挺好,跑到第五轮、第十轮开始退化。退化的样子很有辨识度——重复调用刚刚调过的工具、拿一个三轮前已经被推翻的中间结论继续往下推、或者忘了最初的目标而在某个分支上一直转。这时候窗口往往还没满,问题出在有效信息被稀释了。

分清的办法很土但很有效:把某一轮实际发出去的请求体完整落盘,人眼扫一遍。你只需要知道两件事——总长度是不是逼近了上限,以及这一大坨里有多少是这一步真正用得上的。

# 在你的调用层加一个开关,把每轮送出的 messages 原样存成一个文件
export AGENT_DUMP_CONTEXT=1
# 落盘之后,先看四类内容各占多少长度(这里按字符粗估,够用来判断比例)
import json

with open("dump/round_07.json", encoding="utf-8") as f:
    msgs = json.load(f)

buckets = {"system": 0, "tools": 0, "history": 0, "retrieved": 0, "task": 0}
for m in msgs:
    # 没打标记的归到 history;标记写错的会自己多出一行,不至于让脚本抛 KeyError
    key = m.get("bucket") or "history"
    buckets[key] = buckets.get(key, 0) + len(json.dumps(m, ensure_ascii=False))

total = sum(buckets.values()) or 1
for k, v in sorted(buckets.items(), key=lambda x: -x[1]):
    print(f"{k:10s} {v:8d} {v / total:6.1%}")

这段代码要求你在拼装上下文的时候给每条消息打个来源标记。加这个标记是一次性成本,但从此以后所有的上下文问题都能量化。没有这个数,后面的排查全是猜。字符数和实际 token 数不是一回事,但用来判断「谁占了大头」这个量级问题,精度完全够。真要精确计数,参考怎么统计 token 用量

二、按现象定位:预算被哪一块吃掉了

把上面那个比例表拉出来之后,对照下面这张表判别。这是我在排查时实际走的顺序,从最常见、最容易改的往下排。

现象大概率成因怎么验证处置动作
第一轮就占掉很大一块,且每轮固定不变工具定义膨胀,或系统说明写成了长文档看首轮请求里 tools 和 system 两块的绝对长度;把工具全摘掉再发一次对比按任务阶段动态挂载工具,只保留这一步可能用到的;系统说明里的示例和边界情况挪到工具描述内部
轮次越多退化越明显,前几轮正常历史累积,有效信息被稀释打印每轮总长度画一条曲线,看是不是线性上涨加滚动摘要:保留最近若干轮原文,更早的压成结论条目
一次检索之后立刻变笨检索结果整块倒进去了,且召回质量差把这一轮的检索片段单独拎出来读,看有几条真的相关限制注入条数并做重排;只注入片段而非整篇文档
模型开始「发明」文件路径或函数名相关代码根本没进上下文,模型在补空拿真实存在的那个路径或函数名去搜请求体:搜不到就是检索没召回;搜得到却写错,那是理解问题不是预算问题前者改检索策略而不是改说明,见大仓库上下文不足;后者去改提示和示例
工具选错、参数填错的频率明显上升工具挂太多,描述之间语义重叠挑出描述能互相替代的那两三个,临时只留一个,跑同一组用例:选错率明显掉下来就坐实了合并或下线重叠工具,见MCP 工具数量
目标漂移,跑着跑着做别的去了当前任务描述被埋在了最前面,离最新一轮太远先看任务描述在请求体里的位置,再做对照:临时在末尾原样追加一遍目标,同一用例重跑,漂移消失就是它每轮在末尾重申一次当前目标和完成判据

表里最后一行值得单独说:很多人把任务描述放在系统说明里写一次就不管了,中间隔着几十轮历史,模型对它的注意力自然下降。每轮末尾用两三行重申目标,成本极低,效果立竿见影。

三、四块各占多少:一个起始配比,不是标准答案

先说清楚这是我的判断,不是什么行业规范,也没有哪家官方给过这种比例。它的用处是给你一个起点,让你有东西可以往上调、往下调。

  • 系统说明与角色设定:占一小块就够,越短越好。它的作用是定行为边界,不是塞知识。判断标准很简单——如果某条规则在九成以上的轮次里都用不到,它就不该常驻,应该挪到对应工具的描述里,或者做成条件注入。
  • 工具定义:这块最容易失控,因为它每轮都要重复发一遍,而且大多数轮次里九成的工具根本用不上。理想状态是按阶段挂载:规划阶段挂读取类工具,执行阶段挂写入类,收尾阶段挂验证类。做不到动态挂载的话,至少把描述写短。
  • 历史:这是唯一会随时间线性增长的部分,所以必须给它设一个上限,而不是让它自然膨胀。超过上限就压缩,压缩的对象是过程,保留的是结论和还没关闭的待办。
  • 检索结果:占比应该跟任务类型强相关。纯代码修改类任务,检索结果可以占大头;纯推理规划类任务,检索结果占比应该压到很低甚至为零。同一套配比套所有任务是最常见的错误。
  • 当前任务与最近一轮的观察:这块必须留足,而且要放在最后。它是模型做下一步决策的直接依据,前面所有内容都是为它服务的。

一个可操作的排序原则:当窗口紧张、必须砍东西的时候,砍的优先级是工具定义 > 检索结果 > 历史过程 > 系统说明 > 当前任务。当前任务和最近一轮观察永远不砍,砍了就等于让模型闭眼走路。

四、按顺序动手:三刀下去,每刀都验

不要一次改三个地方,那样你不知道是哪一刀起了作用,也不知道是哪一刀带来了新问题。

这里的下刀顺序跟上一节那条排序不是一回事,别看混了:上一节排的是「内容价值谁低谁先让位」,这一节排的是「动手成本和翻车风险谁低谁先做」。检索在价值上比历史更该压,但改注入条数往往要连带碰重排和召回策略,一旦调过头会直接损失有用信息;历史压缩只要加个上限和一个固定的摘要结构就能上线,退回去也容易。所以真动手时,历史排在检索前面。

第一刀砍工具定义。 这是收益最大、风险最低的一刀,因为它是纯重复开销。先把当前挂载的工具列出来,逐个问:这个任务的这个阶段,模型有没有可能用到它?答案是「理论上可能但基本不会」的,全部下线。改完跑一遍你的固定用例,看成功率有没有下降。

第二刀处理历史。 给历史设硬上限,超过就触发压缩。压缩要注意保留的是可判定的事实:哪些文件改过了、哪个方案已经试过并失败、当前还剩哪几件事没做。别保留「模型说它准备去做某事」这种意图性描述,那些是噪声。压缩本身也要花一次调用的成本,所以别每轮都压,攒够了再压。

第三刀收检索。 给注入条数设上限,并且强制做一次相关性重排——召回二十条、只注入排前面的几条,比直接注入二十条效果好得多。另外把整篇文档注入改成片段注入,同时保留文件路径和行号,模型需要更多内容时可以主动再读。

每刀改完都要跑同一组用例对比。没有固定用例就先造几个,哪怕只有三五个典型任务,也比凭感觉判断可靠。

五、什么情况下别再折腾了

调上下文预算是有收益上限的,超过这个上限继续调是在浪费时间。以下几种情况出现,就该换路而不是继续磨。

你已经把所有能砍的都砍了,硬需求还是装不下。 这说明任务粒度本身不对。正确的动作是把任务拆开,用几次独立的调用分别完成,中间用文件或数据库传递状态,而不是让一个会话扛全部。窗口外面存状态、每次只加载需要的那一小块,这是绕开容量上限的正路。

同一个用例,你改了三轮配比,成功率在原地上下抖动。 配比不是这个问题的瓶颈。这时候该去看别的:工具本身的返回值是不是太啰嗦、任务描述是不是本身就有歧义、或者这一步就是超出了当前模型的能力。

改完之后成功率反而下降,且回滚就恢复。 立刻回滚,别硬扛。上下文这块特别容易出现「看起来更干净了但效果更差」的情况,通常是压缩时丢了某个关键的中间结论。回滚点建议做成配置而不是代码——把上限、注入条数、压缩触发点都放进配置文件,出问题改一行就能退回去。

# 配比参数单独成文件,改动可回滚、可对比
git diff HEAD~1 -- config/context_budget.yaml

换更大的窗口能解决,且成本可接受。 这也是一个合理的答案,跟开头那句「别一上来就换大窗口」不冲突——差别在于顺序:开头反对的是没量过比例就换,这里说的是量过之后确认属于第一节那种「总量不够」,砍无可砍才换。工程上没必要为了优雅而优雅,如果加大窗口就能稳定跑通,而这个任务的调用频次又不高,那就加。判断依据是频次:一天跑几次的任务不值得花两天优化,一分钟跑几十次的任务值得。

顺带提一句:如果你在评估的方案里包含海外的模型或工具,先确认可用性问题。相当一部分海外 AI 产品官方明确不向中国大陆提供服务、也不支持直连,市面上存在第三方中转服务,但那属于你自己的风险判断,我不做推荐也不给渠道。把方案的可持续性想清楚再往上搭,比事后迁移便宜。

六、避坑清单

坑一:先换大窗口再谈优化。 为什么会踩:换模型是一个动作就能完成的事,看起来比重构上下文拼装逻辑便宜得多。怎么避:换之前先花十分钟把请求体落盘看一眼比例。如果发现有效信息占比很低,换窗口只会让这个比例更低。先量再换,不亏。

坑二:把系统说明当知识库写。 为什么会踩:写的时候总担心模型不知道某件事,于是不停往里加规则、加示例、加例外。加到后来它自己就成了一篇长文档,每轮都要重发。怎么避:定一条规则——系统说明里只放「每一轮都成立」的内容。跟特定工具相关的写进工具描述,跟特定阶段相关的做条件注入。

坑三:历史压缩把关键结论压没了。 为什么会踩:压缩通常是让模型自己总结前面聊了什么,而模型倾向于总结过程,不倾向于保留「某方案已验证失败」这种否定性结论。怎么避:压缩时给一个固定的结构——已完成、已排除、待办、关键约束,四个槽位分别填。槽位固定之后,关键结论的丢失率明显下降。

坑四:检索模块和上下文预算各管各的。 为什么会踩:检索通常是独立模块,作者只关心召回率,不关心注入之后占了多少预算。召回二十条全塞进去,指标很好看,实际效果很差。怎么避:把注入条数的上限放在上下文拼装层,而不是检索层。检索可以召回很多,注入必须过闸。

坑五:把当前任务描述放在最前面就不管了。 为什么会踩:符合直觉——任务当然写在开头。但历史越积越多,它离最新一轮越来越远。怎么避:末尾重申。这条改动成本最低、见效最快,值得优先做。

坑六:改了配比但没有可比的用例。 为什么会踩:调完之后跑一次感觉「好像好点了」就上线了,实际上是随机波动。怎么避:固定三到五个典型任务,每次改动前后各跑一遍,记录成功率和轮次数。轮次数这个指标很有用——同样的任务用更少轮次完成,通常说明上下文质量提升了。

小结

上下文预算这件事的核心判断只有一条:窗口大小决定你能装多少,配比决定装进去的东西有没有用,而后者才是 Agent 跑偏的主因。

排查时对着这张清单走一遍:

  • 请求体落盘了吗?四类内容的占比数出来了吗?
  • 是总量硬需求装不下,还是跑几轮之后才退化?
  • 工具定义能不能按阶段挂载?下线一半会怎样?
  • 历史有硬上限吗?压缩保留的是结论还是过程?
  • 检索注入有条数闸门吗?重排做了吗?
  • 当前任务和完成判据,每轮末尾重申了吗?
  • 配比参数在配置文件里吗?出问题能一行回滚吗?
  • 有固定用例做前后对比吗?

前三条能做到,多数症状就消了。剩下的属于精调,等有了量化基线再慢慢磨。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。