参考:《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_calls 的 assistant 消息。
  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. 上下文压缩

《AI Agents in Depth》第 2 章:上下文工程 的上下文压缩不太适合系统化记忆,这里我按照Claude Code里面的三层压缩进行学习。

Layer 1:micro_compact

无api调用,仅仅是把很久之前的工具调用结果进行一个占位符替换,只保留最近k条工具执行结果。
实际上为了考虑到缓存复用,Claude Code里面的micro_compact是用API 层的 cache_edits 对已经缓存的历史块进行就地编辑,从而尽量保留已有缓存。我们作为调用者没有这个权限,就只能在外部做一个占位符替换。

Layer 2:auto_compact

当上下文长度超过阈值时,自动触发压缩。把对话历史落盘到文件中,并利用上下文感知压缩仅在文中留一个摘要

Layer 3:manual_compact

由模型自己决定是否需要压缩上下文或者用户强制触发,并生成一个摘要。这个摘要会被放在上下文中,替代原来的对话历史,从而减少上下文长度。效果跟Layer 2类似,但是是由模型自己决定是否需要压缩上下文,而不是由系统自动触发。

摘要时具体需要保留什么:

  1. 架构决策和关键约束 :不得摘要
  2. 已修改的文件列表和关键的变更记录 :完整保留
  3. 验证状态 (pass/fail):必须保留
  4. 未解决的 TODO 和回滚笔记 :必须保留
  5. 工具输出 :可以删除,仅保留 pass/fail 结论
优先级 要保留什么 例子
⭐⭐⭐⭐⭐ 当前任务/目标 “正在实现一个 RAG 检索模块”
⭐⭐⭐⭐⭐ 已经完成的工作 “已经实现 BM25 + 向量检索,尚未实现 reranker”
⭐⭐⭐⭐⭐ 关键结论/决策 “最终选择 BGE-M3,而不是 E5”
⭐⭐⭐⭐⭐ 当前状态 / 进度 “代码已经改完,但测试还有 2 个失败”
⭐⭐⭐⭐⭐ 未解决的问题 “Milvus 在启动时出现连接超时”
⭐⭐⭐⭐ 用户的明确要求/约束 “不要修改 KG 的结构”
⭐⭐⭐⭐ 关键上下文/背景 项目的架构、重要文件、模块关系
⭐⭐⭐⭐ 工具执行的重要结果 测试结果、编译错误、命令输出中的关键结论
⭐⭐⭐ 重要文件/代码位置 src/retriever.py 中的 retrieve() 已修改
⭐⭐ 普通对话过程 一般可以丢掉
⭐ 寒暄、重复解释、无关工具输出 基本可以直接丢