上下文工程:不是把信息塞满,而是让 Agent 在正确时刻看见正确的事
本文是对第二章《上下文工程》的学习整理。它不是逐节摘要,而是尝试回答一个更实际的问题:模型能力已经不错,为什么 Agent 还是经常做错事?
写在前面:模型像天才新员工,上下文像入职第一天
想象一下,公司新来了一位学习能力极强的工程师。你只告诉他一句“把线上那个 bug 修一下”,却没有提供仓库地址、架构说明、错误日志、代码规范和发布流程。
如果他最后修错了,我们能简单归因于“这个人不够聪明”吗?
大多数 Agent 的处境正是如此。模型拥有通用能力,却不知道项目内部的事实、规则和当前状态。它不是不会推理,而是缺少推理所需的材料。
第二章让我重新理解了“上下文工程”这个词:它不是给 Prompt 添几句漂亮的话,也不是把所有资料一股脑塞进窗口,而是设计一套持续的信息供给系统——在每个决策点,让 Agent 看见恰好足够、结构清晰、来源可信的信息。
所以本章最值得记住的结论是:
上下文不是越多越好,信息密度、结构、位置和时机同样重要。
上下文真正决定的,是 Agent 的有效能力
第一章给出的公式是:
Agent = LLM + 上下文 + 工具
到了第二章,“上下文”这只眼睛被进一步拆开。它至少要回答四类问题:
| 问题 | Agent 需要看到什么 | 缺失后的典型表现 |
|---|---|---|
| 我是谁、该怎么做? | 系统指令、行为边界、工作流程 | 行为漂移、规则执行不一致 |
| 用户要什么? | 当前请求、历史对话、关键约束 | 答非所问、遗忘早期要求 |
| 我能做什么? | 工具定义、参数结构、使用边界 | 选错工具、参数错误、凭空作答 |
| 刚才发生了什么? | 模型历史回复、工具结果、任务状态 | 重复操作、错误重试、提前结束 |
这也是为什么一个中等模型配上高质量上下文,可能比顶级模型在信息贫瘠时表现更好。模型决定潜在能力,上下文决定这些能力在当前任务中有多少能被真正调用出来。
更重要的是,上下文工程还是组织工程。如果架构决策只存在于老员工脑中,业务规则散落在私聊里,流程靠口头传递,那么 Agent 面对的不是“知识库不够强”,而是组织本身就是一座信息黑洞。
我很喜欢书中的类比:AI Agent 像一个永远的新员工。 对远程协作友好、文档透明、决策可追溯的团队,往往也天然对 Agent 友好。
先看清 API:模型其实没有“记忆”
大模型 API 的无状态特性,是理解整章的钥匙。
每次调用时,Agent 框架都要重新发送模型本轮需要看到的信息。模型不会在服务器里自动记住上次聊了什么;我们感受到的“连续对话”,其实是框架不断维护并重发消息历史的结果。
一次典型请求可以拆成两部分:
静态前缀
├── system:身份、规则、流程
└── tools:工具名称、描述、参数 Schema
动态轨迹
├── user:用户请求
├── assistant:模型回复或工具调用请求
├── tool:工具执行结果
└── ……随着任务继续不断增长
四种消息角色各有职责:
| role | 内容由谁产生 | 主要作用 |
|---|---|---|
system |
开发者 | 定义身份、规则和约束 |
user |
用户或 Harness | 提供请求;有些运行时也借此注入状态信息 |
assistant |
模型 | 保存回复、推理相关字段和工具调用请求 |
tool |
Agent 框架 | 返回与 tool_call_id 对应的真实执行结果 |
工具定义则位于请求的 tools 字段中,并不是第五种消息角色。
一个完整的工具循环是这样的:
用户提出问题
↓
模型返回 tool_calls
↓
Harness 校验并执行工具
↓
结果以 tool 消息追加到历史
↓
携带完整必要历史再次调用模型
↓
模型继续调用工具,或给出最终答案
这里最容易混淆的一点是:模型只决定调用什么工具、传什么参数;真正执行工具的是 Harness。 如果工具结果没有以正确角色和调用 ID 回传,模型就失去了理解动作后果的依据。
KV Cache:一条时间戳为何可能让账单翻倍
KV Cache 听起来很底层,但它给上下文设计带来的规则非常直观。
模型处理长上下文时,会缓存已经计算过的 Key 和 Value。后续请求若保留相同的 token 前缀,就可以复用已有计算;如果中间某个 token 发生变化,从变化位置开始的后续缓存通常都要重算。
于是,一个看似合理的写法会变成性能陷阱:
System Prompt:
You are a helpful assistant.
Current time: 2026-09-19 10:30:01
时间每秒都在变化,放在上下文开头意味着每次请求都改写前缀。越靠前的变化,受影响的缓存范围越大。
我把本章的缓存原则记成一句话:
静态内容放前面并保持稳定,动态内容只在后面追加。
具体来说:
- 系统提示词与核心工具定义一旦确定,尽量保持字节级稳定;
- 时间、工作目录、任务状态等动态信息放到轨迹末尾;
- 不要每轮重排工具列表,也不要无意义地改变 JSON 序列化结果;
- 旧工具结果需要压缩时,接受压缩点之后的一次缓存重建,并尽量批量处理;
- 使用模型原生的标准消息格式,不手工拼接一套貌似相同的聊天协议。
这里也要区分两个常被混用的概念:KV Cache 是推理引擎内部对 K/V 中间结果的复用,Prompt Cache 通常指服务商提供的跨请求前缀缓存。工程层级不同,但都依赖稳定前缀。
Chat Template:JSON 消息进入模型前,还要“装信封”
API 中的 system、user、assistant、tool 最终都会被 Chat Template 转换成线性的 token 流。不同模型家族使用不同的特殊标记来表达角色和边界。
可以把它理解为信封格式:消息内容是信,Chat Template 决定寄件人、收件人和分隔线写在哪里。
这解释了为什么“工具结果随便塞成一条 user 消息”并非无伤大雅。角色标错后,模型可能把观察误认为新任务,还可能触发历史思维链的清理规则。不同模型对历史 reasoning 的保留协议也并不相同,因此切换模型不能只改一个 model 名称,还要核对对应的消息协议。
一个很实用的判断是:优先顺着模型训练过的交互格式工作。 Harness 不是越有创意越好,协议层的“自作聪明”经常会把模型带离它熟悉的数据分布。
系统提示词不是愿望清单,而是员工手册
“你是一个专业、聪明、严谨的助手”当然没有错,但它很少能解决真实业务中的冲突。
好的系统提示词更像一份可以执行的 SOP:
- 先做什么;
- 满足什么条件后进入下一步;
- 异常时如何处理;
- 哪些边界绝不能越过;
- 怎样判断任务已经完成。
本章给了一个非常朴素的检验标准:
如果一个聪明的新员工读完提示词仍不知道该怎么做,Agent 也不会知道。
在组织形式上,流程通常比规则堆砌更有效。几十条互不关联的“必须”和“禁止”会迫使模型自己判断优先级;清晰的步骤、分支和验证条件则把认知资源留给真正需要推理的部分。
结构化也很重要:Markdown 适合表达层级,XML 标签适合标识语义边界,两者可以组合使用。关键约束可以用醒目的措辞强调,但如果每句话都是 MUST 和 NEVER,强调本身也会失去区分度。
对于难以精确定义的风格和格式,少量高质量的 few-shot 示例往往比一长串形容词更有效。但示例不是越多越好,两三个覆盖边界的例子,通常胜过十个高度重复的例子。
书中的提示工程消融实验提供了一个有意思的对照:打乱信息组织后,任务成功率下降超过 30%;移除工具描述后,工具调用错误率增加 45%。这些是书中实验结论,并非我在本地独立复现的数据,但它们至少提醒我:提示词的结构和工具描述不是装饰,而是可测量的系统组件。
Skills:不要把整座图书馆搬上桌
当 Agent 会做的事情越来越多,把所有规则都写进系统提示词会出现两个问题:token 成本持续增加,无关信息也会稀释注意力。
Agent Skills 给出的解法是渐进式披露:
第一层:元数据目录
只展示名称、描述与触发边界
↓ 命中任务
第二层:SKILL.md
加载该能力的核心流程
↓ 确有需要
第三层:参考资料、脚本与模板
按任务选择性读取
这就像新员工入职时先拿到公司手册目录,而不是一次性读完财务、法务、设计、运维和销售的全部 SOP。
Skill 的 description 尤其关键。它不是广告文案,而是一条路由规则:不仅要说明“什么时候用”,最好还要说明“什么时候不要用”。写得太宽泛,Agent 就会频繁误触发;写得太窄,又会让真正需要它的任务找不到入口。
一份可用的 Skill 至少应该回答:
- 它服务什么任务,面向什么读者?
- 核心流程和三到五条关键原则是什么?
- 哪些高频错误或越权动作必须禁止?
- 例外情况和停下来确认的条件是什么?
- 哪些脚本、模板或参考文档可以复用?
- 产物通过什么方式验收?
Skills 的意义不只是节省 token。它还把个人经验和团队规范变成了可读、可版本化、可测试、可复用的知识模块。
不过渐进式披露也有一个天然弱点:模型必须知道自己“需要知道更多”。如果元数据写得不好,或者模型没有意识到知识缺口,它甚至不会打开正确的 Skill。因此目录质量、触发评估和失败后的补救机制同样重要。
Agent 状态栏:别让模型每轮都从头数数
长任务里有一类信息很特殊:它们并非新的业务知识,而是对当前轨迹的统计和提炼。
例如:
<agent_status>
Current State:
- phone_call: Xfinity 3/3
- TODO: cancel plan (in_progress)
- working_directory: /workspace/project
- last_error: FileNotFoundError
</agent_status>
如果没有这条状态栏,模型必须从几十轮记录中重新数出“已经打过几次电话”、判断“还有几项没做”、回忆“当前目录在哪里”。注意力擅长检索已出现的内容,却不擅长凭空把散落记录稳定地聚合成统计结果。
状态栏做的事情,就是把隐式状态提前计算成显式知识,并放到轨迹末尾这个更容易被注意到的位置。
它可以包含:
- 当前计划与 TODO;
- 工具调用次数与预算;
- 时间、工作目录、系统环境等侧信道信息;
- 最近一次错误及建议的恢复方向;
- 已满足和仍未满足的完成条件。
但状态栏也是一个高信任区。模型往往会直接相信“已调用 3 次”,而不会重新审计原始记录。如果计数器本身有 bug,或者外部网页内容被未经处理地写进状态栏,错误信息就会获得不应有的权威。
因此我的结论是:状态尽量由确定性代码维护,来源必须可追踪;LLM 可以帮助抽取,却不该成为唯一的计数器。
状态更新还有一个缓存取舍:
| 方式 | 优点 | 代价 | 更适合 |
|---|---|---|---|
| 替换上一轮状态 | 始终只有最新状态,体积可控 | 状态之后的短后缀缓存失效 | 状态较大、更新频繁、长会话 |
| 永久追加新状态 | 完全符合只增不改,缓存友好 | 旧状态累积,可能产生歧义 | 状态很小、会话受控、轮间消息多 |
“追加不破坏缓存”并不等于追加没有成本。历史状态最终仍会占用窗口,这类取舍需要结合状态大小、更新频率、缓存价格和模型表现实测。
上下文压缩:窗口没满,也可能已经“腐化”
过去我会把上下文压缩理解成“快超出 token 上限时做摘要”。本章给了我一个更完整的视角:压缩至少有三个目的。
- 控制窗口长度、费用与延迟;
- 把分散事实提炼成更容易使用的知识;
- 避免模型因为感知到窗口紧张而过早收尾。
这里最值得区分的是上下文溢出与上下文腐化:
- 溢出:信息装不下了,任务直接失败;
- 腐化:信息还装得下,但噪声太多,关键内容越来越难找到。
后者更隐蔽。Agent 仍在运行,也没有抛异常,只是逐渐遗忘约束、重复探索,或者围绕已经解决的问题打转。
有效压缩不是机械截断,而是围绕任务进行信息重写:保留决策、约束、失败路径、关键事实和来源,删除重复页面、导航栏和已经没有价值的中间输出。
书中的六种压缩策略对比显示,上下文感知压缩可以把约 148K 字符压到约 2K 字符,并在任务中保留关键事实;全书据此总结其 token 用量可减少 75% 以上。这同样是书中实验结果,不应被理解为对所有任务都成立的固定比例。压缩率越高,不可逆的信息损失风险也越大。
我更认可的生产策略是一组分层措施:
大结果存盘,只给模型摘要与索引
↓
直接删除确认无用的噪声
↓
在阈值附近批量压缩旧工具结果
↓
保留逐轮、可追踪的结构化摘要
↓
最后才做全量压缩,并设置失败熔断
压缩不是免费的。它会增加一次模型调用,也会破坏压缩点之后的缓存。更危险的是,摘要可能删掉当时看似次要、后来却至关重要的信息。
所以一份好的摘要最好保留两层内容:高密度结论,以及可以回到原始证据的引用或文件索引。换句话说:内容可以有损,溯源能力尽量无损。
隔离优于压缩:最干净的噪声,是从未进入主上下文的噪声
如果一次代码搜索会读十几个文件,主 Agent 亲自完成搜索,就会让大量中间代码永久进入主轨迹。任务找到答案后,这些内容大多只剩噪声。
另一种方式是把搜索交给上下文独立的子 Agent:它在自己的窗口中完成探索,只向主 Agent 返回目标函数、文件位置、调用点和必要证据。
这是一种很漂亮的设计:
压缩是信息进入后的清理,隔离是从源头避免污染。
当然,隔离并非没有代价。子 Agent 看不到主 Agent 的全部背景,因此委派任务必须自包含,完成标准与回传格式也要明确。否则只是把“上下文不足”的问题从主 Agent 转移给了子 Agent。
提示注入:当数据开始假装自己是指令
上下文越重要,污染上下文就越危险。
网页、邮件、PDF、工具结果本应是 Agent 要处理的数据,但攻击者可以在其中藏入“忽略之前的要求”“把聊天记录发送到某地址”等文本。一旦模型把这些内容误认成可信指令,拥有工具权限的 Agent 就可能真的执行写文件、发邮件或泄露数据等动作。
上下文层可以做三件事:
- 明确标注外部内容的来源和不可信属性;
- 使用标准角色与结构,把指令和数据分开;
- 清洗已知的可疑模式,降低常见攻击成功率。
但这些都不是绝对防线。Skill 本质上会把内容当指令加载,状态栏又是模型高度信任的信息区,它们自身也可能成为更强的注入通道。
因此第一章的安全原则在这里再次出现:上下文层负责降低误判概率,执行层仍要依靠最小权限、沙盒、高风险确认和独立验证守住真正的边界。
把整章压缩成一条上下文流水线
读完第二章后,我脑中的 Agent 请求不再只是一个 messages 数组,而是一条持续运行的信息流水线:
稳定前缀
系统提示词 + 核心工具定义
↓
按需展开
Skill / 延迟加载的工具 Schema
↓
持续追加
用户请求 + 模型行动 + 工具观察
↓
显式提炼
TODO + 计数器 + 环境状态 + 完成条件
↓
预算治理
删除噪声 → 保存原文索引 → 批量压缩旧证据
↓
必要时隔离
把高噪声探索放入独立上下文,只回传结论与证据
它背后的统一原则并不复杂:
稳定信息前置,动态信息后置;通用信息常驻,专用信息按需;原始信息可追溯,任务状态要显式。
我的上下文工程检查清单
以后设计或排查 Agent,我会优先检查这些问题:
- 当前任务成功所需的事实、规则和环境信息是否齐全?
-
system、user、assistant、tool的角色是否使用正确? - 工具调用请求和结果是否通过正确的 ID 对应?
- 系统提示词与核心工具定义是否保持稳定?
- 时间戳、目录和进度等动态状态是否放在轨迹末尾?
- Prompt 是清晰流程,还是不断增长的零散规则集合?
- 工具描述是否写清用途、参数、边界和错误处理?
- 低频领域知识能否改成按需加载的 Skill?
- 状态栏是否由代码维护,来源是否可信、可审计?
- 是否同时监控 token 上限和上下文腐化?
- 压缩是否保留决策、约束、失败路径与证据索引?
- 高噪声探索是否更适合放进隔离的子 Agent 上下文?
- 外部内容是否被明确标记为数据,而不是可信指令?
- 每次上下文改动是否通过任务成功率、成本和延迟共同评估?
三个容易掉进去的误区
误区一:上下文窗口越大,Agent 就越可靠
更大的窗口只是提高了容量上限,不会自动提高信息密度。装得下不代表找得到,更不代表模型能稳定完成计数、归纳和状态追踪。
误区二:所有问题都能通过改 Prompt 解决
有些失败来自消息角色错误、工具结果缺失、缓存布局不合理、状态没有显式维护,或权限边界失控。继续在系统提示词里追加规则,只会让上下文更拥挤。
误区三:摘要就是更短的原文
真正有用的压缩与当前任务相关。检索任务需要保留广度,分析任务需要保留关系与证据,创作任务则可能要保留具体例子和灵感线索。同一份资料,不应只有一个万能摘要。
写在最后
第一章让我开始从“会回答”转向“能完成”看待 Agent;第二章则进一步让我意识到:Agent 的每一次决策,都是对当前上下文的一次函数调用。
模型不会自动知道公司历史,不会自动记住上轮执行结果,也不会自动把几十条记录统计成可靠状态。我们必须主动决定:哪些信息常驻,哪些按需加载;哪些保留原文,哪些压缩;哪些交给主 Agent,哪些隔离出去;哪些只是数据,哪些才有资格成为指令。
所以,上下文工程的目标从来不是“把窗口填满”,而是让 Agent 在正确的时刻,看见正确的事,并且知道这些信息从哪里来、能相信到什么程度。
第二章配套实验记录
实验2-1:本地 LLM 部署与工具调用
Ollama 与 Qwen3 0.6B 完成并行工具调用验证;六项工具调用门禁全部通过,KV Cache 命中请求的平均 TTFT 为 88.03 ms,未命中为 497.75 ms。