距离 “prompt engineering” 这个词火起来已经过去几年。人们发明这个词的时候,默认的问题是”怎么把话说好”;但现在真正卡住生产环境的,是一个更大的问题——每一轮推理之前,模型到底”看见”了什么。这篇文章想聊的就是这个:上下文工程(Context Engineering)。
一、一个被误诊了一万次的问题
先讲一个几乎所有做过 Agent 的人都经历过的场景。
你花了一个周末调出一个客服 Agent:五轮以内的对话,它对答如流,工具调用干脆利落,语气也恰到好处。你把它接上真实流量,然后开始收到这样的反馈:
- 第 30 轮对话时,它忘了用户在第二轮说过”我对青霉素过敏”;
- 明明检索到了正确文档,它却引用了另一篇过时的 FAQ;
- 它开始反复调用同一个搜索工具,像一台坏掉的唱片机;
- 它的回答风格悄悄漂移,从简洁专业变成啰嗦的客套话。
大多数团队的第一反应是:模型不行,换一个更强的。第二反应是:prompt 没调好,再改改 system prompt。
这两种反应都把问题想浅了。这些失败现象的共性,不是”推理能力不足”,而是”上下文供给失败”——要么该在的信息不在,要么不该在的信息占了太多地方,要么在、但埋在了模型注意不到的位置。
这里有一个必须先建立的事实基础:LLM 是无状态的。你以为的”连续对话”,实际上是每次都把全部历史重新发送一遍;你以为模型”记得”用户过敏,其实是那段文字还留在上下文里、并且模型注意到了它。一旦那段文字被截断、被摘要稀释、或者被淹没在两万 token 的工具输出里,模型就会”忘记”——不是它健忘,而是那一轮推理里它根本没有看见。
所以,当 Agent 在长对话中崩溃,问题往往不在模型的大脑,而在我们给它递过去的”草稿纸”。这张草稿纸越写越乱,而模型只能基于纸面上可见的内容思考。
二、LLM 是一台”上下文压缩机”
要理解上下文工程的必要性,得先接受三个关于 Transformer 的事实。它们都不复杂,但合在一起,推翻了很多人对”大上下文窗口”的直觉。
第一,窗口大小不等于可用注意力。 厂商标注的超长上下文,说的是模型”能处理的最大输入长度”,而不是”能可靠利用的信息量”。2023 年 Liu 等人的论文 Lost in the Middle 用一个很干净的实验揭示了这一点:把关键事实放在长上下文的开头或结尾,模型检索得很准;放在中间,性能显著下滑,呈现一条 U 型曲线。后来业界把这组现象统称为 context rot(上下文腐烂):随着注入的 token 越来越多,模型对每一段信息的利用效率都在下降。
第二,注意力是一种被摊薄的预算。 Attention 机制的本质,是让每个 token 和上下文里所有 token 计算相关度。信息越多,每份信息分到的”注意力密度”越低。这和人类开会一样:3 个人的会议人人都有发言机会,300 个人的会议,大多数人只是在场而已。把 100 份检索文档一股脑塞进上下文,指望模型”自己挑重点”,本质上就是那场 300 人的会议。
第三,没有沉淀,只有重放。 人类对话中,”我记得你说过”是一种主动检索;而 LLM 没有任何跨请求的状态,第 N 轮的推理质量完全由你第 N 次发过去的上下文决定。这意味着:上下文的质量天花板,就是这一轮推理的质量天花板。 模型再强,也不能从一份被污染的上下文里推理出正确答案——garbage in, garbage out。只是这里的 garbage 多数时候不是错误信息,而是噪声、冗余和错位。
把这三条合起来,可以得到一个对工程实践影响深远的结论:
上下文窗口应该被当作 CPU cache 来管理,而不是当作硬盘来使用。
很多团队的实际做法恰恰是把上下文窗口当硬盘:RAG 检索 50 条全部塞进去,工具返回原样保留在历史里,用户全程对话一字不删。这在 8K 窗口时代就会爆炸;在几百 K 窗口的时代不爆炸了,但换来的是延迟上涨、成本翻倍,以及一种更隐蔽的失败——模型开始对无关信息过度敏感,行为变得不可预测。
三、解剖:一个生产 Agent 的上下文里到底有什么
在做任何优化之前,先把”上下文”这个词拆开。一个典型的生产级 Agent,每次请求发送的内容大致由六部分组成:
| 组成部分 | 作用 | 增长方式 | 主要风险 |
|---|---|---|---|
| System Prompt | 角色、规则、输出约束 | 基本固定 | 写得太长会稀释后续所有内容的注意力 |
| 工具定义 | 工具名、参数 schema、描述 | 随工具数量增长 | 描述含糊是误调用的一大来源 |
| 长期记忆 | 用户偏好、历史结论 | 随使用累积 | 无写入标准时沦为垃圾抽屉 |
| 检索内容 | RAG 拉回的文档片段 | 波动最大 | 噪声的主要来源,放错位置最致命 |
| 对话历史 | 全部轮次的用户/助手消息 | 线性增长 | 稀释核心任务,携带过时信息 |
| 工具结果 | 每次调用的返回值 | 往往是最大单项 | 一条 2000 行的日志能毁掉整轮推理 |
这张表值得盯着看十秒,因为它揭示了一个容易被忽略的事实:除了用户的最新一句话,上下文里的一切都是你设计出来的。工具怎么定义、检索多少条、历史保不保留、记忆写什么——每一个都是工程决策。模型的行为,就是这些决策的总和。
换句话说,当 Agent 的行为不符合预期时,你几乎总能在上面这张表的某一列里找到原因。这就是为什么我把上下文工程定义为一种”调度问题”,而不是”措辞问题”。
四、核心框架:上下文工程是信息调度问题
如果把上面的一切收敛成一句话,我愿意这样定义:
上下文工程,是在有限的注意力预算下,决定什么信息、以什么形式、在什么时机进入上下文,以及它何时应该离开。
它和 prompt engineering 的区别不在于抽象程度,而在于作用域:prompt engineering 优化的是”单次投递的措辞”,上下文工程设计的是”跨轮次的信息流”。前者是文案问题,后者接近操作系统对内存的管理——写什么进去(写入策略)、怎么压缩和淘汰(维护策略)、什么该隔离到别处(隔离策略)。
围绕这三个动作,展开四个我认为最有杠杆效应的工程实践。
4.1 工具结果治理:最大的污染源
如果你去统计一个真实 Agent 的上下文构成,多半会发现一个反直觉的事实:占用 token 最多的不是对话,而是工具返回值。搜索接口返回 3000 字网页、数据库工具返回 500 行结果、日志工具返回整个堆栈——这些内容一旦原样进入历史,就会在后续的每一轮里持续存在,持续收税。
第一原则是:模型需要的是”可决策的信息”,不是”原始数据”。让 grep 的完整输出留在文件系统里,让模型看到的是行号引用和关键片段。
一个实践效果很好的模式是”落盘 + 引用”:
def on_tool_result(raw: str, budget: int = 500) -> str:
"""工具结果超过预算时:落盘,返回引用式摘要"""
if len(raw) <= budget:
return raw
path = store_workspace(raw) # 完整内容写入临时工作区
digest = extract_key_lines(raw, k=20) # 抽取关键行:命中行 + 上下文行
return (
f"[结果共 {len(raw)} 字符,已存至 {path}]\n"
f"关键片段:\n{digest}\n"
f"需要更多内容时,调用 read_workspace(path, offset, limit)"
)
这个模式的关键不只是省 token,而是它把”看多少”变成了模型的主动决策——注意力预算花在哪,由推理过程按需决定。配套地,工具描述里必须写清楚如何展开(read_workspace 的参数语义),否则模型会反复全文重读,反而更贵。
同样重要的还有历史清理:一旦某条工具结果已被后续动作消费完(比如文件已经改完了),就没有理由让它继续留在历史里占着注意力。不少成熟的 Agent 框架会在轮次推进时对旧的工具结果做”占位符化”——保留”这里曾经发生过一次什么调用”的痕迹,释放掉内容本身。
4.2 历史压缩:丢什么比省多少更重要
当对话历史逼近阈值,就需要 compaction(压缩)。这件事人人都在做,但做对的很少,因为直觉会把它做成”摘要续写”:把前面 40 轮对话交给模型总结成一段话,接在摘要后面继续跑。问题在于,均匀压缩必然均匀丢失信息,而对话里的信息价值从来不是均匀分布的。
好的压缩是有结构的保留。以任务型 Agent 为例,压缩时真正值得保留的是:
- 目标与约束:用户到底要什么,有什么不能碰的底线;
- 已完成与未完成:哪些步骤已确认做完,哪些还挂着;
- 关键决策及其理由:为什么选了方案 A 而不是 B——这是最容易被均匀摘要抹掉、又最容易导致行为回退的信息;
- 未消化的关键事实:比如用户第二句话里说的那个过敏史。
触发条件: tokens(history) > 0.7 × window
保留:
1. system prompt 与工具定义 —— 原样保留
2. 最近 K 轮对话(通常是最近 3-5 轮) —— 原样保留
3. 更早的历史 —— 结构化压缩:
目标/约束 → 决策及理由 → TODO → 关键事实清单
丢弃:
- 已消费完的工具结果 → 占位符
- 客套、重复、与当前目标无关的分支对话
压缩的时机也有讲究。等到爆窗口才压缩,模型往往已经在”满仓”状态下带病运行了若干轮;更稳妥的做法是设置水位线提前触发。另外,压缩这一步本身要用强模型——用最弱的模型做压缩、再用最强的模型做推理,等于让实习生给 CEO 写会议纪要。
4.3 记忆分层:记忆是写出来的,不是存出来的
“给 Agent 加个记忆”是最容易做浅的模块。很多人把它做成一个向量库存所有历史对话,每次检索 top-k 注入。结果就是记忆越多,Agent 越笨——因为这个”记忆”实际上是一个没有淘汰策略的垃圾抽屉。
更有效的心智模型是把记忆按生命周期分三层,每层有不同的写入和召回规则:
- 工作记忆:就是当前上下文窗口本身。它的问题不是存储而是容量,靠 4.1 和 4.2 的手段维护;
- 会话状态:当前任务的中间产物(待办清单、已验证的假设、文件路径)。它应该在任务结束时被显式清理或归档,而不是自然遗留在历史里;
- 长期记忆:跨会话有复用价值的信息——用户偏好、项目结构、历史决策。它的关键不是”存”,而是写入标准。
所谓写入标准,指的是一条信息要成为长期记忆,至少要回答三个问题:这是稳定的事实,还是一次性的情绪表达?未来什么场景会用到它?它和已有的记忆是否冲突?没有这三个问题把关的记忆系统,会在第 200 次会话之后变成一堆互相矛盾的陈旧信息,而召回它们比不召回更危险——模型会把噪声当作权威指令执行。
另一个常被忽略的细节:记忆召回后的位置。Lost in the middle 的教训在这里同样适用——召回的记忆应该放在上下文的结构化位置(比如紧跟 system prompt 之后),并且标注来源和时间(”2026 年 3 月,用户明确表示……”),让模型能对新旧信息做时效性判断。
4.4 多 Agent:用上下文隔离换注意力聚焦
这两年”多 Agent”很热,但很多团队上多 Agent 的理由是错的——把它当成了并行加速器,或者公司组织架构的映射。多 Agent 真正的价值在于一件事:上下文隔离。
回忆第二节:注意力是稀缺预算。当一个 Agent 需要同时记住用户需求、十条工具规则、三份文档和二十轮历史时,它的注意力被摊薄到什么都做不好。而子 Agent 的本质是:把一整段昂贵的探索过程(比如翻 30 个文件)封装在一个独立的、用完即弃的上下文里,主上下文只接收最终结论。
判断要不要拆子 Agent,我用的标准是:这个子任务能否带着一份自包含的简报独立完成?
- 能。比如”在代码库里定位支付逻辑的实现”——简报就是一句话的任务描述,子 Agent 自己翻 30 个文件,最后返回一段 500 字的结论。主上下文省下的,是那 30 个文件的全部 token 和它们带来的噪声。
- 不能。比如”和用户澄清需求边界”——这个任务的信息密度就在对话本身,拆出去反而制造信息搬运成本,还可能丢失语气和语境。
反模式也要点名:让多个 Agent 同时看到全部信息、各自独立推理再”投票”。这不是隔离,这是用三倍的 token 复制同一份上下文污染。真正有意义的多 Agent,每个角色的上下文应该是不同的——正因为它们看到的世界不同,分工才有意义。
五、反直觉的结论:更大的上下文几乎总是错的
写到这里,可以回应一个流行观点了:”模型窗口越来越大,上下文工程会不会失去意义?”
我的判断恰恰相反。窗口越大,上下文工程越重要,原因有三层。
成本与延迟不会消失。 注意力的计算量随长度增长(工程实现上做了各种近似优化),KV cache 随长度线性膨胀。长上下文意味着每一轮都更贵、更慢,而 Agent 恰恰是循环调用次数最多的应用形态——把 200K token 的上下文跑 50 轮,成本是逐轮叠加的。缓存命中能缓解一部分,但前提恰恰是你的上下文前缀设计得足够稳定,这本身就是上下文工程的课题。
信息密度决定推理质量。 模型在 20K 高信噪比上下文里的表现,几乎总是好于 200K 低信噪比上下文——不是因为后者”装不下”,而是因为噪声本身参与注意力计算,会主动干扰正确信息的权重。context rot 的观察反复验证了这一点:更多的 token,更低的单位信息利用率。
可调试性崩塌。 上下文越长,失败归因越困难。当一个 200K 的上下文产出错误答案时,你几乎无法定位是哪一段信息造成的干扰;而一个精心维护的 20K 上下文,失败时人可以通读全篇找到病灶。工程上,可调试性的价值经常高于模型表现本身的微小差异。
所以长窗口的正确用法,不是”塞更多东西”,而是”多一层保险”——它改变了预算的上限,但没有改变预算稀缺的本质。就像城市扩张不会让土地变得不稀缺,只会改变你该如何规划它。
六、没有评估,一切上下文优化都是玄学
最后聊一个不太”酷”但决定成败的话题。上下文工程的手段——压缩、淘汰、隔离、记忆——每一个都会改变模型看到的信息,也就都可能引入新的失败。没有评估闭环的上下文优化,本质上是玄学调参。
实践中我推荐一个很轻量但有效的做法:建立失败归因的分类账本。每当 Agent 出错,先别急着改 prompt,而是把失败 case 归到四类上下文缺陷里:
| 缺陷类型 | 典型表现 | 对应处方 |
|---|---|---|
| 信息缺失 | 该用的约束根本不在上下文里 | 写入策略:补记忆、补 RAG、补工具 |
| 信息过载 | 关键信息在,但被噪声淹没 | 淘汰策略:压缩、占位符化 |
| 信息过期 | 在上下文里,但已经不成立了 | 时效策略:版本化、失效清理 |
| 位置不当 | 信息在,但埋在中间被忽略 | 结构策略:重排、前置关键约束 |
归因的价值在于它把”感觉不准”变成了”可统计的缺陷分布”。如果你的失败 case 里 60% 是信息过载,那么任何 prompt 措辞优化都治不了这个病,该做的是压缩和淘汰;如果 40% 是信息缺失,问题就在记忆和检索的覆盖面。先看账本,再动手,这个顺序决定了迭代速度的量级差异。
再配一个 20-50 条的回归集(覆盖四类缺陷的典型 case),每次上下文策略调整后跑一遍。不需要多复杂——重要的是让每一次”我觉得这样会更好”,变成”回归集通过率从 71% 提升到 83%”。
七、写在最后
回顾一下这篇文章的推理链条:
- LLM 无状态,每一轮推理的质量天花板,就是当次上下文的质量天花板;
- 注意力是稀缺且分布不均的预算,窗口大小不等于可用注意力;
- 因此,Agent 的稳定性本质上是一个信息调度问题——写入、维护、隔离;
- 更大的窗口没有改变预算稀缺的本质,反而让”精心管理”的价值更高。
从 prompt engineering 到 context engineering,表面上是换了一个热词,实质上是工程重心的迁移:从优化”一句话怎么说”,到设计”信息如何流入和流出模型的注意力”。前者是文案功力,后者是系统设计能力——而后者,才是应用层面对逐年增强的基座模型时,真正拿得出手的护城河。
模型会一直变强、变便宜、变得更能容忍混乱的输入。但只要注意力的分配机制还是现在的样子,”在正确的时机,把正确的信息,以正确的形态,放进模型的视野”,就会一直是那个看起来朴素、做起来见功力的问题。











暂无评论内容