caveman `/caveman-stats` 技能解析:从会话日志读取真实 Token 用量并计算净节省
caveman-stats 是 caveman 项目用于回答"这个会话到底用了多少 token、省了多少"的统计技能。它的核心特点是不依赖模型估算:数字直接读自 Claude Code 的会话日志(JSONL transcript),由 hooks/caveman-stats.js 脚本计算后,经 mode-tracker 钩子注入给用户。读完后你能掌握该技能的完整触发链路、会话日志解析方式、节省/开销/净节省三项指标的精确计算口径、每模式归属(per-mode attribution)机制,以及如何直接用命令行运行脚本做离线验证。
技能定位与触发方式
技能定义见 SKILL.md(仓库中还有同内容的镜像 SKILL.md),其 frontmatter 声明:
name: caveman-stats;- 用途:Show real token usage and estimated savings for the current session. Reads directly from the Claude Code session log — no AI estimation.
- 触发词:
/caveman-stats; - 关键设计:模型本身不计算任何数字,输出由 mode-tracker 钩子注入。SKILL.md 原文表述为"hook 以格式化统计作为 reason 返回
decision: "block"",而当前 mode-tracker 实现 的做法等价且更温和:把脚本输出放进hookSpecificOutput.additionalContext,并附一句指令"Print this stats block verbatim inside a fenced code block. Say nothing else.",让模型原样转发统计块。用户随即看到数字,无需等待模型生成。
对应的斜杠命令包装在 caveman-stats.toml 中,仅两行:prompt = "/caveman-stats {{args}}",即把附加参数透传给技能。
执行链路:从斜杠命令到统计输出
当用户在 Claude Code 中输入 /caveman-stats 时,链路如下(依据 caveman-mode-tracker.js):
- UserPromptSubmit 钩子拦截。钩子注册为
UserPromptSubmit事件,收到用户输入的 JSON payload; - 匹配 stats 命令。正则
/^\/caveman(?::caveman)?-stats(?:\s+(.*))?$/同时接受/caveman-stats与命名空间形式/caveman:caveman-stats; - 组装子进程参数并执行
src/hooks/caveman-stats.js:--session-file <transcript_path>:透传 hook payload 中的 transcript 路径,保证读取当前活动会话,而不是最近修改的某个 JSONL;--session-id <id>:让统计脚本丢弃属于其他窗口的模式切换日志行;- 透传尾部参数:
--share、--all、--since <Nh|Nd>。
- 2.5 秒看门狗。子进程
execFileSync设timeout: 2500(hook 注册允许 30 秒,Windows 进程启动慢,脚本自身已有 Node 启动开销,故留了余量);超时或失败时降级输出caveman-stats: could not run stats script.并提示手动运行node hooks/caveman-stats.js。 - 注入 additionalContext,模型按要求原样输出统计块。
一个值得注意的实现细节:钩子按 chunk 解析 stdin JSON(而非等 EOF),因为 Windows 管道上 EOF 关闭可能任意延迟,而该钩子只有 5 秒预算;解析完成后对 stdin 执行 pause() + unref(),避免事件循环空转直到宿主杀掉进程(见 caveman-mode-tracker.js 尾部的 stdin 处理)。
数据来源:会话 JSONL 的解析口径
caveman-stats.js 的 parseSession 逐行读取会话 transcript(每行一个 JSON 对象),只统计 type === 'assistant' 且带 message.usage 的行:
| 字段 | 来源 | 含义 |
|---|---|---|
| Output tokens | usage.output_tokens 累加 |
模型实际产出的输出 token |
| Cache-read tokens | usage.cache_read_input_tokens 累加 |
缓存命中的输入 token(只读展示,不参与节省计算) |
| Turns | assistant 带 usage 的行数 | 轮次,用于规则开销计算 |
| Model | 首个 message.model |
用于匹配输出单价 |
| messages | 每行 {ts, outputTokens} |
供按模式归属使用 |
找不到会话时(未传 --session-file 且 ~/.claude/projects 下没有 .jsonl),脚本向 stderr 输出 caveman-stats: no Claude Code session found. 并以非零码退出。配置目录可由环境变量 CLAUDE_CONFIG_DIR 覆盖,默认为 ~/.claude。
节省估算:压缩率与输出单价
压缩率:COMPRESSION 目前只有一个条目 { 'full': 0.65 },来自 benchmarks/ 中 10 个任务、sonnet-4-20250514 的平均 avg_savings: 65。lite / ultra / wenyan 等模式没有基准数据,输出会直接写明 No savings estimate for '<mode>' mode — only 'full' has benchmark data.,不做猜测。
估算公式(deriveSavings):
estSavedTokens = round( tokens / (1 - ratio) ) - tokens
即:已知压缩率 65% 时,用当前输出量反推"不用 caveman 时大约会有多少输出",差额即节省量。
美元换算:脚本内置一张按模型 id 前缀匹配的输出单价表(MODEL_OUTPUT_PRICE_PER_M,取第一个命中的前缀,最具体的前缀必须排在最前),例如:
| 前缀 | 输出单价(USD/M tokens) |
|---|---|
claude-fable-5 / claude-mythos-5 |
50 |
claude-opus-5 |
25 |
claude-sonnet-5 |
10 |
claude-opus-4-0 / claude-opus-4-1 / claude-opus-4-2025(4.0/4.1 旧档) |
75 |
claude-opus-4(4.5 及以上) |
25 |
claude-sonnet-4 |
15 |
claude-haiku-4 |
5 |
claude-3-5-sonnet / claude-3-5-haiku / claude-3-opus |
15 / 4 / 75 |
模型无法匹配任何前缀时,只报 token 数、不报美元,避免给出无依据的金额。
规则开销与净节省:不藏着"净负"区间
这是 SKILL.md 强调的第二部分。只要存在已知轮次的节省估算,输出就会包含两行:
Est. rule overhead:规则开销 = 每轮注入 caveman 规则的输入 token 成本 × 轮次。默认 1,250 tokens/turn(SKILL.md ~5 KB 规则注入 + 每轮强化提醒的量级),可用环境变量CAVEMAN_RULE_OVERHEAD_TOKENS覆盖——ruleOverheadPerTurn 要求它是正整数,否则回落到默认值;Est. net:净节省 = 节省量 − 开销(deriveNet)。注意节省量是 输出 token、开销是 输入 token,分属不同计费桶,但把两者相加是"整个预算口径"下唯一诚实的差值(脚本注释明确引用 HONEST-NUMBERS.md)。
当净值为负时,netLines 会直白输出:
Est. net: -2350 (caveman cost more than it saved for this workload — consider turning it off)
即直接建议对该工作负载关闭 caveman,而不是用总节省数字掩盖净负区间。这与 docs/HONEST-NUMBERS.md 的立场一致:该页面明确列出 caveman 净负的场景(简短编码问答、按请求计费的 Copilot 场景等),并建议用 A/B 对照服务商账单做最终裁决。
另外,Est. output reduction: ~X% 这一行(终身视图)在 outputReductionPct 中有严格限定:它只是 saved / (saved + used) 的输出 token 缩减比例,不是"占会话用量或配额的比例"——因为代理式会话中输入 + 缓存 token 才是大头且不受 caveman 影响,源码注释特意要求永远不要把它标成 usage/budget 占比。
每模式归属:会话中途切换模式时不虚报
attributeByMode 解决"会话中途换过模式"的归属问题:不能把整个会话的 token 全记在统计时刻的 flag 值上(verbose 轮次会被算成压缩轮次,或反过来)。归属依据按精确度分三档:
log:mode-tracker 的recordModeChange会在每次真实切换时向.caveman-mode-log.jsonl追加{ts, mode, prev}行;stats 用消息时间戳与切换日志做 join,第一条行之前的区间归属该行的prev。readModeLog 会按session_id过滤掉其他窗口的行,防止多窗口交错污染时间线;flag-mtime:无切换日志、但 flag 文件 mtime 晚于首条消息——说明模式是会话中途写的,只有写入之后的 token 可归属当前模式,之前的 token 记为 unattributed 并排除出估算(宁可少算,不猜);whole-session:既无日志也无中途变更证据——按当前模式覆盖全会话(模式从未改变时即为正确答案)。
混合模式时,输出会给出逐模式明细(Mode changed mid-session — output attributed per mode:),无基准数据模式标注 no benchmark estimate,未知区间标注 unattributed: N tokens (mode unknown — excluded from estimate)。
终身统计、记忆压缩与状态栏后缀
- 终身视图:每次运行(turns > 0)都会向
~/.claude/.caveman-history.jsonl追加一条会话快照(含session_id、output_tokens、turns、est_saved_tokens、est_saved_usd)。--all或--since 7d/--since 24h(parseDuration 仅接受Nh/Nd,非法格式退出码 2)触发终身聚合;aggregateHistory 对同一 session 只取最新一条快照,且净节省只累加真实记录了turns的行(老格式行没有 turns 字段,混入会扭曲开销计算)。 - 记忆压缩:findCompressedPairs 扫描
*.original.md备份对(caveman-compress 的产物),以字节差 ÷ 4(英文约 4 字符/token)估算每次会话启动的被动输入节省,输出Memory compressed: N files, ~X tokens saved per session start (approx)。 - 状态栏:每次运行都会把聚合节省写成
~/.claude/.caveman-statusline-suffix(如⛏ 1.2M),shell 状态栏脚本可直接 cat 该文件,无需解析 JSONL。 --share:输出一行可分享的总结,例如🪨 Saved 650 output tokens (~$0.010) across 3 turns this session — caveman.sh。
直接运行与测试验证
不依赖 Claude Code 也可以直接运行脚本:
# 读取最近修改的会话
node src/hooks/caveman-stats.js
# 指定 transcript 与窗口过滤(配置目录可用 CLAUDE_CONFIG_DIR 覆盖)
CLAUDE_CONFIG_DIR=~/.claude node src/hooks/caveman-stats.js --session-file <path>.jsonl --since 7d
完整性守护方面,脚本对同目录的 caveman-config.js 做了两层防护:文件缺失时输出一行可操作的报错(提示 /plugin update caveman 或重跑 install.sh)并退出码 1;文件存在但导出形状不符(插件缓存漂移)同样拒绝运行——因为 stats 没有"降级半报表"的可用输出。opencode 安装布局把同目录模块改名为 .cjs,脚本按"错误信息点名本模块"这一条件做了一次精确的重试。
tests/test_caveman_stats.js(约 900 行)用临时目录伪造 .claude/projects 下的会话 JSONL 验证了这些口径,例如:两条 assistant 消息分别 100/50 输出 token、200/50 缓存 token 时,断言输出含 Turns: 2、Output tokens: 150、Cache-read tokens: 250;flag 为 full、输出 350 token 时断言 Est. without caveman: 1,000、Est. tokens saved: 650 (~65% of output)(即 350/0.35 = 1000);flag 为 ultra 时断言出现 No savings estimate for 'ultra' mode。另有针对 mode-tracker 注入路径的用例,确认 /caveman-stats 经钩子投递。
适用前提小结
- 数字的"真实"部分(输出/缓存 token、轮次)严格来自会话日志;节省估算是基准压缩率 × 前缀匹配的公开输出单价,属于估计值,输出中始终带
Est.与approx标注; - 规则开销默认 1,250/turn,若你的注入规则集不同,建议实测后用
CAVEMAN_RULE_OVERHEAD_TOKENS覆盖; - 该技能面向 Claude Code 会话日志格式;HONEST-NUMBERS.md 建议最终裁决以同一任务开/关 caveman 的服务商账单 A/B 为准,
/caveman-stats提供的是会话内可复核的明细与口径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00