在这里插入图片描述

1. 为什么需要上下文工程

第 1 篇定义过 prompt → context → harness 三个同心圆。这一篇,我们把镜头推近到中间那一圈:单次 LLM 调用之前发生的一切对窗口的整理动作

Karpathy 那句被反复引用的判断仍然是最干净的入口:

LLMs are a new kind of operating system.
The context window is the RAM.
Context engineering is the delicate art and science of filling the context window with just the right information for the next step.
——Andrej Karpathy

把 LLM 当 OS 看,那么上下文窗口就是 RAM——既宝贵又有限:

  • 宝贵:所有当下推理需要的事实、指令、状态都得装进来;它是模型"活着的当下"。
  • 有限:8K → 200K → 1M 一路涨,但 token 是按数量计费的,且长度越长延迟越大、cache miss 越频繁
  • 易丢:长上下文里,context rot(上下文腐烂)是被实证的现象——同一组事实,淹在 200K token 里 vs 单独喂给模型,召回准确率差距巨大。Anthropic cookbook 直白地写着:

As the number of tokens in the context window increases, the model’s ability to accurately recall information from that context decreases.

Cognition 团队甚至断言:“Context engineering is effectively the #1 job of engineers building AI agents.”

但"工程"二字成立的前提,是要有可分类的动作。LangChain 在 2025 年中把整套实践收敛成一张表——写(Write)/ 选(Select)/ 压(Compress)/ 隔(Isolate)——这是本篇的主轴。我们会逐项展开,再补上 Anthropic 的五层压缩管线、prompt cache 角色、双记忆架构、observation masking 等工业落地的细节。

┌──────────────────────────────────────────────────────────┐
│                     CONTEXT  WINDOW                      │
│                  (LLM 的 RAM,有限/易丢)                │
│                                                          │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐  │
│   │  Write   │  │  Select  │  │ Compress │  │ Isolate│  │
│   │  写出去  │→ │  选进来  │→ │   压缩   │→ │  隔离  │  │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬───┘  │
│        │             │             │             │      │
│  scratchpad      RAG/工具检索   summarize    multi-agent │
│  memory.md       embedding      trim         sandbox     │
│  CLAUDE.md       knowledge      Provence     state schema│
│                                                          │
└──────────────────────────────────────────────────────────┘

2. 四种失败模式:上下文为什么会"坏掉"

在讲怎么管理之前,先说为什么必须管理。Drew Breunig 在《How Long Contexts Fail》里把上下文窗口"出事"的方式归纳成四种失败模式,是目前业界引用最广的分类法:

失败模式 一句话定义 典型证据
Context Poisoning(污染) 一个幻觉或错误进入上下文后被反复引用,越滚越实 Pokémon Gemini agent 把幻觉的游戏状态写进 goals 区,从此"执着于不可能完成的目标"
Context Distraction(分心) 上下文太长,模型过度依赖历史而非训练知识,反复重复过去动作 同一 Pokémon agent 超过 100K token 后倾向于复读历史动作;Databricks 实测 Llama 3.1 405B 在 32K 左右开始正确率下滑
Context Confusion(迷惑) 多余的工具/文档让模型用错路径 Berkeley Function-Calling Leaderboard:所有模型只要工具数 > 1 性能就下滑;量化 Llama 3.1 8B 给 46 个工具失败、给 19 个工具成功
Context Clash(冲突) 后续信息与早期内容冲突,模型在早期假设上骑虎难下 Microsoft/Salesforce 分片 prompt 实验:平均下滑 39%,OpenAI o3 从 98.1 跌到 64.1

四种失败合起来传达一个朴素事实:"塞更多进去"绝对不是默认正解。每一个 token 都在抢模型的注意力,模型必须为每个 token 付出注意力——你必须主动决定哪些进、哪些出、什么时候进、压缩到什么程度。这就是接下来要讲的四种策略。

3. Write 写:把状态外置出去

核心思想:让模型把要长期保留的内容"写出去",而不是堆在上下文里。

Write 又分两层:会话内的 scratchpad,和跨会话的 memory

3.1 Scratchpad:会话内的草稿纸

scratchpad 是 agent 在当前任务期间用来"打草稿、记中间状态"的写入位置。它可以是:

  • 一个临时文件(progress.txttodos.mdplan.md
  • 一个 state 字段(LangGraph 的 state schema)
  • 一段被 tag 包住的工具输出

Anthropic 的 Effective Harnesses for Long-Running Agents 给出了一个非常工程化的范例:让 coding agent 在每一轮结束时把进度写进 claude-progress.txt,把已完成的特性在 feature_list.json 里翻面成 passed: true。下一次会话开始,agent 第一件事就是 “Read the git logs and progress files to get up to speed on what was recently worked on.” 这套机制让一个上下文已经被压缩过的 agent 仍能像人一样"接着昨天的工作干"。

scratchpad 的本质,是把易失的工作记忆变成可重读的文件系统状态。这就是为什么把 LLM 类比成 OS 时,scratchpad 对应的是 swap 区——RAM 装不下,就把它换到磁盘去。

3.2 Memory:跨会话的长期记忆

memory 比 scratchpad 长寿一个量级:它是跨会话、跨任务的。学术上把它分三类:

  • Episodic(情节记忆):发生过什么——会话转录、用户在某次任务中的偏好。
  • Semantic(语义记忆):稳定的事实——项目结构、API 端点、约定俗成的命名风格。
  • Procedural(程序记忆):怎么做——固定的工作流、检查清单、规则。

代表性的工业实现:

  • Reflexion(2023):让 agent 在每次任务后做反思总结,写进自生成 memory,下次任务前注入回 prompt。
  • Generative Agents(2023 Stanford):用一个"记忆流 + 周期性合成"的机制把过去事件总结成 higher-level reflection。
  • ChatGPT memories / Cursor / Windsurf:自动跨会话记录用户偏好。
  • Anthropic 多代理研究 agent:在 200K 上下文逼近极限时,把当前 plan 写进 memory,新一轮 agent 起来后先读 memory——这避免了"上下文压缩把 plan 截断"的灾难性失误。

记忆的写入路径有两种取向:模型主动决定写什么(Memory tool 的"agent-driven"模式),或者框架按规则强写(每轮结束自动 dump 关键字段)。前者灵活但不稳定,后者稳定但容易写垃圾——工业实践通常是两者混合。

3.3 这一层最容易被忽略的真相

Write 不是"记得越多越好",而是"哪些事实必须能被未来的我看见"。Anthropic 反复强调:plan 必须写到 memory,而不能只活在 transcript 里。原因很简单——transcript 会被 auto-compact 削掉,memory 不会。

Plans are too important to lose to compaction.

4. Select 选:在恰当时机把恰当东西拿进来

如果 Write 是"出",Select 就是"入"。它的关键词是 just-in-time——不是把所有相关材料一开始就塞进 prompt,而是到了需要的那一刻才检索进来

Select 的四个子动作:

4.1 从 scratchpad 选

scratchpad 写得多,agent 不可能每轮都把整个 progress.txt 读回来——那样 RAM 又被填满了。工程上常见做法是按 tag 拉取:<task id="auth-fix"> 段落只在工作 auth 模块时拿回来。LangGraph 的 state schema 让这件事更显式:开发者声明哪些字段对哪些节点可见,agent 根本看不到不该看的字段。

4.2 从 memory 选

memory 长到一定规模,全量回灌就违背初衷。常见路线:

  • Embedding 检索:把 memory 切成块,用向量库做语义搜索。优点是泛化好、易接入;缺点是会把"相关但不是当前需要"的内容拽进来。
  • 知识图谱(KG):把人、项目、文件做成节点,关系做成边,按 query 走子图。MemPalace 等系统走这条路;准确度更高但建图开销大。
  • 结构化检索:CLAUDE.md / MEMORY.md 直接按行加载。简单粗暴但极其耐用——markdown 不会幻觉,文件系统不会丢消息。

ChatGPT 的 memory 功能曾出现一个反面教材:用户随口说自己"住在某地",后续生图时模型把这条记忆误注入进画面,结果画了一张带地理元素的图——这是 Select 选错的经典案例:记忆里的事实不一定都是当前任务相关的

4.3 选工具(Tool Selection via RAG)

工业里近年的一个共识:当工具数超过 10–20,模型选工具的准确率会显著下滑——这正是上面 §2 里 Berkeley benchmark 的现象。解法是把"选工具"也变成一次 RAG:

  1. 给每个工具写一段 description(其实你本来就要写,schema 的一部分)。
  2. 把 descriptions embedding 索引。
  3. 用户问题进来后,先 semantic search 选出 Top-K(比如 5)相关工具的 schema,再把这 K 个塞进 system prompt。

Hu 等人 2024 的工作显示:这种 RAG-over-tools 让 tool selection accuracy 提高 3 倍。Claude Code 的 ToolSearch 工具走的就是这条路:MCP server 注册的几十、几百个工具不一开始全摆出来,先只暴露 ToolSearch,模型用关键词检索后再加载具体工具的 schema。这是把"上下文成本梯度"硬编码进 harness 的典型例子。

4.4 选知识(Knowledge Retrieval)

Coding agent 的 RAG 从来不是单跑 vector search 这一招。Cursor、Claude Code、Cognition 的实践是 多路融合 + 重排

query
  │
  ├── semantic search(embedding)   ─┐
  ├── AST 结构索引(按符号/函数名) ─┼─→  candidate set  ─→  re-rank  ─→  top-K
  ├── grep / ripgrep(精确串)      ─┤              (cross-encoder)
  └── git history(最近改动加权)   ─┘

向量搜索擅长"语义近"、grep 擅长"精确串"、AST 擅长"结构相邻"、git history 擅长"时间相邻"——四种召回各自的盲区不同,融合后再用 cross-encoder 做精排。Cursor 早期写过 blog 解释为什么"光靠 embedding 在代码里不够用"——一个变量名拼写改了一个字母,向量距离飙升,但 grep 一秒就能找到。

Select 子动作 触发时机 关键技术 风险
from scratchpad 任务子阶段切换 tag / state field 漏读
from memory 用户提问 / 任务初始化 embedding / KG / md 加载 错注入
tools 每轮决策前 RAG over descriptions 漏选
knowledge RAG / coding 子任务 semantic + AST + grep + re-rank 召回偏

5. Compress 压:把已经在窗口里的东西削小

Write 与 Select 都是"窗口外"的动作;Compress 是窗口内的动作——已经堆在那里的 token,想办法变小。

两种主流路径:summarization(总结)trimming(裁剪)

5.1 Summarization

最常见的实现:把会话尾部最近 N 轮保留,往前的全部喂给一个总结模型,产出一段紧凑摘要替换掉原始消息。

代表实践:

  • Claude Code 的 auto-compact:当上下文使用率逼近上限(业界普遍引用的阈值是 ~95%)时自动触发。系统消息会带上 subtype: "compact_boundary",外部能观测到这一刻的发生。
  • Cognition 的 fine-tuned 总结模型:他们专门训了一个小模型,目标是抓"决策性陈述"——“我们决定 X,因为 Y”——而不是一律均匀压缩。普通总结模型在 agent 场景里有个老毛病:会按"对话重要性"压,但 agent 的关键往往是几个分支点的判断,对话主线反倒可以丢。
  • 服务端 compaction:Anthropic Cookbook 给出的 compact_20260112 API 配置允许声明触发阈值(比如 input_tokens 达到 150K)和定制 instructions(比如"保留所有定量数据和效应大小")。

5.2 Trimming

总结要花 LLM 调用,trimming 不用——直接按规则删

  • 启发式裁剪:丢掉最旧的 N 条消息;丢掉早期的 tool_result(保留 tool_use,因为再调一次还能拿回来)。Anthropic 的 clear_tool_uses_20250919 配置就是这一类,实测让一个研究 agent 的峰值上下文从 335K 降到 173K,砍掉 67%
  • Provence:MIT 等 2024 年提出的训练好的"上下文裁剪器"——专门训一个小模型来判断段落对当前 query 是否相关,按段抹掉。
  • 观察遮蔽(observation masking):把巨型工具输出直接换成占位符 [cleared to save context],agent 知道它存在,但不再在每轮 prefill 时复述它。

5.3 压缩 vs 检索:哪个更划算?

工业里两种思路常常对立:

  • 压缩派:把所有信息都"留在" transcript 里,但越来越浓缩。
  • 检索派:把信息都"踢出去"放到外部存储,需要时再 just-in-time 拉回来。

实战折中:

  • 可重新拉取的内容(文件、网页、命令输出)→ 检索。这一类东西用 trimming + 占位符最划算(67% 节省、几乎零代价、能再调)。
  • 不可重新生成的对话历史(agent 的推理链、用户决定)→ 总结。这一类一旦丢了 agent 自己也复现不出来,必须靠 summarization 留个轮廓。
  • 跨会话的事实(项目知识、用户偏好)→ 写进 memory

Anthropic Cookbook 的诊断框架可以原样照抄:先看 context 里 token 是被什么吃掉的;如果是 file-read 占了 96%(实测过的真实数字),那 trimming 就是答案;如果是用户和 agent 的多轮交流,summarization 才是。

6. Isolate 隔:把上下文切成多块

最后一种策略,是结构性的——根本不让所有东西挤在同一个上下文里。

6.1 Multi-agent

最直接的隔离:把 concerns 切到独立 agent,每个 agent 一个独立的上下文窗口、独立的工具白名单。

  • OpenAI Swarm:handoff 范式,把"客服分类 → 退款流程 → 物流流程"切成三个 agent,每个看到的上下文小且专。
  • Anthropic 多代理研究 agent:主 agent 派 N 个并行子 agent 各跑一段子调研,子 agent 只把最终结论返回主 agent。这个设计让单代理 200K 装不下的问题,被 N 个独立 200K 解掉了——但代价是 token 消耗最多到 chat 的 15×。Anthropic 自己也承认:“这是个用 token 换任务通过率的明显折中。”
  • Anthropic 的 initializer + coder 双代理(第 5 篇会展开):本质也是隔离——initializer 看仓库结构、coder 看代码细节,两人不互相污染。

6.2 环境隔离(sandbox)

不是所有动作的副产物都需要回到 LLM 上下文:

  • HuggingFace 的 Deep Researcher:用 CodeAgent 把图像、视频、二进制文件扔进 sandbox 计算后只返回数值结论,原始字节数据从不进入主 context。这一招让一个推理任务的 context 从几百 K 缩到几 K。
  • Claude Code 的 Bash 沙箱:长输出(比如 find . -type f)截断后只把摘要返回;要看全的,agent 自己去 disk 上读。
  • MCP 的 25K 阈值:当 MCP 工具返回的内容超过 25K token,Claude Code 会把它持久化到磁盘,仅在 transcript 里留一个 reference 字符串(类似 <file:/tmp/mcp-output-abc123.json>)。agent 后续要用,再用 Read 工具拉回——这是观察遮蔽与 just-in-time 检索的合体。

6.3 State Object(字段级隔离)

LangGraph 提出了一个比"agent 边界"更细的隔离单位——state schema。每个节点声明它读哪些字段、写哪些字段,整个 graph 像 React 组件树一样有清晰的"props 边界":

class GraphState(TypedDict):
    user_question: str
    retrieved_docs: list[Doc]   # only retriever node sees this
    draft_answer: str           # only writer node sees this
    final_answer: str           # only reviewer node returns this

LLM 节点只能看到声明的字段。这样即便整个 graph 跑 20 步,每一步喂给 LLM 的 context 都极薄。

隔离方式 边界单位 适用场景 代价
Multi-agent 整个 LLM 实例 长任务 / 异质子任务 / 并行调研 token 成本可达 15×
Sandbox 工具执行环境 大体积副产物(图像、长 bash) 实现工程量
State Object 字段 / 属性 DAG 化的工作流 表达力受限

7. Anthropic 的五层压缩管线:从工业实现看 Compress

LangChain 框架是抽象的;Anthropic 在 Claude Code 里把"压"做成了一个五层流水线——每一层在不同时机、不同代价、不同力度上削上下文。论文 Dive into Claude Code 把它拆得很清楚:

# 触发 做什么 代价
1 Budget Reduction(预算削减) 每次模型调用前 单消息超过 maxResultSizeChars 时换成 reference,不删全文,可重建 0
2 Snip HISTORY_SNIP flag 触发 删除较老的对话片段;显式回报释放的 token 数 极低
3 Microcompact 总是运行 tool_use_id 对工具结果细粒度压缩;可走 cache-aware 路径
4 Context Collapse CONTEXT_COLLAPSE flag 不修改原始 history,只在读时做"投影视图",模型看到的是 collapsed 版本
5 Auto-Compact 上下文压力仍 > 阈值(≈ 95%) 调模型生成完整摘要(compactConversation());先触发 PreCompact hook 高(一次额外 LLM 调用)

注意几个工程上很重要的细节:

  • 管线是"先便宜后贵":第 1 层零代价就能解决一大半压力;第 5 层才是最后兜底。这正是 OS 设计里的 graduated layering——不是用一招砸所有问题。
  • compact_boundary 消息是观测点:当第 5 层真的开启,stream 里会出现 subtype: "compact_boundary",TypeScript SDK 上有专门的 SDKCompactBoundaryMessage 类型。这让外部代码(监控、日志、告警)可以精确知道"刚刚被压了一次"。
  • PreCompact hook 是干预点:你可以注册一个 hook 在第 5 层触发前跑——常见用法是把完整 transcript 归档到磁盘,避免压缩造成的"知识丢失"。这是 harness 工程里"挂在边界上写守护逻辑"的经典 pattern。
  • Context Collapse 不是 mutation,是 projection:第 4 层的优雅之处在于"原始 history 没动",模型只是看到一个被压扁的视图。如果上层逻辑需要 replay(比如断点续传 / fork session),底层数据还在。

8. Prompt Cache:稳定前缀决定经济模型

讲压缩绕不开 prompt cache。Anthropic 的 prompt cache 机制让"稳定前缀"在 5 分钟 TTL 内被复用——命中时这部分 token 只算 cache read(约 10% 价格),不命中则全价付费。这对上下文工程的意义比想象中大:

  • 稳定前缀是金子:system prompt + tool 定义 + CLAUDE.md 这三段,如果没每轮变,就是 cache 的甜点区。Claude Code SDK 文档明确写了:“Content that stays the same across turns (system prompt, tool definitions, CLAUDE.md) is automatically prompt cached.”
  • 5 分钟 TTL 决定了 sleep 模式:长任务 agent 之间间隔超过 5 分钟,cache 就失效——这是 Anthropic 团队建议"长任务别让 agent 完全 idle"的理由。
  • 每次压缩都是一次 cache miss:第 7 节那五层管线,只要任何一层动了 transcript 的前缀部分,cache 就废掉,下一轮要重 prefill 全部。所以 Anthropic 在第 3 层 microcompact 的实现里很在意 cache-aware path——尽量让压缩后的前缀仍然能命中之前的 cache 段。

这是个反直觉但很关键的工程考量:压缩本身有显式 token 成本(一次 LLM 调用 + cache miss)——你必须保证它省下的远多于它花掉的。

9. Claude Code 的双记忆架构:transcript + CLAUDE.md

讨论 Write 时我们提到 episodic / semantic / procedural 三种记忆。Claude Code 在工程上把它们浓缩成一个非常"无聊但耐用"的双记忆架构:

  • 会话内 transcript(episodic):当前任务发生过的所有消息。被五层管线持续修剪,是动态的、易失的。
  • 跨会话 markdown(semantic + procedural)CLAUDE.md / MEMORY.md / ~/.claude/CLAUDE.md。被 settingSources 加载进每次请求;变化少所以始终命中 prompt cache;用户和 agent 都能直接读写。

很多人会问:为什么不上向量库?为什么 Claude Code、Cursor、Codex 都偏好 markdown?答案是markdown 在工程上更耐用

  1. 可读可写、人和模型都看得见——出问题时人能直接打开看 / 改。
  2. diff 友好、git 友好——记忆变更可审计、可回滚。
  3. 零基础设施——不依赖 ChromaDB、不依赖 Postgres + pgvector,不会因为一个服务挂掉整个 agent 失忆。
  4. prompt cache 友好——markdown 文件路径稳定,重读时 cache 命中。

向量库不是不能用,而是在多数 coding agent 场景里 markdown 是 80/20 的最佳选择。等到 memory 真的膨胀到几百 MB,再加 vector 索引层不迟。

10. 观察遮蔽与 just-in-time retrieval:上下文工程的高阶动作

最后讲一类越来越主流的 pattern——先看着结果,再决定要不要把它留在上下文里

朴素 ReAct 的做法是:tool call → observation 全文塞回 context。这在工具输出小的时候没问题,但放到生产环境就崩——一次 find / -name '*.log' 可能拉回 50K token。

进阶 pattern:

  • 观察遮蔽(observation masking):tool 调用时立刻判定输出体积。超阈值(Claude Code 的默认是 25K token)就把全文持久化到磁盘,仅在 transcript 里留一个 reference 字符串。下一轮模型如果真要看,自己用 Read 工具去拉。
  • Just-in-time retrieval:与上一条同根——所有"可重新拉取"的内容都不该一开始就在 context 里。文件、网页、命令输出、数据库记录都算可重新拉取。
  • Memory 工具的 agent-driven 写入:Anthropic 的 Memory tool 让 模型自己决定 哪些事实值得写到 memory,而不是框架 hardcode 规则。这把"什么东西重要"的判断推给了模型本身——它见过更多上下文,反而最有资格判断。

观察遮蔽的关键洞察:结果出来之前你不知道它有多大;所以判断必须发生在 tool call 之后、消息提交回 LLM 之前。这是 harness 才能做的事,不是 prompt 能做的事。

11. 可量化的收益

讲了这么多策略,最后给几条有数字的工业证据,证明这些动作不是花架子:

  • ACON 研究(arXiv 2510.00615):用上下文压缩在 3 个长任务 benchmark 上做到 26–54% 的 token 削减95%+ 准确率保留;甚至在 OfficeBench 等任务上反而提升 20–46%——因为剪掉了"分心"的内容。
  • Anthropic clear_tool_uses 实测:研究 agent 上下文从 335K 降到 173K(67% 削减),任务完成率不降反升。
  • LangChain TerminalBench 案例:同模型(GPT-5.2-Codex)只改 harness(系统提示 + 工具 + middleware),benchmark 排名 从 Top 30 跳到 Top 5+13.7 个百分点(52.8% → 66.5%),相当于 20+ 位的位次提升。
  • Vercel v0:把工具数砍掉 80%,反而让 agent 更可靠——更少工具 = 更少分心 = 更高成功率。
  • 整体 harness 改进:业界普遍报告任务成功率 2–3×——这里面相当大一部分来自上下文工程的精细化。

数字背后的统一信号:模型已经不是瓶颈了,瓶颈是窗口里塞了什么。

关键 takeaway

  • 写、选、压、隔是上下文工程的四种基本动作。它们不是互斥的,工业实践通常四种同时启用。
  • "塞更多"不是默认正解——四种失败模式(污染 / 分心 / 迷惑 / 冲突)会让长上下文反过来伤害 agent。
  • Compress 不只一招:Anthropic 的五层管线(budget reduction → snip → microcompact → context collapse → auto-compact)按代价梯度递进,先便宜后贵。
  • Markdown 比向量库更耐用:CLAUDE.md / MEMORY.md 在工程上的可观测性、可审计性、prompt cache 友好性远胜花哨记忆系统。
  • 观察遮蔽是 harness 才能做的事:先看结果再决定保留/丢弃,这种"消息提交前的中间态干预"是 prompt engineering 永远做不到的。

下一篇我们走出窗口,看看模型与外部世界之间的那一层——工具系统与权限模型——19 个工具是怎么被注册、调度、沙箱化、并阻止 prompt injection 劫持权限的。


参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐