上下文比作 Agent 的“眼睛”——Agent 只能基于它看到的信息做决策。上下文的设计和管理——即上下文工程Context Engineering。AI会看见什么。上下文决定 Agent 能力上限的关键OpenAI 研究员翁家翌曾精辟地总结这个观点“人和模型一样最重要的是 Context。”他以自身经历举例——“自己在 OpenAI 的工作也没有那么难如果换一个其他人如果有他所有的 context也是能干的。”同样的道理适用于 Agent决定 Agent 能力上限的不是模型参数量而是它在每个决策点能获得多少、多精准的上下文。翁家翌还指出“团队合作中最大的问题也是 context 的不一致”而“AI 短时间内无法取代人的最大原因也是 context——因为 AI 跟人并不在同一个环境里面”。这恰恰是上下文工程要解决的核心问题如何把 Agent 需要的背景信息系统性地、结构化地送到模型面前。Agent 如何调用大模型理解 API 的上下文结构消息的四种角色¶大模型 API 的核心是一个消息列表messages列表中的每条消息都有一个角色role标识模型根据角色来理解每条消息的含义和来源system系统提示词。由开发者编写定义 Agent 的身份、行为规则、约束条件。模型将其视为最高优先级的指令。整个对话过程中通常只有一条放在消息列表的最前面。user用户消息。来自终端用户的输入是 Agent 需要响应的请求。assistant助手消息。模型之前的回复包括文本回复和工具调用请求。在多轮对话中之前的 assistant 消息会被放回消息列表让模型“记住”自己说过什么。tool工具结果。Agent 框架执行工具后将结果以 tool 角色的消息送回给模型。每条 tool 消息通过tool_call_id与对应的工具调用请求关联。带工具调用的多轮交互Agent 的核心循环真正的 Agent 场景远比单轮问答复杂。当用户问 “Whats the current time and weather in Vancouver?” 时模型无法凭自身知识回答它不知道“现在”是什么时候需要调用外部工具。下面完整展示这个过程中 Agent 框架与模型之间的每一步交互。第一次 API 调用——Agent 框架发送初始请求// ═══ Request constructed by the Agent framework (1st call) ═══ { model: Qwen3-0.6B, messages: [ { role: system, // ← Written by developer content: You are a helpful assistant. Use the provided tools to get real-time information when needed. }, { role: user, // ← User input content: Whats the current time and weather in Vancouver? } ], tools: [ // ← Tools defined by developer { type: function, function: { name: get_current_time, description: Get the current date and time in a specific timezone, parameters: { type: object, properties: { timezone: { type: string, description: Timezone name, e.g. America/Vancouver } } } } }, { type: function, function: { name: get_weather, description: Get the current weather for a specific city, parameters: { type: object, properties: { city: { type: string, description: City name }, unit: { type: string, enum: [celsius, fahrenheit] } } } } } ] }模型返回工具调用请求不是最终回复Agent 框架执行工具然后发起第二次 API 调用// ═══ Request constructed by the Agent framework (2nd call) ═══ { model: Qwen3-0.6B, messages: [ { role: system, // ← Same as 1st call content: You are a helpful assistant. Use the provided tools to get real-time information when needed. }, { role: user, // ← Same as 1st call content: Whats the current time and weather in Vancouver? }, { role: assistant, // ← Model output from 1st call, included verbatim content: null, tool_calls: [ { id: call_abc123, function: { name: get_current_time, arguments: {\timezone\: \America/Vancouver\} } }, { id: call_def456, function: { name: get_weather, arguments: {\city\: \Vancouver\, \unit\: \celsius\} } } ] }, { role: tool, // ← Generated by Agent framework (tool execution result) tool_call_id: call_abc123, content: {\timezone\: \America/Vancouver\, \datetime\: \2025-09-13T05:18:47\, \day_of_week\: \Saturday\} }, { role: tool, // ← Generated by Agent framework (tool execution result) tool_call_id: call_def456, content: {\city\: \Vancouver\, \temperature\: 13.2, \unit\: \celsius\, \conditions\: \clear\, \humidity\: 93} } ], tools: [ ... ] // ← Same tool definitions as above, omitted }从 API 视角看上下文的构成通过上面的例子我们可以清晰地看到 Agent 每次调用模型时上下文的完整构成上半部分System Prompt Tool Definitions在整个对话过程中保持不变下半部分对话历史即第一章所定义的轨迹随着交互的进行不断增长。KV Cache 友好的上下文设计在进入故事之前先把KV Cache的直觉建立起来。模型每生成一个 token都要回头看一遍前文所有 token 的中间计算结果。如果每轮都从头算一次开销会随上下文长度爆炸式增长。KV Cache 的做法是把前文的中间计算结果缓存下来下一轮只需要计算新增 token 的部分。前提是前缀完全不变——只要前缀里有一个字符被改写缓存就全部作废模型不得不从被修改的位置重算。顺带说明本节讲到跨请求的“缓存命中”时在 API 服务商的语境下叫 Prompt Cache——它是构建在推理引擎 KV Cache 之上的跨请求缓存两个层级的完整辨析见本节末尾。系统提示词和工具定义一旦确定就不要改。任何改动哪怕多一个空格都会导致缓存全部失效延迟成倍增加、成本上升具体幅度视模型与配置而定。动态信息永远追加到末尾——时间戳、用户状态等变化的内容作为新消息追加到对话末尾而不是修改已有的系统提示词。使用标准 API 格式不要自行拼接消息结构化消息会被 Chat Template 翻译成模型训练时见过的固定 token 序列自行用字符串拼成USER: ... ASSISTANT: ...的根本问题是偏离了这种训练格式会削弱模型的多步思考能力。至于缓存——它只认 token 字节序列只要拼出的前缀字节级稳定照样能命中但若拼接方式不稳定如每次向前缀注入动态内容缓存也会随之失效。从 API 消息到模型 TokenChat TemplateChat Template 是一块贯穿全书的地基它不只关系到 KV Cache还决定了多轮工具调用、思维链保留、状态栏注入等诸多机制能否正确工作因此值得单独讲清楚。注意力可视化实验中的 token 序列如|im_start|、|im_end|等特殊标记看起来与前面 API 的 JSON 格式很不一样。这是因为 API 层面的结构化消息需要被转换为模型能理解的线性 token 流——负责这个转换的就是Chat Template聊天模板。KV Cache 的原理与约束要理解 KV Cache 的价值先看看没有它时会发生什么。假设一个 Agent 在进行第 6 轮对话上下文已经累积了 2000 个 token。在没有缓存的情况下模型每生成一个新 token都需要重新计算这 2000 个 token 的 K、V 向量——相当于重跑整个前缀的前向计算。尽管前 5 轮的内容完全没变第 6 轮仍要像第 1 轮那样从头计算整个前缀而且此时前缀更长代价比第 1 轮大得多。无缓存时prefill 阶段即模型正式生成回复之前一次性处理输入端全部 token 的阶段的注意力计算量随上下文长度平方级增长随着对话深入延迟和成本都会急剧攀升。这对于需要几十轮工具调用的 Agent 任务来说是不可接受的。实验 2-3 ★★常见的错误上下文管理模式在kv-cache实验中我们系统性地测试了几种常见但有害的上下文管理模式。这些模式不仅会破坏 KV Cache 的有效性有些甚至会影响 Agent 的核心能力。动态系统提示词是最常见的错误之一。一些开发者为了让 Agent“知道”当前时间会在系统提示词中嵌入时间戳如 “Current time: 2025-09-14 10:30:45.123456”。这种做法看似提供了有用的上下文信息但每次请求时时间戳都会变化导致整个系统提示词不同从而使 KV Cache 完全失效。正确的做法是将时间信息作为用户消息的一部分追加到对话末尾或者只在真正需要时通过工具调用来获取。动态用户配置模式试图在每次请求中更新用户的状态信息如剩余的 API 调用次数或账户余额将这些信息嵌入上下文中会破坏缓存。更好的方案是在需要时通过专门的状态管理机制来处理。工具定义的动态排序是另一个隐蔽的陷阱。有些系统会根据使用频率动态调整工具的顺序但工具定义通常占据上下文很大的一部分每个工具可能包含数百个 token 的描述和参数说明改变顺序就会导致整个缓存失效。实验表明保持固定的顺序对模型选择工具的能力几乎没有影响但对性能提升却是显著的。滑动窗口Sliding Window对话历史通过只保留最近几条消息来控制上下文长度。举个例子如果窗口大小设为 10 条消息那么第 11 条消息进来时最早的一条就会被丢弃。这种做法存在两个严重的问题。第一它会破坏上下文的前缀一致性导致 KV Cache 失效。第二它可能丢失关键的工具调用结果。举例滑动窗口大小为 10 轮时Agent 在第 2 轮调用了文件读取工具拿到关键内容到第 15 轮还需要回引这段内容——但此时窗口已滑出原始结果模型只能依赖被截断的对话尝试推断错误率显著上升。在实验中使用滑动窗口的 Agent 经常陷入循环反复执行相同的工具调用因为它“忘记”了之前已经获得的结果。文本格式化方法是最具破坏性的模式之一。它把结构化的 role-content 消息转换为 “USER: ... ASSISTANT: ...” 这样的纯文本流。需要说明的是问题的关键并不在缓存——缓存作用于 token 字节序列只要拼接出的前缀字节级稳定照样能命中只有当拼接方式不稳定如每次向前缀注入动态内容时才会破坏缓存。真正的破坏在于文本格式化偏离了模型训练时使用的标准消息格式——模型在训练阶段接受了大量基于角色的对话数据已经学会解析这种结构化格式。当消息被转为纯文本时模型需要额外消耗注意力资源来推断角色的边界和对话的结构从而产生各种问题重复执行已完成的操作、忽略工具调用结果、在应该调用工具时却生成文本响应、格式解析错误等。KV Cache 与 Prompt Cache两个层级的缓存KV Cache是模型内部的优化——在一次推理过程中缓存已计算的 token 的键值对避免重复计算。Prompt Cache则是 API 服务层的优化——跨多次 API 请求之间缓存相同前缀的计算结果。两者的优化原理相似都利用前缀不变性但作用层级不同KV Cache 加速单次请求内的 token 生成Prompt Cache 减少跨请求的重复计算成本。Prompt Cache 的工作方式是API 服务商对请求的前缀进行匹配如果多次请求的前缀相同比如系统提示词和工具定义不变就直接复用之前计算好的 KV Cache而不需要重新计算这部分 token 的键值对。缓存作为架构约束Claude Code 的实践揭示了一个深层的模式当 Prompt Cache 的经济效益足够显著时缓存一致性会反过来主导系统的架构选择。以下是几个体现这种约束的设计决策提示词的结构由缓存边界决定。子 Agent 必须与父 Agent 字节级对齐。工具结果的替换字符串在首次出现时就被冻结。KV Cache 未必是一次性的可编辑、可组合的“笔记本节到此为止都建立在一条铁律上前缀里改一个字节后面的缓存就全废。这条铁律在今天的推理引擎里确实成立但笔者想指出它未必是必然的。松动它的出发点是一个反直觉的观察2在 prefill 阶段模型其实在“做笔记”。当它读到上下文里的某个字段比如“用户所在城市北京”时并不是把这个字段原封不动地缓存下来而是顺手把“这个字段意味着什么”的结论写进了后面每一层的 KV 状态里。测量发现一个字段自己那几个 token 的 KV对最终决策的贡献往往不到 1%——真正影响输出的是它在下游留下的那些“读书笔记”。对 Agent 而言这一点的意义在于那个被反复重建的长上下文——换一批工具、更新一个记忆字段、注入一条新状态正是下一节状态栏要做的事——也许不必每轮都推倒重来。它指向一种“上下文可变、但缓存收益还在”的可能把上下文的组装从 O(L²) 的重算变成 O(L) 的“笔记拼接”。这仍属研究阶段本节前面的三条实践结论在当前生产系统中依然是应当遵守的默认原则。理解了缓存机制后接下来的问题自然变成既然我们知道了上下文是怎么被处理和缓存的那该如何设计送进去的内容本身接下来几节围绕“上下文里到底放什么、怎么组织”展开可以分为三条相对独立的线索提示工程、提示注入与动态提示词Agent Skills系统提示词该怎么写、写什么——这是上下文工程最直接的部分工具定义与系统提示词并列的另一个静态组成部分的设计也直接影响 Agent 的工具使用准确性本章给出核心原则第四章将详细展开。紧随其后的是安全问题——提示注入当外部内容试图劫持精心设计的上下文时如何在上下文层面构筑防御。而当提示词越写越长、覆盖的场景越来越多时把所有内容塞进一个系统提示词就不再可行了既浪费 token也会导致注意力被稀释于是自然演化出 Agent Skills 的渐进式披露机制——按需加载而非一次性塞满。Agent 状态栏Agent Status Bar一种独立的机制通过在上下文末尾注入动态的元信息任务进度、环境状态、工具调用计数等弥补模型无法主动归纳隐式状态的不足。就像手机屏幕顶部始终显示时间、电量、网络信号一样Agent 状态栏让模型随时能“瞥一眼”就知道当前的运行状态。上下文压缩策略解决上下文不断膨胀的问题——什么时候压缩、怎么压缩、压缩如何与 KV Cache 共存。动态提示词与 Agent Skills随着 Agent 覆盖的业务场景越来越多系统提示词会不断膨胀——客服场景的退款规则、编程场景的代码规范、文档场景的格式要求……全部塞进一个提示词会带来两个问题浪费 token大部分内容与当前任务无关注意力被稀释上下文中无关信息过多会稀释模型对关键内容的注意力这一问题将在后文上下文压缩策略部分以“上下文腐化”的概念详细讨论这就是从静态提示工程到动态提示词的自然演进不是把所有知识一次性塞给 Agent而是让它按需加载。Agent Skills 系统正是这一理念的工程化实现。Skills领域能力的可组合单元Agent Skills 的核心思想是将 Agent 的能力模块化为独立的、可按需加载的知识包6。每个 Skill 本质上是一套包含专业领域指导的提示词集合就像为新员工准备的某个专项任务的操作手册。与传统的将所有指令塞入单一系统提示词的做法不同Skills 采用了渐进式披露Progressive Disclosure的设计哲学——先给 Agent 看一份目录摘要需要时再加载完整内容就像你不会把公司所有部门的操作手册都堆到新员工桌上而是先给一份总目录需要哪本再去取。第一层元数据每个 Skill 必须包含一个SKILL.md文件开头是 YAML frontmatter即文件顶部用---分隔的元数据块类似书籍的版权页包含name和description两个字段。Agent 框架在启动时扫描所有已安装的 Skill将它们的name和description仅占数百个 token注入到对话上下文中注入位置的设计权衡见下一小节使 Agent 在不消耗大量上下文的前提下知晓自己拥有哪些专业能力。元数据中的description字段是路由决策的关键——它应当足够短控制常驻的 token 量但写法要像路由条件而非功能介绍。最直接的写法是 “Use when / Dont use when” 加上几条反例即明确列出“不该触发此 Skill”的场景。实践中缺少反例的 Skill 描述会让路由准确率明显下降——宽泛的描述会在不相关的任务上频繁误触发补上反例后路由准确率会显著回升。反例不是可选项而是 Skill 路由能否准确触发的关键。描述太宽泛如 “help with backend”等于任何后端相关的工作都能触发路由就会失准真正有效的描述是路由条件——“何时该用我”比“我能做什么”重要得多。第二层核心流程当 Agent 判断某个任务需要特定的 Skill 时通过专用的 Skill 工具加载完整的SKILL.md内容作为 tool result 出现在对话历史中。以 PPTX Skill7 为例其中包含处理 PowerPoint 文件的核心流程如何通过 markitdownMicrosoft 开源的文档转 Markdown 工具提取文本如何解压 PPTX 文件访问原始的 XML 结构以及关键文件的路径约定。第三层细则通过文件引用深入到更详细的子文档。主文件引用了html2pptx.md通过 HTML 模板创建 PowerPoint 的详细工作流、reference.md格式技术细节等。Agent 会根据具体的需求选择性地深入阅读相关的子文档。Skills 的实现方式与权衡理解了 Skills 是什么之后接下来是一个更具体的工程问题Skill 内容放在上下文的什么位置这是一个根本性的设计决策直接关系到 KV Cache 效率和模型的指令遵循效果。理论上有两种朴素方案但都存在明显的代价生产实现如 Claude Code采用的是一种回避了两者痛点的第三种方案。方式一注入系统提示词system 消息。将 Skill 内容直接追加到 system prompt 中。模型对 system 位置的指令遵循能力最强因为训练时大量使用了这个位置的指令所以 Skill 的执行效果最好。但问题在于每次加载新的 Skill 都会改变 system 消息的内容导致 KV Cache 前缀失效。如果 Agent 频繁切换 Skill比如一个任务需要先用搜索 Skill再用文档 Skill缓存会反复失效延迟和成本显著增加。方式二作为普通文件读取内容出现在上下文中间。Agent 通过通用文件读取工具读取 Skill 文件文件内容作为 tool result 出现在对话历史中——也就是上下文的中间位置。这种方式完全不影响 KV Cachesystem prompt 不变但对模型的指令遵循instruction following能力提出了更高要求模型需要在长上下文的中间位置准确识别并遵循 Skill 中的指令而不是把它当作普通的工具输出来“参考”。实践中不同模型对这种模式的支持差异很大——Claude 因为在训练中大量使用了中间位置的指令遵循数据表现最为可靠而其他模型在遵循上下文中间注入的指令时往往会打折扣。方式三生产实现元数据作为动态上下文提供完整内容通过专用工具按需加载。Claude Code 的核心思路是将 Skill 的“路由”和“执行”分离模型首先获得可用 Skill 的元数据用于判断当前任务是否需要某个 Skill只有在 Skill 被选中后才进一步加载完整的SKILL.md。这种设计兼顾了上下文开销、Prompt Cache 复用和指令遵循能力。Agent 状态栏通过元信息增强 Agent 轨迹管理上下文末尾的 user-role meta 消息”是一条通用的元信息注入通道——Skill 元数据列表只是它的一个使用场景。本节将系统地展开这一通道它是 Agent 框架向模型同步各种动态状态的统一机制称为Agent 状态栏Agent Status Bar。前面讨论的提示工程解决了“给模型什么样的静态指令”的问题。但在实际执行过程中Agent 还需要动态地感知自身的状态和任务的进展——这就是 Agent 状态栏的用武之地。Agent 状态栏的理论基础Agent 状态栏之所以有效源于注意力机制的一个本质特性上下文学习更像检索而非推理——模型擅长从已有内容中查找信息但不擅长主动归纳和总结这里说的是模型在单次前向传播中如何消费已经在上下文里的信息并不否定模型可以通过生成思维链来完成多步思考。一个更形象的说法是上下文窗口是一台只有一半的检索引擎。它“检索”的这一半非常强——你问什么注意力就能从成千上万个 token 里把相关的原始记录捞出来相当于把检索增强生成RAG内置进了每一次前向传播。但它缺了另一半没有“提炼层”。上下文里的东西从来不会被自动数一遍、建个索引、或就地总结成一条结论任何“关于这些内容的结论”——一共多少条、有没有超标、进展到哪一步——模型每次要用都得从原始记录里现算一遍。而“现算一遍”的代价会随上下文里堆积的内容量记作 N一起往上涨。Agent 状态栏的构成基于上述的理论基础Agent 状态栏包括以下几种类型的信息任务规划当 Agent 处理复杂的多步骤任务时轨迹会变得很长。Agent 容易过分关注当前的局部子任务而忘记用户的原始诉求、核心约束以及后续工作。通过引入 TODO 列表将任务分解为清晰的步骤放在轨迹末尾不断提醒模型当前的进展和未来的目标确保行动与总体规划保持一致。事件的侧信道信息Side-channel Information为每个事件附加元数据——精确的时间、地理位置、距上次 Agent 回复的时间间隔等。侧信道信息是指不在主要数据通道中传递、但对理解事件很有帮助的辅助信息。这些信息帮助模型理解事件的时序关系和环境背景从而做出更符合情境的决策。环境的当前状态包括动态的环境信息系统时间、工作目录等、异常操作提醒“该工具已被重复调用 N 次”、以及从隐式状态到显式状态的转换。这一设计原则同样适用于人类界面——命令行CLI和图形界面GUI都致力于让用户清晰地感知系统的当前状态。可用能力清单当 Agent 框架支持插件化的能力扩展如上一节的 Skills 系统时所有已安装 Skill 的元数据列表也走这条同一的末尾注入通道相当于告诉模型“你现在拥有哪些可调用的专业能力”。它变化频率最低仅在用户安装/卸载 Skill 时才变其增量发送机制已在上一节 Skills 中详述此处不再重复。侧信道信息和可用能力清单一经添加就不再改变对 KV Cache 很友好因为不会破坏已缓存的前缀。而任务规划和环境状态是动态变化的需要以特殊的用户消息追加到上下文末尾并随任务推进不断更新——更新方式的选择直接关系到 KV Cache 的代价下面结合具体的消息结构展开讨论。Agent 状态栏在上下文中的具体位置一个重要的实现细节是Agent 状态栏在 API 层面实际上是作为一条 user 角色的消息插入到上下文末尾的——而不是修改开头的 system 消息。原因正是前面讨论的 KV Cache 约束修改 system 消息会破坏整个前缀的缓存。这里需要澄清一个容易混淆的地方这里的 user 角色只是 API 协议层面的技术选择并不等同于第一章定义的“来自终端用户的输入”。换句话说Harness 是在借用 user 角色这个消息槽位向模型注入由 Agent 框架自动生成的系统状态信息——内容并非来自真实用户只是复用了 user 角色的消息格式来挂到上下文末尾。状态更新的两种实现与缓存代价“追加不破坏缓存”只在单次注入时成立。状态是会变的——下一轮 TODO 完成了一项、工具计数加了一次状态消息就过时了。如何更新它存在两种实现各有明确的缓存代价实现一每轮替换。每次 API 调用前从消息列表中移除上一轮的状态消息在末尾追加最新状态。这保证了上下文中只有一份状态、永远是最新的。但代价是移除旧状态会使其位置之后的所有缓存失效——这与本章批评的“动态时间戳”是同一个失效机制区别只在于状态消息位于上下文末尾失效范围仅限于最近几轮消息而不是整个前缀。实现二持久追加。状态消息一旦注入就永久留在轨迹中每轮只在末尾追加新的状态。Claude Code 的system-reminder采用的就是这种方式——历史状态消息保留在会话记录transcript中从不删改。这种方式对缓存完全友好所有消息只追加、不修改前缀始终稳定。代价是陈旧的状态会在上下文中累积——既占用 token也要求模型自己关注“最新一条”状态而忽略已过时的旧状态。取舍的经验法则是状态更新频繁且轨迹很长时选择实现二——每轮替换带来的缓存失效会在长轨迹上反复累积代价远超陈旧状态占用的 token轨迹较短或单条状态消息很大如完整的 TODO 列表加环境快照时选择实现一——末尾几轮的缓存失效本来就便宜换来的是上下文的整洁和无歧义。实验 2-8 ★★几种好用的 Agent 状态栏技术agent-status-bar实验框架实现了五种状态栏技术每种都可以独立启用或禁用时间戳跟踪工具调用计数器TODO 列表管理详细错误信息系统状态感知从读数到策略Agent 的物理时间感知实验 2-8 的五种技术里时间戳跟踪和工具调用计数器看起来是两条互不相干的元信息但把它们放在一起看会发现二者指向同一种更本质的能力——让 Agent感知物理时间并据此调节自己做事的节奏。一个人被要求“三分钟写一段话”和“三十分钟写一段话”交出来的东西是不一样的可当下的前沿 Agent无论你说三分钟还是三十分钟产出几乎没有区别。它既说不清一件事到底做完了没有也分不清眼前这堵墙是真的走不通、还是稍等一下就好更察觉不到一个已经跑了三分钟的工具调用是仍在推进、还是早就卡死了。笔者和合作者把这种缺失的能力称为时间感time sense并把它拆成三个可以分别度量的轴10紧迫度urgency坚持度persistence警觉度vigilance这套三轴框架直接落在状态栏上时间戳跟踪供的是紧迫度和警觉度的读数工具调用计数器供的是坚持度的读数。但这里有一个容易踩空、也最值得记住的发现光把读数摆到模型面前并不足以改变它的行为。在一个专门测量时间感的基准上同一批任务被放在四种条件下运行什么都不给、只给原始时间戳、给时间戳外加一份“这些读数该怎么用”的操作手册、以及让 Agent 自己上报节奏状态。结果相当反直觉只给原始时间戳这一档和什么都不给几乎没有区别前后相差不过两三个百分点真正把通过率从一成出头拉到四五成的幅度 19 到 49 个百分点是那份操作手册。换句话说把elapsed_ms5000 expected_ms500这行读数放进上下文模型确实“看见”了却不会自动据此改变干活的节奏——它缺的不是读数而是拿这个读数该怎么办的策略。上下文压缩策略前面几节讨论了如何往上下文里放内容——提示工程决定写什么Skills 决定按需加载什么Agent 状态栏决定注入什么元信息。但随着多轮交互的深入上下文会不断膨胀。本节讨论的是相反的方向如何从上下文中减少内容——什么时候压缩、怎么压缩、为什么即使上下文没满也应该压缩。为什么需要压缩不只是长度问题第一解决长度约束和成本约束。这是最直观的原因上下文窗口有限比如 128K token工具调用结果动辄数万字符几轮交互就可能撑满窗口任务被迫中断。同时 token 越多API 成本越高推理延迟也会急剧上升。第二提升思考质量——总结后的知识比原始形式更利于模型使用。这个动机更深层也更容易被忽视。即使上下文窗口足够大把所有原始信息堆在上下文里也不是最优选择。上下文学习的内部机制检索而非推理简单回顾一下这条机制详细的界定、证据和做法都在状态栏一节所谓检索而非推理是说注意力擅长在已有内容里“查找”却不擅长在一次前向传播里主动“归纳统计”——这并不否定模型可以靠生成思维链一步步想只是说“在单次前向传播里消费已有上下文”这件事更像检索。它对压缩的含义是状态栏的做法是把算好的结论加进上下文而压缩是把臃肿的原始记录换成算好的结论——两者是同一枚硬币的两面都在给那台“只有一半”的检索引擎补上缺失的“提炼”。区别只在于状态栏往往由代码每一步确定性地维护压缩则更多是用一次 LLM 调用把大段原文蒸馏掉。而如果我们提前做一次总结在上下文中直接写入“当前统计黑猫 90 只白猫 10 只”模型就能立即检索到这个结论无需重新思考。这就是压缩的第二个价值把需要思考才能得到的结论变成可以直接检索的知识。明明上下文窗口还远没有满但 Agent 突然找不到关键信息了或者反复纠结于一个早已解决的问题——这种现象被称为上下文腐化Context Rot。上下文腐化与上下文溢出窗口用完是不同的问题溢出是“装不下了”腐化是“装得下但找不到了”——后者更隐蔽因为 Agent 表面上还在正常工作只是决策质量悄然下降。随着上下文长度的增加注意力权重被分散到更多的 token 上每个 token 获得的权重变小更关键的是无关的内容一旦占到了上下文的大头Agent 的决策质量就会明显下滑。压缩与 KV Cache看似矛盾实则互补在讨论具体的压缩策略之前需要解释一个看似矛盾的问题前面反复强调 KV Cache 要求上下文前缀保持不变但压缩不就是要修改上下文中间的内容吗关键在于理解压缩发生的时机和位置。压缩不是在单次 API 调用的过程中修改上下文而是在两次 API 调用之间由 Agent 框架对消息列表进行预处理System Prompt 和 Tool Definitions 永远不动——这是上下文最前面的“静态前缀”KV Cache 持续缓存。压缩的对象是对话历史中的 tool results——当 Agent 框架用压缩后的摘要替换原始的工具输出时替换位置之后的缓存会失效但之前的缓存仍然有效。这是一个有意识的权衡不压缩上下文膨胀到超出窗口限制任务直接失败压缩后虽然损失了部分缓存但上下文长度可控且信息密度更高。因此压缩的频次需要权衡——频繁压缩会频繁破坏缓存最好在上下文接近阈值时批量压缩而不是每轮都压。生产级的分层压缩机制上面的实验展示了不同压缩策略的效果差异。在生产环境中成熟的 Agent 系统通常不会只采用单一策略而是将多种策略组合为分层的压缩机制——不同类型的信息有不同的保质期压缩策略应当与信息的预期生命周期匹配。以 Claude Code 的做法为参照一个成熟的上下文管理系统通常包含五个层次工具结果预算控制大体积的工具输出存到磁盘模型只看摘要预览。替换决策一旦做出就被冻结以保证缓存的一致性。噪声直接删除低价值的内容如大量搜索结果中只被使用了几行的内容直接移除不做摘要——对噪声做摘要只是在浪费 token。API 层微压缩通过 API 层的上下文编辑能力指示服务端从前缀中移除指定的工具结果本地消息保持不变。这一层的优势是零本地实现成本、由服务端一次性完成但按本章的前缀不变性原理移除点之后的缓存同样会失效产生一次缓存重建。因此它适合在上下文即将溢出、反正要付出这次重建代价时使用而不是频繁触发。归档式摘要逐轮做结构化摘要像 git log 那样保留每轮的独立记录而非像 git squash 那样合并成一条保留对话的逻辑脉络。全量压缩由 LLM 驱动的完整压缩作为最后手段。即便如此也是分两个阶段的先尝试压缩会话记忆不行再做全量压缩。全量压缩还配备了连续失败的熔断器即连续失败达到一定次数后自动停止重试的机制——生产数据表明大量会话会被困在反复压缩失败的循环中熔断器避免了在这些会话上持续烧钱。注意这五层的排列顺序前三层实现成本最低、对缓存的扰动可控应当优先使用后两层成本较高但压缩效果更强作为兜底手段。压缩策略的设计原则前面已经分析了压缩的两个动机控制长度与提升思考质量和“上下文学习本质上是检索”的内部机制。在此基础上我们可以提炼出指导具体压缩策略设计的四条原则。这里的压缩服务于当前任务当多次任务的轨迹需要被离线整理为持久经验时则进入第八章讨论的持续进化问题。信息价值的非均匀分布关键的决策点如人员名单的价值高于支撑性的证据如新闻细节更高于冗余的噪声如网页导航栏、页脚广告等元素语义完整性“Sutskever 于 2024 年 5 月离开 OpenAI”不能压缩成“Sutskever 离开”——时间和公司名是不可丢失的关键信息任务相关性同样的内容在“查找创始人名单”和“了解个人背景”两个不同的任务下应该产生不同的压缩结果压缩即理解有效的压缩需要深层的语义理解能力——用更精炼的表达来捕捉上下文的精髓。而且显式压缩的结果是可审查的、可跨会话复用的对 Agent 架构设计的启示上下文压缩策略的研究触及了 Agent 系统设计的本质问题。压缩即理解——负责压缩的模块本身需要接近主模型的语言理解能力形成“模型调用模型”的递归架构。压缩策略与任务类型耦合——信息检索类的任务需要保留广度分析类的任务需要保留深度创作类的任务需要保留灵感触发点未来的 Agent 应当具备根据任务类型自适应选择压缩策略的能力。虽然压缩需要额外的计算开销每次压缩就是一次额外的 LLM 调用但相比节省的 token 成本和提升的任务成功率投资回报率是极高的——实验显示上下文感知压缩将 token 使用量减少了 75% 以上。压缩最容易丢失的不是细节本身而是早期的架构决策、约束背后的理由和失败的路径——LLM 通常会优先删除那些看起来还可以重新获取的信息。在生产级的 Agent 系统中建议显式定义压缩时的保留优先级架构决策和关键约束不得摘要已修改的文件列表和关键的变更记录完整保留验证状态pass/fail必须保留未解决的 TODO 和回滚笔记必须保留工具输出可以删除仅保留 pass/fail 结论此外UUID通用唯一识别码、hash哈希值、IP 地址、端口号、URL、文件名等标识符必须原样保留——一旦把 PR 编号或 commit hash 改错一位后续的工具调用就会直接失效。隔离优于压缩子 Agent 上下文隔离压缩是在信息已经进入上下文之后做减法而一个更釜底抽薪的思路是让大体积的中间信息根本不进入主上下文。这就是子 Agent 上下文隔离——主 Agent 把“读取大量文件”“在代码库中大范围搜索”这类会产生海量中间内容的任务委派给一个独立的子 Agent子 Agent 在自己的上下文中完成探索只把几百 token 的结论性摘要回传给主 Agent。对比一下两种做法处理同一个任务——“在代码库中找到处理支付回调的函数”。主 Agent 亲自搜索可能要让十几个文件、数万 token 的原始代码进入主上下文其中绝大部分在找到目标后就沦为永久占据窗口的噪声还得靠后续压缩来清理。而委派给一个搜索子 Agent主上下文只增加两条消息一条任务描述一条结论“函数位于 src/payment/callbacks.py 的 handle_callback另有两处调用点”——中间过程的数万 token 随子 Agent 的上下文一起被丢弃。本章小结本章绕来绕去其实在说一件事给模型看什么、怎么组织比模型本身有多聪明更影响最终的结果。API 的消息结构定义了上下文的骨架KV Cache 约束了你能改什么、不能改什么提示工程和 Agent Skills 决定了如何高效地向模型提供静态指令和动态知识Agent 状态栏把隐式的状态变成可直接使用的显式信息压缩策略则解决了上下文不断膨胀的问题——不仅是控制长度更是通过主动总结把原始数据变成高密度的结构化知识。这些技术的共同点是显式的、工程化的信息管理——不要让模型被动地在海量上下文中寻找线索而要主动提供经过提炼的结构化状态。回到 Rich Sutton 的《苦涩的教训》那些能更有效地利用更多算力的通用方法将最终胜出。本章展示的每一项技术——从 KV Cache 友好的上下文布局到上下文感知压缩——都是在当前模型能力边界下用工程手段最大化信息利用效率的具体实践。需要明确的是本章处理的是一次任务之内的状态更新与上下文腐化第八章“Agent 的持续进化”处理的是另一时间尺度的问题如何评价跨任务轨迹并把其中的共性转化为会改变未来版本的持久更新。
上下文工程
上下文比作 Agent 的“眼睛”——Agent 只能基于它看到的信息做决策。上下文的设计和管理——即上下文工程Context Engineering。AI会看见什么。上下文决定 Agent 能力上限的关键OpenAI 研究员翁家翌曾精辟地总结这个观点“人和模型一样最重要的是 Context。”他以自身经历举例——“自己在 OpenAI 的工作也没有那么难如果换一个其他人如果有他所有的 context也是能干的。”同样的道理适用于 Agent决定 Agent 能力上限的不是模型参数量而是它在每个决策点能获得多少、多精准的上下文。翁家翌还指出“团队合作中最大的问题也是 context 的不一致”而“AI 短时间内无法取代人的最大原因也是 context——因为 AI 跟人并不在同一个环境里面”。这恰恰是上下文工程要解决的核心问题如何把 Agent 需要的背景信息系统性地、结构化地送到模型面前。Agent 如何调用大模型理解 API 的上下文结构消息的四种角色¶大模型 API 的核心是一个消息列表messages列表中的每条消息都有一个角色role标识模型根据角色来理解每条消息的含义和来源system系统提示词。由开发者编写定义 Agent 的身份、行为规则、约束条件。模型将其视为最高优先级的指令。整个对话过程中通常只有一条放在消息列表的最前面。user用户消息。来自终端用户的输入是 Agent 需要响应的请求。assistant助手消息。模型之前的回复包括文本回复和工具调用请求。在多轮对话中之前的 assistant 消息会被放回消息列表让模型“记住”自己说过什么。tool工具结果。Agent 框架执行工具后将结果以 tool 角色的消息送回给模型。每条 tool 消息通过tool_call_id与对应的工具调用请求关联。带工具调用的多轮交互Agent 的核心循环真正的 Agent 场景远比单轮问答复杂。当用户问 “Whats the current time and weather in Vancouver?” 时模型无法凭自身知识回答它不知道“现在”是什么时候需要调用外部工具。下面完整展示这个过程中 Agent 框架与模型之间的每一步交互。第一次 API 调用——Agent 框架发送初始请求// ═══ Request constructed by the Agent framework (1st call) ═══ { model: Qwen3-0.6B, messages: [ { role: system, // ← Written by developer content: You are a helpful assistant. Use the provided tools to get real-time information when needed. }, { role: user, // ← User input content: Whats the current time and weather in Vancouver? } ], tools: [ // ← Tools defined by developer { type: function, function: { name: get_current_time, description: Get the current date and time in a specific timezone, parameters: { type: object, properties: { timezone: { type: string, description: Timezone name, e.g. America/Vancouver } } } } }, { type: function, function: { name: get_weather, description: Get the current weather for a specific city, parameters: { type: object, properties: { city: { type: string, description: City name }, unit: { type: string, enum: [celsius, fahrenheit] } } } } } ] }模型返回工具调用请求不是最终回复Agent 框架执行工具然后发起第二次 API 调用// ═══ Request constructed by the Agent framework (2nd call) ═══ { model: Qwen3-0.6B, messages: [ { role: system, // ← Same as 1st call content: You are a helpful assistant. Use the provided tools to get real-time information when needed. }, { role: user, // ← Same as 1st call content: Whats the current time and weather in Vancouver? }, { role: assistant, // ← Model output from 1st call, included verbatim content: null, tool_calls: [ { id: call_abc123, function: { name: get_current_time, arguments: {\timezone\: \America/Vancouver\} } }, { id: call_def456, function: { name: get_weather, arguments: {\city\: \Vancouver\, \unit\: \celsius\} } } ] }, { role: tool, // ← Generated by Agent framework (tool execution result) tool_call_id: call_abc123, content: {\timezone\: \America/Vancouver\, \datetime\: \2025-09-13T05:18:47\, \day_of_week\: \Saturday\} }, { role: tool, // ← Generated by Agent framework (tool execution result) tool_call_id: call_def456, content: {\city\: \Vancouver\, \temperature\: 13.2, \unit\: \celsius\, \conditions\: \clear\, \humidity\: 93} } ], tools: [ ... ] // ← Same tool definitions as above, omitted }从 API 视角看上下文的构成通过上面的例子我们可以清晰地看到 Agent 每次调用模型时上下文的完整构成上半部分System Prompt Tool Definitions在整个对话过程中保持不变下半部分对话历史即第一章所定义的轨迹随着交互的进行不断增长。KV Cache 友好的上下文设计在进入故事之前先把KV Cache的直觉建立起来。模型每生成一个 token都要回头看一遍前文所有 token 的中间计算结果。如果每轮都从头算一次开销会随上下文长度爆炸式增长。KV Cache 的做法是把前文的中间计算结果缓存下来下一轮只需要计算新增 token 的部分。前提是前缀完全不变——只要前缀里有一个字符被改写缓存就全部作废模型不得不从被修改的位置重算。顺带说明本节讲到跨请求的“缓存命中”时在 API 服务商的语境下叫 Prompt Cache——它是构建在推理引擎 KV Cache 之上的跨请求缓存两个层级的完整辨析见本节末尾。系统提示词和工具定义一旦确定就不要改。任何改动哪怕多一个空格都会导致缓存全部失效延迟成倍增加、成本上升具体幅度视模型与配置而定。动态信息永远追加到末尾——时间戳、用户状态等变化的内容作为新消息追加到对话末尾而不是修改已有的系统提示词。使用标准 API 格式不要自行拼接消息结构化消息会被 Chat Template 翻译成模型训练时见过的固定 token 序列自行用字符串拼成USER: ... ASSISTANT: ...的根本问题是偏离了这种训练格式会削弱模型的多步思考能力。至于缓存——它只认 token 字节序列只要拼出的前缀字节级稳定照样能命中但若拼接方式不稳定如每次向前缀注入动态内容缓存也会随之失效。从 API 消息到模型 TokenChat TemplateChat Template 是一块贯穿全书的地基它不只关系到 KV Cache还决定了多轮工具调用、思维链保留、状态栏注入等诸多机制能否正确工作因此值得单独讲清楚。注意力可视化实验中的 token 序列如|im_start|、|im_end|等特殊标记看起来与前面 API 的 JSON 格式很不一样。这是因为 API 层面的结构化消息需要被转换为模型能理解的线性 token 流——负责这个转换的就是Chat Template聊天模板。KV Cache 的原理与约束要理解 KV Cache 的价值先看看没有它时会发生什么。假设一个 Agent 在进行第 6 轮对话上下文已经累积了 2000 个 token。在没有缓存的情况下模型每生成一个新 token都需要重新计算这 2000 个 token 的 K、V 向量——相当于重跑整个前缀的前向计算。尽管前 5 轮的内容完全没变第 6 轮仍要像第 1 轮那样从头计算整个前缀而且此时前缀更长代价比第 1 轮大得多。无缓存时prefill 阶段即模型正式生成回复之前一次性处理输入端全部 token 的阶段的注意力计算量随上下文长度平方级增长随着对话深入延迟和成本都会急剧攀升。这对于需要几十轮工具调用的 Agent 任务来说是不可接受的。实验 2-3 ★★常见的错误上下文管理模式在kv-cache实验中我们系统性地测试了几种常见但有害的上下文管理模式。这些模式不仅会破坏 KV Cache 的有效性有些甚至会影响 Agent 的核心能力。动态系统提示词是最常见的错误之一。一些开发者为了让 Agent“知道”当前时间会在系统提示词中嵌入时间戳如 “Current time: 2025-09-14 10:30:45.123456”。这种做法看似提供了有用的上下文信息但每次请求时时间戳都会变化导致整个系统提示词不同从而使 KV Cache 完全失效。正确的做法是将时间信息作为用户消息的一部分追加到对话末尾或者只在真正需要时通过工具调用来获取。动态用户配置模式试图在每次请求中更新用户的状态信息如剩余的 API 调用次数或账户余额将这些信息嵌入上下文中会破坏缓存。更好的方案是在需要时通过专门的状态管理机制来处理。工具定义的动态排序是另一个隐蔽的陷阱。有些系统会根据使用频率动态调整工具的顺序但工具定义通常占据上下文很大的一部分每个工具可能包含数百个 token 的描述和参数说明改变顺序就会导致整个缓存失效。实验表明保持固定的顺序对模型选择工具的能力几乎没有影响但对性能提升却是显著的。滑动窗口Sliding Window对话历史通过只保留最近几条消息来控制上下文长度。举个例子如果窗口大小设为 10 条消息那么第 11 条消息进来时最早的一条就会被丢弃。这种做法存在两个严重的问题。第一它会破坏上下文的前缀一致性导致 KV Cache 失效。第二它可能丢失关键的工具调用结果。举例滑动窗口大小为 10 轮时Agent 在第 2 轮调用了文件读取工具拿到关键内容到第 15 轮还需要回引这段内容——但此时窗口已滑出原始结果模型只能依赖被截断的对话尝试推断错误率显著上升。在实验中使用滑动窗口的 Agent 经常陷入循环反复执行相同的工具调用因为它“忘记”了之前已经获得的结果。文本格式化方法是最具破坏性的模式之一。它把结构化的 role-content 消息转换为 “USER: ... ASSISTANT: ...” 这样的纯文本流。需要说明的是问题的关键并不在缓存——缓存作用于 token 字节序列只要拼接出的前缀字节级稳定照样能命中只有当拼接方式不稳定如每次向前缀注入动态内容时才会破坏缓存。真正的破坏在于文本格式化偏离了模型训练时使用的标准消息格式——模型在训练阶段接受了大量基于角色的对话数据已经学会解析这种结构化格式。当消息被转为纯文本时模型需要额外消耗注意力资源来推断角色的边界和对话的结构从而产生各种问题重复执行已完成的操作、忽略工具调用结果、在应该调用工具时却生成文本响应、格式解析错误等。KV Cache 与 Prompt Cache两个层级的缓存KV Cache是模型内部的优化——在一次推理过程中缓存已计算的 token 的键值对避免重复计算。Prompt Cache则是 API 服务层的优化——跨多次 API 请求之间缓存相同前缀的计算结果。两者的优化原理相似都利用前缀不变性但作用层级不同KV Cache 加速单次请求内的 token 生成Prompt Cache 减少跨请求的重复计算成本。Prompt Cache 的工作方式是API 服务商对请求的前缀进行匹配如果多次请求的前缀相同比如系统提示词和工具定义不变就直接复用之前计算好的 KV Cache而不需要重新计算这部分 token 的键值对。缓存作为架构约束Claude Code 的实践揭示了一个深层的模式当 Prompt Cache 的经济效益足够显著时缓存一致性会反过来主导系统的架构选择。以下是几个体现这种约束的设计决策提示词的结构由缓存边界决定。子 Agent 必须与父 Agent 字节级对齐。工具结果的替换字符串在首次出现时就被冻结。KV Cache 未必是一次性的可编辑、可组合的“笔记本节到此为止都建立在一条铁律上前缀里改一个字节后面的缓存就全废。这条铁律在今天的推理引擎里确实成立但笔者想指出它未必是必然的。松动它的出发点是一个反直觉的观察2在 prefill 阶段模型其实在“做笔记”。当它读到上下文里的某个字段比如“用户所在城市北京”时并不是把这个字段原封不动地缓存下来而是顺手把“这个字段意味着什么”的结论写进了后面每一层的 KV 状态里。测量发现一个字段自己那几个 token 的 KV对最终决策的贡献往往不到 1%——真正影响输出的是它在下游留下的那些“读书笔记”。对 Agent 而言这一点的意义在于那个被反复重建的长上下文——换一批工具、更新一个记忆字段、注入一条新状态正是下一节状态栏要做的事——也许不必每轮都推倒重来。它指向一种“上下文可变、但缓存收益还在”的可能把上下文的组装从 O(L²) 的重算变成 O(L) 的“笔记拼接”。这仍属研究阶段本节前面的三条实践结论在当前生产系统中依然是应当遵守的默认原则。理解了缓存机制后接下来的问题自然变成既然我们知道了上下文是怎么被处理和缓存的那该如何设计送进去的内容本身接下来几节围绕“上下文里到底放什么、怎么组织”展开可以分为三条相对独立的线索提示工程、提示注入与动态提示词Agent Skills系统提示词该怎么写、写什么——这是上下文工程最直接的部分工具定义与系统提示词并列的另一个静态组成部分的设计也直接影响 Agent 的工具使用准确性本章给出核心原则第四章将详细展开。紧随其后的是安全问题——提示注入当外部内容试图劫持精心设计的上下文时如何在上下文层面构筑防御。而当提示词越写越长、覆盖的场景越来越多时把所有内容塞进一个系统提示词就不再可行了既浪费 token也会导致注意力被稀释于是自然演化出 Agent Skills 的渐进式披露机制——按需加载而非一次性塞满。Agent 状态栏Agent Status Bar一种独立的机制通过在上下文末尾注入动态的元信息任务进度、环境状态、工具调用计数等弥补模型无法主动归纳隐式状态的不足。就像手机屏幕顶部始终显示时间、电量、网络信号一样Agent 状态栏让模型随时能“瞥一眼”就知道当前的运行状态。上下文压缩策略解决上下文不断膨胀的问题——什么时候压缩、怎么压缩、压缩如何与 KV Cache 共存。动态提示词与 Agent Skills随着 Agent 覆盖的业务场景越来越多系统提示词会不断膨胀——客服场景的退款规则、编程场景的代码规范、文档场景的格式要求……全部塞进一个提示词会带来两个问题浪费 token大部分内容与当前任务无关注意力被稀释上下文中无关信息过多会稀释模型对关键内容的注意力这一问题将在后文上下文压缩策略部分以“上下文腐化”的概念详细讨论这就是从静态提示工程到动态提示词的自然演进不是把所有知识一次性塞给 Agent而是让它按需加载。Agent Skills 系统正是这一理念的工程化实现。Skills领域能力的可组合单元Agent Skills 的核心思想是将 Agent 的能力模块化为独立的、可按需加载的知识包6。每个 Skill 本质上是一套包含专业领域指导的提示词集合就像为新员工准备的某个专项任务的操作手册。与传统的将所有指令塞入单一系统提示词的做法不同Skills 采用了渐进式披露Progressive Disclosure的设计哲学——先给 Agent 看一份目录摘要需要时再加载完整内容就像你不会把公司所有部门的操作手册都堆到新员工桌上而是先给一份总目录需要哪本再去取。第一层元数据每个 Skill 必须包含一个SKILL.md文件开头是 YAML frontmatter即文件顶部用---分隔的元数据块类似书籍的版权页包含name和description两个字段。Agent 框架在启动时扫描所有已安装的 Skill将它们的name和description仅占数百个 token注入到对话上下文中注入位置的设计权衡见下一小节使 Agent 在不消耗大量上下文的前提下知晓自己拥有哪些专业能力。元数据中的description字段是路由决策的关键——它应当足够短控制常驻的 token 量但写法要像路由条件而非功能介绍。最直接的写法是 “Use when / Dont use when” 加上几条反例即明确列出“不该触发此 Skill”的场景。实践中缺少反例的 Skill 描述会让路由准确率明显下降——宽泛的描述会在不相关的任务上频繁误触发补上反例后路由准确率会显著回升。反例不是可选项而是 Skill 路由能否准确触发的关键。描述太宽泛如 “help with backend”等于任何后端相关的工作都能触发路由就会失准真正有效的描述是路由条件——“何时该用我”比“我能做什么”重要得多。第二层核心流程当 Agent 判断某个任务需要特定的 Skill 时通过专用的 Skill 工具加载完整的SKILL.md内容作为 tool result 出现在对话历史中。以 PPTX Skill7 为例其中包含处理 PowerPoint 文件的核心流程如何通过 markitdownMicrosoft 开源的文档转 Markdown 工具提取文本如何解压 PPTX 文件访问原始的 XML 结构以及关键文件的路径约定。第三层细则通过文件引用深入到更详细的子文档。主文件引用了html2pptx.md通过 HTML 模板创建 PowerPoint 的详细工作流、reference.md格式技术细节等。Agent 会根据具体的需求选择性地深入阅读相关的子文档。Skills 的实现方式与权衡理解了 Skills 是什么之后接下来是一个更具体的工程问题Skill 内容放在上下文的什么位置这是一个根本性的设计决策直接关系到 KV Cache 效率和模型的指令遵循效果。理论上有两种朴素方案但都存在明显的代价生产实现如 Claude Code采用的是一种回避了两者痛点的第三种方案。方式一注入系统提示词system 消息。将 Skill 内容直接追加到 system prompt 中。模型对 system 位置的指令遵循能力最强因为训练时大量使用了这个位置的指令所以 Skill 的执行效果最好。但问题在于每次加载新的 Skill 都会改变 system 消息的内容导致 KV Cache 前缀失效。如果 Agent 频繁切换 Skill比如一个任务需要先用搜索 Skill再用文档 Skill缓存会反复失效延迟和成本显著增加。方式二作为普通文件读取内容出现在上下文中间。Agent 通过通用文件读取工具读取 Skill 文件文件内容作为 tool result 出现在对话历史中——也就是上下文的中间位置。这种方式完全不影响 KV Cachesystem prompt 不变但对模型的指令遵循instruction following能力提出了更高要求模型需要在长上下文的中间位置准确识别并遵循 Skill 中的指令而不是把它当作普通的工具输出来“参考”。实践中不同模型对这种模式的支持差异很大——Claude 因为在训练中大量使用了中间位置的指令遵循数据表现最为可靠而其他模型在遵循上下文中间注入的指令时往往会打折扣。方式三生产实现元数据作为动态上下文提供完整内容通过专用工具按需加载。Claude Code 的核心思路是将 Skill 的“路由”和“执行”分离模型首先获得可用 Skill 的元数据用于判断当前任务是否需要某个 Skill只有在 Skill 被选中后才进一步加载完整的SKILL.md。这种设计兼顾了上下文开销、Prompt Cache 复用和指令遵循能力。Agent 状态栏通过元信息增强 Agent 轨迹管理上下文末尾的 user-role meta 消息”是一条通用的元信息注入通道——Skill 元数据列表只是它的一个使用场景。本节将系统地展开这一通道它是 Agent 框架向模型同步各种动态状态的统一机制称为Agent 状态栏Agent Status Bar。前面讨论的提示工程解决了“给模型什么样的静态指令”的问题。但在实际执行过程中Agent 还需要动态地感知自身的状态和任务的进展——这就是 Agent 状态栏的用武之地。Agent 状态栏的理论基础Agent 状态栏之所以有效源于注意力机制的一个本质特性上下文学习更像检索而非推理——模型擅长从已有内容中查找信息但不擅长主动归纳和总结这里说的是模型在单次前向传播中如何消费已经在上下文里的信息并不否定模型可以通过生成思维链来完成多步思考。一个更形象的说法是上下文窗口是一台只有一半的检索引擎。它“检索”的这一半非常强——你问什么注意力就能从成千上万个 token 里把相关的原始记录捞出来相当于把检索增强生成RAG内置进了每一次前向传播。但它缺了另一半没有“提炼层”。上下文里的东西从来不会被自动数一遍、建个索引、或就地总结成一条结论任何“关于这些内容的结论”——一共多少条、有没有超标、进展到哪一步——模型每次要用都得从原始记录里现算一遍。而“现算一遍”的代价会随上下文里堆积的内容量记作 N一起往上涨。Agent 状态栏的构成基于上述的理论基础Agent 状态栏包括以下几种类型的信息任务规划当 Agent 处理复杂的多步骤任务时轨迹会变得很长。Agent 容易过分关注当前的局部子任务而忘记用户的原始诉求、核心约束以及后续工作。通过引入 TODO 列表将任务分解为清晰的步骤放在轨迹末尾不断提醒模型当前的进展和未来的目标确保行动与总体规划保持一致。事件的侧信道信息Side-channel Information为每个事件附加元数据——精确的时间、地理位置、距上次 Agent 回复的时间间隔等。侧信道信息是指不在主要数据通道中传递、但对理解事件很有帮助的辅助信息。这些信息帮助模型理解事件的时序关系和环境背景从而做出更符合情境的决策。环境的当前状态包括动态的环境信息系统时间、工作目录等、异常操作提醒“该工具已被重复调用 N 次”、以及从隐式状态到显式状态的转换。这一设计原则同样适用于人类界面——命令行CLI和图形界面GUI都致力于让用户清晰地感知系统的当前状态。可用能力清单当 Agent 框架支持插件化的能力扩展如上一节的 Skills 系统时所有已安装 Skill 的元数据列表也走这条同一的末尾注入通道相当于告诉模型“你现在拥有哪些可调用的专业能力”。它变化频率最低仅在用户安装/卸载 Skill 时才变其增量发送机制已在上一节 Skills 中详述此处不再重复。侧信道信息和可用能力清单一经添加就不再改变对 KV Cache 很友好因为不会破坏已缓存的前缀。而任务规划和环境状态是动态变化的需要以特殊的用户消息追加到上下文末尾并随任务推进不断更新——更新方式的选择直接关系到 KV Cache 的代价下面结合具体的消息结构展开讨论。Agent 状态栏在上下文中的具体位置一个重要的实现细节是Agent 状态栏在 API 层面实际上是作为一条 user 角色的消息插入到上下文末尾的——而不是修改开头的 system 消息。原因正是前面讨论的 KV Cache 约束修改 system 消息会破坏整个前缀的缓存。这里需要澄清一个容易混淆的地方这里的 user 角色只是 API 协议层面的技术选择并不等同于第一章定义的“来自终端用户的输入”。换句话说Harness 是在借用 user 角色这个消息槽位向模型注入由 Agent 框架自动生成的系统状态信息——内容并非来自真实用户只是复用了 user 角色的消息格式来挂到上下文末尾。状态更新的两种实现与缓存代价“追加不破坏缓存”只在单次注入时成立。状态是会变的——下一轮 TODO 完成了一项、工具计数加了一次状态消息就过时了。如何更新它存在两种实现各有明确的缓存代价实现一每轮替换。每次 API 调用前从消息列表中移除上一轮的状态消息在末尾追加最新状态。这保证了上下文中只有一份状态、永远是最新的。但代价是移除旧状态会使其位置之后的所有缓存失效——这与本章批评的“动态时间戳”是同一个失效机制区别只在于状态消息位于上下文末尾失效范围仅限于最近几轮消息而不是整个前缀。实现二持久追加。状态消息一旦注入就永久留在轨迹中每轮只在末尾追加新的状态。Claude Code 的system-reminder采用的就是这种方式——历史状态消息保留在会话记录transcript中从不删改。这种方式对缓存完全友好所有消息只追加、不修改前缀始终稳定。代价是陈旧的状态会在上下文中累积——既占用 token也要求模型自己关注“最新一条”状态而忽略已过时的旧状态。取舍的经验法则是状态更新频繁且轨迹很长时选择实现二——每轮替换带来的缓存失效会在长轨迹上反复累积代价远超陈旧状态占用的 token轨迹较短或单条状态消息很大如完整的 TODO 列表加环境快照时选择实现一——末尾几轮的缓存失效本来就便宜换来的是上下文的整洁和无歧义。实验 2-8 ★★几种好用的 Agent 状态栏技术agent-status-bar实验框架实现了五种状态栏技术每种都可以独立启用或禁用时间戳跟踪工具调用计数器TODO 列表管理详细错误信息系统状态感知从读数到策略Agent 的物理时间感知实验 2-8 的五种技术里时间戳跟踪和工具调用计数器看起来是两条互不相干的元信息但把它们放在一起看会发现二者指向同一种更本质的能力——让 Agent感知物理时间并据此调节自己做事的节奏。一个人被要求“三分钟写一段话”和“三十分钟写一段话”交出来的东西是不一样的可当下的前沿 Agent无论你说三分钟还是三十分钟产出几乎没有区别。它既说不清一件事到底做完了没有也分不清眼前这堵墙是真的走不通、还是稍等一下就好更察觉不到一个已经跑了三分钟的工具调用是仍在推进、还是早就卡死了。笔者和合作者把这种缺失的能力称为时间感time sense并把它拆成三个可以分别度量的轴10紧迫度urgency坚持度persistence警觉度vigilance这套三轴框架直接落在状态栏上时间戳跟踪供的是紧迫度和警觉度的读数工具调用计数器供的是坚持度的读数。但这里有一个容易踩空、也最值得记住的发现光把读数摆到模型面前并不足以改变它的行为。在一个专门测量时间感的基准上同一批任务被放在四种条件下运行什么都不给、只给原始时间戳、给时间戳外加一份“这些读数该怎么用”的操作手册、以及让 Agent 自己上报节奏状态。结果相当反直觉只给原始时间戳这一档和什么都不给几乎没有区别前后相差不过两三个百分点真正把通过率从一成出头拉到四五成的幅度 19 到 49 个百分点是那份操作手册。换句话说把elapsed_ms5000 expected_ms500这行读数放进上下文模型确实“看见”了却不会自动据此改变干活的节奏——它缺的不是读数而是拿这个读数该怎么办的策略。上下文压缩策略前面几节讨论了如何往上下文里放内容——提示工程决定写什么Skills 决定按需加载什么Agent 状态栏决定注入什么元信息。但随着多轮交互的深入上下文会不断膨胀。本节讨论的是相反的方向如何从上下文中减少内容——什么时候压缩、怎么压缩、为什么即使上下文没满也应该压缩。为什么需要压缩不只是长度问题第一解决长度约束和成本约束。这是最直观的原因上下文窗口有限比如 128K token工具调用结果动辄数万字符几轮交互就可能撑满窗口任务被迫中断。同时 token 越多API 成本越高推理延迟也会急剧上升。第二提升思考质量——总结后的知识比原始形式更利于模型使用。这个动机更深层也更容易被忽视。即使上下文窗口足够大把所有原始信息堆在上下文里也不是最优选择。上下文学习的内部机制检索而非推理简单回顾一下这条机制详细的界定、证据和做法都在状态栏一节所谓检索而非推理是说注意力擅长在已有内容里“查找”却不擅长在一次前向传播里主动“归纳统计”——这并不否定模型可以靠生成思维链一步步想只是说“在单次前向传播里消费已有上下文”这件事更像检索。它对压缩的含义是状态栏的做法是把算好的结论加进上下文而压缩是把臃肿的原始记录换成算好的结论——两者是同一枚硬币的两面都在给那台“只有一半”的检索引擎补上缺失的“提炼”。区别只在于状态栏往往由代码每一步确定性地维护压缩则更多是用一次 LLM 调用把大段原文蒸馏掉。而如果我们提前做一次总结在上下文中直接写入“当前统计黑猫 90 只白猫 10 只”模型就能立即检索到这个结论无需重新思考。这就是压缩的第二个价值把需要思考才能得到的结论变成可以直接检索的知识。明明上下文窗口还远没有满但 Agent 突然找不到关键信息了或者反复纠结于一个早已解决的问题——这种现象被称为上下文腐化Context Rot。上下文腐化与上下文溢出窗口用完是不同的问题溢出是“装不下了”腐化是“装得下但找不到了”——后者更隐蔽因为 Agent 表面上还在正常工作只是决策质量悄然下降。随着上下文长度的增加注意力权重被分散到更多的 token 上每个 token 获得的权重变小更关键的是无关的内容一旦占到了上下文的大头Agent 的决策质量就会明显下滑。压缩与 KV Cache看似矛盾实则互补在讨论具体的压缩策略之前需要解释一个看似矛盾的问题前面反复强调 KV Cache 要求上下文前缀保持不变但压缩不就是要修改上下文中间的内容吗关键在于理解压缩发生的时机和位置。压缩不是在单次 API 调用的过程中修改上下文而是在两次 API 调用之间由 Agent 框架对消息列表进行预处理System Prompt 和 Tool Definitions 永远不动——这是上下文最前面的“静态前缀”KV Cache 持续缓存。压缩的对象是对话历史中的 tool results——当 Agent 框架用压缩后的摘要替换原始的工具输出时替换位置之后的缓存会失效但之前的缓存仍然有效。这是一个有意识的权衡不压缩上下文膨胀到超出窗口限制任务直接失败压缩后虽然损失了部分缓存但上下文长度可控且信息密度更高。因此压缩的频次需要权衡——频繁压缩会频繁破坏缓存最好在上下文接近阈值时批量压缩而不是每轮都压。生产级的分层压缩机制上面的实验展示了不同压缩策略的效果差异。在生产环境中成熟的 Agent 系统通常不会只采用单一策略而是将多种策略组合为分层的压缩机制——不同类型的信息有不同的保质期压缩策略应当与信息的预期生命周期匹配。以 Claude Code 的做法为参照一个成熟的上下文管理系统通常包含五个层次工具结果预算控制大体积的工具输出存到磁盘模型只看摘要预览。替换决策一旦做出就被冻结以保证缓存的一致性。噪声直接删除低价值的内容如大量搜索结果中只被使用了几行的内容直接移除不做摘要——对噪声做摘要只是在浪费 token。API 层微压缩通过 API 层的上下文编辑能力指示服务端从前缀中移除指定的工具结果本地消息保持不变。这一层的优势是零本地实现成本、由服务端一次性完成但按本章的前缀不变性原理移除点之后的缓存同样会失效产生一次缓存重建。因此它适合在上下文即将溢出、反正要付出这次重建代价时使用而不是频繁触发。归档式摘要逐轮做结构化摘要像 git log 那样保留每轮的独立记录而非像 git squash 那样合并成一条保留对话的逻辑脉络。全量压缩由 LLM 驱动的完整压缩作为最后手段。即便如此也是分两个阶段的先尝试压缩会话记忆不行再做全量压缩。全量压缩还配备了连续失败的熔断器即连续失败达到一定次数后自动停止重试的机制——生产数据表明大量会话会被困在反复压缩失败的循环中熔断器避免了在这些会话上持续烧钱。注意这五层的排列顺序前三层实现成本最低、对缓存的扰动可控应当优先使用后两层成本较高但压缩效果更强作为兜底手段。压缩策略的设计原则前面已经分析了压缩的两个动机控制长度与提升思考质量和“上下文学习本质上是检索”的内部机制。在此基础上我们可以提炼出指导具体压缩策略设计的四条原则。这里的压缩服务于当前任务当多次任务的轨迹需要被离线整理为持久经验时则进入第八章讨论的持续进化问题。信息价值的非均匀分布关键的决策点如人员名单的价值高于支撑性的证据如新闻细节更高于冗余的噪声如网页导航栏、页脚广告等元素语义完整性“Sutskever 于 2024 年 5 月离开 OpenAI”不能压缩成“Sutskever 离开”——时间和公司名是不可丢失的关键信息任务相关性同样的内容在“查找创始人名单”和“了解个人背景”两个不同的任务下应该产生不同的压缩结果压缩即理解有效的压缩需要深层的语义理解能力——用更精炼的表达来捕捉上下文的精髓。而且显式压缩的结果是可审查的、可跨会话复用的对 Agent 架构设计的启示上下文压缩策略的研究触及了 Agent 系统设计的本质问题。压缩即理解——负责压缩的模块本身需要接近主模型的语言理解能力形成“模型调用模型”的递归架构。压缩策略与任务类型耦合——信息检索类的任务需要保留广度分析类的任务需要保留深度创作类的任务需要保留灵感触发点未来的 Agent 应当具备根据任务类型自适应选择压缩策略的能力。虽然压缩需要额外的计算开销每次压缩就是一次额外的 LLM 调用但相比节省的 token 成本和提升的任务成功率投资回报率是极高的——实验显示上下文感知压缩将 token 使用量减少了 75% 以上。压缩最容易丢失的不是细节本身而是早期的架构决策、约束背后的理由和失败的路径——LLM 通常会优先删除那些看起来还可以重新获取的信息。在生产级的 Agent 系统中建议显式定义压缩时的保留优先级架构决策和关键约束不得摘要已修改的文件列表和关键的变更记录完整保留验证状态pass/fail必须保留未解决的 TODO 和回滚笔记必须保留工具输出可以删除仅保留 pass/fail 结论此外UUID通用唯一识别码、hash哈希值、IP 地址、端口号、URL、文件名等标识符必须原样保留——一旦把 PR 编号或 commit hash 改错一位后续的工具调用就会直接失效。隔离优于压缩子 Agent 上下文隔离压缩是在信息已经进入上下文之后做减法而一个更釜底抽薪的思路是让大体积的中间信息根本不进入主上下文。这就是子 Agent 上下文隔离——主 Agent 把“读取大量文件”“在代码库中大范围搜索”这类会产生海量中间内容的任务委派给一个独立的子 Agent子 Agent 在自己的上下文中完成探索只把几百 token 的结论性摘要回传给主 Agent。对比一下两种做法处理同一个任务——“在代码库中找到处理支付回调的函数”。主 Agent 亲自搜索可能要让十几个文件、数万 token 的原始代码进入主上下文其中绝大部分在找到目标后就沦为永久占据窗口的噪声还得靠后续压缩来清理。而委派给一个搜索子 Agent主上下文只增加两条消息一条任务描述一条结论“函数位于 src/payment/callbacks.py 的 handle_callback另有两处调用点”——中间过程的数万 token 随子 Agent 的上下文一起被丢弃。本章小结本章绕来绕去其实在说一件事给模型看什么、怎么组织比模型本身有多聪明更影响最终的结果。API 的消息结构定义了上下文的骨架KV Cache 约束了你能改什么、不能改什么提示工程和 Agent Skills 决定了如何高效地向模型提供静态指令和动态知识Agent 状态栏把隐式的状态变成可直接使用的显式信息压缩策略则解决了上下文不断膨胀的问题——不仅是控制长度更是通过主动总结把原始数据变成高密度的结构化知识。这些技术的共同点是显式的、工程化的信息管理——不要让模型被动地在海量上下文中寻找线索而要主动提供经过提炼的结构化状态。回到 Rich Sutton 的《苦涩的教训》那些能更有效地利用更多算力的通用方法将最终胜出。本章展示的每一项技术——从 KV Cache 友好的上下文布局到上下文感知压缩——都是在当前模型能力边界下用工程手段最大化信息利用效率的具体实践。需要明确的是本章处理的是一次任务之内的状态更新与上下文腐化第八章“Agent 的持续进化”处理的是另一时间尺度的问题如何评价跨任务轨迹并把其中的共性转化为会改变未来版本的持久更新。