首页
/ caveman 的 /caveman-stats:opencode 插件中的会话 Token 节省统计命令实现详解

caveman 的 /caveman-stats:opencode 插件中的会话 Token 节省统计命令实现详解

2026-09-06 17:19:22作者:姚月梅Lane

在 caveman 的 opencode 插件中,/caveman-stats 是一个由纯 Markdown 提示词模板驱动的斜杠命令:它不执行任何脚本,而是让模型读取 caveman 的终身历史日志,输出一张包含总节省 token、会话数与平均压缩比的简短统计表。读完本文,你将理解这条命令在 opencode 插件体系中的完整定义、它所依赖的历史日志数据格式、与之对应的 Claude Code 端 caveman-stats.js 脚本的源码级实现细节,以及测试如何保证每一个数字都可验证、不夸大。

命令定义文件:frontmatter 与提示词正文

/caveman-stats 的完整定义位于 caveman-stats.md,全文如下:

---
description: Show caveman lifetime token-savings stats
---
Show caveman stats — total tokens saved, sessions, average compression ratio.

Read the lifetime history log at `~/.config/caveman/.caveman-history.jsonl`
(or wherever the caveman-stats script writes it). Output: total saved,
sessions counted, avg ratio. One short table.

这份文件由两部分构成:

  1. YAML frontmatterdescription 字段供 opencode 的命令补全/列表展示,声明该命令的用途是"显示 caveman 终身 token 节省统计"。
  2. 提示词正文:这才是命令的真正逻辑。opencode 在用户输入 /caveman-stats 时,会把命令文件中的正文替换进消息发给模型(plugin.js 源码注释中明确说明:opencode 会在 chat.message 钩子看到消息之前,把输入的斜杠命令替换为命令文件的提示词文本)。也就是说,这段文字本质上是一份给模型的执行指令,包含三个明确约束:
    • 做什么:展示 caveman 统计——总节省 token 数(total tokens saved)、会话数(sessions)、平均压缩比(average compression ratio);
    • 从哪读:终身历史日志,默认路径为 ~/.config/caveman/.caveman-history.jsonl,括号内的补充说明承认实际路径取决于 caveman-stats 脚本的写入位置;
    • 输出形式:total saved、sessions counted、avg ratio 三项数据,且要求压缩成"一张简短表格"(One short table),符合 caveman 自身的简洁风格。

这与 caveman 在 Claude Code 端的实现形成对照:Claude Code 中 /caveman-stats 是通过 caveman-mode-tracker.js 这个 UserPromptSubmit 钩子拦截后,直接执行 caveman-stats.js 子进程(2.5 秒超时),并把脚本输出以 additionalContext 的形式要求模型逐字转述;而 opencode 端没有等价的脚本执行通道,因此 README 中明确列出该插件"不提供 statusline 徽章"等限制,统计能力退化为"模型读文件 + 汇总"的提示词方案。

数据来源:终身历史日志 .caveman-history.jsonl

命令提示词指向的 ~/.config/caveman/.caveman-history.jsonl 是 caveman 的终身累计统计账本。从源码 caveman-stats.js 看,脚本实际写入的路径是 $CLAUDE_CONFIG_DIR(未设置时为 ~/.claude)下的 .caveman-history.jsonl——这正是模板里 "(or wherever the caveman-stats script writes it)" 这句留白的原因:路径随宿主与安装布局而变,命令让模型自行寻找。

每次运行 stats 且会话已有至少一个回合时,脚本会向该文件追加一行 JSON 快照main 函数):

{
  "ts": 1725000000000,
  "session_id": "s",
  "mode": "full",
  "model": "claude-sonnet-4-7",
  "output_tokens": 350,
  "turns": 1,
  "est_saved_tokens": 650,
  "est_saved_usd": 0.00975
}

各字段的写入逻辑与语义:

字段 来源 说明
ts Date.now() 快照时间戳,用于时间窗过滤与"每会话取最新"去重
session_id 钩子转发或 transcript 文件名 会话标识,终身聚合的主键
mode .caveman-active 标志文件 当前 caveman 模式(如 full),非 active 则为 null
model 会话 JSONL 中首个带 usage 的 assistant 消息 用于查输出 token 单价
output_tokens / turns 会话日志解析 输出 token 总量与助手回合数
est_saved_tokens 按模式归属后的估算 见下文"按模式归属"
est_saved_usd token 数 × 模型输出单价 未知模型时为 0

追加写入走 caveman-config.js 提供的 appendFlag,该写入器是符号链接安全的(拒绝 symlinked 目标、原子 temp+rename、0600 权限),这一点有专门测试 appendFlag is symlink-safe (refuses symlinked target)test_caveman_stats.js)守护。

同一会话内多次运行 /caveman-stats 会写入多行,但聚合时只保留每个 session_id 时间戳最新的一行——aggregateHistoryMapsession_id 覆盖实现(无 session_id 的旧行归入 '_' 桶)。测试 --all aggregates latest entry per session 验证了这一点:两个会话、其中一个会话有两条快照时,只计最新一条,185 + 371 = 556

底层脚本:会话解析、模式归属与节省估算

虽然 opencode 端由模型直接读历史文件,但同一套数据的生产端是 caveman-stats.js(约 680 行),理解它才能理解历史日志里每个数字是怎么来的。

会话日志解析

parseSession 逐行解析 Claude Code 的会话 JSONL,只统计 type === 'assistant' 且带 message.usage 的条目,累计 output_tokenscache_read_input_tokens 与回合数 turns,同时记录每条消息的 {ts, outputTokens} 供后续时间对齐。无 --session-file 参数时,findRecentSession 会在 $CLAUDE_CONFIG_DIR/projects 下深度优先搜索 mtime 最新的 .jsonl 文件。

按模式归属(#601):不把整段会话算给当前模式

统计中最容易出错的是"整段会话的 token 都记在 stats 时刻 flag 所显示的模式下"——会话中途开启 caveman 会虚增节省,中途关闭则归零。attributeByMode 采用三级证据链解决:

  1. log(最精确):模式追踪钩子(caveman-mode-tracker.js 中的 recordModeChange)在每次真实模式切换时向 .caveman-mode-log.jsonl 追加 {ts, mode, prev} 行;stats 把这些时间戳与会话消息时间戳对齐,每条消息的 token 记在其生成时刻实际激活的模式名下。会话隔离由 --session-id 参数保证,其他窗口的切换行会被丢弃(readModeLog)。
  2. flag-mtime:无切换日志但 flag 文件在会话中途被写入时,只有写入之后的 token 可归属;之前的 token 标记为 unknown 并排除而非猜测(源码称之为 no-fake-savings 原则)。
  3. whole-session:既无日志也无中途变更证据时,才按"当前模式覆盖整段会话"处理(#601 之前的行为,仅在模式从未变化时正确)。

测试 attributes tokens to the mode active when each message happened (#601)test_caveman_stats.js)构造了"开启前 300 verbose token + 开启后 350 token"的会话:正确结果是只有 350 个 full 模式 token 产生 650 的节省估算,而旧的全会话算法会虚报 1,207。

压缩比与金额估算

节省估算的核心是一张模式→压缩比表(第 81 行):

const COMPRESSION = { 'full': 0.65 };

目前只有 full 模式有 benchmarks/ 目录中 10 个任务、sonnet-4 的实测均值 65% 支撑;lite/ultra/wenyan 等模式在跑过基准并提交结果前不产生任何估算值——对 ultra 模式直接输出 No savings estimate for 'ultra' mode — only 'full' has benchmark data.

估算公式(deriveSavings):已知有 caveman 时输出为 T、压缩比为 r,则无 caveman 时的等效输出约为 T / (1 - r),节省即两者之差。测试用例验证:350 个 full 模式 token → 350 / 0.35 = 1000 → 节省 650,约 65%。

金额换算由 MODEL_OUTPUT_PRICE_PER_M 这张按前缀匹配的输出单价表完成,覆盖 Claude 5 系列(Fable/Mythos $50/M、Opus 5 $25/M、Sonnet 5 $10/M)到 Claude 3 系列的各档,且"最具体的前缀必须排在前面"(因为 priceForModel 返回首个命中)。未知模型(如 gpt-4)返回 null,此时只保留 token 估算、省略 USD 行——测试 omits USD line when model is unknown 守护了该行为。

规则开销与净额:不藏起负收益

caveman 的规则每回合都会注入约 1,000–1,500 个输入 token(约 5 KB 的 SKILL.md 加每回合的强化提醒,详见 HONEST-NUMBERS.md)。脚本用每回合 1,250 token 的默认值(第 89 行,可用环境变量 CAVEMAN_RULE_OVERHEAD_TOKENS 覆盖)计算开销,deriveNet 输出 Est. net = 节省输出 − 规则输入开销。关键设计:

  • 净额为正时显示 Est. net: +1,536 (net saving after rule overhead)
  • 净额为时直白提示 caveman cost more than it saved for this workload — consider turning it off——测试 session shows a NEGATIVE net and tells the user to consider turning caveman off (#145) 专门验证了这条文案;
  • 若节省区间本身无法归属(unknown tokens)或模式无基准数据,则干脆不输出净额行,避免用猜测的数字拼出净额。

CAVEMAN_RULE_OVERHEAD_TOKENS 的输入校验同样有测试:非数字、0、负数、小数一律回落到默认 1250,而不是产出无意义的开销。

输出缩减比例:只敢说"输出占比"

outputReductionPct 计算的唯一比例是 saved / (saved + used),即 caveman 避免了多大比例的本会产生的输出 token。源码注释明确说明:agentic 会话中 input + cache token 占绝对大头且计入 Pro/Max 限额,而 caveman 并不减少它们,所以这个数字绝不能被标榜为"会话用量占比"或"预算占比"。测试 session view never claims a % of usage/budget — only output reduction 甚至用正则断言输出中不得出现 budget / of your usage 等字样,也不得凭空编造 Anthropic 配额尺寸。

命令行接口:脚本可直接运行的完整参数

在 Claude Code 之外,脚本本身是一个可独立运行的 CLI(main),/caveman-stats 斜杠命令的参数会被模式追踪钩子原样转发:

参数 行为 验证测试
--session-file <path> 指定要解析的会话 JSONL(钩子用 transcript_path 传入,保证读的是活跃会话而非最新 mtime 文件) reads --session-file directly and sums output tokens
--session-id <id> 按会话过滤模式切换日志,避免其他窗口的切换行污染本会话时间线 #601 系列测试
--share 输出单行可分享摘要,如 🪨 Saved 650 output tokens (~$0.0098) across 1 turns this session — caveman.sh;无基准数据时退化为 🪨 1 turns, 200 output tokens this session --share prints single-line tweetable summary
--all 输出终身聚合视图:SessionsOutput tokensEst. tokens savedEst. output reductionEst. saved (USD) 及净额块;无历史时提示 No sessions logged yet --all aggregates latest entry per session
--since <Nh|Nd> 时间窗过滤,如 7d24h;非法格式(如 sometime)以退出码 2 报错 --since takes Nh or Nd (e.g. 7d, 24h), got: ... --since rejects malformed durations

此外脚本还有两个"副作用"输出:

  1. statusline 后缀文件:每次运行后把终身节省总量渲染成 ⛏ 2.8k 形式写入 .caveman-statusline-suffix,供 caveman-statusline.sh / caveman-statusline.ps1 直接 cat 而无需解析 JSONL;写入同样经过 symlink 安全通道,且测试验证了 statusline 会对该文件做控制字节剥离,防止 ANSI 转义注入终端。
  2. 记忆文件压缩检测findCompressedPairs 扫描 $CLAUDE_CONFIG_DIR 与当前工作目录下的 *.original.md 备份对(/caveman-compress 留下的原件),若压缩版更小则按约 4 字符/token 折算,输出 Memory compressed: N files, ~X tokens saved per session start (approx);压缩版不小于原件的伪压缩对被跳过。

测试矩阵:从端到端到纯函数

tests/test_caveman_stats.js 用真实子进程调用覆盖了整条链路,运行方式即文件头注释:node tests/test_caveman_stats.js。代表性用例:

  • 端到端(脚本直跑):造一个 .claude/projects/p/s.jsonl 假会话 + 临时 CLAUDE_CONFIG_DIR,断言 Turns / Output tokens / Cache-read tokens 求和正确;
  • 端到端(经模式追踪钩子):向 caveman-mode-tracker.js{"prompt": "/caveman-stats --share", "transcript_path": sess},断言输出 JSON 的 hookSpecificOutput.additionalContext 含统计块,且运行 stats 不改变模式 flagmode tracker preserves caveman flag when /caveman-stats fires);
  • 纯函数级priceForModel 前缀匹配(含带 [1m] 后缀的模型 id 与 null 输入)、humanizeTokens2786 → '2.8k'1_250_000 → '1.3M')、outputReductionPct 的边界(0 节省返回 null 而非 0%)、deriveNet 与开销覆盖值。

skill 侧的交付说明见 skills/caveman-stats/SKILL.md:该 skill 由 hooks/caveman-stats.js 交付,模型在 skill 触发时无需做任何事——钩子直接以格式化好的统计作为回复上下文,用户立即看到数字。

opencode 插件侧的运行前提与边界

/caveman-stats 放进 opencode 使用时,需要理解 opencode 插件 的整体形态(package.json 标记为 ESM 的 caveman-opencode-plugin):

  • 插件由 session.created 事件写入默认模式到 ~/.config/opencode/.caveman-active,通过 chat.message 拦截 /caveman 命令与自然语言切换,通过 experimental.chat.system.transform 注入每回合强化行;安装布局与文件角色见 src/plugins/opencode/README.md
  • commands/ 目录下共六个斜杠命令模板(/caveman/caveman-commit/caveman-compress/caveman-help/caveman-review/caveman-stats),全部以提示词展开方式工作,因此 /caveman-stats 在 opencode 中的实际效果是"模型按模板指令读历史文件并生成一张表",而不是像 Claude Code 端那样由脚本生成确定性数字。
  • 相应地,opencode 端没有 statusline 徽章(TUI 未暴露插件可写的 statusline),若想在 shell 中查看模式,只能自行读取 ~/.config/opencode/.caveman-active 标志文件。
  • 历史文件路径的差异也随之而来:模板写的是 ~/.config/caveman/.caveman-history.jsonl,而脚本生产端写在 CLAUDE_CONFIG_DIR(默认 ~/.claude)下——若你的环境从未跑过 Claude Code 端的 stats 脚本,历史文件可能尚不存在,此时 opencode 端的命令应如实报告"暂无统计",而不是编造数字。

诚实数字边界:统计输出能声称什么

stats 的所有输出都受 docs/HONEST-NUMBERS.md 约束,值得随命令一并理解:

  • caveman 的响应 skill 只压缩输出,不压缩输入、上下文、文件与思考 token(本地引擎与代理是独立组件);
  • skill 每回合新增约 1–1.5k 输入 token 的规则成本,若输出节省低于该成本即为净亏(简短编码问答、按请求计费的平台如 Copilot 是典型场景);
  • 官方规则是:对同一任务开/关 caveman 各跑一次,以服务商账单总额对比为准;A/B 结果为净负时直接关掉。

这也是为什么 caveman-stats.js 的输出措辞如此克制:节省一律标注 "est." 与 "output tokens only; input/cache usage is unchanged",比例只称 "output reduction",未知模型不报金额,无法归属的 token 不猜。

小结

/caveman-stats 在 opencode 插件里只是一段三行提示词(caveman-stats.md),但它背后的数据链是完整的:模式追踪钩子记录带时间戳的模式切换(.caveman-mode-log.jsonl),stats 脚本解析会话 JSONL、按模式归属 token、用唯一有基准支撑的 full 模式 65% 压缩比估算节省、按模型前缀查价折算美元、再减去每回合 1,250 token 的规则开销给出净额,最后把每会话最新快照追加进 .caveman-history.jsonl 终身账本。opencode 端的命令模板则负责在宿主没有脚本执行通道时,让模型基于这份账本输出"total saved / sessions / avg ratio"三要素的简短表格——数字的生产与呈现被分离在两个宿主上,而口径(估算、仅输出 token、不夸大)由同一套源码与 tests/test_caveman_stats.js 中的数十个断言共同保证。

登录后查看全文
热门项目推荐
相关项目推荐