Featured image of post 从调用次数到 token 账单:会话成本实测与长程任务的跨会话接力

从调用次数到 token 账单:会话成本实测与长程任务的跨会话接力

从调用次数到 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×):

1
billed = (input − cachedRead) × 1 + cachedRead × 0.1 + output × 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.7M83%
未命中输入21.9M13%
输出7.4M4%

单次调用多少钱:只看当前上下文有多大


42 个调用数 ≥100 的主会话,按 ctx/call 排开。这两个量几乎是同一条直线。

会话调用次数ctx/callbilled/call
01a105d833158.8k8,377
01a105aa33481.4k10,685
01a124f3310112.2k14,210
01a11024218131.8k15,278
01a116ce226142.2k15,750
01a1106e257173.1k19,011
01a124b7285184.7k19,996
01a0ed2c838185.1k21,759
01a1239c320199.1k21,557
01a12058248207.9k23,970
01a1232d353235.8k26,106

换成一眼能用的口诀:每 10k 常驻上下文,每次调用多 ~1.0k 计费 token。注意第一行和最后一行——调用次数差 2.5 倍,而单次成本差 2.6 倍,可见决定单次价格的是 ctx/call,跟调用次数没关系。调用次数本身不产生成本,“上下文规模 × 调用次数"才产生成本。 缓存让第二次之后的重发便宜十倍,但重发这个动作没有消失。

增长律:累计成本对调用次数是平方的


会话里的上下文自己会长:工具输出、文件读、模型思考都往里加。实测 30 个未压缩主会话的逐轮上下文,线性拟合的斜率中位数:

1
2
3
ctx(k) ≈ 28,000 + 1,062 × (k − 1)   raw token
  g 中位 1,062(p25 937,p75 1,366)
  c0 取实测冷启动第一调用的中值档(19k–34k,见下节)

代进计费式,得到两个可以直接用的公式:

1
2
第 k 次调用   billed(k) ≈ 3,915 + 123 × (k − 1)
累计 n 次     cum(n)    ≈ 3,850·n + 61.3·n²
k单次边际累计到 n
13.9k—
10016.1k1.0M
30040.6k6.7M
800101.9k42.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.5M9.9×
890k(85% 自动压缩)166.5M2.50×
400k78.8M1.18×
300k(观测到的手动水位)62.4M0.94×
200k43.7M0.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:461,858 B19,127
10-07 21:276,612 B22,464
10-09 19:0720,924 B29,420
10-10 13:3737,655 B33,249
10-10 14:1441,195 B34,408
10-10 15:29(瘦身后)6,351 B23,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。

1
注入载荷每多 1 KB ≈ 每次冷启动多 ~390 token

600 行的 AGENTS.md 不是理论风险。它在我的账本上把"进入工作空间"这件事的成本抬高了 80%,而且它每次调用都跟着重发一遍。我后来给 handoff skill 加了防膨胀条款(下面细说),起因就是这组数。

handoff:把"重置"的时机和内容拿回自己手里


压缩和换会话是同一个机制(丢掉上下文、留下摘要)的两种实现。区别是压缩的触发权在水位线,换会话的触发权在任务边界。handoff 是我为此写的 skill。

它只做一件事:把当前会话的工作进度固化成下一个会话能自动接续的记录,再通过 AGENTS.md 的入口摘要块建立关联。结构是两层:

1
2
3
AGENTS.md 摘要块(≤10 行/块,入口层——恢复场景读到这里通常就够)
  └─ .handoff/<任务名>.md(热层详情,覆盖写;完成 → archive/ 冷层)
       └─ 卡内指针 → 专题文档(规格分册 / 取证附录 / pitfalls)

几条我认为是设计上的关键:

  • 卡核心只有两块:①上一批完成记录(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 input582.1M(98.9% 是缓存读)
billed(0.1× 口径)66.62M
billed/call20,480
ctx/call178,935
.handoff 相关工具调用138(占模型调用 4.2%)
每次接力的重启税23.2k × 15 ≈ 0.35M(链内 input 的 0.06%)

三条结论:

  1. token 上打平。 对照已经用手动压缩控制水位的单会话(01a0ed2c),接力链是 0.94–1.0×——不省,也不亏。差距出现在对照"放任到自动阈值"那一档,那里是 2.5×。
  2. 胶水很便宜。 .handoff 读写调用占模型调用 4.2%,重启税占链内 input 的 0.06%。handoff 的机制开销可以忽略,也就意味着它的收益上限不在这里——它不是靠省操作费取胜的。
  3. 省 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 每轮都要按缓存价再交一遍。手上真正能立刻改小的东西不多,这两个是其中最大的两个。