2025 年 7 月,向量数据库公司 Chroma 发了一份技术报告,测了 18 个模型,包括 GPT-4.1、Claude 4、Gemini 2.5 和 Qwen3。任务很简单,在一大段文字里找出一句指定的话,或者回答跟这句话有关的问题。结论是输入越长,表现越不稳定,所有实验里都能看到下滑,任务再简单也一样。
报告里有两个结果挺反直觉。往文本里加一句和答案相似、但不对的干扰句,正确率就会下降,加四句降得更多。另一个是把背景文字的句子随机打乱,模型反而答得比读通顺的原文更好。Chroma 给这个现象起了个名字,叫 context rot,上下文腐烂。
用编程智能体久了,多半见过它的样子。让智能体修一个 bug,前十分钟它表现很好。一个小时后它开始重复读同一个文件,忘了你最早说过不要改公共接口,还把已经否掉的方案又拿出来试一遍。这一个小时里,几十次文件读取、命令输出和报错日志都堆进了对话历史,你开头那几句约束早就埋在十几万个 token 中间。
上下文工程要解决的就是这个,让模型在每一步看到的内容尽量恰好够用。
窗口里装着什么
模型没有记忆,每次生成只能依靠这次调用传进去的 token,这些 token 就是上下文。编程智能体的上下文一般由下面几部分组成。
| 组成部分 | 举例 | 什么时候进入 |
|---|---|---|
| 系统提示词 | 智能体产品自带的行为规范 | 每次都有 |
| 工具定义 | 读文件、跑命令、各个 MCP 工具的名称和参数 | 每次都有 |
| 项目说明 | CLAUDE.md、AGENTS.md | 会话开始时 |
| Skill 摘要 | 每个 Skill 的名称和描述 | 会话开始时 |
| 对话历史 | 你说的话、它的回复 | 一直累积 |
| 工具调用结果 | 读到的文件、命令输出、搜索结果 | 一直累积,通常占大头 |
| 按需加载的内容 | 触发了的 Skill 全文、子目录的规则 | 用到时 |
提示词工程关心的是其中一两行。上下文工程要管整张表,每一行占多少,什么时候进来,什么时候该出去。
Anthropic 在 2025 年 9 月的《Effective context engineering for AI agents》里,把上下文看成一笔有限的注意力预算。Transformer 里每个 token 都要和其他所有 token 建立关联,token 越多,关联越多,分到每条关联上的注意越稀。窗口装得下,不代表模型用得好。
第一种办法,把历史变短
对话快到上限时,把前面的历史总结成一份摘要,用摘要替换原文继续干活。Claude Code 的 /compact 做的就是这件事,接近上限时它也会自动压缩。
压缩难在取舍。摘要太狠,后面才发现重要的细节就丢了;太保守,又省不下多少空间。还没做完的任务、已经做出的决定和否掉的方案、用户给的约束,这些要留。处理完的工具输出、中间试错的过程,可以丢。文件内容也可以丢,留下路径,需要时重新读。
自动压缩只能猜什么重要,你自己更清楚。所以在一个阶段做完、开始下一阶段之前,主动压缩一次效果更好。Claude Code 的压缩命令可以带一句话说明保留什么,比如只保留认证模块重构的结论和剩下的待办。
更轻的一种做法是清理旧的工具结果。一个文件读过、改完了,完整内容就没必要一直留着。Anthropic 在 API 里提供了这个功能,叫上下文编辑。按他们 2025 年 9 月公布的内部评估,在一个 100 轮的网页搜索任务里,上下文编辑让 token 消耗降低了 84%,配合记忆工具,任务表现比基线高了 39%。
第二种办法,用到时再去读
很多早期的系统会先把可能相关的资料一次检索出来,塞进上下文,再让模型开始干活。问题是开始前很难判断哪些相关,要么漏了,要么塞了一堆没用的。
现在的编程智能体大多是用到时再读。上下文里只放线索,比如文件路径、函数名、文档链接,需要时由模型自己调用工具去取。Claude Code 不会在一开始把整个代码库读进来,它用 grep 和 glob 一步步定位,只读真正需要的几个文件。这和人类工程师差不多,代码库没人背得下来,大概知道在哪,要用时去查。
代价是工具调用变多,速度慢一些。好处是上下文一直比较干净,读到的也总是最新的文件,没有索引过期的问题。
这种方式对工具的设计有要求。读文件时最好能指定行范围,搜索结果要有数量上限,返回的格式要对模型友好。带行号的代码比纯文本好定位,精简过的 JSON 比原样转发的完整 API 响应好理解。一次查询返回五十个字段、两百条记录,模型只用得上其中三个字段的十条,这样的工具每调用一次,窗口就被灌一次水。
第三种办法,写到窗口外面
窗口里的东西会话一结束就没了,压缩时也会被总结掉。要长期保留的信息,应该写到窗口外面,需要时再读回来。
项目说明文件是最基本的一种。CLAUDE.md、AGENTS.md 每次会话自动加载,压缩后也会重新加载,适合放每次都要知道的事实。
长任务里还可以让智能体维护一份进度文件。Manus 的联合创始人季逸超在 2025 年 7 月写过他们的经验,一个典型的 Manus 任务要调用大约 50 次工具,智能体会建一个 todo.md,做完一项勾一项。这样做除了记进度,还有一个作用。每次重写清单,目标就被写到了上下文的末尾,模型最近注意到的位置。任务一长,模型容易跑偏,或者忽略中间的信息,这个办法能缓解一些。他们管这个叫复述。
文件系统本身也是一种外部记忆。Manus 那篇文章把它当成无限大、能持久保存的上下文来用,日志存成文件,用到时 grep 需要的几行。他们还强调压缩要能还原,网页内容可以从上下文里拿掉,只要 URL 还在;文档可以拿掉,只要沙箱里的路径还在。
外部记忆也会坏。一个只追加、从不整理的记忆文件,最后会和膨胀的上下文一样难用。
第四种办法,把脏活交给别的窗口
有些工作天生会产生大量上下文。在大代码库里找某个功能的实现可能要读几十个文件,调研一个技术问题可能要翻十几个网页。这些都在主会话里做,主窗口很快会被中间结果占满。
子智能体就是为这个准备的。主智能体把子任务交给一个有独立窗口的子智能体,它在自己的窗口里读文件、试错、探索,最后只交回一份几百字的结论。
主智能体,窗口保持干净
│
├─→ 子智能体 A:找出所有调用 createUser 的地方
│ 在自己的窗口里搜索、读文件
│ ←─ 返回:共 7 处,分别在……
│
└─→ 子智能体 B:查这个库 v3 的迁移指南
在自己的窗口里搜索、读网页
←─ 返回:主要变化有三点……这是用 token 换质量。子智能体花掉的总 token 更多,主智能体的上下文保持干净,判断更准。
它也有代价。子智能体看不到主会话的背景,交给它的任务描述得自己说得清楚;它带回的结论有错,主智能体也很难核实。所以适合交出去的是边界清楚、结果容易描述的活,比如搜索、调研、独立模块的评审。互相牵扯、需要大量共同背景的工作,留在主会话里做更稳。
几个用得上的习惯
一个任务开一个会话,这是最省事的一条。修完一个 bug 再开新会话做下一件,前一个任务的上下文对后一个只有干扰。
方向走错了,退回去往往比在对话里纠正有效。错误的尝试一直留在上下文里,会持续影响它。Claude Code 有 /rewind 和检查点,可以退回出错之前,带着新的说明重来。Manus 的经验在这一点上看起来相反,他们建议把失败的操作和报错留在上下文里,让模型看到这条路走不通,别再重复。两者其实适用的情况不同。智能体自己试错时,报错是有用的信息;整体方向被带偏时,留着那段历史只会让它继续偏。
大块输出要防着点。测试只看失败的用例,日志用 tail 或 grep,大文件指定行范围读。一次几万行的命令输出,就能吃掉窗口的一大块。
MCP Server 别连太多。每个 Server 都会把它的工具定义放进上下文,连十几个、上百个工具,光工具定义就占掉几万 token,模型还会在相似的工具之间选错。Claude Code 现在默认开启工具搜索,会话开始只加载工具名,用到时再取完整定义,可以缓解这个问题。
自己搭智能体的话,尽量让上下文开头那部分保持不变,只在末尾追加。Manus 那篇文章把缓存命中率说成生产环境里最重要的一个指标。他们的任务输入和输出的 token 比大约是 100 比 1,用 Claude Sonnet 时,命中缓存的输入每百万 token 0.3 美元,没命中是 3 美元,差了十倍。系统提示词开头放一个精确到秒的时间戳,缓存就全废了。需要临时禁用某个工具时,他们也不把它从列表里删掉,改成在解码时屏蔽,原因同样是保住缓存。
这一层管不了的事
上下文工程能决定模型这一步看到什么,管不了它能做什么。窗口里写着不要删除生产数据,和系统层面根本不给它删除权限,是两回事。关键信息只在某个同事脑子里,或者在一个没接入的系统里,检索策略再好也找不到。模型看到了全部正确的信息,写出来的代码仍然可能有 bug。
把知识放到智能体读得到的地方,用权限和沙箱约束它的行为,用自动检查验证结果,这些属于模型外面那套系统,见 Harness 工程。
参考资料