上下文工程
上下文是 Agent 的工作台,台面就那么大。放什么、按什么顺序摆、旧的什么时候清走、常用的挂在哪面墙——这些决策直接决定产出质量,这就是上下文工程。
先把画面立起来:工作台
把 LLM 想象成一个手艺极好的工匠,但它有个怪癖:只能看见工作台台面上的东西。台面上摆了图纸,它就按图纸干活;台面上堆满废纸,它就开始照着废纸胡说。而且台面大小是固定的——这就是上下文窗口。
上下文工程(Context Engineering)就是研究怎么布置这张工作台的学问:放什么、按什么顺序摆、放不下的怎么办、常用的工具挂在哪面墙上随时能取。提示词工程只关心"图纸怎么写",上下文工程关心的是整张台面乃至整个工作间的布局——这是从"写好一句话"到"运营一个信息环境"的升级。
为什么这件事值得单独一章?因为模型的输出质量几乎完全由输入决定。同一个模型,给它精心组织的三页材料,和给它五十页未经筛选的日志,产出天差地别。一个 Agent 做得好不好,一半是模型能力,另一半是上下文管理。 而上下文管理里有大量反直觉的实证结论——"信息越多越好"恰恰是其中最错的一个。
构造策略:放什么、什么顺序
放什么:少而准,不是多而全
直觉告诉我们"信息越多越好,让模型自己挑",实测恰恰相反。上下文里每多一段无关内容,模型被带偏的概率就高一分。先记住这个结论,后面用实验数据展开。
一个典型的 Agent 上下文组装顺序大致是:
const messages = [
{ role: "system", content: persona + rules }, // 身份与规则:最稳定
{ role: "system", content: retrievedDocs }, // 本次任务相关的资料:按相关性精选
...recentTurns, // 最近几轮对话:原样保留
{ role: "user", content: summaryOfOlderTurns }, // 更早的历史:压缩成摘要
{ role: "user", content: currentTask }, // 当前任务:放在最后,离生成最近
];
原则就两条:
- 稳定的信息放前面:系统提示词、工具定义几乎不变,放前面能命中 KV Cache(推理引擎会把前缀的注意力计算结果缓存下来复用),省钱省延迟
- 最重要的信息放最后:模型的注意力呈"U 型分布"——开头和结尾记得牢,中间容易丢。所以当前任务、关键约束要靠近末尾
什么顺序:Lost in the Middle 的实验
"U 型注意力"不是经验玄学,有扎实的实验支撑。2023 年的论文《Lost in the Middle: How Language Models Use Long Contexts》做了这样的实验:
实验设计:拿 NaturalQuestions 问答数据集改造成多文档问答——给模型一堆检索回来的文档(10、20、30 篇三档),其中只有一篇包含答案("金文档"),其余是相关但不含答案的干扰文档。然后系统性地把金文档放在第 1、2、3……直到最后一位,看模型答对率随位置怎么变。
结果:一条清晰的 U 型曲线——
- 金文档在第 1 位时准确率最高;往后挪,准确率单调下滑
- 金文档藏在中间位置时,GPT-3.5-Turbo 的答对率比放在开头低了 20 个百分点以上
- 最反直觉的一刀:在 30 篇文档的设置下,金文档放中间的表现甚至低于完全不给任何文档的闭卷基线——也就是说,给了资料反而比不给还差,因为模型被一堆干扰文档带偏,忘了自己本来知道答案
论文还补了一个合成的键值检索任务(从一堆 JSON 键值对里取指定 key),结论一致:模型擅长取开头和结尾的 key,中间的经常取错。
工程推论很直接:
- 检索回来的 N 段资料,最相关的放最前或最后,次相关的塞中间——中间是注意力的"百慕大"
- 关键指令("绝对不能做 X")不要埋在一大段中间,要么放开头,要么在结尾重申一遍
- 长文档问答时,把文档原文放前面、问题放最后,让问题离答案的生成位置最近
- 排工具定义、排 few-shot 示例时同理:最重要的示例放两端
Context Rot:上下文退化是实证现象
"塞太多会变笨"也有系统性的测量。2025 年 Chroma 发布的技术报告《Context Rot》测试了 18 个主流模型,方法很干净:任务本身不变,只往输入里加长度——加无关填充、把"大海捞针"的针换成语义相近的干扰项、把针改写成需要一点点推理才能识别的变体。
几个值得记住的发现:
- 性能随输入变长普遍下滑,而且是渐变不是悬崖——不存在一个"安全长度",过了就崩;从几千 token 开始,衰减就已经在发生
- 干扰项与目标语义越像,衰减越狠。模型找"与众不同的针"很容易,找"混在一堆相似表述里的那条"就明显吃力
- 甚至简单的重复任务(把一段词列表原样复制出来)都会随长度掉准确率——说明这不是"理解力"问题,而是注意力预算被稀释的底层现象
Anthropic 在官方文章里把上下文称为**"一种边际收益递减的有限资源"**:模型每读一个 token 都要花"注意力预算",预算总量有限,塞得越多,每个 token 分到的注意力越少。
这里有争议,正反方都摆出来:
- 乐观方:新一代模型在经典的"大海捞针"(NIAH)测试上接近满分,context rot 是旧模型的病,会被规模化治好
- 谨慎方:NIAH 只需要字面匹配,太简单了;真实任务要在多段信息间做推理和交叉验证,衰减依然显著。而且"测不出来"不等于"不存在",可能只是探针太钝
工程上的稳妥立场是后者:即使模型在进步,控制上下文规模、保持信噪比,依然是当下性价比最高的优化。
压缩与摘要:长会话不爆窗口
会话一长,窗口迟早要满。四种主流方案,按"信息量损失 vs token 成本"排开:
| 方案 | 做法 | 信息保留 | token 成本 | 主要风险 |
|---|---|---|---|---|
| 硬截断 | 砍掉最早的消息 | 砍掉的部分 100% 丢失 | 最低 | 用户开头说的核心需求可能正是被砍的那段 |
| 滑动窗口 | 只保留最近 k 轮 | 窗口外 100% 丢失 | 低且可预测 | 长程任务做到后半段,前半段的决策全忘了 |
| 分层摘要 | 旧历史交给模型压缩成摘要 | 保留决策与结论,细节有损 | 约为原文的 1/10 ~ 1/20 | 文件路径、ID、错误码这类"小细节"最容易被概括掉;摘要的摘要会漂移 |
| 向量化外置 | 原文进向量库,上下文只留指针,需要时检索 | 原文零损失,但召回率不是 100% | 上下文内几乎为零,检索时按 top-k 付费 | 检索不到 = 事实性丢失;多一跳检索延迟 |
定量感受一下:一轮 4000 token 的对话,分层摘要后大概剩 200~400 token,压缩比 10:1 到 20:1——摘要适合保留"发生了什么决策",不适合保留"具体字面值"。向量化外置则相反:字面值一个不少,但你得先检索得到它。所以成熟的系统几乎总是混用:近期原文保留 + 中期摘要 + 远期向量化归档。
滚动摘要的最小实现
当历史超过阈值,把最老的一批压缩成摘要,用摘要替换原文:
async function compact(history: Message[]): Promise<Message[]> {
if (tokenCount(history) < LIMIT * 0.8) return history; // 80% 就动手,别等爆
const cutoff = Math.floor(history.length / 2);
const [old, recent] = [history.slice(0, cutoff), history.slice(cutoff)];
// 给模型约束:保留决策、结论、具体字面值(路径/ID/错误码),丢弃试错过程
const summary = await llm.summarize(old, {
keep: ["已做的决策及原因", "排除过的方案", "文件路径/ID/错误码等字面值"],
drop: ["中间试错过程", "寒暄与重复确认"],
});
return [{ role: "system", content: `前情提要:${summary}` }, ...recent];
}
要点有三个:触发时机留余量(80% 就压缩,别等 100%);摘要要有模板(自由发挥的摘要最爱丢字面值);压缩是有损的——关键事实要么进结构化状态,要么原样保留。
更省的思路:让上下文放指针,不放数据
工具返回了一大段结果(比如 500 行日志),别全塞进上下文。存到文件,上下文里只放路径和一句话结论:
async function runAndExternalize(cmd: string): Promise<string> {
const output = await exec(cmd);
if (output.length < 2000) return output; // 小结果直接回填
const path = `.agent/logs/${Date.now()}.log`;
await writeFile(path, output);
const failures = output.split("\n").filter((l) => l.includes("FAIL"));
// 上下文里只放指针 + 最有信息量的几行
return `输出过长已写入 ${path},失败 ${failures.length} 处:\n${failures.slice(0, 5).join("\n")}`;
}
模型需要细节时自己会去读文件。上下文里放指针,数据放外部存储——这是长任务 Agent 最重要的省 token 技巧,本质上和 MemGPT 的分页是同一个思想(后面展开)。
压缩路线的争议:一方认为有损摘要不可避免地带入漂移(摘要的摘要像传话游戏,几十轮后前情提要已经和原文相去甚远),所以长任务应该维护结构化状态(任务清单、决策记录)而不是自由文本摘要;另一方(包括 Claude Code 的 /compact 实践)认为带约束模板的摘要已经足够好,且自由文本对模型更"好消化"。现实的解法是两者结合:字面值走结构化状态,叙事性历史走摘要。
RAG vs 长上下文:先算账,再站队
窗口动辄 200K、1M token 的今天,一个绕不开的问题是:"既然整本书都塞得下,还要 RAG 干什么?"别急着站队,先把乘法账算清楚。
设定一个具体场景:语料 50 万 token(一个中型代码库或一套产品文档),一次会话 20 轮,每轮都要"看到"语料。以某主流模型公开价位为例(输入约 3 美元/百万 token,缓存写约 3.75,缓存读约 0.3):
- 全塞,无缓存:50 万 × 20 轮 × $3/百万 ≈ 30 美元
- 全塞 + prompt caching:首轮缓存写 ≈ $1.9,之后 19 轮按缓存读 50 万 × $0.3/百万 ≈ $0.15/轮,合计 ≈ 4.8 美元
- RAG:每轮检索 top-5 片段 ≈ 4000 token,20 × 4000 × $3/百万 ≈ 0.24 美元,外加向量库的建设和运维(embedding 成本几乎可忽略)
同一个任务,三种做法差了两个数量级。而且这还没算另外两笔账:
- 延迟:50 万 token 的预填充(prefill)即使命中缓存也要好几秒,RAG 的几千 token 几乎无感
- 准确性:上一节说过,context rot 意味着"全塞"的准确率未必更高——你付了 100 倍的钱,买到的可能是更差的答案
两派的争论也摆出来:
- 长上下文派:长窗口模型在部分长文 benchmark 上已经超过 naive RAG,检索环节会引入"检索不到就彻底没戏"的硬失败,全塞没有这个问题
- RAG 派:企业语料是 PB 级的,物理上不可能全塞;且成本、延迟、rot 三本账都站在精选一边
实践结论:这不是二选一,而是流水线的前后两段——
- 语料能稳定装进窗口、且每次都要通读(审合同、读整个仓库)→ 长上下文 + 缓存
- 语料是窗口的几十倍以上、每次只用一小角(知识库问答)→ RAG
- 多数真实系统是混合的:RAG 从海量语料粗筛出候选,再大方地把候选放进长上下文细读。Anthropic 的 Contextual Retrieval 就是这条路上的改进——给每个片段补一句上下文说明再入库,检索质量明显更好
一句话:长上下文解决"放得下",RAG 解决"找得着"。
记忆分层:工作记忆 / 情景记忆 / 语义记忆
借用认知科学的分法,Agent 的记忆分三层。MemGPT 那篇论文干脆把操作系统的虚拟内存管理整个搬到了 LLM 上,值得展开讲讲它的机制,因为它几乎就是一套可直接抄的记忆架构。
MemGPT 的分页机制
MemGPT 把上下文分成两级存储,对应 OS 的内存与磁盘:
- Main Context(主上下文,≈ 内存):窗口内可见的部分,又分三块——
- 系统指令:告诉模型"你有一套内存管理函数可用"
- Core Memory(核心记忆):两个固定小块,
persona(我是谁、我该怎么做)和human(用户是谁、偏好什么)。模型可以用函数自己改写这两块——"自我编辑的记忆" - FIFO 消息队列:最近的对话和工具结果,滚动前进
- External Context(外部上下文,≈ 磁盘):窗口外的全部历史与知识——
- Recall Storage:完整对话历史,按时间/关键词检索
- Archival Storage:任意事实与文档,向量化后按相似度检索
关键在于换页是自发的:模型手里有一组内存管理函数(core_memory_append、archival_insert、archival_search、recall_search……)。当 FIFO 队列逼近上限,系统会插入一条"内存压力警告"——相当于 OS 的中断——模型收到后自己决定:把哪些内容写入核心记忆或归档(page out),把哪些外部内容检索回来(page in)。旧消息被驱逐出窗口前,重要信息已经由模型自己"存盘"了。
这就是"LLM as OS"的字面含义:模型既是 CPU,也是内存管理单元(MMU),分页策略不是写死的程序,而是模型通过函数调用动态决策的。
走查一遍具体流程,感受这套机制怎么转起来:用户在第 1 轮随口说"我对花生过敏"。几十轮后队列滚动,这条消息即将被挤出窗口——系统发出内存压力警告,模型判断"这值得长期记住",调用 core_memory_append 写进 human 块;从此以后每一轮上下文里都稳定带着它,不再依赖那条早已驱逐的原始消息。又过了一个星期,用户问"上次我们讨论的那家餐厅怎么样"——模型对当前窗口里的内容没有印象,于是调用 recall_search 翻出历史记录,把相关几页"换入"窗口再回答。整个过程不需要工程师写任何"第 N 轮该保留什么"的规则,保留什么、检索什么,都是模型自己判断的——这正是它和普通"聊天历史截断器"的本质区别。
三层记忆的对号入座
把 MemGPT 的机制映射回认知科学的三层:
- 工作记忆(Working Memory):当前窗口本身。快、贵、容量小、会话结束即清空。只放"现在正在做的事"——对应 MemGPT 的主上下文
- 情景记忆(Episodic Memory):"发生过什么事"——上次报错怎么解决的、用户否决过哪个方案。按相似度/时间检索召回,对应 recall + archival storage
- 语义记忆(Semantic Memory):沉淀下来的稳定知识——用户偏好、项目事实("这个仓库的测试命令是 pnpm test")。每轮稳定加载,对应 core memory,工程上也常落地为一份 Markdown(比如 Claude Code 的 CLAUDE.md)
三层的协作:
设计要点:每层有自己的写入策略。工作记忆自动滚动;情景记忆在任务结束时由 Agent 总结写入;语义记忆的写入要克制——什么碎碎念都往里写,很快变成噪声库,每次加载都在稀释注意力。
工程实践要点
落成 checklist:
- 给上下文记 token 账:每轮组装后记录总量和构成(系统/检索/历史各占多少),超预算时知道砍哪块
- 稳定前缀 + 变动后缀:不变的内容固定开头吃 KV Cache,当前任务放最后
- 重要信息放两端:利用 U 型注意力,关键约束在结尾重申
- 压缩触发线设在 80%:摘要带模板(保留决策和字面值),结构化状态与自由摘要结合
- 大结果落盘,上下文放指针:工具输出超过几百行就写文件
- 先算成本乘法账再选架构:token 单价 × 上下文长度 × 轮次,一个小数乘三边就拉开两个数量级
- 记忆分三层管:工作记忆滚动、情景记忆检索、语义记忆克制写入
- 监控衰减,不迷信窗口大小:上线前用自己的真实任务测不同上下文长度下的准确率,画出自己系统的"rot 曲线"
常见误区
- 塞得越多越保险:恰恰相反,无关内容稀释注意力,context rot 是实测存在的现象。上下文要做减法,不做加法
- 窗口大了就不用管上下文:1M token 的窗口装 1M token 的垃圾,照样产出垃圾。窗口大小解决"能不能放",上下文工程解决"该不该放"
- 摘要交给模型自由发挥:不给约束,模型会把文件路径、错误码这类关键细节"概括"掉,而且摘要的摘要会逐轮漂移
- 记忆只有一个大向量库:所有记忆混在一个库里按相似度捞,用户偏好和上周的报错日志抢同一个召回名额。分层,各管各的
- 把 RAG 和长上下文当非此即彼:它们是流水线的前后两段,真实系统几乎都是混合架构
- 在提示词里雕花,在上下文上躺平:系统提示词改了二十版,历史消息却原封不动堆了几百轮——优化错了地方
术语表
| 名词 | 定义 | 一句话直觉 | 常见混淆 |
|---|---|---|---|
| 上下文窗口 | 模型单次调用可见的最大 token 数 | 工作台的台面大小 | 窗口大 ≠ 可以随便塞,信噪比同样重要 |
| 上下文工程 | 设计"每轮往窗口里放什么、怎么放"的工程学科 | 布置整个工作间,而非只写图纸 | 提示词工程是它的子集,不是同义词 |
| KV Cache / prompt caching | 缓存前缀 token 的注意力计算结果,重复前缀不必重算 | 台面上没动过的部分不用重新看一遍 | 是性能优化,不扩大窗口也不提升质量 |
| Lost in the Middle | 模型对长输入中间位置的信息召回率显著低于两端的现象 | 注意力的"百慕大"在中段 | 不是"中间完全读不到",是相对衰减 20+ 个百分点 |
| context rot | 性能随输入变长而渐进退化的实证现象 | 台面越堆越乱,手艺再好也发挥不出来 | 不是到某个长度突然崩溃的悬崖,是渐变 |
| 硬截断 | 直接丢弃最早的消息 | 把旧图纸直接扔掉 | 与压缩完全不同:丢失 100%,压缩是有损但保留要点 |
| 滑动窗口 | 只保留最近 k 轮对话 | 只看手边最近几张纸 | 不等于摘要:窗口外的内容彻底没了 |
| 分层摘要 | 用模型把旧历史压缩成摘要,逐层滚动 | 旧图纸归档成一页备忘录 | 压缩 ≠ 无损;字面值(路径/ID)最易被"概括"掉 |
| 向量化外置 | 原文存向量库,上下文只留指针,用时检索 | 东西收进仓库,台面只留便签 | 检索不到 = 事实性丢失;召回率永远不是 100% |
| 工作记忆 | 当前窗口内的活跃信息,会话结束即清空 | 台面上正在用的东西 | 不是"短期记忆"的同义词——它直接参与每轮生成 |
| 情景记忆 | "发生过什么事"的记录,按相似度/时间检索召回 | 过去的项目档案 | 不等于聊天历史全量回放:是检索式召回 |
| 语义记忆 | 沉淀的稳定知识与偏好,每轮稳定加载 | 挂在墙上的常用工具 | 写入要克制,什么都存会变成噪声库 |
| MemGPT | 把 OS 虚拟内存/分页机制搬到 LLM 的记忆架构论文 | 模型自己当内存管理单元 | 不是具体产品,是一套可复用的架构思想 |
| RAG | 检索增强生成:先检索相关片段,再放进上下文生成 | 先查资料再动笔 | 与长上下文不互斥——是流水线前后两段 |
参考材料
- Effective Context Engineering for AI Agents — Anthropic 官方对上下文工程的系统阐述,本章的核心参考
- Lost in the Middle: How Language Models Use Long Contexts — U 型注意力的原始论文,实验设计的细节值得读原文
- Context Rot: How Increasing Input Tokens Impacts LLM Performance — Chroma 对 18 个模型的上下文退化实测报告
- MemGPT: Towards LLMs as Operating Systems — 记忆分层与分页机制,把 OS 内存管理搬到 LLM
- Contextual Retrieval — Anthropic 的 RAG 改进方案,混合架构的范例
- LLM Powered Autonomous Agents — Lilian Weng 的综述,记忆分类的经典梳理
小结
上下文工程的底层心智模型就一句话:模型只能看见台面上的东西,而台面永远不够大,且堆得越满手艺越打折扣。 围绕这个约束,本章的工具箱是:构造策略决定放什么、什么顺序(U 型注意力,Lost in the Middle 的实验证据);context rot 提醒我们"多"不等于"好",控制信噪比是性价比最高的优化;压缩有截断、滑动窗口、分层摘要、向量化外置四条路线,按信息保留度和成本混用;RAG 与长上下文先算乘法账再站队,多数系统是混合架构;记忆分工作、情景、语义三层,MemGPT 给了一套"模型自己分页"的完整范式。台面布置好了,Agent 的手艺才发挥得出来。