从调用次数到 token 账单:会话成本实测与长程任务的跨会话接力
这篇是对我自己 agent 会话账本的一次完整测算:133 个会话目录、65 个主会话落账本、合计 9,623 次模型调用、175.0M 计费等价 token。前半段是成本结构——单次调用多少钱、为什么累计成本对调用次数是平方的、压缩把曲线压到哪、换会话要交多少入场费;后半段是我为长程任务写的 handoff skill,以及接力链在同一个口径下的实测数字。所有数字都能从本机
~/.grok下的账本复现,全文不带价目表、不用金额。
账本在哪,怎么算
先把计量口径钉死,后面所有结论都建立在这几个等式上。
| 文件 | 粒度 | 用途 |
|---|---|---|
~/.grok/sessions/<cwd>/<session-id>/usage.json | 每会话 + 每轮 | 权威口径:inputTokens / cachedReadTokens / cacheCreationTokens / outputTokens / modelCalls / turnCount |
~/.grok/cache/model-usage.jsonl | 每次 API 调用一条 | promptTokens / cachedPromptTokens / completionTokens + 时间戳(无 sessionId,跨会话归属只能靠时间窗近似) |
<session>/prompt_context.json、tool_definitions.json、system_prompt.txt | 每会话 | 前缀载荷:这次注入了哪份 AGENTS.md、工具定义多大、系统提示多长 |
一个先说的坑:updates.jsonl 里每条流事件的 _meta.totalTokens 是当前上下文规模的实时指针,压缩后会回落,拿它累加会得出荒谬的结论。
计费等价口径(同一模型费率,缓存读按 0.1×):
| |
0.1× 这个系数不是从价目表抄的。在 42 个调用数 ≥100 的主会话上做
billed/call对ctx/call的回归,得到billed/call = 2,869 + 0.1005 × ctx/call,R² = 0.911——斜率落在缓存读的费率上,而不是 1。另一个佐证是缓存读占 raw input 的 98.5%:如果缓存读按全价计费,这个回归的斜率不可能停在 0.1。
本机全量(65 个主会话、9,623 次调用、175.0M billed)的成本构成:
| 项 | 量 | 占比 |
|---|---|---|
缓存读(cachedRead × 0.1) | 145.7M | 83% |
| 未命中输入 | 21.9M | 13% |
| 输出 | 7.4M | 4% |
单次调用多少钱:只看当前上下文有多大
42 个调用数 ≥100 的主会话,按 ctx/call 排开。这两个量几乎是同一条直线。
| 会话 | 调用次数 | ctx/call | billed/call |
|---|---|---|---|
| 01a105d8 | 331 | 58.8k | 8,377 |
| 01a105aa | 334 | 81.4k | 10,685 |
| 01a124f3 | 310 | 112.2k | 14,210 |
| 01a11024 | 218 | 131.8k | 15,278 |
| 01a116ce | 226 | 142.2k | 15,750 |
| 01a1106e | 257 | 173.1k | 19,011 |
| 01a124b7 | 285 | 184.7k | 19,996 |
| 01a0ed2c | 838 | 185.1k | 21,759 |
| 01a1239c | 320 | 199.1k | 21,557 |
| 01a12058 | 248 | 207.9k | 23,970 |
| 01a1232d | 353 | 235.8k | 26,106 |
换成一眼能用的口诀:每 10k 常驻上下文,每次调用多 ~1.0k 计费 token。注意第一行和最后一行——调用次数差 2.5 倍,而单次成本差 2.6 倍,可见决定单次价格的是 ctx/call,跟调用次数没关系。调用次数本身不产生成本,“上下文规模 × 调用次数"才产生成本。 缓存让第二次之后的重发便宜十倍,但重发这个动作没有消失。
增长律:累计成本对调用次数是平方的
会话里的上下文自己会长:工具输出、文件读、模型思考都往里加。实测 30 个未压缩主会话的逐轮上下文,线性拟合的斜率中位数:
| |
代进计费式,得到两个可以直接用的公式:
| |
| k | 单次边际 | 累计到 n |
|---|---|---|
| 1 | 3.9k | — |
| 100 | 16.1k | 1.0M |
| 300 | 40.6k | 6.7M |
| 800 | 101.9k | 42.3M |
margin 里 86% 是缓存重读项。也就是说:哪怕每一次调用都便宜十倍,只要上下文一直涨,“重发一个越来越大的前缀"这件事本身就把账单推成平方。
累计那一列是这篇最该记住的数字:调用次数翻倍,累计成本涨到接近四倍。一个"能干"的长会话不是线性地贵,是平方地贵——这跟我直觉里"多跑几轮没多少钱"完全相反。
谁在重置上下文:压缩
本机实测到的压缩事件:7 个会话、12 次压缩请求,全部 trigger: manual。config.toml 里 context_window = 1,048,576,85% 自动阈值 ≈ 890k,而实测触发水位是 213k / 235k / 255k / 262k / 312k / 388k / 394k——自动压缩一次都没被踩到。
| 项 | 实测 |
|---|---|
| 手动压缩触发水位 | 21 万–39 万 token |
| 自动阈值(85% × 1M) | ~890k,从未触发 |
| 12 次请求中失败 | 3 次(1 次 API 400,2 次网络错误重试 3 次仍失败) |
第三行值得单独记一笔:压缩不是无条件可用的重置手段,它自己会失败。失败之后会话还停在原地、上下文还在涨,你得重试或者换手段。
拿链内实测的工作量(后面那 3,253 次调用)做同口径对照,把同一批活放进一个会话,只改重置水位:
| 重置策略 | billed | 相对 handoff 接力链 |
|---|---|---|
| 不重置 | 661.5M | 9.9× |
| 890k(85% 自动压缩) | 166.5M | 2.50× |
| 400k | 78.8M | 1.18× |
| 300k(观测到的手动水位) | 62.4M | 0.94× |
| 200k | 43.7M | 0.66× |
这张表只能看量级和方向,不能看小数点。单会话离散非常大:我用同一套参数去预测 20 个真实未压缩长会话,模型与实测的比值从 0.57 到 2.88 都有。原因是起手上下文、每轮增量、未命中占比逐会话不同,
g的 p25–p75 本身就有 1.4 倍差距。
一个不用任何模型的实测锚点:01a0ed2c 是单会话 838 次调用、2 次手动 /compact,实测 18.23M billed、21.8k billed/call、ctx/call 185k。接力链是 20.5k billed/call、ctx/call 179k。在同样把上下文水位按住的前提下,换会话和手动压缩的 token 成本是打平的。
换会话不是免费的:重启税
每个新会话的第一次调用是一笔固定入场费:系统提示 + 工具定义 + 注入的项目文档 + 首条用户消息,全部未命中。
pgo-fork 项目的冷启动第一调用实测(cachedPromptTokens = 0):23,243 / 23,434 / 24,370 token。拆开 23.2k:
| 层 | 载荷 | token(估算) |
|---|---|---|
| 工具定义 27 个 | 61,485 B | ≈15.4k |
| 系统提示 | 11,913 B | ≈3.0k |
| AGENTS.md 注入 | 7,676 B | ≈1.9k |
| 首条用户消息 + 脚手架 | — | ≈2.9k |
大头是工具定义,比项目文档高一个数量级,而且它每轮都在上下文里、每次调用都重发(按缓存价 0.1×)。这一项基本由"工具面上挂了多少工具"直接决定——这也是我一直在给 MCP 工具面做减法的原因。
AGENTS.md 是唯一我能在几分钟里改掉的那一项。17 个会话的冷启动第一调用对注入载荷,是一条很干净的线:
| 会话启动 | 注入的 AGENTS.md | 冷启动第一调用 |
|---|---|---|
| 10-04 14:46 | 1,858 B | 19,127 |
| 10-07 21:27 | 6,612 B | 22,464 |
| 10-09 19:07 | 20,924 B | 29,420 |
| 10-10 13:37 | 37,655 B | 33,249 |
| 10-10 14:14 | 41,195 B | 34,408 |
| 10-10 15:29(瘦身后) | 6,351 B | 23,243 |
六天里这份文件从 1.8 KB 涨到 41.2 KB,冷启动第一调用跟着从 19.1k 涨到 34.4k;砍回 6.4 KB 后掉回 23.2k。按 ≥12 KB 和 <12 KB 分两组,中位数分别是 31,159 和 22,464 token。
| |
600 行的 AGENTS.md 不是理论风险。它在我的账本上把"进入工作空间"这件事的成本抬高了 80%,而且它每次调用都跟着重发一遍。我后来给 handoff skill 加了防膨胀条款(下面细说),起因就是这组数。
handoff:把"重置"的时机和内容拿回自己手里
压缩和换会话是同一个机制(丢掉上下文、留下摘要)的两种实现。区别是压缩的触发权在水位线,换会话的触发权在任务边界。handoff 是我为此写的 skill。
它只做一件事:把当前会话的工作进度固化成下一个会话能自动接续的记录,再通过 AGENTS.md 的入口摘要块建立关联。结构是两层:
| |
几条我认为是设计上的关键:
- 卡核心只有两块:①上一批完成记录(commit、测试证据路径、权威规格各一行,接手者能审计"上批真落地了吗”)②交接任务单(做什么 / 计划在哪 / 怎么验收 / 约束)。其余一律摘要 + 指针。
- 入口层是状态快照,不是增量日志:单块 ≤10 行、原地整块覆盖写、禁止"上一批/上一阶段"式的历史叙事。这条被自己的项目验证过反面案例:AGENTS.md 每轮追加一张批次卡,涨到 500+ 行,冷启动 +80%,最后只能下令砍回摘要。
- 冷热分离:已完成的小节、已验证的检查项、失效的开口项随每次更新移出热层,历史整段进
.handoff/archive/(只追加、不改写)。 - 不重复存储:外部文档已有的内容只放指针,卡里整段复述 = 腐化,当场砍。这条直接决定卡的尺寸。
- 渐进式披露:
SKILL.md只留路由和原则(4.9 KB / 51 行),写交接和恢复交接的细节分别在references/writing.md和references/recovery.md,按场景加载,不整篇吞。 - 交接链自足:凭入口摘要块 + 卡内指针就能定位全部详情文档并交付任务,脱离会话记忆。指针靠仓库自身的文件,不靠记忆。
git 同步开关
.handoff/ 默认写进 .gitignore。理由是交接记录是本地现场,带着噪音和可能的隐私(密钥、私有配置),不该进仓库历史。用户明确要求跨设备同步时,才摘掉 ignore,让交接记录跟着仓库走。
顺带实测一个 gitignore 的坑:父目录被整体排除后,目录内文件的
!反选一律不生效。/.handoff/还在.gitignore里时,写!.handoff/handoff.md是个静默的 no-op——git status里什么都不会出现,你甚至不会收到任何警告。要么删掉整行,要么改写成/.handoff/*+!/.handoff/handoff.md。这两种写法我在一个临时仓库里各验证过一遍。
跨设备还有第二个前提:卡里的指针得跟着走。我的 pgo-fork 项目里卡有两份——.handoff/handoff.md,以及知识库中央库里的 wiki/port/handoff.md。后者跟着知识库自己同步,因为项目里的 wiki/ 是符号链接、不在 git 里;只同步 .handoff/ 的话,卡里那些 wiki/port/... 指针换台机器就悬空了。
接力链的实测
口径:pgo-fork 项目 10-07 到 10-10,凡是读写过
.handoff/的主会话都算链内会话。识别方法是updates.jsonl里工具调用的rawInput路径含.handoff。
| 指标 | 实测 |
|---|---|
| 会话 / 轮次 | 16(15 个已落账本)/ 49 |
| 模型调用 | 3,253 |
| raw input | 582.1M(98.9% 是缓存读) |
| billed(0.1× 口径) | 66.62M |
| billed/call | 20,480 |
| ctx/call | 178,935 |
.handoff 相关工具调用 | 138(占模型调用 4.2%) |
| 每次接力的重启税 | 23.2k × 15 ≈ 0.35M(链内 input 的 0.06%) |
三条结论:
- token 上打平。 对照已经用手动压缩控制水位的单会话(
01a0ed2c),接力链是 0.94–1.0×——不省,也不亏。差距出现在对照"放任到自动阈值"那一档,那里是 2.5×。 - 胶水很便宜。
.handoff读写调用占模型调用 4.2%,重启税占链内 input 的 0.06%。handoff 的机制开销可以忽略,也就意味着它的收益上限不在这里——它不是靠省操作费取胜的。 - 省 token 的大头在"卡有多小”。 把 27.3 KB 的卡(393 行)瘦到 2.96 KB(52 行),每个接力会话在读完卡之后的每一次调用都少 7k token 的缓存重发。按链内平均会话长度 217 次调用算,单会话省约 15.8 万计费 token,整链约 2.4M,占链内计费 3.6%。
我的结论
handoff 不是省 token 的工具。它的价值是把"上下文重置"的时机和内容从系统手里拿回来:什么时候重置由任务边界决定,而不是由水位线决定;重置后保留什么是可核查的完成记录 + 任务单,而不是模型生成的一段摘要。
压缩之后你还敢不敢接着干,取决于摘要里有没有留下"上批真落地了吗"的证据。这一点上压缩和 handoff 不是一回事——压缩换来的上下文空间,代价是你无法审计它丢了什么。
而省下来的 token 是副产品,省在哪还挺反直觉:不是省调用次数(胶水调用才 4%),不是省重启税(0.06%),而是省"每次调用都要重发一遍的那点东西"。这条规律对项目文档和工具面同样成立:AGENTS.md 膨胀到 41 KB 那次,代价是每次冷启动 +15k token;而只要会话还在继续,这多出来的 15k 每轮都要按缓存价再交一遍。手上真正能立刻改小的东西不多,这两个是其中最大的两个。