参考:《AI Agents in Depth》第 2 章:上下文工程

思维导图

1. 什么是上下文工程

上下文(Context)是模型在某一次推理时实际看到的全部信息,包括系统提示词、工具定义、用户消息、模型之前的回复、工具执行结果、按需加载的 Skill、当前状态摘要等。

提示词(Prompt)只是上下文的一部分。Prompt Engineering 主要解决“指令怎么写”,Context Engineering 则解决一个更完整的问题:

在每一个决策点,应该让 Agent 看到什么信息、以什么结构看到、哪些信息应当保留或删除,以及如何兼顾正确性、成本、延迟与安全。

从形式上看,模型每一步的输出可以理解为:

$$ y_t = f_{\theta}(c_t) $$

其中 $\theta$ 是模型参数,决定模型具备的通用知识与推理能力;$c_t$ 是第 $t$ 步的上下文,决定模型此刻能依据哪些信息做决策。模型参数与上下文不是二选一:模型参数提供能力,Context 为能力提供任务条件和外部事实。

“Context 决定 Agent 的能力上限”更准确的理解是:在模型能力固定时,Agent 的有效能力上限受它能够观察到的信息限制。上下文无法凭空赋予模型完全不具备的能力,但缺少业务规则、环境状态或工具结果时,再强的模型也只能猜测。

1.1 哪些信息应进入 Context

  • 当前任务目标、成功标准和不可违反的约束。
  • 与当前决策直接相关的业务规则和领域知识。
  • 环境观察,例如工作目录、代码结构、时间、错误信息。
  • 可用工具及其使用边界。
  • 必要的历史轨迹:用户请求、关键决策、工具调用及结果。
  • 当前任务状态:已完成事项、未解决问题、失败次数、下一步计划。

不应长期放入 Context 的内容包括:与当前任务无关的完整手册、已经失效的状态、重复的原始工具输出、可以随时重新获取的低价值噪声,以及来源不可信却被包装成高优先级指令的外部内容。

1.2 为什么不只讲 Prompt Engineering

生产级 Agent 面对的已经不是“一次写好一句提示词”,而是持续构造和维护上下文:

  • 静态指令与动态状态需要分层。
  • 对话历史和工具结果会不断增长。
  • 专业知识需要按需加载。
  • 长上下文需要压缩、归档或隔离。
  • 外部内容可能携带提示注入。
  • 上下文布局会直接影响缓存命中、延迟和费用。

因此,Prompt Engineering 是 Context Engineering 的一个子集。

即使未来上下文窗口无限大,Context Engineering 仍然必要。窗口“装得下”不代表模型“找得到、分得清、用得对”;无关信息会造成上下文腐化,恶意内容会扩大攻击面,重复内容仍会增加推理成本和延迟。

2. 从 API 看 Agent 的上下文

2.1 四种消息角色与工具定义

主流模型使用结构化 Chat API,而不是直接接收一整段任意字符串。典型请求由四种消息角色和独立的工具定义构成:

组成 含义 解决的问题
system 开发者提供的身份、规则、约束与流程 确立高优先级行为边界
user 用户请求;部分 Harness 也借此角色注入运行时状态 表达当前任务或追加信息
assistant 模型之前的回复或工具调用请求 让模型看到自己此前的判断与行动
tool 工具执行结果,通过 tool_call_id 对应调用 将外部世界的观察送回模型
tools 请求顶层的工具名称、描述和参数 schema 告诉模型有哪些动作以及如何调用

结构化消息最终会经由 Chat Template 转成模型实际处理的 token 序列。角色不是单纯的显示标签,而是模型在训练中学会识别的语义边界。

2.2 模型并不记得历史

模型 API 本身是无状态的。所谓“记得之前的对话”,实际上是 Agent 框架在每次请求时重新发送必要的历史消息。

一次带工具调用的核心循环是:

  1. 框架发送 system + user + tools
  2. 模型返回带有 tool_callsassistant 消息。
  3. 框架执行工具,将结果作为 tool 消息追加。
  4. 框架把更新后的消息列表再次发送给模型。
  5. 模型继续调用工具,或在信息充分时返回最终文本。

因此,Agent 框架的一项核心职责就是管理不断增长的 messages 列表。

2.3 上下文的基本布局

一个实用的抽象是:

1
2
3
4
5
稳定前缀:System Prompt + 核心 Tool Definitions

任务轨迹:User / Assistant / Tool / 按需加载的 Skill

动态尾部:最新用户输入 + 当前 Agent Status

稳定前缀尽量不变,轨迹只追加;旧轨迹在必要时批量压缩,当前状态放在靠近输出的位置,使模型不必每轮从长历史中重新推导。

3. KV Cache 友好的上下文设计

3.1 KV Cache 与 Prompt Cache

  • KV Cache 是单次推理过程中的模型内部缓存。模型保存已有 token 的 Key 和 Value,新 token 只需计算新增部分。
  • Prompt Cache 是推理服务跨 API 请求复用相同前缀计算结果的机制,本质上复用了已有的 KV 状态。

两者作用层级不同,但都依赖同一个条件:要复用的 token 前缀必须保持一致。如果序列从某个 token 开始变化,该位置及其后的缓存就需要重新计算;修改位置越靠前,代价通常越大。

3.2 特别需要注意的三条原则

  1. 系统提示词和核心工具定义一旦确定,尽量不要修改。 哪怕只增加一个空格,也可能改变 token 序列,使首个差异 token 之后的缓存无法复用。
  2. 动态信息尽量追加到上下文末尾。 时间戳、用户状态、当前任务进度等不应每轮写回系统提示词。
  3. 使用标准 API 消息格式,不要自行拼接角色字符串。 自己拼成 USER: ... ASSISTANT: ... 不一定破坏缓存——缓存只认 token 前缀——但它偏离模型训练时使用的 Chat Template,容易造成角色边界、工具调用和多轮行为异常。

例如,把 Current time: {{now}} 放进 system prompt,会使每次请求都从时间戳位置开始产生不同 token,导致后面的大段前缀重新计算。正确做法是把时间作为新的尾部消息或事件元数据追加。

3.3 推理质量与缓存命中的取舍

上下文设计同时追求两个目标:

  1. 推理效果:把最重要的信息放到模型容易识别和利用的位置。
  2. 推理效率:保持前缀稳定,尽可能复用缓存。

当二者冲突时,应遵循:

正确性与 Agent 决策质量 > KV Cache 命中率。

例如状态栏每轮都会变化。为了缓存而把状态藏在不明显的位置,可能导致 Agent 忘记当前阶段;合理做法是将结构化状态放在上下文尾部。即使更新状态会让一小段尾部缓存失效,也比模型基于错误状态行动更可接受。

3.4 工具渐进式披露的缓存边界

传统做法是让 System Prompt 与完整工具定义共同组成稳定前缀。这种方式简单、稳定,但工具数量很多时会浪费 token 并稀释注意力。

渐进式披露只先暴露工具名和简述,在确定需要时再加载完整 schema。不过需要注意:

  • 如果 Harness 将新增 schema 作为末尾消息或工具结果追加,已有前缀仍然可以复用。
  • 如果 Harness 修改顶层 tools 字段,并由 Chat Template 把整个工具块序列化到上下文前部,那么工具集合变化仍可能破坏对应位置后的缓存。

所以“动态加载工具不破坏缓存”不是无条件成立的,必须结合具体 Harness、模型接口和 Chat Template 判断。

3.5 前沿方向:可编辑、可组合的 KV Cache

传统 KV Cache 是严格的顺序缓存,不能随意修改。前沿研究开始把它看作模型阅读上下文后形成的内部“笔记”,尝试实现:

  • Editing:局部事实变化时,只更新相关内部表示。
  • Composition:复用并拼接不同上下文块的缓存。

这可能把部分长上下文重算变成缓存块组合,但目前仍属于研究阶段。生产系统仍应默认遵守“稳定前缀、动态追加”的原则。

4. 系统提示词与工具定义

4.1 系统提示词的设计

可以把系统提示词理解为给聪明新员工的工作手册:新员工读完后,应知道目标是什么、按什么流程执行、遇到异常怎么办、哪些边界不能越过。

设计时重点考虑:

  • 语气与风格:明确回答长度、表达方式和交互语气;强调词只留给真正关键的约束。
  • 结构化提示:使用清晰的 Markdown 层级或语义明确的 XML 标签区分规则、环境与外部数据。
  • 流程驱动:优先写成带顺序、分支和终止条件的 SOP,而不是堆砌大量平级规则。
  • 业务规则细化:将分类标准、阈值、计算方式和例外情况写到可执行、可验证的程度。
  • 减少自由裁量:让模型把能力用于真正需要判断的部分,而不是自行发明业务规则。

4.2 Few-shot 示例

当输出风格、格式或边界很难仅靠抽象规则说清时,可以提供少量高质量示例。模型已经擅长且规则容易描述的任务,则不必浪费 token。

示例有两种常见位置:

  • 放入系统提示词,作为所有请求共享的静态前缀。
  • 作为首轮伪造的 user/assistant 消息,适合不同会话类型使用不同的固定示例集。

无论放在哪里,示例通常位于上下文前部。确定后应尽量保持字节级稳定。按每个请求动态检索不同示例虽然可能提高相关性,却会改变前缀,需要在任务质量与缓存成本之间实测取舍。

4.3 工具定义也是 Context Engineering

工具 schema 不只是参数说明,它还在教模型:

  • 什么时候应该调用。
  • 什么时候不应调用。
  • 参数应采用什么格式。
  • 常见错误与使用边界是什么。
  • 与其他工具如何配合,哪些调用可以并行。

工具描述的质量会直接影响调用准确率。一个好的定义应包含明确边界、代表性示例、必要的性能提示和工具间协作关系。

5. 提示注入:上下文安全

注入方式 攻击入口 核心风险 主要防御
直接注入 用户输入 用户试图覆盖系统约束或套取敏感指令 明确指令优先级;权限控制;高风险操作确认
间接注入 网页、PDF、邮件、RAG、Tool Result Agent 把外部数据中的文本误当成可执行指令 指令与数据分离;来源标记;保留结构化角色;限制工具权限
记忆注入 长期 Memory、状态、Skill 等持久化信息 一次攻击变成跨轮次、跨会话污染 写入审核;来源追踪;可删除、可更新;外部数据不得自动晋升为高信任状态
状态或 Skill 污染 状态栏、Skill 文件、Agent 配置 恶意信息进入高信任上下文并持续影响决策 加载与更新权限校验;审查第三方 Skill;状态由可信代码维护

需要特别注意:仅在系统提示词中写“不要被注入”并不可靠。上下文层只能作为第一道防线,真正的高风险动作还需要执行层的权限控制、沙箱、独立校验和用户确认。

外部内容进入上下文时,最好显式标记来源,例如:

1
2
3
<external_content source="webpage" trust="untrusted">
...
</external_content>

把工具结果直接混入普通 user 文本,会抹掉模型区分“用户指令”和“外部数据”的依据。

6. 动态提示词与 Agent Skills

如果把所有任务规范、业务流程和专业知识都写进 system prompt,会产生两个问题:

  • 浪费 token:大多数规则与当前任务无关。
  • 稀释注意力:无关信息越多,关键规则越难被检索到。

Skills 将领域能力拆成可组合、可按需加载的知识包,采用渐进式披露:

  1. 元数据层:常驻 Skill 名称和简短描述,让 Agent 判断是否需要加载。
  2. 核心流程层:触发后加载完整 SKILL.md
  3. 细则层:根据具体任务继续读取引用文档、脚本或模板。

其中 description 本质上是路由条件,应说明“何时使用、何时不使用”,而不仅是泛泛介绍功能。描述过宽会导致误触发,描述过窄则会漏掉适用任务。

Skills 与工具的关系可以概括为:

  • 工具提供可执行动作,即“能做什么”。
  • Skill 提供任务知识与操作流程,即“何时做、按什么顺序做、如何验收”。

渐进式披露对缓存友好,但不是零成本:目录首次进入、Skill 正文首次加载时仍需计算;收益来自无需一开始加载所有正文,也无需回头重写已有轨迹。

7. Agent 状态栏

7.1 为什么需要状态栏

模型擅长从上下文中检索已有结论,却不擅长在每一轮都主动遍历长轨迹、统计次数并重新归纳当前状态。复杂任务中,这容易造成:

  • 无限循环或重复调用同一工具。
  • 忘记任务进度和原始目标。
  • 忽略已经达到的次数、费用或权限上限。
  • 因局部子任务而偏离整体计划。

状态栏把原本隐藏在历史中的状态预先计算成结构化信息,让模型可以直接检索。

7.2 状态栏应包含什么

  • 任务目标、当前阶段、TODO 及完成情况。
  • 时间、位置、事件间隔等侧信道信息。
  • 当前工作目录、运行环境等环境观察。
  • 工具调用计数、成本或重试上限。
  • 详细错误类别、关键参数与可行的修复方向。
  • 当前阻塞项、验证结果和下一步计划。

示例:

1
2
3
4
5
6
7
<agent_status>
current_task: 修复支付 Bug
phase: 测试
unresolved: 数据库事务问题
tests: 18 passed, 1 failed
retry_count: 2/3
</agent_status>

7.3 放置位置与维护原则

状态栏通常作为一条由 Agent 框架生成的消息放在上下文末尾,而不是修改开头的 system prompt。教程示例借用 user 角色承载状态,但这属于具体 Harness 的协议选择,不表示状态真的来自终端用户。

状态栏最好由确定性代码维护,不要让 LLM 一次性扫描完整历史后自行统计。

能用代码计算的次数、TODO 状态、工作目录和测试结果,应由代码维护;确实需要模型抽取时,也应逐条抽取后再由代码汇总。

状态栏是有损投影,默认不应因为有了状态栏就立刻删除全部原始证据。模型往往高度信任状态栏,因此状态栏准确率应作为生产指标监控,并防止外部内容污染状态。

7.4 两种更新方式

方式 优点 代价 适用情况
每轮替换旧状态 上下文中只有一份最新状态,不会积累陈旧信息 旧状态之后的短后缀缓存失效 状态较大、更新频繁、轨迹较长
持久追加新状态 只追加不修改,最利于缓存复用 旧状态占用 token,可能造成歧义 状态较小、会话受控、两次更新之间新增内容较多

这里没有绝对最优方案,应根据状态大小、更新频率、后缀长度、上下文预算和服务商缓存计费实测选择。

8. 上下文压缩

8.1 为什么需要压缩

压缩有两个目的:

  1. 控制长度、成本和延迟:避免消息历史与大体积工具结果撑满窗口。
  2. 提高思考质量:把分散的原始记录变成结构化、高密度、易检索的结论。

上下文窗口没有溢出,也可能发生上下文腐化(Context Rot):信息虽然装得下,但无关内容太多,模型越来越难找到关键事实,甚至反复纠结已经解决的问题。

状态栏与压缩实际上是一体两面:状态栏把推导出的结论加入上下文;压缩则用结论替换臃肿的原始记录。二者都在把“每轮重新归纳”变成“直接检索已提炼知识”。

8.2 压缩与 KV Cache 的关系

压缩会修改历史消息,因此替换点之后的缓存会失效。但这并不意味着不能压缩:

  • System Prompt 和核心工具定义保持不动。
  • 优先压缩体积大的旧工具结果和低价值轨迹。
  • 接近预算阈值时批量压缩,不要每轮修改历史。
  • 用一次可控的缓存重建,换取更短、更高质量的后续上下文。

正确性、任务可继续执行和信息密度,都比维持百分之百的缓存命中更重要。

8.3 分层压缩策略

生产系统可以按信息价值逐层处理:

  1. 工具结果预算控制:大体积原文存到文件或外部存储,Context 只保留摘要与引用。
  2. 直接删除噪声:导航栏、重复日志、无关搜索结果不值得专门摘要。
  3. 批量压缩旧工具结果:在接近阈值时统一处理,并标记已压缩内容以避免重复压缩。
  4. 归档式摘要:保留逐轮逻辑脉络、关键决策和失败路径。
  5. 全量压缩:作为最后手段,并为连续失败设置熔断机制。

压缩时应优先保留:

  1. 架构决策、关键约束及其理由。
  2. 已修改文件与关键变更。
  3. 验证结果和失败结论。
  4. 未解决 TODO、回滚方案和已尝试但失败的路径。
  5. 事实来源、引用或可以回溯原文的索引。

工具输出原文通常可以删除,只保留与当前任务相关的证据和结论。

8.4 隔离优于压缩

压缩是在信息进入主 Context 后做减法;更好的办法常常是让大量中间信息一开始就不要进入主 Context。

例如,让子 Agent 在独立上下文中搜索十几个文件,再只把“目标函数位置、调用点、关键证据”返回给主 Agent。这样既减少主上下文噪声,也降低后续压缩成本。

9. 一套可落地的多轮 Agent 组织原则

综合效果、效率与安全,可以采用以下默认策略:

  1. 固定 System Prompt、核心工具定义和模型配置,形成稳定前缀。
  2. 使用标准 Chat API 与角色结构,保留 assistant → tool 的调用对应关系。
  3. 只把当前任务需要的 Skill、知识和工具细则按需加载。
  4. 用户输入、工具结果、动态环境信息只追加,不改写已有前缀。
  5. 在尾部放置结构化状态栏,显式呈现目标、进度、约束和异常。
  6. 状态由代码维护;外部数据不能未经审核直接写入状态或长期记忆。
  7. 监控 token 使用率和上下文腐化,在阈值附近批量压缩旧轨迹。
  8. 压缩时保留决策、约束、验证、失败路径和来源索引。
  9. 对会产生大量中间信息的探索任务,优先使用子 Agent 隔离。
  10. 当缓存收益与任务正确性冲突时,优先保证正确性。

10. 核心结论

  • 模型参数决定通用能力,Context 决定能力在当前任务中能否发挥。
  • Agent 并不天然拥有记忆;框架通过重建消息列表制造连续性。
  • Context 的基本结构是“稳定前缀 + 动态轨迹 + 最新状态”。
  • 缓存要求前缀稳定,因此静态内容靠前、动态内容靠后。
  • 上下文不是越长越好,关键是信息相关、结构清晰、来源可信、状态准确。
  • Prompt、工具定义、Skills、状态栏、压缩和安全防御,都是 Context Engineering 的组成部分。
  • Context Engineering 的最终目标不是把更多内容塞给模型,而是在每个决策点提供足够、准确、可用且成本可控的信息