深入理解 AI Agent(四):Agent Skills、状态栏与上下文压缩

李博杰《深入理解 AI Agent》第2章下篇:渐进式披露的 Skills、把隐式状态提炼成显式知识的状态栏(上下文蒸馏)、以及上下文压缩策略

深入理解 AI Agent(四):Agent Skills、状态栏与上下文压缩

资料源:李博杰《深入理解 AI Agent:设计原理与工程实践》第2章(2.5-2.7 节) 开源仓库:https://github.com/bojieli/ai-agent-book (v1.4,2026-08-13)

一句话主线

上一篇(三)讲静态前缀里放什么(提示工程)。这一篇讲三个动态机制——它们回答同一个问题:上下文会膨胀、状态会变化、知识会积累,怎么让 Agent 始终"看到"最该看的东西?

三个机制对应三种思路:

  • Agent Skills:按需加载——别把所有知识一次性塞给 Agent,给目录,需要哪本取哪本
  • 状态栏:提前算好——把分散在轨迹里的隐式状态提炼成显式结论,模型"瞥一眼"就行
  • 上下文压缩:做减法——把臃肿的原始记录换成算好的结论,控制长度同时提升思考质量

贯穿三者的理论基础是同一句话:上下文学习的本质是检索而非推理——所以不要期望模型从冗长上下文里自动归纳,要主动为模型提供经过提炼的结构化知识。

Agent Skills:渐进式披露

为什么需要动态提示词

随着业务场景增多,系统提示词不断膨胀——客服退款规则、代码规范、文档格式要求全部塞进一个提示词,带来两个问题:

  • 浪费 token:大部分内容与当前任务无关
  • 注意力被稀释:上下文中无关信息过多,稀释模型对关键内容的注意力(后面会讲"上下文腐化")

于是从静态提示工程自然演进到动态提示词:不是把所有知识一次性塞给 Agent,而是让它按需加载。 Agent Skills 系统正是这一理念的工程化实现。

渐进式披露:三层结构

核心设计哲学是渐进式披露(Progressive Disclosure)——先给 Agent 看目录摘要,需要时再加载完整内容。就像你不会把公司所有部门的操作手册都堆到新员工桌上,而是先给一份总目录,需要哪本再去取。

第一层·元数据(启动时加载,~300 tokens):每个 Skill 的 SKILL.md 开头是 YAML frontmatter,含 name 和 description。目录必须在主体正文加载前对 Agent 可见,让 Agent 先判断当前任务是否需要这项能力,不必为所有能力支付完整上下文成本。

关键细节:description 字段是路由决策的关键——写法要像路由条件而非功能介绍。可以明确写出"何时使用"和"何时不使用"的边界,并给几条典型反例减少误触发。“help with backend"这种宽泛描述等于任何后端工作都能触发,路由失准;真正有效的描述是"何时该用我"比"我能做什么"重要得多。

第二层·核心流程(按需加载,~2K tokens):当 Agent 判断需要某 Skill 时,运行时才加载完整 SKILL.md——遇到什么任务时使用、按什么顺序行动、哪些情况停下来确认、什么结果才算完成。Claude Code 在调用位置把 Skill 指令作为 user 消息加入会话。

第三层·子文档(选择性深入):通过文件引用加载更细的细节——html2pptx.md(HTML 模板创建 PPT 的详细工作流)、reference.md(格式技术细节)、scripts/*.py(可执行工具)。

如何编写一份可用的 Skill

一份实用的 Skill 不应只是背景知识或一次成功对话的摘要,而应让一个刚加入团队的员工知道:遇到什么任务时使用它、按什么顺序行动、哪些情况需要停下来确认、什么结果才算完成。

参考提示工程师宝玉《图解 Skill》的四部分结构:

  1. 角色与读者说明:这份 Skill 服务谁、面向什么任务、输出达到什么标准
  2. 核心原则:只保留三到五条最重要的判断,为关键原则配正例和反例
  3. 禁止清单:记录高频错误、越权动作、容易误解的表达,同时写清合法例外
  4. 参考资料:术语表、模板、范文、更详细的子文档

规则尽量写成"作用域 + 动作 + 例外 + 验证方式”,避免把所有可能情况堆成越来越长的禁用词表。

写作型 Skill 的实操路径(实验 2-7):从三到五篇自己最满意的原创文章开始,让 Agent 归纳用词、句式、段落结构和语气,生成二十行左右的初版;再用它处理一篇真实任务,作者逐句改稿——原文与修改稿的差异比抽象地说"更自然一点"更有信息量;把反复出现的改动整理回 Skill,为每条规则保留正例、反例和适用范围。

Skills 的价值不止于上下文管理,更在于为领域知识积累提供可持续路径:每个 Skill 是自包含的知识模块,可独立开发、测试、版本控制、分享——能力扩展从集中式的系统提示词编辑,转变为分布式的、社区驱动的 Skill 生态(与 Python 的 pip、Node 的 npm 有深刻相似性)。

Skills 与 KV Cache、工具的关系

与 KV Cache(2.5.3):元数据目录从启动起就在 system prompt 中——属于稳定前缀,可复用 Prompt Cache;完整 Skill 正文只在被调用的位置作为 user 消息追加到轨迹末尾——追加不破坏缓存,正文首次加载算一次"一次性缓存写入",此后持续命中。“对 KV Cache 友好"并非零成本,但收益是:无需启动时加载所有正文,也无需每次调用新 Skill 时回头改写已建立的上下文。不同 Harness 实现不同(Claude Code 渐进式目录 + 调用点追加;Codex 每轮重新渲染 catalog 作为 developer 上下文片段),但都遵循"少量目录常驻、完整正文按需加载”。

与工具(2.5.4):在 Skill + 通用执行器模式下,工具数量始终很少(第5章说仅需七个核心工具),Skill 内容按需加载,不会影响已缓存前缀。这与第1章"工具设计核心原则"(通用基础能力用于组合与探索;专用工具用于约束高风险操作)是同一枚硬币的两面——Skill 是"知识形态的按需加载",工具是"执行形态的接口"。

Agent 状态栏:让模型"瞥一眼"就知道状态

问题:Agent 会陷入"状态失明"

生产级 Agent 容易陷入三种陷阱:无限循环、状态遗忘、任务目标偏离。根源是 Agent 缺乏对自身状态的感知——它不知道"已经打了 3 次电话"、“TODO 还剩 2 项”。

实际案例:系统提示词要求拨打每个商家不超过 3 次,但打了 3 次之后 Agent 经常数不清、又打第 4 次,甚至陷入循环反复拨打。因为"已经打了几次"的知识没有被自动提炼出来,而是以原始通话记录的形式分散在 KV Cache 的向量表示里——模型每次决策都得花思考 token 去扫描上下文重新统计,效率低且错误率高。

理论基础:上下文学习是"检索而非推理"

为什么模型不会自己数?因为注意力机制擅长在已有内容里"查找",却不擅长在单次前向传播里主动"归纳统计"。一个更形象的说法:上下文窗口是一台只有一半的检索引擎——检索这一半非常强(注意力能从成千上万个 token 里捞出相关记录,相当于把 RAG 内置进每次前向传播),但缺了"提炼层":上下文里的东西从来不会被自动数一遍、建个索引、就地总结成结论。任何"关于这些内容的结论"——一共多少条、有没有超标、进展到哪一步——模型每次要用,都得从原始记录里现算一遍,代价随内容量增长。

书中用宠物店例子演示:上下文中是 100 个笼子的巡查记录(90 只黑猫、10 只白猫),问"各有多少只"——不启用思维链,模型很难直接答对(注意力擅长"笼子 37 里是什么猫",不擅长统计归纳);启用思维链能数对,但每次被问到都要从头数一遍,累积思考成本很高。而如果提前在上下文里写入"当前统计:黑猫 90 只,白猫 10 只",模型就能立即检索到这个结论——这就是"提炼"的价值。

解决方案:Agent 状态栏

Agent 状态栏(Agent Status Bar)就是把分散的隐式状态提炼为可直接使用的显式知识:Agent 框架把动态信息整理成结构化摘要,注入上下文末尾——“已呼叫 3/3 次”、“当前时间”、“TODO 剩余 2 项”。类比手机屏幕顶部的状态栏:时间、电量、信号,不是 App 主界面内容,但随时瞥一眼就掌握设备状态。

放在上下文末尾有两个原因:

  1. 紧邻模型即将生成的新 token,获得最高注意力权重——“强制性的注意力引导”,对抗长上下文中的注意力衰减
  2. 追加而非修改,不破坏 KV Cache——这是第(二)篇"动态信息追加末尾"原则的直接应用

状态栏的构成(2.6.2)三类信息:

  1. 任务规划:TODO 列表——把任务分解成清晰步骤放在轨迹末尾,防止 Agent 过分关注局部子任务而忘记原始诉求和核心约束
  2. 事件的侧信道信息(Side-channel Information):为每个事件附加元数据——精确时间、地理位置、距上次回复的时间间隔等,帮助模型理解时序关系和环境背景
  3. 环境状态的观察摘要:动态环境信息(系统时间、工作目录、操作系统)、异常操作提醒(“该工具已被重复调用 N 次”)、从隐式状态到显式观察的转换

一个实现细节:状态栏在 API 层面是作为一条 user 角色消息插入上下文末尾的——不是修改开头的 system 消息(那会破坏前缀缓存)。这里的 user 角色只是协议层面的技术选择,内容并非来自真实用户,只是复用了 user 消息格式挂到上下文末尾。

上下文蒸馏:状态栏能带来什么(实验 2-8 延伸)

作者和合作者用专门的基准量化了这套做法(统一命名为上下文蒸馏,Context Distillation),结论很有冲击力:

  • 弱模型补回来的是准确率:最弱模型准确率涨 40-54 个百分点;一个 2B 本地小模型在这类任务上直接追平了不带状态栏的前沿大模型
  • 强模型省下来的是效率:思考量、延迟、花费各降约一个数量级(思考 token 砍掉八九成以上)
  • 最本质的变化:不带状态栏时,每次查询的思考量随上下文变长持续增长;带上状态栏后基本恒定——管上下文堆到多长,模型都只是"瞥一眼"那几格状态

三条实战经验(做对和做错天壤之别)

  1. 状态栏要用代码维护,别拿大模型去维护——一个 20 行的正则函数就能达到"标准答案"级别的准确度;让前沿大模型一次性读完整段历史批量统计,反而在大多数格子上出错,把下游准确率拖得比"根本不用状态栏"还低(等于把"扫描整段上下文"这个难题原封不动搬了个家)。可行替代:能用代码算就用代码算;实在要用 LLM,逐条抽取、再由代码汇总
  2. 不要删掉原始上下文——状态栏是原始上下文的一次有损投影,只提前算了"你预想会被问到"的维度。如果状态栏够用(计数、状态跟踪),可以整段删掉原始记录省 token;但只要有一个问题落到状态栏没算过的维度上,只留状态栏的准确率会断崖式崩塌
  3. 把状态栏的准确率当成一线生产指标盯——模型几乎无条件相信状态栏:你写"打了 3 次",它就当真是 3 次,既不核对也不重算。这既是状态栏有效的原因,也意味着状态栏一旦写错,错误会原样传进最终答案——也意味着投毒风险值得认真对待(第(三)篇提示注入里提到的状态栏投毒)

状态更新的两种实现与缓存代价(2.6.4)

状态是会变的,如何更新它有两种实现:

  • 实现一·每轮替换:移除旧状态消息、末尾追加新状态。保证上下文只有一份最新状态,但移除旧状态使其位置之后的所有缓存失效——好在状态位于末尾,失效范围只覆盖上次注入后的短后缀,整个前缀仍可复用
  • 实现二·持久追加:状态消息一旦注入就永久留在轨迹中,每轮只追加新状态(Claude Code 的 <system-reminder> 采用)。缓存完全友好(只追加不修改),但陈旧状态在上下文中累积,占用 token 并要求模型区分"最新一条"

书里给了粗略成本模型:每条状态 S token、两次更新间新增后缀 R token、更新 N 次——当 𝛼𝑆𝑁/2 < (1−𝛼)𝑅 时倾向持久追加(状态小、后缀增长快),否则倾向每轮替换。实际选择还要结合服务商的缓存计费与实测命中率。

上下文压缩:把"检索"变成"可查的结论"

两个动机:不只是长度问题

  • 长度和成本约束(直观):上下文窗口有限(128K token),工具调用结果动辄数万字符,几轮就撑满,任务被迫中断;token 越多成本越高、推理延迟越大
  • 提升思考质量(更深层,容易被忽视):总结后的知识比原始形式更利于模型使用。Agent 通过 10 次网页搜索积累的信息以原始形式散落在上下文各处,做最终决策时要在数万 token 里反复"检索";而如果在第 10 次搜索后先用一次 LLM 调用做结构化总结——“目前已知:A 是…,B 是…,还缺 C 的信息”——模型后续思考就能直接使用精炼的知识表示

第二个动机的机制在"检索而非推理"视角下很清楚:把需要思考才能得到的结论,变成可以直接检索的知识。 不压缩时模型不是"信息完整",而是"要在大海捞针"——注意力会稀释(位置偏好、无关内容占大头),捞的过程本身就有损还可能捞错捞漏。压缩不是丢信息,是换信息形态:丢失的是噪声和冗余,保留的是关键事实。质量取决于"压缩者"的理解能力——“压缩即理解”,所以是"模型调用模型"的递归架构。

上下文腐化 vs 上下文溢出

  • 溢出是"装不下了"(窗口用完,任务失败)
  • **腐化(Context Rot)**是"装得下但找不到了"——更隐蔽:Agent 表面正常,但决策质量悄然下降。注意力权重被分散到更多 token 上,无关内容占了大头,关键信息被稀释——就像在巨大图书馆里找某本书,书架上无关书籍越多越难找

设计原则由此而来:与其期望模型从冗长上下文中自动学习,不如主动、显式地进行知识提炼——虽然需要额外 LLM 调用做总结,但产生的是高密度知识表示。

压缩与 KV Cache:看似矛盾,实则互补

压缩发生在两次 API 调用之间,由 Agent 框架对消息列表预处理,不是单次调用内修改:

  1. System Prompt 和 Tool Definitions 永远不动——静态前缀持续缓存
  2. 压缩对象是对话历史中的 tool results——替换位置之后的缓存失效,但之前的缓存仍有效
  3. 有意识权衡:不压缩,上下文膨胀到超限,任务失败;压缩后损失部分缓存,但长度可控、信息密度更高。压缩频次要权衡——频繁压缩频繁破坏缓存,最好接近阈值时批量压缩

实验 2-10:六种压缩策略对比

研究任务(追踪 OpenAI 联合创始人的职业状态,7 次工具调用累计约 367K 字符,刻意把上下文预算限制在 128K 触发压缩),六种策略:

策略结果解读
无压缩5 轮就超限,任务失败数万字符的搜索结果几轮耗尽窗口
个体摘要12 轮 / 277K token,成功每个结果独立摘要,信息碎片化、重复内容浪费
组合摘要10 轮 / 93K token,成功所有结果合并后统一摘要,但超长输入截断可能丢尾部信息
上下文感知压缩7 轮 / 40K token,成功(省 76%)把查询意图和已有信息纳入压缩决策(“Given the search query: {query}” + “Current context: {context}"),不同任务阶段调整压缩侧重点
感知 + 引用10 轮 / 223K token,成功每条事实附 URL 引用,有损压缩 + 无损索引结合
自适应窗口7 轮 / 175K token,成功低于窗口 80% 不压,触发后批量压缩 + [COMPRESSED] 防重复

关键洞察:多步骤任务中不同阶段需要的信息密度和类型不同——初期需要广泛收集,中期需要精确核验,后期需要综合整合。上下文感知压缩通过动态调整压缩侧重点实现信息价值最大化。

生产级分层压缩(Claude Code 参照)

成熟系统不只用单一策略,而是分层组合,与信息的预期生命周期匹配:

  1. 工具结果预算控制:大体积工具输出存磁盘,模型只看摘要预览;替换决策一旦做出就被冻结(保证缓存一致)
  2. 噪声直接删除:低价值内容(如大量搜索结果中只被使用了几行的内容)直接移除不做摘要——对噪声做摘要只是浪费 token
  3. API 层微压缩:通过 API 上下文编辑能力让服务端从前缀中移除指定工具结果;适合上下文即将溢出时使用(反正要付出缓存重建代价)
  4. 归档式摘要:逐轮做结构化摘要(像 git log 保留每轮独立记录,而非 git squash 合并成一条)
  5. 全量压缩:LLM 驱动的完整压缩作为最后手段,且分两阶段(先压缩会话记忆,不行再全量);配连续失败熔断器——生产数据显示大量会话被困在反复压缩失败的循环里,熔断器避免持续烧钱

压缩时的保留优先级

压缩最容易丢失的不是细节本身,而是早期的架构决策、约束背后的理由和失败的路径——LLM 通常优先删除"看起来还可以重新获取"的信息。建议显式定义保留优先级:

  1. 架构决策和关键约束:不得摘要
  2. 已修改文件列表和关键变更记录:完整保留
  3. 验证状态(pass/fail):必须保留
  4. 未解决 TODO 和回滚笔记:必须保留
  5. 工具输出:可以删除,仅保留 pass/fail 结论

隔离优于压缩(2.7.7):釜底抽薪

压缩是信息进入上下文之后做减法;更根本的思路是让大体积中间信息根本不进入主上下文——子 Agent 上下文隔离。

对比两种做法处理同一个任务"在代码库中找到处理支付回调的函数”:主 Agent 亲自搜索,要让十几个文件、数万 token 的原始代码进入主上下文,其中绝大部分在找到目标后沦为永久占据窗口的噪声;委派给一个搜索子 Agent,主上下文只增加两条消息(任务描述 + 结论:“函数位于 src/payment/callbacks.py 的 handle_callback,另有两处调用点”),中间过程的数万 token 随子 Agent 的上下文一起被丢弃。

压缩是有损的、需要额外 LLM 调用的事后补救;隔离让噪声从一开始就与主上下文绝缘,主 Agent 的 KV Cache 前缀完全不受影响。 代价是子 Agent 看不到主 Agent 完整上下文,任务描述必须自包含、目标明确——“上下文的质量决定能力上限"对子 Agent 同样成立。Claude Code 的 Task 工具、Deep Research 的检索子 Agent 都是这个模式。

思考与延伸:状态栏的价值与压缩的边界

Agent 状态栏:为什么这个机制有意思

学习状态栏时记下的几个确认:

Agent 状态栏我觉得挺有意思。让 agent 看到 runtime status(比如任务进度/todolist、环境变化、工具调用出场次数等)。

上下文学习更像检索而非推理。它擅长检索,但是难以进行"提炼”。比如说"之前用了几次 tool1",它经常记不清楚自己调用了几次,要么从原始记录重新算一遍(上下文堆积),也可能陷入重复。不过是否使用可以看场景。

上下文蒸馏(context distillation)。弱模型能增加准确率,强模型能省效率。

“是否使用可以看场景"这个判断与书里数据一致:弱模型靠状态栏涨准确率 40-54 个百分点,强模型省一个数量级效率——所以强模型 + 简单任务可以不装,弱模型 + 长任务必装。另外两条硬经验值得反复强调:用代码维护状态栏(别用 LLM 批量统计,20 行正则函数就够)和不要删掉原始上下文(状态栏是有损投影,只算了"预想会被问到"的维度)。

压缩提升思考质量?——一个被认真质疑过的点

上下文压缩两个动机:成本因素、提升思考质量(?我觉得未必,压缩意味着信息的丢失)。

质疑合理,但答案在"检索而非推理"前提里:不压缩时模型不是"信息完整”,而是"要在大海捞针"——注意力会稀释,捞的过程本身就有损。压缩不是丢信息,是换信息形态:把分散的原始记录换成精炼的结构化结论,丢失的是噪声和冗余,保留的是关键事实。“压缩即理解”——质量取决于压缩者的能力,所以是"模型调用模型"的递归架构。实验数据支持:上下文感知压缩省 76% token 且迭代次数最少。不过质疑指出的真实边界也成立:压缩是有损的,所以才有保留优先级和"隔离优于压缩"(无损的替代方案)。

思考题中的几个设计问题

Q1(滑动窗口 vs 膨胀)——替代方案是组合拳:老消息不滑出而是压缩成归档式摘要 + 状态栏显式维护关键状态(“tool1 用了 3 次"不依赖历史)+ 隔离优于压缩(海量中间信息不进主上下文)。不破坏 KV Cache:压缩发生在两次调用之间、批量触发、system prompt 永远不动。

Q3(极端压缩的不可逆损失)——答案是有损压缩 + 无损索引:压缩前用保留优先级兜底(架构决策/约束/验证状态/TODO 不可摘要),压缩后信息落盘可回溯(正是分层压缩第 1 层"大输出存磁盘、模型只看摘要”)。

Q4(状态栏错误信息导致有害决策)——缓解分三层:生成侧(用代码计算而非 LLM 统计)+ 校验侧(一致性检查、把状态栏准确率当生产指标)+ 信任分级(高风险决策不完全依赖状态栏,重要操作回查原始记录;状态栏来源必须是可信数据源,防投毒)。

Q6(检索而非推理成立,怎么突破"塞更多信息")——三条路:主动提炼(压缩/状态栏/上下文蒸馏,“Distill, Don’t Retrieve”)、隔离优于压缩(让提炼发生在主上下文之外)、终极突破是后训练内化(把领域知识写进参数,不再依赖上下文塞知识——第7章)。上下文学习是"快速适配机制而非真正的学习"(Dherin “Learning without training”)。

Q7(Skills 元认知:模型不知道自己不知道什么)——分层解法:元数据目录常驻(让模型至少"知道有这些能力")+ 描述写成"路由条件 + 反例"降低误触发 + 触发词/用户手动指定(如 /skill:xxx)+ Harness 主动预加载(根据任务类型提示相关 skill,不完全依赖模型自知)。

Q8(Skill 动态读取后能否遵从)——动态注入的指令在长上下文里会被后续消息稀释,应靠近使用位置注入、执行完可"结算"移除;模型差异书里有明确答案:较新模型(GPT-5.4+/Claude 4.5+)训练时见过"工具定义出现在对话中间",较旧模型没有,所以支持度不同。

小结:三件动态机制的共同哲学

  1. Skills(按需加载):渐进式披露——元数据目录常驻(~300 token),完整正文按需加载(~2K token),子文档选择性深入;description 是路由条件不是功能介绍
  2. 状态栏(提前算好):把隐式状态提炼成显式知识;上下文蒸馏让弱模型追平强模型、强模型省一个数量级;用代码维护、不删原始上下文、把准确率当生产指标
  3. 压缩(做减法):控制长度 + 提升思考质量;上下文感知压缩省 76% token;分层压缩 + 保留优先级;隔离优于压缩

三者共享的理论基础:上下文学习的本质是检索而非推理——模型擅长从已有内容中查找,不擅长主动归纳。所以 Agent 框架的职责是:该加载的按需加载(Skills)、该提炼的提前提炼(状态栏)、该减少的主动减少(压缩)。下一章把同一思路从"单次任务内的上下文管理"扩展到"跨会话的持久化知识体系"——用户记忆和知识库(RAG)。