博客.
博客.
(联系方式)

保持联系

如果你想联系我,无论是讨论合作机会、分享想法,还是只是打个招呼,都可以通过以下方式找到我。

(最后更新)

版权所有 / 2026

(快捷键)C
返回学习路线
(01 · AI 基础认知)2026年8月

上下文工程

上下文是 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 型分布"——开头和结尾记得牢,中间容易丢。所以当前任务、关键约束要靠近末尾

上下文组装顺序与 U 型注意力:稳定信息放前面吃 KV Cache,当前任务放最后离生成最近,中间位置最容易被忽略

什么顺序: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_appendarchival_insertarchival_searchrecall_search……)。当 FIFO 队列逼近上限,系统会插入一条"内存压力警告"——相当于 OS 的中断——模型收到后自己决定:把哪些内容写入核心记忆或归档(page out),把哪些外部内容检索回来(page in)。旧消息被驱逐出窗口前,重要信息已经由模型自己"存盘"了。

MemGPT 两级存储:主上下文像内存,外部上下文像磁盘,内存压力警告触发模型自己决定 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:

  1. 给上下文记 token 账:每轮组装后记录总量和构成(系统/检索/历史各占多少),超预算时知道砍哪块
  2. 稳定前缀 + 变动后缀:不变的内容固定开头吃 KV Cache,当前任务放最后
  3. 重要信息放两端:利用 U 型注意力,关键约束在结尾重申
  4. 压缩触发线设在 80%:摘要带模板(保留决策和字面值),结构化状态与自由摘要结合
  5. 大结果落盘,上下文放指针:工具输出超过几百行就写文件
  6. 先算成本乘法账再选架构:token 单价 × 上下文长度 × 轮次,一个小数乘三边就拉开两个数量级
  7. 记忆分三层管:工作记忆滚动、情景记忆检索、语义记忆克制写入
  8. 监控衰减,不迷信窗口大小:上线前用自己的真实任务测不同上下文长度下的准确率,画出自己系统的"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检索增强生成:先检索相关片段,再放进上下文生成先查资料再动笔与长上下文不互斥——是流水线前后两段

参考材料


小结

上下文工程的底层心智模型就一句话:模型只能看见台面上的东西,而台面永远不够大,且堆得越满手艺越打折扣。 围绕这个约束,本章的工具箱是:构造策略决定放什么、什么顺序(U 型注意力,Lost in the Middle 的实验证据);context rot 提醒我们"多"不等于"好",控制信噪比是性价比最高的优化;压缩有截断、滑动窗口、分层摘要、向量化外置四条路线,按信息保留度和成本混用;RAG 与长上下文先算乘法账再站队,多数系统是混合架构;记忆分工作、情景、语义三层,MemGPT 给了一套"模型自己分页"的完整范式。台面布置好了,Agent 的手艺才发挥得出来。