上下文工程
思维导图
mindmap
root((上下文工程))
核心问题
每个决策点让 Agent 看到什么
信息以什么结构出现
如何兼顾正确性、效率与安全
基础认知
Context 是单次推理看到的全部信息
Prompt 只是 Context 的一部分
模型参数提供通用能力
Context 提供任务条件与外部事实
窗口无限也仍需管理信息质量
API 上下文结构
四种消息角色
system:规则与约束
user:任务与追加信息
assistant:历史回复与工具调用
tool:外部工具结果
tools:工具定义与参数 schema
Chat Template 转换为 token
API 无状态
Agent 框架维护消息轨迹
ReAct 循环
模型决策
框架执行工具
结果写回 Context
KV Cache 友好设计
KV Cache:单次推理复用 K/V
Prompt Cache:跨请求复用前缀
稳定前缀
System Prompt 尽量不变
核心工具定义尽量不变
动态信息追加到末尾
使用标准消息格式
正确性优先于缓存命中率
前沿方向
Cache Editing
Cache Composition
系统提示词与工具
语气与风格
Markdown 或 XML 结构化
流程驱动而非规则堆砌
业务规则细化到可执行
Few-shot 覆盖关键边界
工具定义说明调用时机与边界
提示注入防御
指令与数据分离
标记外部内容来源
权限控制与高风险确认
Agent Skills
渐进式披露
元数据目录常驻
SKILL.md 按需加载
细则、脚本和模板选择性读取
description 是路由条件
Skill 提供流程与知识
Tool 提供可执行动作
Agent 状态栏
将隐式状态提炼为显式信息
状态内容
任务规划与 TODO
事件侧信道信息
环境当前状态
工具计数、错误与验证结果
放在 Context 尾部
三条经验
用确定性代码维护
默认保留原始证据
将准确率作为生产指标
更新策略
每轮替换
持久追加
上下文压缩
两个目的
控制长度、成本与延迟
提高信息密度与思考质量
上下文腐化
装得下但找不到
检索而非主动归纳
接近阈值后批量压缩
分层策略
工具结果预算控制
删除低价值噪声
归档式摘要
全量压缩兜底
重点保留
决策与约束
修改与验证
失败路径与未完成事项
来源引用
隔离优于压缩
子 Agent 处理大量中间信息
主 Agent 只接收结论与证据
1. 什么是上下文工程
上下文(Context)是模型在某一次推理时实际看到的全部信息,包括系统提示词、工具定义、用户消息、模型之前的回复、工具执行结果、按需加载的 Skill、当前状态摘要等。
提示词(Prompt)只是上下文的一部分。Prompt Engineering 主要解决“指令怎么写”,Context Engineering 则解决一个更完整的问题:
在每一个决策点,应该让 Agent 看到什么信息、以什么结构看到、哪些信息应当保留或删除,以及如何兼顾正确性、成本、延迟与安全。
从形式上看,模型每一步的输出可以理解为:
其中 $\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 框架在每次请求时重新发送必要的历史消息。
一次带工具调用的核心循环是:
- 框架发送
system + user + tools。 - 模型返回带有
tool_calls的assistant消息。 - 框架执行工具,将结果作为
tool消息追加。 - 框架把更新后的消息列表再次发送给模型。
- 模型继续调用工具,或在信息充分时返回最终文本。
因此,Agent 框架的一项核心职责就是管理不断增长的 messages 列表。
2.3 上下文的基本布局
一个实用的抽象是:
1 | 稳定前缀:System Prompt + 核心 Tool Definitions |
稳定前缀尽量不变,轨迹只追加;旧轨迹在必要时批量压缩,当前状态放在靠近输出的位置,使模型不必每轮从长历史中重新推导。
3. KV Cache 友好的上下文设计
3.1 KV Cache 与 Prompt Cache
- KV Cache 是单次推理过程中的模型内部缓存。模型保存已有 token 的 Key 和 Value,新 token 只需计算新增部分。
- Prompt Cache 是推理服务跨 API 请求复用相同前缀计算结果的机制,本质上复用了已有的 KV 状态。
两者作用层级不同,但都依赖同一个条件:要复用的 token 前缀必须保持一致。如果序列从某个 token 开始变化,该位置及其后的缓存就需要重新计算;修改位置越靠前,代价通常越大。
3.2 特别需要注意的三条原则
- 系统提示词和核心工具定义一旦确定,尽量不要修改。 哪怕只增加一个空格,也可能改变 token 序列,使首个差异 token 之后的缓存无法复用。
- 动态信息尽量追加到上下文末尾。 时间戳、用户状态、当前任务进度等不应每轮写回系统提示词。
- 使用标准 API 消息格式,不要自行拼接角色字符串。 自己拼成
USER: ... ASSISTANT: ...不一定破坏缓存——缓存只认 token 前缀——但它偏离模型训练时使用的 Chat Template,容易造成角色边界、工具调用和多轮行为异常。
例如,把 Current time: {{now}} 放进 system prompt,会使每次请求都从时间戳位置开始产生不同 token,导致后面的大段前缀重新计算。正确做法是把时间作为新的尾部消息或事件元数据追加。
3.3 推理质量与缓存命中的取舍
上下文设计同时追求两个目标:
- 推理效果:把最重要的信息放到模型容易识别和利用的位置。
- 推理效率:保持前缀稳定,尽可能复用缓存。
当二者冲突时,应遵循:
正确性与 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 | <external_content source="webpage" trust="untrusted"> |
把工具结果直接混入普通 user 文本,会抹掉模型区分“用户指令”和“外部数据”的依据。
6. 动态提示词与 Agent Skills
如果把所有任务规范、业务流程和专业知识都写进 system prompt,会产生两个问题:
- 浪费 token:大多数规则与当前任务无关。
- 稀释注意力:无关信息越多,关键规则越难被检索到。
Skills 将领域能力拆成可组合、可按需加载的知识包,采用渐进式披露:
- 元数据层:常驻 Skill 名称和简短描述,让 Agent 判断是否需要加载。
- 核心流程层:触发后加载完整
SKILL.md。 - 细则层:根据具体任务继续读取引用文档、脚本或模板。
其中 description 本质上是路由条件,应说明“何时使用、何时不使用”,而不仅是泛泛介绍功能。描述过宽会导致误触发,描述过窄则会漏掉适用任务。
Skills 与工具的关系可以概括为:
- 工具提供可执行动作,即“能做什么”。
- Skill 提供任务知识与操作流程,即“何时做、按什么顺序做、如何验收”。
渐进式披露对缓存友好,但不是零成本:目录首次进入、Skill 正文首次加载时仍需计算;收益来自无需一开始加载所有正文,也无需回头重写已有轨迹。
7. Agent 状态栏
7.1 为什么需要状态栏
模型擅长从上下文中检索已有结论,却不擅长在每一轮都主动遍历长轨迹、统计次数并重新归纳当前状态。复杂任务中,这容易造成:
- 无限循环或重复调用同一工具。
- 忘记任务进度和原始目标。
- 忽略已经达到的次数、费用或权限上限。
- 因局部子任务而偏离整体计划。
状态栏把原本隐藏在历史中的状态预先计算成结构化信息,让模型可以直接检索。
7.2 状态栏应包含什么
- 任务目标、当前阶段、TODO 及完成情况。
- 时间、位置、事件间隔等侧信道信息。
- 当前工作目录、运行环境等环境观察。
- 工具调用计数、成本或重试上限。
- 详细错误类别、关键参数与可行的修复方向。
- 当前阻塞项、验证结果和下一步计划。
示例:
1 | <agent_status> |
7.3 放置位置与维护原则
状态栏通常作为一条由 Agent 框架生成的消息放在上下文末尾,而不是修改开头的 system prompt。教程示例借用 user 角色承载状态,但这属于具体 Harness 的协议选择,不表示状态真的来自终端用户。
状态栏最好由确定性代码维护,不要让 LLM 一次性扫描完整历史后自行统计。
能用代码计算的次数、TODO 状态、工作目录和测试结果,应由代码维护;确实需要模型抽取时,也应逐条抽取后再由代码汇总。
状态栏是有损投影,默认不应因为有了状态栏就立刻删除全部原始证据。模型往往高度信任状态栏,因此状态栏准确率应作为生产指标监控,并防止外部内容污染状态。
7.4 两种更新方式
| 方式 | 优点 | 代价 | 适用情况 |
|---|---|---|---|
| 每轮替换旧状态 | 上下文中只有一份最新状态,不会积累陈旧信息 | 旧状态之后的短后缀缓存失效 | 状态较大、更新频繁、轨迹较长 |
| 持久追加新状态 | 只追加不修改,最利于缓存复用 | 旧状态占用 token,可能造成歧义 | 状态较小、会话受控、两次更新之间新增内容较多 |
这里没有绝对最优方案,应根据状态大小、更新频率、后缀长度、上下文预算和服务商缓存计费实测选择。
8. 上下文压缩
8.1 为什么需要压缩
压缩有两个目的:
- 控制长度、成本和延迟:避免消息历史与大体积工具结果撑满窗口。
- 提高思考质量:把分散的原始记录变成结构化、高密度、易检索的结论。
上下文窗口没有溢出,也可能发生上下文腐化(Context Rot):信息虽然装得下,但无关内容太多,模型越来越难找到关键事实,甚至反复纠结已经解决的问题。
状态栏与压缩实际上是一体两面:状态栏把推导出的结论加入上下文;压缩则用结论替换臃肿的原始记录。二者都在把“每轮重新归纳”变成“直接检索已提炼知识”。
8.2 压缩与 KV Cache 的关系
压缩会修改历史消息,因此替换点之后的缓存会失效。但这并不意味着不能压缩:
- System Prompt 和核心工具定义保持不动。
- 优先压缩体积大的旧工具结果和低价值轨迹。
- 接近预算阈值时批量压缩,不要每轮修改历史。
- 用一次可控的缓存重建,换取更短、更高质量的后续上下文。
正确性、任务可继续执行和信息密度,都比维持百分之百的缓存命中更重要。
8.3 分层压缩策略
生产系统可以按信息价值逐层处理:
- 工具结果预算控制:大体积原文存到文件或外部存储,Context 只保留摘要与引用。
- 直接删除噪声:导航栏、重复日志、无关搜索结果不值得专门摘要。
- 批量压缩旧工具结果:在接近阈值时统一处理,并标记已压缩内容以避免重复压缩。
- 归档式摘要:保留逐轮逻辑脉络、关键决策和失败路径。
- 全量压缩:作为最后手段,并为连续失败设置熔断机制。
压缩时应优先保留:
- 架构决策、关键约束及其理由。
- 已修改文件与关键变更。
- 验证结果和失败结论。
- 未解决 TODO、回滚方案和已尝试但失败的路径。
- 事实来源、引用或可以回溯原文的索引。
工具输出原文通常可以删除,只保留与当前任务相关的证据和结论。
8.4 隔离优于压缩
压缩是在信息进入主 Context 后做减法;更好的办法常常是让大量中间信息一开始就不要进入主 Context。
例如,让子 Agent 在独立上下文中搜索十几个文件,再只把“目标函数位置、调用点、关键证据”返回给主 Agent。这样既减少主上下文噪声,也降低后续压缩成本。
9. 一套可落地的多轮 Agent 组织原则
综合效果、效率与安全,可以采用以下默认策略:
- 固定 System Prompt、核心工具定义和模型配置,形成稳定前缀。
- 使用标准 Chat API 与角色结构,保留
assistant → tool的调用对应关系。 - 只把当前任务需要的 Skill、知识和工具细则按需加载。
- 用户输入、工具结果、动态环境信息只追加,不改写已有前缀。
- 在尾部放置结构化状态栏,显式呈现目标、进度、约束和异常。
- 状态由代码维护;外部数据不能未经审核直接写入状态或长期记忆。
- 监控 token 使用率和上下文腐化,在阈值附近批量压缩旧轨迹。
- 压缩时保留决策、约束、验证、失败路径和来源索引。
- 对会产生大量中间信息的探索任务,优先使用子 Agent 隔离。
- 当缓存收益与任务正确性冲突时,优先保证正确性。
10. 核心结论
- 模型参数决定通用能力,Context 决定能力在当前任务中能否发挥。
- Agent 并不天然拥有记忆;框架通过重建消息列表制造连续性。
- Context 的基本结构是“稳定前缀 + 动态轨迹 + 最新状态”。
- 缓存要求前缀稳定,因此静态内容靠前、动态内容靠后。
- 上下文不是越长越好,关键是信息相关、结构清晰、来源可信、状态准确。
- Prompt、工具定义、Skills、状态栏、压缩和安全防御,都是 Context Engineering 的组成部分。
- Context Engineering 的最终目标不是把更多内容塞给模型,而是在每个决策点提供足够、准确、可用且成本可控的信息。
