首页
/ caveman subagent-tax 深度解析:本地测量编码 harness 首请求前缀"税"的方法论与实现

caveman subagent-tax 深度解析:本地测量编码 harness 首请求前缀"税"的方法论与实现

2026-09-06 15:19:49作者:董斯意

本文基于 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=sseAccept: text/event-stream),SSE 事件序列完整模拟(例如 OpenAI Responses 的 response.createdresponse.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),停树时向 -pidSIGTERM,1.5 秒宽限后升级 SIGKILLstopTree);Windows 端用 taskkill /pid <pid> /t /fforceKillTree)。等待循环以 200ms 轮询:一旦"看到 LLM 请求 + 捕获静默满 grace 期"(或到达 timeout、或 harness 已退出)即停树,见 run.mjs#L225-L238。信号处理(SIGINT/SIGTERM/SIGHUP 与 exit)保证 Ctrl-C 不会留下孤儿进程树。

阶段 4:分析器与 primary 选择规则

分析器 lib/analyze.mjs 把捕获拆分为:系统提示词字符数(system_chars)、逐工具 schema 字符数(tools 数组,每项带 namechars)、消息开销(messages_chars),以及——在 MCP 命名约定已被 confirmed 时——MCP 工具与内建工具的拆分。

Primary 选择规则是方法论的核心之一,实现在 pickPrimary

  1. 携带最多工具 schema 的捕获为 primary,平局取最早(seq 最小)。原因:harness 会在真正的 agent turn 之间穿插小型 warmup、标题生成、路由调用,其中有些也带一两个工具——若按"第一个带工具的请求"选,会把路由调用的几百字节误当前缀。
  2. 没有任何捕获带工具,取 body 最大者,且该行 pick_rule 字段会写明 largest-body (no capture carried tools),终端输出也会提示。
  3. 畸形捕获被跳过、绝不致命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: 91mcp_tools_count: 63mcp_tools_chars: 155480tokens: { tokens: 42665, basis: "est", ratio: 6.4 },codex 行的 mcp_tools_countnull(打印为 -)。

配置处理:"绝不修改你的配置"的准确含义

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 中对 touchesRealConfig harness 的 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=1DISABLE_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 列出两个显式例外:

  1. --count-tokens(opt-in):把捕获的 anthropic 协议 body——你的真实系统提示词——POST 到 Anthropic 免费的 count_tokens 端点,把那些行从估算升级为供应商精确值。需要 ANTHROPIC_API_KEY;没有 key 时警告并保持估算;调用失败时标注 est (count_tokens failed) 而非静默降级。源码中这三条全部兑现(run.mjs#L258-L269),且 tokens.mjsbuildCountTokensRequest 把捕获的 model/system/tools/messages 字段原样透传重建请求,保证计数对象就是 harness 实际发送的内容;端点 https://api.anthropic.com/v1/messages/count_tokens 是独立免费速率桶。
  2. 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 原文调用 Anthropic count_tokens。仅适用于 Anthropic 协议行;其他供应商的 tokenizer 绝不被近似为 exact(tokens.mjs#L31-L50)。

打印格式由 report.mjs#L30-L36formatTokens 控制: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.mjsanthropicParts 同时累加 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-L328summarizeTrials:按 total_chars 排序取 medianspread 对象含 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-L83lstatSync 而非 stat,跳过 symlink,避免对 --out 下的链接取哈希或陷入链接环。

写时脱敏(在 sink.mjs 中定义,捕获落盘前执行)覆盖四类:

  1. 凭证类 headerauthorizationx-api-keycookiex-goog-api-key…)与账号/设备/会话标识类 headerx-claude-code-session-idx-codex-turn-metadatax-gemini-api-privileged-user-idx-session-idthread-id…),值替换为 redacted:sha256:<12>——保留 12 位哈希使相等性仍可检查(同一会话的多次请求仍可关联);
  2. 携带凭证的 query 参数?key=api_keyapikeyaccess_token——Gemini 支持 ?key=);
  3. body 中任意深度的同类标识字段user_iddevice_idaccount_uuidprompt_cache_keysafety_identifierconversation_idsession_id/sessionIdinstallation_id),按 key 名递归删除替换;
  4. 邮箱地址(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.AgentRunRequest protobuf,schema 里只有会话轮次、模型标识符和用户自己的 MCP 工具,没有系统提示词字段、没有内建工具 schema,且其 bundle 中找不到任何 LLM 供应商主机名。agent 循环跑在 Cursor 的服务器上,前缀从不经过本地网络。唯一客户端侧本可测量的切片是用户自己的 MCP 工具 schema。注册表中该条目的 unmeasurableReason 与此一致(harnesses.mjs#L137-L152)。
  • opencode 的线协议在此是 OpenAI Responses(经其内建 openai provider),尽管生态文档常称其为 chat completions;使用 @ai-sdk/openai-compatible provider 时它说 chat。分析器两者都处理。
  • 首个捕获后即停意味着多请求启动序列(codex memory agent、opencode title 调用)被捕获但不被穷尽探索;它们仍可见于 all_captures
  • harness 检测运行 <bin> --versionrun.mjs#L108-L127),仅有 shell alias 或函数的 harness 会报 "not installed"。
  • Windows:npm 命令 shim 经 PATH/PATHEXT 解析,然后直接启动其 Node 入口点、不经 shellprocess-tree.mjs#L10-L64resolveWindowsCommand + 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.mjssink.test.mjsanalyze.test.mjsreport.test.mjsprocess-tree.test.mjstokens.test.mjsargs.test.mjs);fixtures/anthropic-first-request.json 是一份脱敏的真实首请求捕获,供分析器测试使用。

小结

subagent-tax 的方法论可以浓缩为四条纪律:测量的是本地捕获的字符数而非账单(basis: inferred, always)、每个数字都带口径标签(est/exact、variant、pick_rule、- 表示未知而非零)、行与行之间不静默混合(variant 列强制标注构造差异)、产物可审计(raw captures + manifest + 写时脱敏 + 拒绝覆盖非自建目录)。它回答的问题是"你的 harness 每次调用——以及它派生的每个 subagent 的每次调用——到底要重发多大的前缀",而答案的形态(哪一列精确、哪一列近似、哪一行是地板、哪一行是真实装载)在 METHOD.mdREADME.md 中都被逐一写明。

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