基于李博杰《深入理解 AI Agent:设计原理与工程实践》第2章学习笔记。
开篇:一条差点让账单翻倍的时间戳
某团队的客服 Agent 每天处理 10 万次对话,原本一切正常。某天,工程师为了让 Agent"知道"当前时间,在系统提示词里加了一行 Current time: {{now}}。第二天监控告警:所有对话的首 token 延迟从 0.5 秒涨到 3-5 秒,月度推理账单几乎翻了一倍。
代码看起来完全没问题,模型也没换------问题出在哪里?
答案是一个藏得很深的机制:KV Cache。那一行动态时间戳让每次请求的缓存前缀都不同,模型不得不从头重新计算整个前缀。一行看似无害的代码,让整条推理链路慢了一个量级。
这篇文章我们继续上下文工程的另一半:KV Cache 友好的上下文设计、系统提示词的艺术、动态提示词(Agent Skills)以及上下文压缩策略。
一、KV Cache:看不见的成本规则
缓存是什么
模型每生成一个 token,都要"回头看"一遍前文所有 token 的中间计算结果。如果每轮都从头算一次,开销会随上下文长度爆炸式增长。KV Cache 的做法是:把前文算过的键值对缓存下来,下一轮只计算新增部分。前提是前缀完全不变------只要前缀里有一个字符被改写,缓存就全部作废。
这就像做菜:如果前几步完全一样,你可以直接从上次切好的地方继续;但如果前面任何一步变了,后面所有步骤都得重来。
三条铁律
- 系统提示词和工具定义一旦确定就不要改。 任何改动,哪怕多一个空格,都会导致缓存全部失效,延迟成倍增加、成本上升。
- 动态信息永远追加到末尾。 时间戳、用户状态等变化内容,作为新消息追加到对话末尾,而不是修改已有的系统提示词。
- 使用标准 API 格式,不要自行拼接消息。 结构化消息会被 Chat Template 翻译成模型训练时见过的固定 token 序列;自行拼成
"USER: ... ASSISTANT: ..."偏离了训练格式,会削弱模型的多步思考能力。
常见错误模式
| 错误模式 | 危害 | 正确做法 |
|---|---|---|
| 动态系统提示词(嵌入时间戳) | KV Cache 完全失效 | 时间作为消息追加末尾,或通过工具获取 |
| 动态用户配置(每次更新余额) | 破坏缓存 | 通过专门状态管理机制处理 |
| 工具定义动态排序 | 缓存全部失效 | 固定顺序(对工具选择能力几乎无影响) |
| 滑动窗口对话历史 | 破坏前缀一致性 + 丢失关键工具结果 | 避免滑动窗口,Agent 会"忘记"之前的工具调用结果 |
| 文本格式化拼接消息 | 偏离训练格式,模型需额外推断角色边界 | 用标准 role-content 消息 |
实验中发现:使用滑动窗口的 Agent 经常陷入循环,反复执行相同的工具调用,因为它"忘记"了之前已经获得的结果。
KV Cache 与 Prompt Cache:两个层级
- KV Cache:模型内部优化,加速单次请求内的 token 生成。
- Prompt Cache:API 服务层优化,跨请求缓存相同前缀的计算结果,直接影响 API 计费。
各家策略不同:Anthropic 需要显式设置 cache_control 断点才会缓存(缓存写入约 1.25 倍加价);OpenAI 自动前缀缓存。缓存读取成本约为首次计算的十分之一。
在生产级系统中,缓存经济性是前置架构约束:Claude Code 的提示词结构由缓存边界决定------可跨用户全局缓存的内容放在边界前,所有动态元素(操作系统、模式、用户偏好)严格归到边界后,否则 N 个二值条件会产生 2^N 种缓存键变体。
二、提示工程:优化系统提示词
提示工程的核心对象是系统提示词------API 消息列表中那条 role: "system" 的消息。它是 Agent 的"员工手册",定义了 Agent 的身份、行为规则、约束条件和工作流程。
检验标准:大语言模型是一位聪明的新员工,能力出众,但对你们的具体工作流程一无所知。如果一个聪明的新员工读完你的系统提示词还不知道该怎么做,Agent 也一样不知道。
语气与风格:提示词的"人格"
- 大写字母(如
NEVER do X)比Please avoid doing X更能引起模型的"注意",但过度使用会稀释效果,应保留给真正关键的约束。 - 简洁明确的约束优于含糊请求:"回答不超过 4 行"比"尽量简短"有效得多。
结构化提示:提示词的"格式"
现代大模型对结构化输入显著敏感,这源于训练数据中包含大量结构化内容:
- XML 标签 :标签名称本身携带语义------
<working_directory>能立即告诉模型这是工作目录信息。 - Markdown:在保持可读性的同时提供轻量级结构。
两者协同构成双层结构:XML 负责机器可解析的精确语义,Markdown 负责人机共读的组织逻辑。
流程驱动 vs 规则堆砌
给新员工一本包含上百条零散规则的手册,没有流程图也没有优先级说明------即使最聪明的人也会困惑。
流程驱动的提示词(SOP 风格)让模型在任何时刻都知道自己处于哪个阶段、当前步骤的目标、完成后进入哪一步:
vbnet
File Processing Standard Operating Procedure:
Step 1: Validation → Check if file exists and is accessible
- If not found → log error and stop
Step 2: Classification → Determine file type by extension and content
Step 3: Preprocessing → Config files → backup; Large files (>1MB) → stream
Step 4: Execution → Execute core logic based on file type
Step 5: Verification → Ensure integrity of the processed file
遇到异常时,模型根据当前所处阶段确定处理方式,而不是遍历所有规则去找匹配项。提示工程消融实验表明,信息组织混乱会导致成功率下降 30% 以上。
三、动态提示词与 Agent Skills
随着 Agent 覆盖的业务场景越来越多,系统提示词会不断膨胀------全部塞进一个提示词会带来两个问题:浪费 token (大部分内容与当前任务无关),以及注意力被稀释。
这就是从静态提示工程到动态提示词的自然演进:不是把所有知识一次性塞给 Agent,而是让它按需加载。
Skills 的渐进式披露
Agent Skills 将 Agent 的能力模块化为独立的、可按需加载的知识包。设计哲学是渐进式披露(Progressive Disclosure)------先给 Agent 看目录摘要,需要时再加载完整内容:
- 第一层(元数据,~300 tokens) :启动时扫描所有已安装 Skill,注入
name和description列表。 - 第二层(SKILL.md 核心流程,~2K tokens) :Agent 判断任务需要时,通过 Skill 工具加载完整的
SKILL.md。 - 第三层(子文档,选择性深入) :
html2pptx.md、reference.md、scripts/*.py等,按需加载。
这种结构对 KV Cache 极其友好:元数据固定 → 缓存友好;动态内容追加 → 不破坏缓存。
Skills 与工具的关系
如果把所有专用工具定义都放进系统提示词,数量膨胀会消耗大量 token,且变更时破坏缓存前缀;而 Skill + 通用执行器模式下,工具数量始终很少,Skill 内容通过渐进式披露按需加载,不影响已缓存前缀。
一个真实例子:用 Claude Code + PPTX Skill 从论文 PDF 生成演示文稿,Agent 的执行流程是:
- 在上下文末尾的 Skill 元数据列表中看到 PPTX Skill 的描述
- 识别任务需要该 Skill
- 通过 Skill 工具加载完整的 SKILL.md
- 选择性加载子文档获取详细方法
- 使用捆绑的工具脚本和模板文件
四、Agent 状态栏:让模型"瞥一眼"就知道状态
为什么需要状态栏
生产级 Agent 容易陷入各种陷阱:无限循环、状态遗忘、任务目标偏离。根源在于 Agent 缺乏对自身状态和任务进展的感知。
Agent 状态栏通过在上下文中嵌入结构化的元信息,为 Agent 提供自我感知机制。最好的类比是手机顶部状态栏------时间、电量、信号强度,随时瞥一眼就掌握设备状态。
与 System Prompt 的区别:系统提示词是入职时发的员工手册,定下来就不变;Agent 状态栏是贴在屏幕边缘的实时仪表盘,随任务推进不断更新。
理论基础:上下文学习是检索而非推理
注意力机制的一个本质特性:模型擅长从已有内容中查找信息,但不擅长主动归纳和总结。上下文窗口是一台只有一半的检索引擎------"检索"的一半非常强,但缺了"提炼层"。
考虑实际场景:系统提示词要求拨打每个商家不超过 3 次。但打了 3 次之后,Agent 经常数不清到底打了几次,又打第 4 次甚至陷入循环。当我们在每个电话的工具调用结果中直接加入"本次是第 3 次呼叫该商家",模型立即发现已达限制,不再继续。把分散的隐式状态提炼为可直接使用的显式知识------这是状态栏的本质。
状态栏的构成与实现
状态栏包含:任务规划(TODO 列表)、工具调用计数器、时间戳跟踪、详细错误信息、系统状态(时间、工作目录、操作系统、Shell 环境)等。
状态更新的两种实现与缓存代价:
- 实现一:每轮替换。移除旧状态、追加最新状态。保证上下文只有一份最新状态,但移除旧状态会使其位置之后的缓存失效。
- 实现二:持久追加 。状态一旦注入就永久留在轨迹中(Claude Code 的
<system-reminder>采用此方式)。对缓存完全友好,但陈旧状态会累积占用 token。
取舍法则:状态更新频繁且轨迹长 → 选持久追加;轨迹短或状态消息大 → 选每轮替换。
实验效果
- 工具调用计数器:显式计数触发模型模式识别------第一次失败检查路径、第二次列出目录、第三次主动放弃找替代方案,实现隐式成本感知。
- TODO 列表:启用后 Agent 平均 15 次迭代完成任务,禁用时需 21 次且常遗漏子任务。
- 详细错误信息(错误类型+参数+调用栈+修复建议):错误场景中找替代方案的成功率从 60% 提升到 95%。
- 系统状态感知:注入时间、工作目录、OS、Shell 等,Agent 能做出平台相关决策(Linux 用 apt、macOS 用 brew)。
这些技术协同会产生涌现效应------完全启用的 Agent 不再是机械执行指令的工具,而更像有自我意识的助手。
一个重要警告:光有读数不够,还要给策略
实验发现一个反直觉结论:只把原始时间戳给模型,和什么都不给几乎没有区别。真正把通过率从一成拉到四五成的,是那份"这些读数该怎么用"的操作手册------显式的读数只是原料,模型还需要把读数翻译成动作的说明书。
五、上下文压缩策略
为什么需要压缩:不只是长度问题
- 解决长度约束和成本约束:上下文窗口有限,工具调用结果动辄数万字符,几轮交互就可能撑满窗口。
- 提升思考质量:总结后的知识比原始形式更利于模型使用。Agent 在数万 token 中反复"检索"关键片段,注意力被分散。
上下文腐化:比溢出更隐蔽
上下文腐化(Context Rot)与溢出是不同问题:溢出是"装不下了",腐化是"装得下但找不到了"。实践中最常见的失效模式不是窗口不够长,而是信息密度不对。
一个例子:100 个笼子(90 只黑猫、10 只白猫)的巡查记录。问"黑猫和白猫各多少只"------不启用思维链,模型很难直接答对;启用思维链每次都要从头数。而如果提前写入"当前统计:黑猫 90 只,白猫 10 只",模型立即检索到结论。这就是压缩的第二个价值:把需要思考才能得到的结论变成可以直接检索的知识。
压缩与 KV Cache:看似矛盾,实则互补
关键:压缩不是在单次 API 调用过程中修改上下文,而是在两次调用之间由 Agent 框架预处理消息列表。System Prompt 和工具定义永远不动;压缩对象是 tool results------替换位置之后的缓存失效,但之前的缓存仍然有效。压缩频次需要权衡:最好在上下文接近阈值时批量压缩,而非每轮都压。
六种压缩策略对比
任务:识别并追踪 OpenAI 联合创始人的职业状态,上下文预算限制在 128K 窗口。
| 策略 | Token 用量 | 结果 |
|---|---|---|
| 无压缩 | >110K | ✗ 溢出失败 |
| 个体摘要 | 123K | ✓ 但信息碎片化 |
| 组合摘要 | 55K | ✓ 长输入截断可能丢信息 |
| 上下文感知压缩 | 25K | ✓ token 减少 77%,成功率最高,迭代最少 |
| 感知+引用 | 45K | ✓ 有损压缩+无损索引 |
| 自适应窗口 | 181K | ✓ 延迟压缩,最大保真 |
上下文感知压缩 的核心创新:将当前查询意图和已积累信息纳入压缩决策------"Given the search query: {query}" + "Current context: {context}"。多步骤任务中不同阶段需要的信息密度不同:初期广泛收集、中期精确核验、后期综合整合。
自适应窗口化 的三个机制:阈值触发(80% 窗口才激活)、批量压缩(一次性压缩所有未标记工具结果)、防重复保护([COMPRESSED] 标记)。
生产级的分层压缩机制
成熟系统不只用单一策略,而是组合为分层机制(以 Claude Code 为参照):
- 工具结果预算控制:大体积输出存磁盘,模型只看摘要预览
- 噪声直接删除:低价值内容直接移除,不做摘要
- API 层微压缩:通过上下文编辑能力指示服务端移除指定结果
- 归档式摘要:逐轮结构化摘要(像 git log 保留每轮记录,而非 git squash 合并)
- 全量压缩:LLM 驱动的完整压缩作为最后手段,配连续失败熔断器
前三层实现成本最低、对缓存扰动可控,优先使用;后两层成本高但压缩效果强,作为兜底。
保留优先级
压缩最容易丢失的不是细节本身,而是早期的架构决策、约束背后的理由和失败的路径。生产系统应显式定义保留优先级:
- 架构决策和关键约束:不得摘要
- 已修改的文件列表和关键变更记录:完整保留
- 验证状态(pass/fail):必须保留
- 未解决的 TODO 和回滚笔记:必须保留
- 工具输出:可删除,仅保留 pass/fail 结论
UUID、hash、IP、端口、URL、文件名等标识符必须原样保留------一旦把 PR 编号或 commit hash 改错一位,后续工具调用直接失效。
隔离优于压缩:子 Agent 上下文隔离
更釜底抽薪的思路是让大体积中间信息根本不进入主上下文------把"读取大量文件""大范围搜索"这类任务委派给子 Agent,子 Agent 在自己的上下文完成探索,只回传几百 token 的结论。
这本质上是用隔离代替压缩:压缩是有损的、需要额外 LLM 调用的事后补救;隔离让噪声从一开始就与主上下文绝缘,主 Agent 的 KV Cache 前缀完全不受影响。代价是任务描述必须自包含、目标明确------这又回到本章主题:上下文的质量决定能力上限。
本章小结:显式的信息管理
本章绕来绕去,其实在说一件事:给模型看什么、怎么组织,比模型本身有多聪明更影响最终的结果。
- API 消息结构定义了上下文的骨架
- KV Cache 约束了你能改什么、不能改什么
- 提示工程和 Agent Skills 决定了如何高效提供静态指令和动态知识
- Agent 状态栏把隐式状态变成显式信息
- 压缩策略解决了上下文膨胀问题------不仅是控制长度,更是把原始数据变成高密度结构化知识
这些技术的共同点是显式的、工程化的信息管理。回到 Rich Sutton 的《苦涩的教训》:那些能更有效地利用更多算力的通用方法将最终胜出。
下篇预告
下一章将把视角从"一次任务之内的上下文管理"延伸到"跨越会话的持久化知识体系"------用户记忆与知识库,让 Agent 能够在实践中不断积累经验,逐步成为真正的领域专家。
本文基于李博杰著《深入理解 AI Agent:设计原理与工程实践》v1.3 第2章学习整理。系列文章同步发布于 ai-agent-study 仓库。