上下文工程的核心不只是“怎么写 Prompt”,而是:把上下文当成一套持续运行的信息供给系统,在每次模型决策前,决定该让模型看到什么、以什么结构看到、哪些内容保持稳定、哪些内容按需加载、哪些历史需要提炼或隔离。
Ct=Pstable+Tt+St C_t = P_{\text{stable}} + T_t + S_tCt=Pstable+Tt+St
- PstableP_{\text{stable}}Pstable:稳定前缀,包括系统提示词、核心工具定义。
- TtT_tTt:不断增长的任务轨迹,包括用户消息、模型回复、工具调用和工具结果。
- StS_tSt:当前状态摘要,包括 TODO、环境状态、调用计数、约束是否触发等。
一条主线
1. 上下文决定的是 Agent 的能力上限
模型本身只有通用能力,不知道具体项目里的:
- 代码结构和业务规则;
- 团队流程和权限边界;
- 当前环境、任务进度和历史决策;
- 哪些信息是用户指令,哪些只是网页、文档或工具返回的数据。
因此,上下文工程的目标不是“尽量塞更多信息”,而是:
在每个决策点,以最低成本给模型提供足够、准确、结构清晰的信息。
一个中等模型配上高质量上下文,可能胜过一个更强模型在信息不足时的盲目探索。它也是组织问题:文档、决策记录和规则越透明,Agent 越容易工作。
2. API 是无状态的,Harness 负责重建状态
每次模型调用本质上都是一次独立请求。模型不会自动记住上一轮发生了什么,Harness 必须重新提交:
system:开发者规则;user:用户输入;assistant:模型之前的回复和工具调用;tool:工具执行结果;tools:模型可调用的工具定义。
典型循环是:
用户请求 → 模型决定调用工具 → Harness 执行工具 → 把 assistant tool_calls 和 tool results 放回历史 → 再次调用模型 → 得到最终回答或继续调用工具这里最重要的思想是:模型负责决策,Harness 负责执行和上下文管理。消息角色、tool_call_id和工具结果的来源不能混淆,否则模型会错误理解历史。
3. 上下文首先要分成“稳定前缀”和“动态轨迹”
这是整章的架构地基:
稳定前缀 ├── System Prompt └── 核心 Tool Definitions 动态轨迹 ├── 用户消息 ├── 模型回复 ├── 工具调用 ├── 工具结果 ├── 按需加载的 Skill/工具定义 └── 当前状态栏稳定前缀尽量不动,动态信息尽量追加到后面。原因既包括语义清晰,也包括 KV Cache 和 Prompt Cache 的经济性。
三条硬规则
- 系统提示词和核心工具定义确定后尽量保持字节级稳定。
- 时间、工作目录、用户状态等动态信息追加到末尾,不要修改系统提示词。
- 使用标准消息角色和 Chat Template,不要手工拼成一大段
"USER: ... TOOL: ..."字符串。
例如把Current time: {{now}}写进系统提示词,会让每次请求的前缀不同,导致后续缓存大面积失效。缓存因此不只是性能优化,而会反过来影响 Agent 的架构设计。
4. 静态规则应该流程化,而不是无限堆砌
系统提示词是 Agent 的“员工手册”。好的提示词应让一个聪明的新员工读完后知道:
- 自己的角色和目标;
- 按什么步骤工作;
- 每一步的验证条件;
- 哪些边界不能越过;
- 遇到异常时什么时候停止或请求确认。
本章主张优先使用:
验证 → 分类 → 执行 → 检查 → 完成而不是几十条没有优先级的零散规则。
提示词设计的几个重点:
- 用 Markdown/XML 明确层次和来源;
- 把业务规则写成可执行判断,不把关键决策留给模型自由发挥;
- 只有难以抽象描述的风格和格式才使用 few-shot;
- 两三个覆盖边界的高质量示例,通常胜过十个相似示例;
- 工具描述需要包含用途、参数语义、使用边界、示例和工具间关系。
本章实验发现,规则内容不变、只把结构打乱,也会显著降低任务成功率;说明“信息如何组织”与“信息是否存在”同样重要。
5. 不要把全部知识常驻:用 Skills 做渐进式披露
随着业务增加,所有领域规则都塞进系统提示词会产生两个问题:
- 浪费 token;
- 无关内容稀释注意力,造成上下文腐化。
Skills 的解决方案是“少量目录常驻,完整知识按需加载”:
第一层:Skill 元数据 name + description ↓ 路由判断 第二层:SKILL.md 核心流程 ↓ 根据任务继续深入 第三层:参考文档、模板、脚本、示例这里的关键不是 Skill 文件格式本身,而是信息生命周期:
- 可发现性信息常驻;
- 完整流程只有被选中时才加入轨迹;
- 更详细资料再按需读取;
- 已经加载的内容追加进入历史,而不是回头重写前缀。
同样的思想也适用于大量工具:启动时只提供工具名称和简述,模型需要时再加载完整 schema。
6. 模型擅长检索,不擅长自动归纳状态
这是状态栏和压缩机制共同的理论基础。
假如三次电话调用分散在几千个 token 的轨迹中,模型虽然能看到它们,却不一定能稳定地计算出“已经调用三次,达到上限”。它每一轮都需要重新扫描、计数和推理。
因此 Harness 应提前把隐式状态提炼成显式知识:
<agent_status> - Goal: cancel subscription - TODO: identity verification completed - phone_call: 3/3, limit reached - Current working directory: /project - Last validation: failed </agent_status>状态栏通常包含:
- 任务目标和 TODO;
- 已完成、进行中、取消的步骤;
- 工具调用次数和重复失败;
- 当前时间、工作目录、操作系统;
- 关键约束是否达到阈值;
- 最近一次验证结果。
核心原则是:
- 能由代码确定性计算的状态,就不要让另一个 LLM 批量总结;
- 状态栏只做投影,不能无条件替代原始证据;
- 模型会高度信任状态栏,所以其准确率本身是生产指标;
- 动态状态放在上下文末尾,而不是修改 system 前缀。
7. 压缩不只是为了“装得下”
上下文过长有两个不同问题:
- 上下文溢出:窗口真的装不下;
- 上下文腐化:虽然装得下,但关键信息被大量噪声淹没,模型找不到或用不好。
所以压缩有两个目标:
- 控制 token、成本和延迟;
- 把需要反复思考才能得到的结论,变成可直接检索的高密度知识。
例如,不要让模型每轮从十几次搜索结果中重新推导人员名单,而应生成:
已确认: - A:当前职位…… - B:于某年离职…… 仍缺: - C 的最新状态 来源: - URL 1 - URL 2生产级分层处理
按照信息价值,可以逐层处理:
- 大型工具输出写入磁盘,上下文只放摘要和路径;
- 导航栏、重复结果等纯噪声直接删除;
- 接近预算时批量移除低价值工具结果;
- 用结构化的逐轮摘要保留逻辑脉络;
- 最后才进行全量 LLM 压缩,并设置失败熔断器。
压缩时应明确保留优先级:
- 架构决策和关键约束;
- 已修改文件及关键变更;
- 验证的 pass/fail 状态;
- 未完成 TODO 和回滚信息;
- 事实来源与引用;
- 原始工具输出可以删除或外置。
8. 隔离通常比事后压缩更好
如果某个探索会产生海量中间信息,最好从一开始就不要把它放进主 Agent 上下文。
例如:
主 Agent → 委派:“找到支付回调函数和所有调用点” → 搜索子 Agent 阅读几十个文件 → 只返回位置、调用关系和关键证据 → 主上下文只增加几百 token这相当于:
- 压缩:噪声先进来,再有损清理;
- 隔离:噪声从未进入主上下文。
隔离还能保护主 Agent 的缓存和注意力,但要求委派任务足够自包含、目标和输出格式明确。
9. 上下文安全:必须区分“指令”和“数据”
网页、邮件、PDF、检索结果和第三方 Skill 都可能包含伪装指令。
上下文层面的第一道防线包括:
- 用
<external_content source="...">标注不可信内容; - 严格使用 system/user/assistant/tool 角色;
- 不把工具结果伪装成用户指令;
- 清洗明显的注入模式;
- 审查第三方 Skill;
- 不把未经验证的外部内容直接写进高可信状态栏。
但上下文防御不能代替权限、沙盒和高风险操作确认。提示注入一旦结合工具执行能力,问题就从“回答错误”升级为“执行危险动作”。
最后压缩成六句话
- 上下文管理的本质是管理 Agent 每一步的观察空间。
- 每次模型调用都无状态,Harness 必须重建必要历史。
- 静态规则保持稳定,动态信息只增不改地放到后面。
- 长期领域能力用 Skills 按需加载,运行时状态由代码提炼成状态栏。
- 当轨迹变长时,删除噪声、外置原文、压缩旧证据,但必须保留决策、约束、失败、验证和来源。
- 能通过子 Agent 隔离的中间过程,就不要先污染主上下文再压缩。
最终要建立的不是一组 Prompt 技巧,而是一个完整的上下文生命周期:
分类 → 组织 → 注入 → 缓存 → 更新 → 提炼 → 压缩/隔离 → 验证