Skip to main content

上下文是有限预算

上下文窗口变大之后,很多人以为"放不下"这个问题消失了。没有。窗口从 4K 涨到 200K,只是把问题从"装不下"变成了"装了但模型不看"。

Anthropic 在 2025 年 9 月的工程文章里给了两个说法,值得记:context rot(上下文越长,模型准确回忆其中信息的能力越差)和注意力预算(每个新 token 都在消耗这个预算)。结论是一句很实在的话——要找的是"最小的高信号 token 集合",而不是"尽可能多的上下文"。

系统指令工具定义检索内容历史消息窗口上限超限压缩到达阈值时总结历史结构化笔记关键状态写到窗口外子智能体脏活隔离在别处每一条进上下文的内容都在消耗注意力预算:多不等于好
图:上下文窗口的构成,以及超限时可用的三种手段

1. 窗口里都装了什么​

一次典型调用送进去的内容大致分四块:系统指令、工具定义、检索回来的外部数据、以及历史消息。

四块里最容易失控的是最后两块。检索内容会随着"想让答案更准"越堆越多;历史消息会随着对话轮数单调增长——而且增长是复利的,因为每一轮都要把之前所有内容再送一遍。

1.1 多不等于好​

有个经典现象叫 lost-in-the-middle(Liu 等,TACL 2024):相关信息放在上下文开头和结尾时容易被用到,放在中间容易被忽略。

直接推论是两条:

  • 堆 50 条切片进上下文,效果通常不如精选 5 条——参见《重排修不了召回》里关于保留条数的部分。
  • 重要的内容要放在首尾,别埋在中间。

2. 三个手段​

长任务跑不下去的时候,可用的办法就三个,各有各的适用场景。

2.1 压缩​

到阈值时把历史总结一下,用摘要替换掉原始消息,然后从摘要继续。

以 Claude Code 的公开行为为例:默认是接近容量(约 95%)时才自动压缩,官方另给了一个环境变量可以把它提前到 70%。实践上确实值得提前——压到 95% 才动手,留给摘要本身的空间已经很小,压缩质量会下降。摘要里保留文件路径、已做的决策、当前计划,丢掉探索过程和冗余的工具输出。

关键是"丢掉什么"。压缩最常见的失败是摘要变成流水账——什么都记一点,结果什么都不清。写摘要的规则应该明确成:保留决策与状态,丢弃过程。

另外有一条成本最低、几乎总能做的清理:工具调用结果只保留最近几次的原文。很早之前调过的工具、它返回的原始结果,对当前步骤几乎没用,却占着最多的 token。

2.2 压缩的另一面:缓存​

上下文工程和提示缓存是一对矛盾:缓存要求前缀稳定,压缩要改写历史。每压一次,从压缩点之后的所有前缀都变了,缓存全部失效。

所以顺序很重要:先算清楚哪些内容能进缓存,再决定压什么。实践上分三段处理:

  • 稳定段(系统指令、工具定义、长期参考资料):一个字都别动,放最前面,让它稳定命中
  • 半稳定段(会话摘要、任务状态):改动频率低,压缩就压这里
  • 易变段(本轮输入、最新工具结果):不参与缓存,随便变

如果工具定义也要动态生成(按用户权限过滤工具),那缓存基本无望——这也是 tool search 这类机制的价值:工具定义不进上下文,就不存在"改工具就废缓存"的问题。成本账见《Token 成本的三个杠杆》。

2.2 结构化笔记​

让 Agent 把关键状态写到上下文之外(文件、数据库、缓存),需要时再读回来。

这个模式简单到不像技术:Claude Code 维护一个待办列表,自定义 Agent 维护一个 NOTES.md。它解决的是一个具体问题——跨几十次工具调用之后,关键信息在上下文里被冲掉了。

笔记要"结构化"才有意义。按"当前目标 / 已完成 / 关键发现 / 待确认"分节,每一节相当于一份检查清单;如果只是一段随手写的话,读回来时模型和人一样抓不住重点。

2.3 子智能体​

把探索性的脏活派给子 Agent,主 Agent 只拿回一份压缩过的结论。

Anthropic 公开的多智能体研究系统经验里,可引用的是量级而不是细节:多智能体整体 token 消耗约为普通聊天的 15 倍,而子 Agent 的搜索过程被隔离在自己的上下文里,主 Agent 只收到浓缩后的结果。好处是"搜索过程"的上下文被隔离在子 Agent 内部,主 Agent 的上下文始终干净。

代价是这种方式更适合"可以并行探索"的任务(研究、检索、遍历代码库),对需要强一致状态的任务反而添乱。

3. 怎么选​

场景优先手段
长时间来回对话、需要保持语感连贯压缩
有明确里程碑的迭代开发结构化笔记
需要大范围并行探索(研究、检索、代码遍历)子智能体

三者不互斥。真实系统里常见的是:主循环用压缩兜底,关键状态写笔记,探索类任务派子 Agent。

4. 几条具体的纪律​

系统指令要写在"合适的高度"。 Anthropic 的说法是 right altitude——具体到能指导行为,但不能写成硬编码逻辑。写成 if-else 的那种指令,遇到没覆盖的情况就失效;写得太虚,模型就自由发挥。

系统指令要分节。 用 XML 标签或 Markdown 标题分成"输出格式 / 工具使用规范 / 边界"几块。分节不只是给人看的,模型在长上下文里定位信息的难度也会降低。

工具集要小而明确。 工具定义本身占 token,而且工具一多,模型在功能重叠的工具之间摇摆,表现为"有时调错"。能合并的合并,边界不清的重写描述。这一点在《无状态 MCP Server 要点》里从实现角度讲过一遍。

给模型留出"思考"的余地。 输出格式别设计成"必须一次写对、没有回头路"的形态。比如要求它输出完整 JSON 且里面嵌大段代码,它得一边转义一边计数,出错率明显上升;让它先写 markdown 再转换,反而更稳。

5. 和 RAG 的关系​

上下文工程和 RAG 讲的是同一件事的两面:前者管"送进去什么",后者管"从外面取什么回来"。

一个容易搞混的点是——两者都在限制"进上下文的量",但手段不同:

  • RAG 侧靠切片和重排控制(《切片决定检索上限》《重排修不了召回》)
  • 上下文侧靠压缩、笔记、子 Agent 控制

两边都做到位,长任务才跑得动。只做一边,另一边会先在某个长度上断掉。

6. 一个判断有没有做对的标准​

跑一个需要几十轮工具调用的长任务,然后问三个问题:

  1. 结束时,模型还知道最初的目标是什么吗?
  2. 中途做过的决策,后面还能引用到吗?
  3. 有没有出现"重复做同一件事"或者"忘了之前已经查过"?

三个问题里出现任何一个"否",说明上下文管理有缺口。这三个症状比任何指标都直观地反映真实状态——这也是《RAG 链路与失败模式》里那条"每一段都要能单独看中间结果"的同一个道理:看不见的东西,改不动。