当前位置: 首页 > news >正文

上下文工程

上下文比作 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 场景远比单轮问答复杂。当用户问 “What's 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": "What's 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": "What's 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 之上的跨请求缓存,两个层级的完整辨析见本节末尾。

  1. 系统提示词和工具定义一旦确定就不要改。任何改动,哪怕多一个空格,都会导致缓存全部失效,延迟成倍增加、成本上升(具体幅度视模型与配置而定)。
  2. 动态信息永远追加到末尾——时间戳、用户状态等变化的内容,作为新消息追加到对话末尾,而不是修改已有的系统提示词。
  3. 使用标准 API 格式,不要自行拼接消息:结构化消息会被 Chat Template 翻译成模型训练时见过的固定 token 序列;自行用字符串拼成"USER: ... ASSISTANT: ..."的根本问题是偏离了这种训练格式,会削弱模型的多步思考能力。至于缓存——它只认 token 字节序列,只要拼出的前缀字节级稳定,照样能命中;但若拼接方式不稳定(如每次向前缀注入动态内容),缓存也会随之失效。

从 API 消息到模型 Token:Chat Template

Chat 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(即文件顶部用---分隔的元数据块,类似书籍的版权页),包含namedescription两个字段。Agent 框架在启动时扫描所有已安装的 Skill,将它们的namedescription(仅占数百个 token)注入到对话上下文中(注入位置的设计权衡见下一小节),使 Agent 在不消耗大量上下文的前提下知晓自己拥有哪些专业能力。

元数据中的description字段是路由决策的关键——它应当足够短(控制常驻的 token 量),但写法要像路由条件而非功能介绍。最直接的写法是 “Use when / Don't use when” 加上几条反例(即明确列出“不该触发此 Skill”的场景)。实践中,缺少反例的 Skill 描述会让路由准确率明显下降——宽泛的描述会在不相关的任务上频繁误触发;补上反例后,路由准确率会显著回升。反例不是可选项,而是 Skill 路由能否准确触发的关键。描述太宽泛(如 “help with backend”)等于任何后端相关的工作都能触发,路由就会失准;真正有效的描述是路由条件——“何时该用我”比“我能做什么”重要得多。

第二层(核心流程):当 Agent 判断某个任务需要特定的 Skill 时,通过专用的 Skill 工具加载完整的SKILL.md,内容作为 tool result 出现在对话历史中。以 PPTX Skill7 为例,其中包含处理 PowerPoint 文件的核心流程:如何通过 markitdown(Microsoft 开源的文档转 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 Cache(system 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:

  1. 紧迫度(urgency)
  2. 坚持度(persistence)
  3. 警觉度(vigilance)

这套三轴框架直接落在状态栏上:时间戳跟踪供的是紧迫度和警觉度的读数,工具调用计数器供的是坚持度的读数。但这里有一个容易踩空、也最值得记住的发现:光把读数摆到模型面前,并不足以改变它的行为。在一个专门测量时间感的基准上,同一批任务被放在四种条件下运行:什么都不给、只给原始时间戳、给时间戳外加一份“这些读数该怎么用”的操作手册、以及让 Agent 自己上报节奏状态。结果相当反直觉:只给原始时间戳这一档,和什么都不给几乎没有区别(前后相差不过两三个百分点);真正把通过率从一成出头拉到四五成的(幅度 +19 到 +49 个百分点),是那份操作手册。换句话说,把elapsed_ms=5000 expected_ms=500这行读数放进上下文,模型确实“看见”了,却不会自动据此改变干活的节奏——它缺的不是读数,而是拿这个读数该怎么办的策略

上下文压缩策略

前面几节讨论了如何往上下文里放内容——提示工程决定写什么,Skills 决定按需加载什么,Agent 状态栏决定注入什么元信息。但随着多轮交互的深入,上下文会不断膨胀。本节讨论的是相反的方向:如何从上下文中减少内容——什么时候压缩、怎么压缩、为什么即使上下文没满也应该压缩。

为什么需要压缩:不只是长度问题

第一,解决长度约束和成本约束。这是最直观的原因:上下文窗口有限(比如 128K token),工具调用结果动辄数万字符,几轮交互就可能撑满窗口,任务被迫中断。同时 token 越多,API 成本越高,推理延迟也会急剧上升。

第二,提升思考质量——总结后的知识比原始形式更利于模型使用。这个动机更深层,也更容易被忽视。即使上下文窗口足够大,把所有原始信息堆在上下文里也不是最优选择。

上下文学习的内部机制:检索而非推理

简单回顾一下这条机制(详细的界定、证据和做法都在状态栏一节):所谓检索而非推理,是说注意力擅长在已有内容里“查找”,却不擅长在一次前向传播里主动“归纳统计”——这并不否定模型可以靠生成思维链一步步想,只是说“在单次前向传播里消费已有上下文”这件事更像检索。它对压缩的含义是:状态栏的做法是把算好的结论进上下文,而压缩是把臃肿的原始记录成算好的结论——两者是同一枚硬币的两面,都在给那台“只有一半”的检索引擎补上缺失的“提炼”。区别只在于:状态栏往往由代码每一步确定性地维护,压缩则更多是用一次 LLM 调用把大段原文蒸馏掉。

而如果我们提前做一次总结,在上下文中直接写入“当前统计:黑猫 90 只,白猫 10 只”,模型就能立即检索到这个结论,无需重新思考。这就是压缩的第二个价值:把需要思考才能得到的结论变成可以直接检索的知识。

明明上下文窗口还远没有满,但 Agent 突然找不到关键信息了,或者反复纠结于一个早已解决的问题——这种现象被称为上下文腐化(Context Rot)。上下文腐化与上下文溢出(窗口用完)是不同的问题:溢出是“装不下了”,腐化是“装得下但找不到了”——后者更隐蔽,因为 Agent 表面上还在正常工作,只是决策质量悄然下降。随着上下文长度的增加,注意力权重被分散到更多的 token 上,每个 token 获得的权重变小;更关键的是,无关的内容一旦占到了上下文的大头,Agent 的决策质量就会明显下滑。

压缩与 KV Cache:看似矛盾,实则互补

在讨论具体的压缩策略之前,需要解释一个看似矛盾的问题:前面反复强调 KV Cache 要求上下文前缀保持不变,但压缩不就是要修改上下文中间的内容吗?

关键在于理解压缩发生的时机和位置。压缩不是在单次 API 调用的过程中修改上下文,而是在两次 API 调用之间,由 Agent 框架对消息列表进行预处理:

  1. System Prompt 和 Tool Definitions 永远不动——这是上下文最前面的“静态前缀”,KV Cache 持续缓存。
  2. 压缩的对象是对话历史中的 tool results——当 Agent 框架用压缩后的摘要替换原始的工具输出时,替换位置之后的缓存会失效,但之前的缓存仍然有效。
  3. 这是一个有意识的权衡:不压缩,上下文膨胀到超出窗口限制,任务直接失败;压缩后,虽然损失了部分缓存,但上下文长度可控且信息密度更高。因此压缩的频次需要权衡——频繁压缩会频繁破坏缓存,最好在上下文接近阈值时批量压缩,而不是每轮都压。

生产级的分层压缩机制

上面的实验展示了不同压缩策略的效果差异。在生产环境中,成熟的 Agent 系统通常不会只采用单一策略,而是将多种策略组合为分层的压缩机制——不同类型的信息有不同的保质期,压缩策略应当与信息的预期生命周期匹配。以 Claude Code 的做法为参照,一个成熟的上下文管理系统通常包含五个层次:

  1. 工具结果预算控制:大体积的工具输出存到磁盘,模型只看摘要预览。替换决策一旦做出就被冻结,以保证缓存的一致性。
  2. 噪声直接删除:低价值的内容(如大量搜索结果中只被使用了几行的内容)直接移除,不做摘要——对噪声做摘要只是在浪费 token。
  3. API 层微压缩:通过 API 层的上下文编辑能力,指示服务端从前缀中移除指定的工具结果,本地消息保持不变。这一层的优势是零本地实现成本、由服务端一次性完成;但按本章的前缀不变性原理,移除点之后的缓存同样会失效,产生一次缓存重建。因此它适合在上下文即将溢出、反正要付出这次重建代价时使用,而不是频繁触发。
  4. 归档式摘要:逐轮做结构化摘要(像 git log 那样保留每轮的独立记录,而非像 git squash 那样合并成一条),保留对话的逻辑脉络。
  5. 全量压缩:由 LLM 驱动的完整压缩,作为最后手段。即便如此也是分两个阶段的:先尝试压缩会话记忆,不行再做全量压缩。全量压缩还配备了连续失败的熔断器(即连续失败达到一定次数后自动停止重试的机制)——生产数据表明,大量会话会被困在反复压缩失败的循环中,熔断器避免了在这些会话上持续烧钱。

注意这五层的排列顺序:前三层实现成本最低、对缓存的扰动可控,应当优先使用;后两层成本较高但压缩效果更强,作为兜底手段。

压缩策略的设计原则

前面已经分析了压缩的两个动机(控制长度与提升思考质量)和“上下文学习本质上是检索”的内部机制。在此基础上,我们可以提炼出指导具体压缩策略设计的四条原则。这里的压缩服务于当前任务;当多次任务的轨迹需要被离线整理为持久经验时,则进入第八章讨论的持续进化问题。

  • 信息价值的非均匀分布:关键的决策点(如人员名单)的价值高于支撑性的证据(如新闻细节),更高于冗余的噪声(如网页导航栏、页脚广告等元素)
  • 语义完整性:“Sutskever 于 2024 年 5 月离开 OpenAI”不能压缩成“Sutskever 离开”——时间和公司名是不可丢失的关键信息
  • 任务相关性:同样的内容在“查找创始人名单”和“了解个人背景”两个不同的任务下,应该产生不同的压缩结果
  • 压缩即理解:有效的压缩需要深层的语义理解能力——用更精炼的表达来捕捉上下文的精髓。而且显式压缩的结果是可审查的、可跨会话复用的

对 Agent 架构设计的启示

上下文压缩策略的研究触及了 Agent 系统设计的本质问题。压缩即理解——负责压缩的模块本身需要接近主模型的语言理解能力,形成“模型调用模型”的递归架构。压缩策略与任务类型耦合——信息检索类的任务需要保留广度,分析类的任务需要保留深度,创作类的任务需要保留灵感触发点,未来的 Agent 应当具备根据任务类型自适应选择压缩策略的能力。

虽然压缩需要额外的计算开销(每次压缩就是一次额外的 LLM 调用),但相比节省的 token 成本和提升的任务成功率,投资回报率是极高的——实验显示上下文感知压缩将 token 使用量减少了 75% 以上。

压缩最容易丢失的不是细节本身,而是早期的架构决策、约束背后的理由和失败的路径——LLM 通常会优先删除那些看起来还可以重新获取的信息。在生产级的 Agent 系统中,建议显式定义压缩时的保留优先级:

  1. 架构决策和关键约束:不得摘要
  2. 已修改的文件列表和关键的变更记录:完整保留
  3. 验证状态(pass/fail):必须保留
  4. 未解决的 TODO 和回滚笔记:必须保留
  5. 工具输出:可以删除,仅保留 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 的持续进化”处理的是另一时间尺度的问题:如何评价跨任务轨迹,并把其中的共性转化为会改变未来版本的持久更新。

http://www.cnnetsun.cn/news/3807894.html

相关文章:

  • DAMA框架:企业数据资产化与治理实践指南
  • 硬核抗老:防晒如何阻挡光老化痕迹
  • TypeScript 入门指南
  • 华为S5700交换机系统文件丢失故障诊断与完整恢复指南
  • 英雄联盟豹女陷阱流玩法解析:符文出装与实战博弈
  • 25 YOLOv8中Bin的偏移量是相对于谁的——网格、乘数与框大小的关系
  • Matlab实现综合能源系统规划的Benders分解法
  • 二叉树遍历算法解析与多语言实现
  • ROS依赖管理深度解析:从rosdep原理到实战问题排查
  • 亚洲服务器管理地址
  • 2019年信奥赛C++提高组真题解析:指针、递归与位运算
  • AES-CBC加密在分布式系统ID转换中的实践与优化
  • 阿里巴巴Spring全家桶笔记解析与实战指南
  • 开源VDI-WEB云桌面部署指南:基于Proxmox VE的私有云桌面实践
  • 并查集原理与优化实现详解
  • 深度对比:Mapbox GL JS vs Maptalks,WebGIS 开发该如何选型?
  • Atom 比 RSS 更出色:关键差异解析与应用困境
  • 算法-DFS+BFS+拓扑排列
  • GetQzonehistory:三步轻松备份你的QQ空间十年回忆
  • boss项目 岗位搜索与详情,简历中心和投递
  • 临床预测模型快速入门:基于Python与AutoML的实践指南
  • 【2026年拼多多暑期实习/秋招- 8月2日-第四题- 环形分厂协调补货】(题目+思路+JavaC++Python解析+在线测试)
  • 射频工程师成长指南:从理论到实践,突破独立设计三大关卡
  • springboot 奖助学金申报与评审系统
  • 从教程到实战:构建个人博客系统的全链路开发思维与工程实践
  • XIAO ESP32-S3开发板快速上手:从硬件解析到实战编程
  • 计网八股--DNS的域名解析过程?
  • Unity移动端内存优化实战:从托管堆到本机堆的全面解决方案
  • 智能临时文件清理系统设计与企业级实践
  • 三相并联有源电力滤波器设计与dq0变换谐波抑制技术