第 3 章 · 上下文工程
上下文窗口是 Agent 最稀缺的资源。上下文工程(context engineering)研究的问题是:在有限的窗口里放什么、不放什么、什么时候替换。它正在取代 prompt engineering 成为 Agent 时代的核心技能——prompt 是静态文本,上下文是动态系统。
3.1 窗口经济学
先把账算清楚。一个典型 Agent 每轮的上下文构成:
[system prompt] 500 - 5,000 tok 常驻
[工具 schema] 200 - 20,000 tok 常驻(随工具数量爆炸)
[历史消息] 无上限增长 每轮 +数百到数万
[新输入] 100 - 10,000 tok
三个推论:
- 历史是唯一的无界项,上下文工程的主战场就是它;
- 工具 schema 可能比对话还贵:50 个工具的 JSON Schema 轻松超过 10k token,每轮都要付——工具列表瘦身是性价比最高的优化(第 5 章);
- 成本随轮数二次增长:第 N 轮要把前 N-1 轮的历史全部重传,即使有前缀缓存折扣,总量也是 O(N²) 级别的累积计费——这就是为什么"上下文管理"直接等于"钱"。
上下文腐烂(context rot):窗口塞得越满,模型对每条信息的注意力越稀释,长上下文中部的信息召回率显著下降(lost in the middle 现象)。所以"窗口还剩很多"不是不管理上下文的理由——更少但相关的上下文,几乎总是优于塞满的上下文。
3.2 压缩技术
滑动窗口(truncation)
最简单:只保留最近 K 轮,更早的直接丢弃。
def truncate(messages, keep_last_k=10, system_protected=1):
system = messages[:system_protected]
return system + messages[-keep_last_k:]
优点:零成本、零延迟。缺点:信息直接丢失。适用于:闲聊机器人(旧轮次价值低)、工具结果特别长的场景(旧结果早已被消化进后续回答)。
摘要压缩(compaction)
把旧历史用一次廉价 LLM 调用压成结构化摘要:
COMPACTION_PROMPT = """将以下对话历史压缩为一份供后续工作使用的上下文摘要,必须保留:
1. 用户的原始目标与所有明确约束
2. 已经做出的关键决定及其原因
3. 重要的文件路径、变量名、ID 等精确标识符(原样保留,不要意译)
4. 当前进行到哪一步、下一步计划
5. 未解决的问题与已知的失败尝试(避免重蹈覆辙)
对话历史:
{history}
"""
def compact(messages, llm):
old, recent = messages[:-4], messages[-4:] # 最近几轮原样保留
summary = llm.complete(COMPACTION_PROMPT.format(history=render(old)))
return [{"role": "system", "content": f"[历史摘要]\n{summary}"}] + recent
摘要压缩的质量关键在提示词,其中最容易被忽略的是第 3 条:摘要天然倾向意译,而 Agent 需要的是精确标识符——把"用户提到的那个配置文件"写成 config/app.yaml,否则后续工具调用直接失败。
触发时机有两个策略:阈值触发(token 数超过窗口的 70-80% 时压缩)和轮数触发(固定间隔)。阈值触发更经济,但要留出摘要调用的余量。
结构化笔记(structured note-taking)
比摘要更进一步:不压缩历史,而是让 Agent 持续维护一份工作笔记(scratchpad),历史本身可以丢弃。
NOTES_TOOL = {
"name": "update_notes",
"description": "更新你的持久工作笔记。关键决定、重要发现、当前状态都应该记入。",
"parameters": {"type": "object", "properties": {
"notes": {"type": "string", "description": "笔记的完整新版本(整体覆盖)"}}},
}
笔记是一个工具调用的结果——由模型自己决定什么值得记。每次压缩时,笔记作为最高优先级内容保留,历史可以激进丢弃。这解决了摘要的根本缺陷:摘要是一次性的有损压缩,而笔记是持续维护的、模型主动筛选的状态。
适用:长任务、多阶段任务。代价:每轮多一次工具调用的决策开销。
三种技术组合使用
system + 工具 schema 常驻
+ 工作笔记 常驻(结构化状态)
+ 最近 K 轮完整历史 保留细节
(更早的历史 → 已被摘要进笔记或丢弃)
3.3 渐进式披露(progressive disclosure)
不要把全部信息一次性放进上下文——先给索引,按需加载。
三个层次的典型实现:
- 文件:给 Agent 一个
search_files/read_file工具,而不是把文档全部塞进 system prompt; - 技能(skill):把"如何完成某类任务"的知识做成技能包,上下文里只放名字和一句话描述,模型判断需要时才读取全文(Claude Code 的 SKILL.md、AgentSkills 机制的核心思想);
- 工具结果:读大文件时先给行号目录,模型请求"读 100-200 行"再给内容——避免一次把 50k token 的日志倒进上下文。
判断标准:这份信息被本次任务用到的概率。> 80% 直接放上下文;20-80% 放索引(名字+描述);< 20% 只放获取手段(工具)。
3.4 长任务的上下文策略
长任务(数小时、数百轮)是上下文工程的极限场景,组合拳:
- 计划锚点:Plan-and-Execute 的计划文本作为常驻锚点,防止几百轮后目标漂移;
- 阶段摘要:每完成一个阶段,强制一次"阶段总结"(输出关键产出物的位置和结论),然后压缩掉该阶段的细节;
- 外部化状态:重要中间产物写文件/数据库(工具调用),上下文里只留引用——"结果已存 /tmp/result_3.json",需要时再读;
- 隔离脏活:把搜索、大量阅读类的子任务派给子智能体,只回传结论(第 7 章)。父上下文只见结论不见过程。
一个实用的度量:每轮决策的信息密度。如果你的 Agent 在第 80 轮还要重读第 10 轮读过的文件,说明上下文设计有缺陷——要么该信息该留在上下文,要么该进笔记,要么该外部化。
3.5 定位类失误与防御
上下文工程最常见的四类事故:
| 事故 | 根因 | 防御 |
|---|---|---|
| 摘要丢标识符 | 摘要 prompt 未要求保留原文 | 显式要求"精确标识符原样保留"(3.2) |
| 关键指令被历史稀释 | 20 轮前的约束被遗忘 | 约束进 system 或笔记,不依赖历史 |
| 工具结果撑爆窗口 | 单次返回 50k token | 工具侧分页/截断 + "读更多"工具(第 5 章) |
| 压缩后模型重复已做的工作 | 压缩把"已完成"的证据丢了 | 摘要必须含"已完成清单"(3.2 第 4 条) |
实现作业
给第 2 章的最小 Agent 加上上下文管理:
- 实现 token 计量(用 tokenizer 或字符数系数)与阈值触发的摘要压缩(保留最近 4 轮);
- 加一个
update_notes工具,实现"笔记常驻 + 历史激进压缩"的模式; - 用一个 30+ 步的长任务对比三种配置(无管理 / 纯截断 / 笔记+压缩)的任务完成质量与总 token 成本——你会看到成本下降数倍、完成率反升。
深入材料
- Anthropic, Effective context engineering for AI agents(2025,本章框架的主要来源)
- Lost in the Middle(arXiv 2307.03172,长上下文中部召回衰减的原始实验)
- Claude Code 的上下文机制公开拆解(compact 时机与保留策略)
读者留言
COMMENTS 暂无还没有留言,来说第一句?