在 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi)上申请到的 Key,本质上只是一串凭证加一个 Base URL,它并不关心你把它喂给谁。但把同一把 Key、同一批模型、同一份任务描述分别塞进 Codex CLI 和 Pi 两个 harness 之后,账单侧看到的 input tokens 往往不会对齐——有时候差值小到可以忽略,有时候同一道 SWE-bench Lite 风格的修 bug 题,Pi 侧的解法和 Codex CLI 侧的解法的调用次数能差出一大截。UC Berkeley 团队那篇 HarnessTax 研究做的正是这件事:固定 7 个模型、3 个 harness(Claude Code、Codex CLI、Pi),在 SWE-bench Lite 和 Terminal-Bench 2.0 上各取 30 个任务、每个组合跑 3 次,观察 harness 本身给 agent 带来的"税"。这篇不聊研究结论的高低,只聊工程上怎么复现:怎么用同一把 Key 把 harness 从 Codex CLI 切到 Pi,怎么把两边消耗记录下来做成差值表,以及切过去之后最容易踩的几类翻车。TaoToken 在这里只承担 Key 与 Base URL 的角色,不参与评测,跑什么、跑多少、怎么统计,全部由你在本地控制。
1. 先把变量拆开:harness 到底在哪一层"吃"Token
要复现 harness 之间的消耗差,第一步不是去改配置,而是想清楚 Token 是被谁消耗掉的。一次 coding agent 任务的调用链大致是四层:
- 传输层:Base URL、鉴权头、协议形态(Chat Completions 还是 Responses)。这一层由供应商决定,TaoToken 的 Base URL 固定为
https://taotoken.net/api,两个 harness 走的是同一个入口。 - harness 层:Codex CLI 和 Pi 各自决定系统提示词写多长、要不要注入仓库地图、工具调用的 schema 长什么样、失败重试几次、上下文什么时候压缩。这是差异最大的地方,也是 HarnessTax 想量化的那部分"税"。
- 模型层:同一批模型,理论上权重一样、采样参数一样,但 harness 传过来的 system prompt 和工具定义不同,模型的行为就会分叉。
- 任务层:仓库状态、依赖能不能装上、测试命令能不能跑通。这一层是噪声的主要来源,必须靠多次重复跑压掉。
把变量拆开之后,"同一把 Key 看消耗差"这件事就变得可操作了:传输层锁死(同一个 Base URL、同一个 Key),模型层锁死(同一批模型名),任务层固定(同一组任务、同一份初始 commit),唯一允许变化的就是 harness 层。如果不去做这层隔离,你测出来的差值里会混进网络抖动、缓存命中、模型侧路由等一堆无关因素,差值表就失去意义。
这里有个容易被忽略的点:harness 的 Token 消耗不是线性的。它往往是"阶梯式"的——正常情况下每轮几十到几百 token,一旦触发上下文压缩或者工具调用重试,单轮就能翻好几倍。所以统计时不能只看总 token,必须把输入 / 输出 / 缓存命中 / 调用轮次四项分开记,否则你只能看到"Pi 比 Codex CLI 多烧了 X",却说不清多烧在哪一步。
2. 拿 Key 与 Base URL:三个 harness 的接入配置怎么写
2.1 第一步:在 TaoToken 申请 Key
浏览器打开 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi ,创建一个 API Key,复制出来先落到本地环境变量文件里,别直接写进任何要提交的配置文件。Key 占位符统一写成YOUR_API_KEY,下文所有示例都按这个占位符来。Base URL 在工具里统一填https://taotoken.net/api,注意这个地址不带任何 UTM 参数,UTM 只用于官网页面跳转统计,写进配置里反而可能被某些客户端当成非法路径。
2.2 Codex CLI:config.toml
Codex CLI 走自定义 provider,配置写在~/.codex/config.toml:
# ~/.codex/config.toml model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "responses"然后在 shell 里导出 Key:
export TAOTOKEN_API_KEY="YOUR_API_KEY"这里要强调一句:别把ANTHROPIC_*那一套搬到 Codex CLI 上。Codex 的config.toml只认env_key指向的变量名,你在 shell 里导出一堆ANTHROPIC_BASE_URL对它没有任何作用,只会让你误以为配置生效了,实际还在走默认端点。
2.3 Claude Code:settings.json
Claude Code 用ANTHROPIC_*系列变量,配置写在~/.claude/settings.json(项目级则放在.claude/settings.json):
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-5", "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5" } }所谓"CC Switch 三件套",指的就是把这三个位置对齐:用户级 settings.json、项目级 .claude/settings.json、当前 shell 的环境变量。三者优先级不同,只要有一处残留旧值,你切换 harness 之后的对照实验就废了。切换前跑一遍env | grep ANTHROPIC确认没有脏变量,是个成本极低但收益很高的习惯。
2.4 Pi:走 OpenAI 兼容入口
Pi 作为 harness,本身也是通过 API 调用模型,所以它同样需要拿到 Base URL 和 Key。不同版本对配置项的命名不完全一致,稳妥做法是走 OpenAI 兼容约定,用双变量注入:
export OPENAI_BASE_URL="https://taotoken.net/api" export OPENAI_API_KEY="YOUR_API_KEY"如果 Pi 自带配置文件,把上面的两个值填进它的 provider / base_url / api_key 对应字段即可;字段名以你本地安装版本的说明为准,不要凭记忆硬填。配置完成后先做一次最小连通性验证,再进入正式对照实验,否则后面出现的所有"消耗差"都可能只是配置错误引发的重试。
3. 切换脚本:用目录隔离做到"一把 Key、两条 harness 通路"
手动改配置最容易出错,写一个切换脚本反而更省事。核心思路是配置目录隔离 + 环境变量统一注入 + 日志目录分开:两个 harness 各自独立的HOME子目录,避免互相读取对方的缓存和会话记录。
#!/usr/bin/env bash # harness_switch.sh —— 同一把 TaoToken Key,在 codex / pi 之间切换 set -euo pipefail export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="${TAOTOKEN_API_KEY:-YOUR_API_KEY}" HARNESS="${1:-}" TASK_ID="${2:-task-001}" ROOT="$HOME/harness-tax" STAMP="$(date +%Y%m%d-%H%M%S)" case "$HARNESS" in codex) export CODEX_HOME="$ROOT/codex/home" export LOG_DIR="$ROOT/codex/logs/$TASK_ID-$STAMP" mkdir -p "$CODEX_HOME" "$LOG_DIR" # Codex 走自己的 provider 配置,不读 ANTHROPIC_* cat > "$CODEX_HOME/config.toml" <<EOF model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "${TAOTOKEN_BASE_URL}" env_key = "TAOTOKEN_API_KEY" wire_api = "responses" EOF echo "[ok] codex 已就绪,日志目录: $LOG_DIR" ;; pi) export PI_CONFIG_DIR="$ROOT/pi/config" export LOG_DIR="$ROOT/pi/logs/$TASK_ID-$STAMP" mkdir -p "$PI_CONFIG_DIR" "$LOG_DIR" # Pi 走 OpenAI 兼容入口,变量只在本进程内可见 export OPENAI_BASE_URL="$TAOTOKEN_BASE_URL" export OPENAI_API_KEY="$TAOTOKEN_API_KEY" echo "[ok] pi 已就绪,日志目录: $LOG_DIR" ;; *) echo "用法: source harness_switch.sh {codex|pi} [task-id]" >&2 exit 1 ;; esac echo "[env] BASE_URL=$TAOTOKEN_BASE_URL" echo "[env] LOG_DIR=$LOG_DIR"使用时必须用source,否则导出变量只存在于子 shell:
source harness_switch.sh codex task-lite-007 # 跑完 codex 侧之后 source harness_switch.sh pi task-lite-007脚本里有两个刻意的设计:一是 codex 分支完全不碰ANTHROPIC_*,pi 分支完全不碰config.toml,从源头杜绝串配置;二是每个 harness 每个任务一个独立LOG_DIR,后面做差值表时可以直接按目录聚合,不用回头猜哪条记录属于哪次运行。
还有一个细节:如果两个 harness 会在同一个仓库目录里留下.git之外的临时文件(比如自建的缓存目录、虚拟环境),务必在两次运行之间把仓库重置到同一个 commit,并清掉未跟踪文件。任务层不干净,harness 层的差值就永远测不准。
4. Token 差值表怎么落地:从原始日志到可对比的数字
4.1 先确定"记什么"
不管 harness 输出的日志格式如何,你要从里面抠出来的字段是固定的:
| 字段 | 含义 | 为什么必须记 |
|---|---|---|
input_tokens | 本轮输入 token | 反映 system prompt + 上下文膨胀程度 |
output_tokens | 本轮输出 token | 反映模型的"话痨"程度和工具调用量 |
cached_tokens | 缓存命中 token | 直接决定实际计费,不记会严重高估 |
rounds | 调用轮次 | harness 重试策略的直接体现 |
status | 成功 / 失败 / 超时 | 失败重试会污染消耗统计 |
tool_calls | 工具调用次数 | 解释轮次差异的来源 |
4.2 差值表模板
每跑完一组,把两侧数据填进同一张表,用中位数而不是单次值:
| 任务 ID | harness | 模型 | input | output | cached | total | rounds | status |
|---|---|---|---|---|---|---|---|---|
| lite-007 | codex-cli | 模型 A | 实测值 | 实测值 | 实测值 | 实测值 | 实测值 | pass |
| lite-007 | pi | 模型 A | 实测值 | 实测值 | 实测值 | 实测值 | 实测值 | pass |
| lite-007 | 差值 | — | Δin | Δout | Δcached | Δtotal | Δrounds | — |
填表时建议加两列派生指标,让差值可解释:
token_per_round = total / rounds:如果这个值在 Pi 侧明显更高,说明差异来自单轮上下文体积,而不是轮次多。cache_ratio = cached / input:如果两侧差距大,说明 harness 的提示词前缀稳定性不同,前缀一变缓存就失效,实际单价会跳。
4.3 用脚本把 JSONL 日志聚合成表
多数 harness 都会把会话过程写成 JSONL,字段名不一定统一,所以脚本要写得"宽容"一点——递归找 token 相关字段,找不到就先打印原始行对照:
#!/usr/bin/env python3 # sum_tokens.py —— 聚合一次 harness 运行的 token 用量 import json, sys, pathlib KEYS = { "input": ("input_tokens", "prompt_tokens"), "output": ("output_tokens", "completion_tokens"), "cached": ("cached_tokens", "cache_read_input_tokens"), } def pick(obj, names): if not isinstance(obj, dict): return 0 for n in names: v = obj.get(n) if isinstance(v, int): return v for v in obj.values(): r = pick(v, names) if r: return r return 0 def main(path): stats = {"input": 0, "output": 0, "cached": 0, "rounds": 0} for line in pathlib.Path(path).read_text(encoding="utf-8", errors="ignore").splitlines(): line = line.strip() if not line: continue try: row = json.loads(line) except json.JSONDecodeError: continue hit = False for k, names in KEYS.items(): v = pick(row, names) if v: stats[k] += v hit = True if hit: stats["rounds"] += 1 stats["total"] = stats["input"] + stats["output"] print(json.dumps(stats, ensure_ascii=False)) if __name__ == "__main__": main(sys.argv[1])跑法:
python3 sum_tokens.py "$LOG_DIR/session.jsonl"脚本第一次跑如果全是 0,不要急着改逻辑,先把某一行原样打出来看一眼真实字段名,再往KEYS里加。这一步花五分钟,能省掉后面几小时的"为什么两边都是零"。
4.4 控制重复次数与统计口径
HarnessTax 的做法是每个组合跑 3 次,这个次数是有讲究的:跑 1 次你看到的是噪声,跑 3 次取中位数才勉强能看出趋势,跑更多次收益递减但成本线性上升。落到你自己的复现里,建议:
- 同一任务、同一 harness 至少跑 3 次,记录全部 3 次,报告时用中位数。
- 两侧的任务初始 commit 必须一致,跑之前用
git status确认干净。 - 失败运行不要删掉,单独放一列——失败重试本身就是 harness 差异的一部分。
- 温度、超时、最大轮次这些参数在两侧尽量对齐,对不齐的在结论里注明。
5. 失败案例清单:切换 harness 时最容易翻的 7 类车
这份清单按"出现频率 × 排查难度"排序,都是切 harness 时会真实撞上的:
- 404 / 405:Base URL 写成了带
/v1或带路径的形式。配置里只写https://taotoken.net/api,多一段少一段都会被服务端按未知路径处理。排查方式:先用 curl 打一次最小请求,确认端点形态再改 harness 配置。 - 鉴权失败但 Key 是对的。最常见原因是环境变量名不匹配——Codex 的
env_key指的是变量名,不是值;你在 shell 里导出了TAOTOKEN_API_KEY,但config.toml里写的是别的名字,一样会 401。 ANTHROPIC_*残留污染。在同一个 shell 里先跑过 Claude Code,再跑 Codex CLI,如果变量没清,某些客户端会误读。切换脚本里显式unset或者用子 shell 是可靠做法。- 模型名不被识别,harness 静默回退到默认模型。这类问题最阴——请求成功了,token 也消耗了,但你测的根本不是你想测的模型。跑之前打印一次实际使用的模型名。
- 工具调用 schema 不兼容导致无限重试。一个 harness 生成的 tool call 参数格式另一个不吃,客户端就会反复重试,Token 在几分钟内翻几倍。看日志里
rounds异常高的记录,八成是这类。 - 上下文压缩策略差异造成的"假性节省"。某个 harness 频繁压缩上下文,看起来单轮 token 低,但它可能因此丢了关键信息、多跑好几轮才完成任务,总消耗反而更高。所以永远不要只看单轮均值。
- 沙箱/权限差异导致重复读文件。一个 harness 可以直接读整个仓库,另一个每次都要重新列举目录,光这一项就能贡献可观的输入 token。这类差异属于 harness 设计,不是 bug,但必须在结论里说明,不能算成"模型差异"。
补充一条流程上的坑:不要用不同任务的任务 ID 混着做对照。有人图省事,codex 跑任务 A、pi 跑任务 B,然后比较总 token,这种对比没有任何意义。差值表的每一行必须是同一个任务 ID 在两侧各跑一遍。
6. 结论:把"税"量化出来,比争论哪个 harness 更强有用
做完上面这套流程,你手里会有三样东西:一份可复用的切换脚本、一张同任务 Token 差值表、一份失败案例清单。这三样东西的价值不在结论本身,而在于它们把"harness 影响多大"这个模糊问题变成了可复现的实验。HarnessTax 之所以值得关注,也是因为它把 harness 当成一个独立的变量来测,而不是把 agent 的表现笼统归因于模型。
几个可以直接带走的判断:
- 同一把 Key 切换 harness 是完全可行的,前提是 Base URL 与鉴权变量按各自的规范写,Codex 走
config.toml、Claude Code 走ANTHROPIC_*、Pi 走 OpenAI 兼容双变量,三者不要互相串。 - Token 差值必须拆项看。输入、输出、缓存、轮次四项里,任何一项单独拿出来都可能得出相反结论。
- 失败运行要计入统计。重试是 harness 行为的一部分,把它排除掉等于人为抹掉了你想测量的差异。
- 模型侧保持不动。切换 harness 期间,模型名、采样参数、温度都不要变,否则你就分不清差值来自哪一层。
如果你还没开始,建议的最小起步路径是:先去官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi 了解入口,然后按下面的顺序往下走——先在模型对话里确认目标模型可用,再决定用哪种计费方式跑批量实验,接着创建独立 Key 用于对照,最后照着文档把命令行工具配起来。四个入口按顺序列在这里:
- 模型对话,先做连通性与模型可用性确认:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi
- Coding Plan,批量跑对照实验前选计费方式:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi
- 创建 API Key,为 codex / pi 两侧各准备一把独立凭证,便于分账:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi
- Claude Code 配置文档,确认
ANTHROPIC_*相关字段的写法:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=codex_to_pi
配置层面记住三个常量就够了:Base URL 用https://taotoken.net/api,Key 用YOUR_API_KEY占位并落到环境变量,Codex 与 Claude Code 的配置绝不混用。剩下的,就是把同一批任务在两个 harness 上各跑三遍,把差值表填满。