caveman subagent-tax 深度解析:本地测量编码 harness 首请求前缀"税"的方法论与实现
本文基于 caveman 仓库中的 METHOD.md,系统讲解 packages/subagent-tax 这个测量工具如何工作:它用本地 HTTP sink 冒充 LLM 服务端点,让每个已安装的编码 harness(Claude Code、Codex、Gemini CLI、OpenCode、pi、cursor-agent)向本地回环端口发出一次真实的首次 agent-turn 请求,捕获并解析其中的"前缀"——系统提示词加全部工具 schema——从而量化每次子代理(subagent)调用都要重新付出的上下文代价。读完后,你将理解其四阶段测量管线、est/exact 两级 token 记账规则、variant 标注体系、写时脱敏机制,以及每个已知局限背后的协议级原因。
测量对象:首请求前缀,以及 basis: inferred
在展开机制之前,必须确立 METHOD.md 开篇就写死的边界声明:
- Basis 永远是
inferred(推断)。该工具捕获的唯一一件事,是每个已安装 harness 发出的第一个 agent-turn 请求的大小——即系统提示词加工具 schema 组成的前缀。这个前缀会在该 harness 每次调用(包括它派生的每个 subagent 的每次调用)中随请求一起重新发送。它不是任何供应商计费用量、花费或节省数字。 - 词表纪律:工具刻意不用 verified 一词——在 caveman 仓库中它是节省记账(savings-accounting)的保留术语;已经核对过的配方与约定一律称为 confirmed。这一词表约定在 lib/harnesses.mjs 的注释中被再次强调。
"subagent tax"这个名称由此而来:agent 每派生一个子代理,harness 的完整前缀就原封不动重发一遍,前缀越大、工具越多,这个"税"越重。
测量管线:四阶段全解析
阶段 1:本地 sink 冒充四种线协议
核心组件是 lib/sink.mjs,一个绑定在回环端口(server.listen(port, "127.0.0.1", ...),见 sink.mjs#L372-L377)上的本地 HTTP 服务器。它"冒充供应商端点"的程度由 classifyRequest 的路径形状分类决定,覆盖四种线协议、JSON 与 SSE 两种形态:
| 协议 kind | 匹配路径 |
|---|---|
anthropic-messages |
/messages |
openai-responses |
/responses |
openai-chat |
/chat/completions |
gemini-generatecontent |
:generateContent / :streamGenerateContent |
anthropic-count-tokens |
/count_tokens |
分类顺序有讲究:count_tokens 必须排在 messages 之前判断。对每个识别出的协议,buildResponse 返回一个最小但合法的 "DONE" 补全(stop_reason: "end_turn" / finish_reason: "stop" / finishReason: "STOP"),并带 1 token 的假 usage,使 harness 认为回合已结束而不会重试。流式请求由 wantsStream 判定(body 的 stream: true、:streamGenerateContent 路径、alt=sse 或 Accept: text/event-stream),SSE 事件序列完整模拟(例如 OpenAI Responses 的 response.created 到 response.completed 九段事件流)。sink 还对 GET /models 返回伪造模型列表,满足 harness 启动时的模型目录探测。
每个带 body 的请求都会被捕获落盘:记录 seq、时间戳、方法、脱敏后的 URL 与 header、body_bytes(原始长度)与脱敏后的 body,文件名形如 001-anthropic-messages.json。sink 也可以独立运行(node sink.mjs --port N --capture DIR --verbose),stdout 输出机器可解析的 SINK_READY / CAPTURE 行。
阶段 2:一次性启动与代理中和
每个 harness 由 lib/harnesses.mjs 注册表中的配方(recipe)以一次性方式启动,LLM 流量被重定向到 sink。统一提示词是 PROMPT = "Reply with exactly: DONE"(harnesses.mjs#L23)。
一个关键安全细节:如果用户环境配置了 HTTP 代理,捕获到的前缀会被代理送离本机——这是工具承诺"绝不允许发生"的事。因此 run.mjs#L200-L205 在子进程环境中把 HTTP_PROXY/HTTPS_PROXY/ALL_PROXY(大小写两套)全部置空,并把 NO_PROXY/no_proxy 设为 127.0.0.1,localhost,::1,确保回环流量不经过任何继承的代理。所有捕获在写入时即被脱敏(见后文 Repro pack 一节)。
阶段 3:进程树停杀——以捕获为准,不以退出为准
METHOD.md 的表述是:"Measurement succeeds on capture; the harness completing its turn is not required, and a harness that never exits is still measured." 实现上,POSIX 端以 detached 选项让子进程独立成进程组(process-tree.mjs#L66-L71),停树时向 -pid 发 SIGTERM,1.5 秒宽限后升级 SIGKILL(stopTree);Windows 端用 taskkill /pid <pid> /t /f(forceKillTree)。等待循环以 200ms 轮询:一旦"看到 LLM 请求 + 捕获静默满 grace 期"(或到达 timeout、或 harness 已退出)即停树,见 run.mjs#L225-L238。信号处理(SIGINT/SIGTERM/SIGHUP 与 exit)保证 Ctrl-C 不会留下孤儿进程树。
阶段 4:分析器与 primary 选择规则
分析器 lib/analyze.mjs 把捕获拆分为:系统提示词字符数(system_chars)、逐工具 schema 字符数(tools 数组,每项带 name 与 chars)、消息开销(messages_chars),以及——在 MCP 命名约定已被 confirmed 时——MCP 工具与内建工具的拆分。
Primary 选择规则是方法论的核心之一,实现在 pickPrimary:
- 携带最多工具 schema 的捕获为 primary,平局取最早(
seq最小)。原因:harness 会在真正的 agent turn 之间穿插小型 warmup、标题生成、路由调用,其中有些也带一两个工具——若按"第一个带工具的请求"选,会把路由调用的几百字节误当前缀。 - 若没有任何捕获带工具,取 body 最大者,且该行
pick_rule字段会写明largest-body (no capture carried tools),终端输出也会提示。 - 畸形捕获被跳过、绝不致命:
tools/messages/input字段以非数组形状到达时(expectArray 抛错),该记录进入skipped_captures并携带错误信息,而不是被静默计成 0 个工具——"假的 0 工具读数会被误读成真实测量"。其他 harness 的测量因此存活。
真实机器上的样例输出
README.md 记录了一台真实机器(2026-08-07)的运行结果——你的机器会不同,这正是该工具的意义:
harness status wire tools mcp system schemas body input tokens variant
------------ ------------ ---------------------- ----- --- ------ ------- ---- ------------ -------------------------------
claude ok anthropic-messages 91 63 42k 219k 267k ~43k (est) real config
opencode ok openai-responses 10 - 68k 20k 87k ~14k (est) isolated config
codex ok openai-responses 11 - 40k 10k 52k ~8.3k (est) minimal home (floor)
gemini ok gemini-generatecontent 8 - 30k 8.3k 39k ~6.3k (est) isolated home (api-key mode)
pi ok anthropic-messages 4 - 23k 2.8k 26k ~4.1k (est) isolated home (4 default tools)
cursor-agent unmeasurable - - - - - - - -
README 对该机器 claude 行的解读值得逐字理解:267k 字符请求体中 219k 是工具 schema,91 个工具中 63 个来自 MCP 服务器/插件——约占 schema 权重的 69%,约合每次调用 24k 估算 token,而 agent 还没做任何事。单个 Workflow schema 就有 20.8k 字符,一个 Notion MCP 工具有 17.1k 字符。
同时它明确说明这行不是在说"claude 是 pi 的 10 倍":那一行是某个人装有 63 个 MCP 工具的真实安装,而 pi 行是 4 个内建工具的地板值。比较二者量的是插件装载,不是 harness 本身。
fixtures/example-report.json 是同一台机器的脱敏完整报告(fixtures/example-report.json),保留了文档化形状供与新鲜运行做 diff——例如其中 claude 行:tools_count: 91、mcp_tools_count: 63、mcp_tools_chars: 155480、tokens: { tokens: 42665, basis: "est", ratio: 6.4 },codex 行的 mcp_tools_count 为 null(打印为 -)。
配置处理:"绝不修改你的配置"的准确含义
METHOD.md 用一个专门章节界定"No recipe modifies the user's configuration files. That is not the same as running in isolation":
- claude 刻意对着真实配置运行,因为用户自己的插件和 MCP 服务器正是被测量的税。启动真实二进制于真实环境会拉起那些 MCP 服务器、运行它们的 hooks,并在
~/.claude/projects留下会话转录。工具在启动任何东西之前打印这段披露——对应 run.mjs#L368-L379 中对touchesRealConfigharness 的 stderr 提示。 - 其他所有 harness 对着隔离的临时 home/config 目录运行。
--isolate把 claude 也切到隔离的CLAUDE_CONFIG_DIR。注意 Claude Code 的登录态就存放在该目录中,因此隔离运行通常以Not logged in退出、报告无捕获——harness 自己的首行输出会被作为原因(harness_said字段)呈现。
各 harness 配方的具体手法(见 harnesses.mjs#L27-L186):
| harness | 重定向方式 | 关键细节 |
|---|---|---|
| claude | ANTHROPIC_BASE_URL 指向 sink + 占位 ANTHROPIC_API_KEY |
额外设 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1、DISABLE_AUTOUPDATER=1 压掉非必要流量;--isolate 时追加 CLAUDE_CONFIG_DIR=homeDir |
| codex | CODEX_HOME 临时目录 + 最小 config.toml(定义名为 sink 的 provider,wire_api = "responses") |
真实配置会触发后台 memory-agent 调用与副作用网络 I/O(插件 git clone、MCP OAuth 握手),零接触公共工具不能默认触发,故用最小 home 作为地板 |
| gemini | 隔离 HOME + GEMINI_API_KEY(api-key 模式)+ GEMINI_BASE_URL |
隔离 HOME 保护用户真实 ~/.gemini 的 OAuth 登录;-m 固定非 gemini-3 模型,跳过对纯文本 sink 回复会陷入重试循环的 strict-JSON 预检分类器 |
| opencode | OPENCODE_CONFIG_CONTENT 内联注入 sink provider + 完整 XDG 隔离(config/data/cache 三目录) |
用户真实配置可能收窄模型目录导致运行失败(已观察到),XDG 隔离同时保证 auth.json 不被查阅 |
| pi | 隔离 home + 最小 models.json(仅 id 的模型条目即可) |
pi 是"精简委派基线",其默认 4 工具集就是对比点;无 env 隔离时 pi 会 fail closed 报 "Unknown provider",绝不回退到真实 provider |
Variant 标注:行是标注的,不是静默混合的
每个 harness 的 variant 字段说明该行测量的是哪种配置(表格完整继承自 METHOD.md):
| harness | variant | 原因 |
|---|---|---|
| claude | real config | 用户实际的税——含插件/MCP;allow/disallow-tool 标志本来也不会把 schema 从请求中剥掉 |
| codex | minimal home (floor) | 真实配置运行会触发后台 memory-agent 调用与副作用网络 I/O;地板值是诚实的零接触默认 |
| gemini | isolated home (api-key mode) | 保护用户的 OAuth 登录;-m 固定非 gemini-3 模型以跳过对 sink 重试循环的 strict-JSON 预检分类器 |
| opencode | isolated config | 用户真实配置可能收窄模型目录并破坏运行;XDG 隔离同时保证其 auth.json 不被查阅 |
| pi | isolated home (4 default tools) | pi 是精简委派基线;其默认工具集即对比点 |
核心纪律:地板行(floor)与真实配置行(real config)是不同构造。比较它们量的是插件装载,不是 harness——表格打印 variant 列就是为了防止有人不小心这么比,诚实性(honesty)区块还会用文字再说一遍。variant 字段的赋值逻辑在 run.mjs#L289-L292:真实配置时取 reg.variant,隔离时取 reg.isolateVariant。
网络承诺:零供应商调用,两个显式例外
测量本身不发起任何供应商 API 调用。METHOD.md 列出两个显式例外:
--count-tokens(opt-in):把捕获的 anthropic 协议 body——你的真实系统提示词——POST 到 Anthropic 免费的count_tokens端点,把那些行从估算升级为供应商精确值。需要ANTHROPIC_API_KEY;没有 key 时警告并保持估算;调用失败时标注est (count_tokens failed)而非静默降级。源码中这三条全部兑现(run.mjs#L258-L269),且 tokens.mjs 的buildCountTokensRequest把捕获的model/system/tools/messages字段原样透传重建请求,保证计数对象就是 harness 实际发送的内容;端点https://api.anthropic.com/v1/messages/count_tokens是独立免费速率桶。- harness 自身的边路流量(遥测、更新检查):配方能压就压(claude 的
DISABLE_AUTOUPDATER等);codex 配方刻意用最小临时 home,因为真实配置会触发插件 git clone 与 MCP OAuth 握手。
Token 记账:两级台阶,绝不混合
lib/tokens.mjs 只实现两种口径:
est(估算):chars ÷ ratio,ratio 默认 6.4,打印到两位有效数字并带~前缀。该校准于 2026-08-07 在一台机器上对照供应商精确的 Claude Code 前缀运行得到,实测区间 5.9–6.9 chars/token(±8% 带宽),6.4 取中点(tokens.mjs#L10-L12),"再多位数就是噪声"。关键警告:该校准源自 Anthropic tokenizer,却应用于所有协议——codex/opencode(o200k)与 gemini 的行在此之外还叠加一层未量化的跨 tokenizer 误差。跨 harness 的 token 比较只能视为近似;字符列(chars)才是精确的。exact(精确):对捕获 body 原文调用 Anthropiccount_tokens。仅适用于 Anthropic 协议行;其他供应商的 tokenizer 绝不被近似为 exact(tokens.mjs#L31-L50)。
打印格式由 report.mjs#L30-L36 的 formatTokens 控制:est 值四舍五入到两位有效数字(~43k (est)),exact 值完整打印(12,345 (exact))。
列可读性与可比性
METHOD.md 对表格各列给出可比性规则:
system/schemas/body是捕获的(已清洗)请求的 JSON 字符数。body是分析后的总量;report.json同时保留脱敏前的原始body_bytes,二者相差几个字符(脱敏替换使 body 轻微变形,body_bytes始终记录原始长度)。system按协议组装——顶层system/instructions加上messages/input内部的 system/developer 角色条目,因为多个 harness 把指令的主体放在那里(对应 analyze.mjs 中anthropicParts同时累加body.system与 system 角色消息、openaiResponsesParts同时累加body.instructions与 system 角色 input 项)。因此该列的跨 harness 比较是近似的。t1st是到首个被捕获 LLM 请求的墙钟时间,包含 harness 启动——它不是延迟基准。mcp列为-(未知)除非该 harness 的 MCP 命名约定已被 confirmed;目前只有 Claude Code 的mcp__server__tool约定(CLAUDE_MCP_PATTERN = /^mcp__/,analyze.mjs#L82)。-绝不意味着零。
重复测量:--repeat N 与中位数行
单次运行是单点观察。--repeat N 让每个 harness 跑 N 次,报告行取中位数试次(total_chars 排序后取中位),并附观察到的 min–max 展布——明确声明"这是一台机器上 N 次运行的区间,不是置信区间、也不是方差声明"。所有试次的捕获都保留(trial-2/、trial-3/…目录),展布可审计。实现见 run.mjs#L315-L328 的 summarizeTrials:按 total_chars 排序取 median,spread 对象含 min_chars/max_chars/median_chars/all_chars。表头随之多出一列 spread (n),显示 min–max (ok/total)。
Repro pack:--out 产物与写时脱敏
--out(默认 ./subagent-tax-report/)产出的 repro pack 包含:report.json(完整逐工具拆分、校准、诚实性行)、每 harness 的原始捕获、harness 输出日志(stdout 与 stderr 合并到 harness-output.log,因为 harness 常把 Not logged in/Model not found 打在 stdout)、所用临时配置、以及覆盖所有产物的 manifest.sha256。
两个工程细节:
- 拒绝写入非自建的目录:claimOutDir 检查目标目录,若非空且不含本工具写入的
.subagent-tax-report标记文件,直接退出码 2 拒绝——防止误清空用户目录。 - manifest 遍历不跟符号链接:report.mjs#L72-L83 用
lstatSync而非stat,跳过 symlink,避免对--out下的链接取哈希或陷入链接环。
写时脱敏(在 sink.mjs 中定义,捕获落盘前执行)覆盖四类:
- 凭证类 header(
authorization、x-api-key、cookie、x-goog-api-key…)与账号/设备/会话标识类 header(x-claude-code-session-id、x-codex-turn-metadata、x-gemini-api-privileged-user-id、x-session-id、thread-id…),值替换为redacted:sha256:<12>——保留 12 位哈希使相等性仍可检查(同一会话的多次请求仍可关联); - 携带凭证的 query 参数(
?key=、api_key、apikey、access_token——Gemini 支持?key=); - body 中任意深度的同类标识字段(
user_id、device_id、account_uuid、prompt_cache_key、safety_identifier、conversation_id、session_id/sessionId、installation_id),按 key 名递归删除替换; - 邮箱地址(Claude Code 把账号邮箱嵌在系统提示词里)与凭证形状字符串(
sk-*、ghp_*/gho_*、AKIA*、xox*、AIza*),仅用高精度正则,替换值保留可比对哈希。
仍然敏感:body 本身就是你 harness 的真实系统提示词——本地路径、skill 清单、MCP 工具名、任何全局指令文件。发布 repro pack 前必须人工审阅。
签名:manifest.sha256 是哈希链而非签名。METHOD.md 说明发布签名结果走 CaveBench Ed25519 receipt 路径及其治理(文档提及治理定义于 docs/cavebench/GOVERNANCE.md,该文件不在本仓库内),founder-keyed,且仅在发布时执行;report.mjs#L85-L87 的注释也重申"hash manifest, not a signature"。
已知局限(协议级原因)
METHOD.md 的 Known limitations 一节逐条给出了可验证的技术原因:
- cursor-agent 报
unmeasurable,且不是因为无法重定向:隐藏的-e/--endpoint标志确实能把它指向本地服务器(2026-08-07 已核查)。但到达的内容不带前缀——客户端在双向 Connect/HTTP-2 RPC 上流式传输agent.v1.AgentRunRequestprotobuf,schema 里只有会话轮次、模型标识符和用户自己的 MCP 工具,没有系统提示词字段、没有内建工具 schema,且其 bundle 中找不到任何 LLM 供应商主机名。agent 循环跑在 Cursor 的服务器上,前缀从不经过本地网络。唯一客户端侧本可测量的切片是用户自己的 MCP 工具 schema。注册表中该条目的unmeasurableReason与此一致(harnesses.mjs#L137-L152)。 - opencode 的线协议在此是 OpenAI Responses(经其内建 openai provider),尽管生态文档常称其为 chat completions;使用
@ai-sdk/openai-compatibleprovider 时它说 chat。分析器两者都处理。 - 首个捕获后即停意味着多请求启动序列(codex memory agent、opencode title 调用)被捕获但不被穷尽探索;它们仍可见于
all_captures。 - harness 检测运行
<bin> --version(run.mjs#L108-L127),仅有 shell alias 或函数的 harness 会报 "not installed"。 - Windows:npm 命令 shim 经
PATH/PATHEXT解析,然后直接启动其 Node 入口点、不经 shell(process-tree.mjs#L10-L64 的resolveWindowsCommand+parseWindowsNodeShim解析 cmd-shim 中的目标脚本,仅接受 ≤256KB 且为 Node 目标的 shim);清理用taskkill /t /f;原生 Windows CI 覆盖启动与进程树契约。
复现与验证
METHOD.md 给出的复现命令(在 packages/subagent-tax 目录下):
node run.mjs # all installed harnesses
node run.mjs --list # what's installed + recipe status
node run.mjs --harness claude,pi --repeat 3
node --test tests/*.test.mjs # harness-free test suite (fake-harness e2e)
完整 flag 一览(默认值来自 run.mjs#L26-L31 的参数解析):
--harness a,b,c 选择 harness(默认:全部已安装)
--out DIR repro pack 位置(默认 ./subagent-tax-report)
--timeout N 每 harness 放弃前的秒数(默认 90)
--grace N 最后一次捕获后的静默秒数再停(默认 3)
--ratio N 估算用 chars-per-token(默认 6.4,已校准)
--repeat N 每 harness 跑 N 次;中位数行 + 观察 min–max 展布
--isolate 测 harness 地板而非真实配置
--count-tokens anthropic 行升级为供应商精确 token;会把捕获 body 发往
api.anthropic.com,需 ANTHROPIC_API_KEY,opt-in,绝不自动
--json 机器可读报告输出到 stdout
--list 注册表 + 检测结果
所有带值 flag 都校验参数:缺失或非数字值会立即报错,而不是挂死(NaN deadline)或在全部测完后崩溃——这是 parseArgs 注释中明确记录的修复动机。测试套件(tests/)完全无需真实 harness,用 fake-harness 做端到端验证(e2e.test.mjs、sink.test.mjs、analyze.test.mjs、report.test.mjs、process-tree.test.mjs、tokens.test.mjs、args.test.mjs);fixtures/anthropic-first-request.json 是一份脱敏的真实首请求捕获,供分析器测试使用。
小结
subagent-tax 的方法论可以浓缩为四条纪律:测量的是本地捕获的字符数而非账单(basis: inferred, always)、每个数字都带口径标签(est/exact、variant、pick_rule、- 表示未知而非零)、行与行之间不静默混合(variant 列强制标注构造差异)、产物可审计(raw captures + manifest + 写时脱敏 + 拒绝覆盖非自建目录)。它回答的问题是"你的 harness 每次调用——以及它派生的每个 subagent 的每次调用——到底要重发多大的前缀",而答案的形态(哪一列精确、哪一列近似、哪一行是地板、哪一行是真实装载)在 METHOD.md 与 README.md 中都被逐一写明。
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