上下文工程:从 KV Cache、Skills 到长程 Agent 的实战思考
最近把《AI Agent Book》第二章“上下文工程”研读了一下,结合自己做 Coding Agent 开发的踩坑经历与近期对Agent架构的调研、学习,写下这篇总结与实践教程。
写这篇内容有两个目的。
一方面,业内经常能听到一种声音,认为 Agent Harness、上下文工程以及 Skill 只是大模型发展初期的临时过渡产物,只要未来模型的上下文窗口足够大、推理能力足够强,这些外围工程就会自然消亡。针对这个观点,我想结合本章的原理与前沿计算架构的研究,分享一些我个人的分析与研判。
另一方面,第二章把大模型 API 交互、KV Cache 物理约束、Chat Template、Skills 渐进式加载、状态栏注入、上下文压缩等技术讲得非常透彻。我希望把这些书中的核心事实与技术方案整理成一份系统的教程,分享给同样在探索 Agent 开发的朋友。
个人思考:为什么我认为 Harness 与上下文工程不会是过渡产物
在进入具体的技术实现之前,我想先讨论这个经常引发争议的焦点。
所谓 Harness,在 Agent 领域指代包围在基础大模型外部的一整套工程支架,负责管理环境交互、工具调用、记忆存储与上下文装配。一些观点认为,随着模型能力提升,Harness 将会彻底淘汰。
结合书中的理论与我自己的工程理解,我认为 Harness 和上下文工程具有长期的生命力。即便未来大模型进化到极高水平,Harness 也更可能逐渐内化为标准基础设施,而不会在逻辑上消失。
我个人认为背后的原因有三点。
第一,上下文质量决定有效推理。大模型在通用测试集上跑分很高,但面对具体的企业业务系统时,它对团队的架构约束、历史包袱、数据库字段约定、权限边界一无所知。把零散、动态的业务环境高效准确地喂给模型,正是上下文工程的工作。
第二,物理成本与延迟约束长期存在。即便未来模型的上下文窗口扩展到一千万 token,无节制地把全部原始日志、十万行代码和几十轮搜索结果硬塞进上下文,依然会带来额外的计算延迟与推理成本。
第三,长上下文“装得下”不代表模型能够同等可靠地利用其中的每一部分信息。随着上下文变长,模型需要从更多位置中筛选和组合真正相关的信息,位置偏置、无关信息干扰以及跨越长距离的依赖,都会增加有效信息利用的难度。未经治理的上下文仍然可能出现上下文腐化(Context Rot):数据都在窗口里,模型却没有稳定地用到关键部分。
有人会问,现在长上下文成本高、速度慢,很大程度上和 Transformer 的计算方式以及 KV Cache 的显存占用有关。未来如果换成更适合长程记忆的架构,例如 DeepMind 提出的 Titans 这类能够在测试期动态更新长期记忆的设计,是不是就能拥有近乎无限的上下文,而且成本极低?
我并不认同这个推论。无论底层模型架构如何演变,信噪比和硬件算力约束依然存在。海量原始日志、无用网页和历史碎片全部塞给模型,不仅成本高,模型筛选有效信息的难度也会直线上升。相比把过滤压力全部交给模型,主动组织高密度、高价值的信息显然更可控些。
所以我现在越来越觉得,上下文管理会长期存在。做 Agent 时,我们本来就会整理代码、记录关键状态、保留执行证据、丢掉已经失去价值的噪声。上下文工程做的其实也是类似的事情:通过分层、状态提炼与按需加载,让模型每一轮都尽量看到当前真正需要的信息。
KV Cache 与 Prompt Cache:为什么稳定前缀这么重要
很多做 Agent 的工程师在本地写 Demo 时一切正常,一旦上线跑多轮交互,立刻发现延迟暴涨、账单飙升。根源之一就在于没有意识到推理引擎底层的缓存匹配逻辑。
缓存生效的前提是前缀稳定
大模型在自回归生成时,需要持续使用前文 token 对应的 Key / Value 状态。为了避免每轮都从头重复计算,现代推理引擎会复用 KV Cache,API 服务商也常以 Prompt Caching 的形式提供类似的前缀复用能力。
这个机制很高效,但对前缀变化非常敏感。对于底层 KV Cache 来说,输入序列中某个位置一旦发生变化:
- 该位置之前的缓存仍然可以复用。
- 从发生变动的位置开始,后续状态需要重新计算。
书中记录的案例很典型。某团队在 System Prompt 里塞了一行动态时间戳 Current time: {{now}},导致每次调用的前缀都不一样。每天 10 万次请求无法稳定命中 Prompt Cache,首字延迟从 0.5 秒拉长到 3 至 5 秒,费用也明显上升。
需要补充一点:API 服务商通常会在底层 KV Cache 之上实现自己的 Prompt Cache 策略,具体的缓存粒度、断点和命中规则并不完全相同,所以实际工程中仍然要以对应服务商的实现为准。
生产中常见的四个反模式
- 动态改写系统提示词。把当前时间、动态用户信息、易变的全局状态塞进 System Prompt 开头,会让公共前缀更难稳定复用。
- 工具列表动态排序。为了所谓的使用频次优化,在每轮请求里动态调整
tools参数的顺序。工具定义通常占用数千 token,顺序一变,公共前缀也跟着变化。 - 自行手拼纯文本对话。自行把结构化消息拍平成纯文本,可能破坏 role、tool call、tool result、reasoning/thinking block 等模型协议所依赖的边界和特殊标记。
- 滑动窗口盲目截断。为了控制长度只保留最近几轮消息,丢弃最早的历史。这样既可能破坏缓存连续性,也可能让模型丢失早期工具执行结果,进而反复执行已经做过的操作。
我们应当遵循的两条工程准则
- 尽量冻结静态前缀。System Prompt、Tools Schema 一旦确定就尽量保持稳定,不要频繁做无意义的改写和重排。
- 动态信息尽量后缀追加(Append-Only)。新状态、新工具返回值、时间戳等易变信息优先作为新消息追加在历史末尾。
工具越来越多、返回值越来越长之后怎么办
当 Agent 的能力扩展到数十个工具,或者工具返回了数万行日志、代码时,上下文很快就会被撑大。这里有两类很实用的治理方式。
1. Skills 与能力的渐进式披露
如果把几十个工具或 Skill 的完整说明全部挂在静态前缀里,会白白占用大量 token,也会增加模型筛选能力的负担。
成熟的 Agent 框架(如 Claude Code、Codex)会使用**渐进式披露(Progressive Disclosure)**的思路。
- 目录常驻。静态前缀里只保留每个 Skill 或工具的简短说明,例如名称与触发场景。
- 正文按需加载。当模型判定任务需要特定能力时,再读取
SKILL.md或对应说明,把核心 SOP 和操作细节加载进当前上下文。 - 细则按需深入。如果遇到复杂分支,再继续读取子文档或执行附属脚本。
这样 Agent 可以拥有很多能力,但默认上下文不需要把所有能力细节全部展开。
2. 工具超长返回值的外部引用化(Offloading)
执行一次代码读取、网页抓取或者日志分析,经常会返回数万字符。如果全部原样塞进上下文,很快就会挤占主上下文空间。
一种常见做法是在工具执行层增加一层结果治理。
- 当工具返回值超过设定阈值时,框架把完整内容写入磁盘、对象存储或其他可回读的位置。
- 上下文中只保留当前需要的摘要片段,以及一个指向原始内容的 Ref-ID。
- 如果后续需要特定行号或细节,模型再通过读取工具和 Ref-ID 重新取回。
这样既保留了原始证据,也避免把所有大结果长期堆在主上下文里。
状态栏为什么能减少 Agent 的重复调用和死循环
在多轮复杂任务中,模型经常会出现“数不清自己试了几次”“忘记最初约束”的问题。比如要求拨打电话不超过 3 次,模型已经调用了 3 次,第 4 轮仍然可能继续尝试。
模型为什么容易算错历史状态?
模型当然能做统计和归纳,但它并不是一个可靠的确定性状态机。
当它需要判断某个工具已经执行多少次、还剩哪些步骤未完成时,往往要重新从散落在长历史里的记录中推导。如果轨迹越来越长,其中还混有失败尝试、重复调用和旧状态,这种隐式统计就很容易出错。
Agent 状态栏(Status Bar) 的思路,是由外部 Harness 代码先做确定性计算,再在上下文末尾给模型一份结构化的当前状态。
<agent_status>
Current State:
- Tool call summary: 'phone_call' 已执行 3 次 (目标商家: Xfinity, 达到上限 3/3)
- Task Progress: [1] 查询账单(已完成) -> [2] 申请降费(进行中) -> [3] 确认退款(待处理)
- Environment: 工作目录 /workspace/project, 系统 Linux x86_64
- Constraint Warning: 本次严禁再次发起 phone_call 工具调用!
</agent_status>
书中的实验结果表明,把这种结构化状态放在上下文末尾,可以明显降低模型每轮重新翻历史、重新统计的负担。它还利用了长上下文中常见的近因优势,让当前状态更容易进入下一步决策。
状态更新的工程权衡
状态每轮都在变,更新状态栏大致有两种路线。
- 每轮替换(In-place Replace)。每次删除上一轮旧状态,在末尾写入最新状态。上下文中始终只有一份最新状态,代价是替换点之后的缓存需要重新建立。
- 持久追加(Append-Only)。状态消息写入后不再修改,每轮只在末尾追加最新状态。这样更有利于保持已有前缀稳定,但历史中会留下旧状态,也会持续消耗 token。
如果状态信息很短,我更倾向于追加;如果状态本身较长,而且会话会运行很多轮,那么替换通常更划算。
最后一个问题:怎样控制上下文长度,又不让 Agent 忘掉已经做过的事
回到第二章留下的核心思考题。
思考题:滑动窗口对话历史会导致 Agent 遗忘早期执行结果,陷入反复调用同一工具的死循环;但完整保留历史又会让上下文不断膨胀。设计一种策略,既能避免信息丢失,又能控制上下文长度,同时尽可能保持 KV Cache 的前缀复用。
结合前面的机制,我目前会倾向于用下面这套方案。
+-----------------------------------------------------------------------------+
| 1. 稳定静态前缀 |
| [ System Prompt ] + [ Tools Schema ] |
| 尽量保持稳定,优先复用公共前缀缓存 |
+-----------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------+
| 2. 大结果外部存储与引用 (Append-Only) |
| 工具返回过大 -> 自动存盘 -> 上下文只保留摘要 + Ref_ID |
| 需要细节时 -> Agent 调用 archive_read(ref_id, offset) 回读 |
+-----------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------+
| 3. 阈值触发的低频批量压缩 |
| - 平常轮次:尽量 Append-Only |
| - 触达高水位:批量压缩较早的历史区段 |
| - 用归档摘要替换旧区段,保留事实、失败记录与 Ref_ID |
| - 接受这一次缓存重建,之后继续在新前缀上追加 |
+-----------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------+
| 4. 状态栏显式追踪当前进展 |
| 末尾注入 <agent_status>:工具调用计数、TODO、关键约束 |
| 减少重复调用和无意义重试 |
+-----------------------------------------------------------------------------+
架构细节与落地要点
-
不采用纯滑动窗口,优先保留执行证据与 Ref-ID。纯滑动窗口的问题在于它只按“新旧”裁剪历史,并不知道哪段内容对后续任务仍然重要。大体积输出可以先外部存储,主上下文里保留短小但足够定位原始证据的执行线索和 Ref-ID。后续如果确实需要细节,再在末尾发起新的读取调用。
-
低频批量压缩,接受少数几次缓存重建。日常对话尽量只增不改。只有 Token 消耗触及预设水位线时,才执行一次批量压缩,把较早的一段历史真正替换为归档摘要。摘要里要保留已经尝试过的参数、失败原因、关键结论和 Ref-ID。这个过程会破坏压缩点之后的旧缓存,所以这里的目标并不是“永远不破坏 KV Cache”,而是把历史重写控制在低频发生。
-
状态栏显式计数,降低盲目重试概率。由 Harness 在末尾状态栏中维护关键工具调用次数、当前 TODO 和失败上限。当某个操作已经失败多次时,直接把这个确定性状态告诉模型,比让它自己重新统计整段历史可靠得多。
-
子 Agent 隔离大范围探索。对于海量全局搜索或长文档分析,可以派生独立子 Agent 在自己的上下文中完成探索,最后只把精简结论和必要证据回传给主 Agent。这样大部分探索噪声不会长期留在主上下文里。
结语
从理解底层推理缓存的约束,到利用 Skills 做能力的渐进式加载;从借助状态栏维护当前状态,到使用外部引用和批量压缩控制长程交互,这些机制其实都在解决同一个问题:模型这一轮到底应该看到什么。
这也是我目前对上下文工程最直接的理解。它已经远远超过“把 Prompt 写得更好”这件事,更像是在设计一套信息供给系统:什么应该长期保留,什么应该按需加载,什么只需要留下索引,什么时候值得为了压缩历史接受一次缓存重建。
对于 Agent 来说,上下文不是越多越好。真正需要控制的是信息密度、当前状态以及可回溯性。
更多推荐



所有评论(0)