好久没有手写博客了,本文纯手敲!(部分配图是AI生成的)

从年初开始,skills引入的渐进式披露概念爆火。这个思路确实能节省模型消耗,但一个很容易被忽略的问题是:文件拆分不只是把大文档切小,它也会改变Agent Loop的执行路径

要想说清楚这个账,我们需要先知道什么是大模型的prompt cache。

Prompt Cache和缓存命中计费

目前所有大模型的服务商都会提供一个正常的输入/输出计费,和缓存输入的计费。其中缓存命中的输入计费是远远低于非缓存输入的。以“梁圣的恩情还不完”的模型为例,dsv4pro缓存命中的价格是未命中的8.3‰

image.png

了解这个计费差距之后,再来简单说一下prompt cache是如何hit命中的。

以目前最主流的openai llm请求格式为例,我们发出去的请求是如下格式的json串:

1
2
3
4
5
6
7
8
9
10
{
"model": "deepseek-v4-pro",
"messages": [
{"role": "system", "content": "你是一个有帮助的AI助手,回答要简明扼要。"},
{"role": "user", "content": question}
],
"stream": False,
"max_tokens": 512,
"temperature": 0.7
}

这里我们可以忽略其他字段,只看messages数组,这就是我们对话的上下文了,这个数组里面会包含system prompt、用户发出去的prompt、以及模型的工具调用和工具返回的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[
{
"role": "system",
"content": "你是一个天气助手。"
},
{
"role": "user",
"content": "北京今天适合出门吗?"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\",\"date\":\"today\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"name": "get_weather",
"content": "{\"city\":\"北京\",\"date\":\"today\",\"weather\":\"晴\",\"temperature\":\"28°C\",\"aqi\":\"良\"}"
},
{
"role": "assistant",
"content": "北京今天晴,气温约 28°C,空气质量良,适合出门。"
}
]

我们在Agent工具里的每一轮请求,都会带上我们先前对话和工具调用的所有历史,请求大模型API。在第一次请求的时候,这批历史消息就会服务端处理,写入KV Cache,后续我们发起请求再次携带这批历史信息的时候,就会命中服务器的KV Cache,从而计入缓存命中,以减少模型调用的资费。

网上常言的“缓存命中率”,在Agent工具支持的好的时候,可以简易地视作已经被写入缓存的数据,和我们每轮对话新增的数据的比例。

$$
缓存命中率 ≈ \frac{历史 token}{历史 token + 本轮新增 token} \times 100%
$$

而如果我们使用的Agent工具很垃圾,在追加本次请求新的消息的时候,把历史消息给重组或者修改了,那就出问题了。因为llm服务端只会匹配你的历史消息的前缀,发现前缀出现变化,缓存就没有命中了,此时就走了全量计费,那烧的钱也是库库涨了。

image.png

有关缓存命中率的事情是另外一个话题,本文不再展开。重点是,我们每轮请求的时候,所有历史消息都会再次被计算一次,计入Tokens资费。

很多刚用大模型的朋友在这个环节上就会产生疑惑:“为什么我的上下文才用了200k,tokens却消耗了好几百万了?”,那是因为每轮的Agent交互都会发出一次请求,每次请求都会进行全量的资费计算,服务端显示的Tokens消耗量是每轮交互的Tokens消耗的叠加!

sequenceDiagram
    participant U as 用户
    participant A as Agent服务
    participant L as LLM接口

    U->>A: 第1轮提问
    A->>L: 发送完整上下文(消息1),计费20k Token
    L-->>A: 返回回答
    Note over A: 累计消耗:20k

    U->>A: 第2轮追问
    A->>L: 发送完整历史(消息1+消息2),计费25k Token
    L-->>A: 返回回答
    Note over A: 累计消耗:45k

    U->>A: 第3轮追问
    A->>L: 发送全部历史(消息1+2+3),计费28k Token
    L-->>A: 返回回答
    Note over A: 累计消耗:73k

    Note over U,A: 用户疑惑:上下文仅28k,账单却显示73k

System Prompt和渐进式披露

在了解了大模型的计费规则之后,我们再来说说Agent工作流和“渐进式披露”的概念。

在早前的Agent系统设计中,会将Agent需要调用工具的各类约束,以及需要用上的MCP工具/Function Call[1],加载到Agent的System Prompt和Tool Use块中,作为第一次请求的参数,携带给大模型。

这就会导致我们所有工具,不管本轮调用时模型用不用得上,都会把完整的工具名字、工具参数和工具描述信息发送给大模型,从而造成Tokens的浪费。还可能会因为工具过多,对LLM选择工具造成心智负担,若选择错工具,流程也就出错了,从而影响整个Agent工作流的准确性和效率。

而渐进式披露的思路与此不同,在发送首轮消息的时候,只会携带每一个skill的名字和简述,不会携带完整的工具调用信息。Agent在识别到自己需要某一个skill的时候,再去读取这个skill的SKILL.md文件,和skill中可能包含的其他需要Agent阅读的参考文件,根据文件约束工作。

image.png

在openclaw、hermes这种个人助理式的Agent工具中,渐进式披露无疑是个非常好的思路,毕竟这些助理式系统需要对接的外部工具实在是太多了。如果使用MCP的方式,把这些工具全都加载到Agent的上下文里,哪怕只是工具调用的信息就足够把大模型context撑爆,openclaw也压根没有办法正常工作了。

而且这样做在绝大部分场景下也能节省成本,原本5个工具调用加上工具调用配套的指导说明,可能会占用1k的上下文token,并在每次请求的时候都被携带。现在可以被缩减成一个简单的skill描述,只需要占用100-200的token,在需要的时候才去加载完整的指导说明。

拆分文件能让总成本下降多少呢?

采用渐进式披露的理念,我们可以把我们工作流里面大的说明文件,按逻辑给拆分成数个小文件,让模型在执行到特定阶段的时候才去读取这个阶段的参考文档,从而减少一次性加载到模型上下文里面的token数量。

举个例子,假设这个工作流的完整说明文档需要占用30k的tokens[2],我们可以分成两个思路来处理:

  1. 全量加载:在第一次请求就让Agent读取完整的工作流说明,30k加载到上下文里面。
  2. 按需加载:把工作流按阶段拆分成几个文件,执行到某个阶段的时候,再去读取这个阶段的说明文件。假设拆分成6个文件,包含1个流程说明文件和5个阶段文件。平均每个文件5k token。

从常规的思路上看,第二种方式更好,即减少了首轮加载的说明文字的token数量(只需要加载流程整体说明文件和阶段1的文件共10k token),又让Agent暂时不知道后续阶段的详情,能更关注当前阶段的工作,避免注意力偏离。

但这种方式最终能节省多少token,不能只看首轮请求变小了多少,我们来算笔账就知道了

假设我们这个工作流涉及到200轮次的模型交互(用简单算法,就是等价于100次模型请求),我们设:

1
2
3
基础模型调用:100 次
每次调用后,历史上下文增加:2k
所以第 n 次请求的普通历史上下文约为:2k * n

先算不含工作流文档的普通上下文:

1
2
3
2k * (1 + 2 + ... + 100)
= 2k * 5050
= 10.1m tokens

全量加载

每次请求都额外带 30k 工作流文档:

1
2
3
4
普通上下文:10.1m
工作流文档:30k * 100 = 3.0m

总计:13.1m tokens

按需加载,不算额外工具调用

这里要注意的是,拆分文档之后,会多出来5次额外阅读阶段文档的模型调用次数。

假设每20次请求进入一个新阶段,阶段文件依次被读取:

1
2
3
4
5
请求 1-19:   10k * 19 = 190k
请求 20-39: 15k * 20 = 300k
请求 40-59: 20k * 20 = 400k
请求 60-79: 25k * 20 = 500k
请求 80-100: 30k * 21 = 630k

工作流文档部分:

1
2.02m tokens

加上普通上下文:

1
10.1m + 2.02m = 12.12m tokens

按需加载,再算 5 次额外工具调用

这 5 次额外请求也要带当时已有的普通历史上下文。假设额外请求发生在大约第 1、20、40、60、80 次附近:

1
2
3
4
普通上下文额外成本:
2k * (1 + 20 + 40 + 60 + 80)
= 2k * 201
= 402k

再加上这些额外请求里已加载的工作流文档。大致是:

1
5k + 10k + 15k + 20k + 25k = 75k

所以额外工具调用成本约为:

1
402k + 75k = 477k

最终按需加载总成本:

1
12.12m + 0.477m = 12.597m tokens

对比全量加载:

1
2
3
4
全量加载:13.1m
按需加载:12.597m
节省:0.503m tokens
节省比例:约 3.84%

诶,确实省钱了!

对,在这个模型里,即使把额外读取阶段文档的请求算进去,按需加载仍然节省了约0.503m tokens。

但这里真正值得注意的是:这个收益不是无限大的,而是约3.84%。

这和很多人直觉中的“大幅节省”并不一样。原因是,在一个很长的Agent Loop里,真正的大头不只是那30k工作流文档,还有每一轮都会增长、并在后续请求里反复携带的普通历史上下文。

换句话说,渐进式披露确实降低了“文档预加载成本”,只是它不会消灭“历史上下文反复计费”这个基本事实。因此,文件拆分之后,真正要关注的不是“它能不能省”,而是“怎么把省下来的部分保住”。

文件拆分会改变Agent Loop

前面的计算里,按需加载比全量加载省了约3.84%。这个收益是真实存在的,但文件拆分会让Agent多出“判断阶段、读取阶段文件、进入下一阶段”的动作。如果这些动作没有被设计好,收益就会被额外loop消耗掉。

常见需要注意的点包括:

  1. 阶段切换时多出额外规划请求;
  2. 反复读取index文件或同一个阶段文件;
  3. 由于幻觉读错阶段文件(这个在最新的模型里面应该很少会出现了);
  4. 拆分后的文件重复携带公共背景、全局约束和术语解释;
  5. 其他异常让Agent Loop多跑了很多轮;

这些问题不是渐进式披露本身的问题,而是文件拆分之后带来的工程问题。文件拆分让工作流更依赖Agent Loop的稳定性:如果Agent能稳定地只读必要文件、只读一次、并且不重复总结,那么按需加载通常会省token;但如果Agent在阶段之间反复规划、重读、摘要、重试,那么原本省下来的token就会被额外请求吃掉。

所以我的结论是:

  • 渐进式披露可以节省文档预加载成本;
  • 文件拆分需要配合稳定的Agent Loop设计;
  • 省下来的token能不能留住,取决于额外请求和重复上下文是否可控;

如果要判断一个工作流到底有没有省钱,不能只看首轮上下文减少了多少,而要按请求链路拆账:

1
2
3
4
5
6
7
总成本 =
普通历史上下文成本
+ 已加载文档在后续请求中的重复成本
+ 额外工具调用请求成本
+ 重复公共上下文成本
+ tool result / summary重复保留成本
+ cache miss带来的计费放大

这也是为什么有时候你做了渐进式披露,账单下降却没有预期中明显。此时优先检查的不是“渐进式披露有没有价值”,而是Agent Loop里是否多出了请求、重复上下文和缓存损耗。

总结

渐进式披露是非常好的Agent设计思路,尤其适合工具很多、skill很多、任务类型分散的个人助理式系统。它能显著降低首轮上下文压力,也能避免模型一开始就被大量无关工具和规则干扰。

但在重Agent工作流里,我们不能只看“单次请求少读了多少tokens的内容”,还要看文件拆分之后,整个循环最终多跑了多少请求、保留了多少重复内容、缓存命中了多少。

一个比较稳妥的判断标准是:

1
2
3
如果文档很大、阶段明确、读取次数可控,渐进式披露通常能省。
如果阶段边界模糊、Agent经常重读和重试,收益会被吃掉。
如果拆分导致大量公共上下文重复,需要重新设计文件结构。

所以,渐进式披露可以节省消耗,但不是把一个大文件机械拆成多个小文件就结束了。它省下的是“提前加载未来信息”的成本,而文件拆分要解决的是另一个问题:让Agent Loop稳定、可预期,并且不要为了读取这些小文件制造新的浪费。


  1. 有关Function Call的概念,可以参考本站文章:点我 ↩︎

  2. 对于一个重Agent工作流而言,说明文字有几十K是很正常的。 ↩︎