起因
用 dsh 跑了几天以后我想知道钱花在哪了。不是估算,是想要每个请求的真实数字。
结果发现 dsh 已经把这些都写下来了,只是没人去读。
数据在哪
dsh 每次运行都会在 $DSH_HOME/sessions/<工作目录>/session-<id>/session.jsonl.zstd 写一份会话日志。解开以后是 jsonl,里面有两类事件正好够用。
第一类是 assistant/chunk,chunk.type 为 usage 的那些。
json
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage",
"usage":{"inputTokens":8246,"outputTokens":118,"cacheReadTokens":0,"reasoningTokens":18}}}}
注意 inputTokens 和 cacheReadTokens 是分开的。这不是 dsh 自己估的。@deepseek-ai/dsh-llm-deepseek 在记录之前就把 DeepSeek 返回的 prompt_cache_hit_tokens 和 prompt_cache_miss_tokens 拆开了,所以这是厂商的数字。
第二类是 request/header,它把实际发出去的完整工具 schema 数组和系统提示词原样记下来了。这意味着你可以量出前缀有多大,一次 API 调用都不用花。
第一个发现,缓存未命中贵 50 倍
先看价格。deepseek-v4-flash 每百万 token,缓存未命中的输入 0.14,缓存命中的输入0.0028,输出 $0.28。这是 2026-08-16 16:00 UTC 调价之前的统一价目表,上面所有测量都在这张表下完成。(按 #2064 的新表把 v4-pro 的运行重算,节省率 20.1% 变 19.8%,而省下的绝对金额接近翻倍。)
命中和未命中差 50 倍。
这条比什么都重要。它意味着提示词大不大不是关键,关键是它被按哪个价格付了几次。
第二个发现,第一个请求就是那 50 倍
我拿一个固定任务跑了一遍。任务是修三个失败的单元测试,一共 6 个请求。
| 请求 | 未命中输入 | 命中输入 | 输出 |
|---|---|---|---|
| 1 | 8,246 | 0 | 118 |
| 2 | 275 | 8,320 | 272 |
| 3 | 1,048 | 8,832 | 538 |
| 4 | 1,600 | 10,368 | 529 |
| 5 | 292 | 12,416 | 72 |
| 6 | 247 | 12,672 | 280 |
从第二个请求开始,cacheReadTokens 一路从 8,320 涨到 12,672,说明缓存在正常工作,对话越长被缓存的部分越多,每次真正未命中的只有新追加的那点内容。
但第一个请求是 8,246 个未命中 token,一分钱缓存都吃不到。
按价格算,仅这一个请求就占了整场会话账单的 52%(三次运行平均,每次都恰好 8,246 个 token)。
那 8,246 个 token 是什么
用 request/header 拆开看。
系统提示词 4,100 字符
工具 schema 26,182 字符,25 个工具
合计 30,282 字符
工具 schema 占了前缀的 86.6%。按大小排,最大的几个是这样。
perl
3,986 workflow
3,242 bash
2,380 str_replace_editor
1,420 subagent
1,344 todo_write
1,296 list_agents
1,141 update_goal
1,128 subagent_fork
1,072 edit
922 glob
我盯着这个列表看了一会儿。workflow、subagent、subagent_fork、list_agents、update_goal,再加上没进前十的 send_message、interrupt_agent、create_goal、get_goal、job_output、job_kill、job_list、ralph。我那几天所有的会话里,一个都没调用过。
它们加起来是 13,730 个字符,占工具 schema 的 52.4%。每一场会话,我都在为它们按 50 倍价格付一次钱。
试着关掉
dsh 的 profile 是一叠 patch 层,所以关掉一行不需要动核心,写个 patch 覆盖就行。
yaml
- id: tool-workflow
disabled: true
- id: tool-subagent
disabled: true
# ... 其余七行同理
只关工具行,它们背后的服务照常挂载,依赖注入的地方仍然能解析到。
关完再量一次。
| 工具数 | 系统提示词 | 工具 schema | 合计 | |
|---|---|---|---|---|
| 默认 | 25 | 4,100 字符 | 26,182 字符 | 30,282 字符 |
| 关掉之后 | 12 | 1,853 字符 | 12,452 字符 | 14,305 字符 |
系统提示词也少了一半多,这个我没预料到。原因是 agent-instructions 会为每个挂载的工具生成一段说明文字,工具行没了,那段话也就没了。等于同一份工具说明本来在前缀里存了两遍。
省了多少
关键是不能只看省了多少钱,还得证明便宜下来不是因为少干了活。所以每个任务都自带测试套件,跑完直接验。
32 次运行,dsh 0.1.0-rc.6,每次都从任务的干净副本开始。
| 任务 | 请求数 | 默认 | 关掉之后 | 省下 | 交付物是否相同 |
|---|---|---|---|---|---|
| 一个问题,不改文件 | 3 | $0.001292 | $0.000755 | 42% | 无测试可跑 |
| 修好 3 个失败测试 | 6 | $0.002201 | $0.001671 | 24% | 相同,两边 9 个测试全过 |
| 按 16 个测试实现一个模块 | 4 | $0.002542 | $0.002373 | 7% | 相同,两边 16 个测试全过 |
先看第三行。每个任务的缓存未命中 token 都稳定下降 22% 到 43%,那是这个方法直接控制的部分;但换算成钱不可靠。在实现任务上精简后的 agent 反而多走了步数(请求 4.4 对 5.4)、输出多了 24%,把省下的大部分吃了回去,两组的逐次花费区间是重叠的。这一行要跑到每组 7 次才稳下来。
每一组配对运行,测试套件两边都是绿的。
说清楚限制
稀释它的是输出,不是会话长度。 这个方法砍掉的是一个固定量,大约 3,700 个未命中 token,就在每场会话最前面,会话里其他所有花费都会稀释它。稀释得最厉害的是输出,输出单价是未命中价的两倍。3 个请求的提问省 42%,4 个请求的实现任务只省 7%,所以变量不是请求数,是输出量。
关掉的东西是真的没了。 如果你在用子智能体、workflow、goal 系统、后台 job 或 ralph 循环,那这套不适合你,模型会直接告诉你没有这个工具。
百分比跟模型走。 机制与厂商无关,但数字取决于缓存命中价和未命中价的比值,v4-pro 的比值是 120 倍,不是 50 倍。
打包了一下
上面那个 patch 我发成了插件。
sh
dsh plugin --profile web add dsh-lean # 然后在模式菜单里选 "Lean"
dsh plugin --profile headless add dsh-lean
补一句机制上的事:在 web profile 上,bundle patch 是够不到工具的。dsh-web-app 在顶层已经把那些行关了,真正的工具目录在 standard 这个 preset 的组合里面,而 patch 层够不到 preset 组合内部。所以在 web 上它改用 ctx.agentPresets.copy() 复制一份 standard 再在副本里关。实测 web 上前缀从 25 个工具 32,436 字符降到 12 个工具 15,334 字符。
仓库在 github.com/sjh9714/dsh... 。基准测试脚本、三个任务、每一次运行的原始 json 全都提交在里面,你可以用自己的 key 把这张表重跑一遍。
如果你只想量自己的会话、什么都不想装,一行就够:
sh
npx dsh-lean audit
它会把逐请求的缓存拆分和你自己前缀里最大的那几个工具 schema 打出来。
最后
这件事真正的收获不是省了那几分钱,是搞清楚了账单的形状。前缀贵不是因为它大,是因为它被按 50 倍价格付了一次。 想清楚这一点以后,该优化哪里就很明显了。
顺带一提,dsh 把请求头原样写进会话日志这个设计,让这类测量的成本降到了零。如果有官方支持的读法,欢迎在评论里告诉我,我更愿意用那个。