1. FLAWED 基准的争议点,恰好是“分档跑”的起点
补丁验证 Agent 最容易踩的坑,不是模型选错,而是所有任务都用同一个推理档位、同一套重试上限去跑,最后把结果混在一起统计。Trail of Bits 对 1Password 那份 FLAWED 报告的质疑,核心也落在这里:当提示词构成、编译约束、推理档位这几项实验设计变量没有被固定,所谓“干净修复率”就失去了可比性。Trail of Bits 同期放出的两个补丁验证 Agent 技能,本质上是在补这堂课——验证环节必须可复现、可归因。
本文不复述争论,只做一件事:用一条 TaoToken Key(官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=flawed_intro)把补丁验证 Agent 技能在 FLAWED 里分档跑起来。TaoToken 在这条链路里只承担两件事——提供 Key、提供 Base URLhttps://taotoken.net/api;模型选择、档位定义、并发、重试、判定规则全部留在你本地。
读完你能拿到三样可直接复用的东西:
- 分档命令:
fast/standard/deep三档,同一 Key、同一 Base URL,只改参数; - Token 消耗表:三档的输入 / 输出 / 轮数口径,以及单 Key 并发下的量级差异;
- FLAWED 修复结果对比:编译通过、测试通过、干净修复三个层级的分档差异,以及为什么不能只跑一档就下结论。
有一个前提要写在前面:如果你打算在自己的机器上复现,先把 Key 拿到手。路径统一走 TaoToken 官网,不要在客户端里零散地贴 Key,更不要把 Key 写进仓库。
2. 前置准备:一条 Key + 一个 Base URL,打通 Claude Code 与 Codex
先说结论:单 Key 分档的可行性,取决于供应商侧是否把“档位”做成了请求参数而不是账号属性。TaoToken 的形态刚好适合这种做法——一条 Key 覆盖全部档位,切换成本从“换账号”降到“换配置项”。
2.1 Key 与 Base URL 的固定写法
在 TaoToken 官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=flawed_setup_keys)创建一条 Key,本文统一用YOUR_API_KEY占位。
Base URL 固定为:
https://taotoken.net/api两条硬性纪律,踩过坑的都懂:
- 不要带结尾斜杠。
https://taotoken.net/api/在某些客户端里会被拼成//v1/...,直接 404; - 不要自己补
/v1。不同客户端对路径的处理策略不一样,有的会自动追加,有的不会。以客户端文档为准,Base URL 只填到/api。
2.2 Claude Code:写进 settings.json,用 ANTHROPIC_* 一族
Claude Code 认的是ANTHROPIC_*环境变量。推荐写进项目级.claude/settings.json,这样分档脚本可以在不同目录间切换而互不干扰:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "<从模型列表中选择的主模型 ID>", "ANTHROPIC_SMALL_FAST_MODEL": "<从模型列表中选择的小模型 ID>", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" } }临时验证的时候用 shell 导出更直接:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="<主模型 ID>"冒烟测试一条命令就够:
claude -p "只回复 ok,不要解释" --output-format text返回ok说明 Key 和 Base URL 都通了。如果这一步就失败,先别去改 Agent 技能,问题一定在配置层。
2.3 Codex:config.toml,不要套 ANTHROPIC_*
这是排障环节出现频率最高的错误:把ANTHROPIC_*塞给 Codex。Codex 读的是~/.codex/config.toml,两套变量体系互不相认。
model = "<从模型列表中选择的模型 ID>" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" [profiles.tier-fast] model_provider = "taotoken" model_reasoning_effort = "low" [profiles.tier-standard] model_provider = "taotoken" model_reasoning_effort = "medium" [profiles.tier-deep] model_provider = "taotoken" model_reasoning_effort = "high"Key 通过环境变量注入,不进配置文件:
export TAOTOKEN_API_KEY="YOUR_API_KEY" codex --profile tier-fast exec "回复 ok"这里的设计意图很明确:profiles承载档位,model_providers承载接入信息。档位换了,接入信息一行都不用动。
2.4 CC Switch 三件套
如果你习惯用 CC Switch 在多个供应商之间切换,只需要填三样东西:
| 字段 | 值 |
|---|---|
| 供应商名称 | taotoken-flawed(自定义,便于日志归因) |
| Base URL | https://taotoken.net/api |
| API Key | YOUR_API_KEY |
一条经验:切换供应商和修改模型 ID 不要同时做。一旦同时改,之后看到的分档结果就无法归因到具体变量,前面的 Token 统计全部作废。
3. 补丁验证 Agent 技能:目录结构与三档边界
分档不是“把温度调一下”这么随意,它需要有明确的边界定义,否则跑出来的表格没有解释力。
3.1 技能目录
flawed-verify/ ├── SKILL.md # 技能说明:输入、输出、禁止事项 ├── tiers.yaml # 三档参数定义 ├── verify.py # 主流程:套补丁 -> 编译 -> 测试 -> 判定 ├── prompt/ │ ├── triage.md # 判断该 case 属于哪种失败模式 │ └── judge.md # 干净修复判定 ├── cases/ │ └── flawed_subset.jsonl └── out/ ├── fast/run.jsonl ├── standard/run.jsonl └── deep/run.jsonlSKILL.md里必须写清楚三件事:允许读哪些文件、允许改哪些文件、判定失败时输出什么结构。这三条不写清楚,Agent 会开始“创造性”地绕过失败。
3.2 三档定义(tiers.yaml)
tiers: fast: reasoning_effort: low max_rounds: 1 max_output_tokens: 2048 concurrency: 4 allow_compile_skip: false allow_test_edit: false standard: reasoning_effort: medium max_rounds: 3 max_output_tokens: 4096 concurrency: 2 allow_compile_skip: false allow_test_edit: false deep: reasoning_effort: high max_rounds: 5 max_output_tokens: 8192 concurrency: 1 allow_compile_skip: false allow_test_edit: false档位可以变的东西:推理强度、重试轮数、输出上限、并发数。
档位不能变的东西:allow_compile_skip和allow_test_edit,三档一律false。这条纪律直接来自 FLAWED 争议里被反复讨论的那个设计选择——如果一部分试验根本没有真正编译过,那它产出的“干净修复”跟另一部分试验的“干净修复”就不是同一个指标,放在一张表里做平均没有任何意义。分档可以调整模型投入多少推理预算,但不能调整“是否真的验证过”。
3.3 干净修复的判定口径
judge.md里把判定固化成四项合取,缺一不可:
- 目标仓库编译通过(退出码为 0);
- 该 case 指定的目标测试通过;
- 全量回归测试集没有新增失败;
- 补丁没有触碰测试文件、构建脚本和断言,也没有用注释、宏开关等方式让失败消失。
第 4 条最容易漏。补丁验证 Agent 在没有约束的情况下,会倾向于“让测试通过”而不是“让缺陷消失”,这两者在指标上看起来一模一样。
对应的本地校验片段:
# verify.py 片段:补丁白名单校验 FORBIDDEN_PREFIXES = ("tests/", "test_", "conftest.py", "CMakeLists.txt", "Makefile") def touched_files(patch_text: str) -> list[str]: files = [] for line in patch_text.splitlines(): if line.startswith("+++ b/"): files.append(line[6:].strip()) return files def patch_is_clean(patch_text: str) -> bool: for f in touched_files(patch_text): if f.startswith(FORBIDDEN_PREFIXES): return False forbidden_markers = ("#if 0", "// NOLINT", "@unittest.skip", "assert True") return not any(m in patch_text for m in forbidden_markers)编译与测试在本地执行,命令来自 case 定义,不经过任何外部服务:
import subprocess from pathlib import Path def run_local(cmd: str, cwd: Path, timeout: int = 900): proc = subprocess.run( cmd, cwd=str(cwd), shell=True, capture_output=True, text=True, timeout=timeout ) return proc.returncode, (proc.stdout + proc.stderr)[-8000:] def compile_then_test(repo: Path, case: dict) -> dict: rc_build, log_build = run_local(case["build_cmd"], repo) if rc_build != 0: return {"compiled": False, "tests_passed": False, "log": log_build} rc_test, log_test = run_local(case["test_cmd"], repo) return {"compiled": True, "tests_passed": rc_test == 0, "log": log_test[-4000:]}4. 分档运行:命令、并发与日志归因
4.1 一个脚本跑三档
#!/usr/bin/env bash # run_tier.sh <fast|standard|deep> set -euo pipefail TIER="${1:-standard}" OUT="out/${TIER}" mkdir -p "$OUT" # 三档共用同一条 Key、同一个 Base URL export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="${TAOTOKEN_API_KEY:?请先 export TAOTOKEN_API_KEY}" python verify.py \ --tier "$TIER" \ --tiers-config tiers.yaml \ --cases cases/flawed_subset.jsonl \ --repo ./repos/flawed-target \ --out "$OUT" \ --log "$OUT/run.jsonl" \ --tag "flawed-${TIER}"三档依次执行:
export TAOTOKEN_API_KEY="YOUR_API_KEY" bash run_tier.sh fast bash run_tier.sh standard bash run_tier.sh deep--tag是刻意加的。同一 Key 跑三档,如果日志里不带 tag,事后只能靠时间戳去猜哪条记录属于哪一档,归因成本会高到让你不想再做第二轮。
4.2 每条记录写什么
{ "tag": "flawed-standard", "case_id": "flawed-0042", "tier": "standard", "rounds": 2, "usage": {"input_tokens": 11320, "output_tokens": 2380}, "compiled": true, "tests_passed": true, "clean_fix": true, "duration_s": 214, "request_ids": ["..."] }request_ids保留下来,出问题时可以拿去对照供应商侧的调用记录,这是排障时最省时间的一个字段。
4.3 汇总脚本
import json, pathlib, collections rows = [] for p in pathlib.Path("out").glob("*/run.jsonl"): for line in p.read_text(encoding="utf-8").splitlines(): if line.strip(): rows.append(json.loads(line)) agg = collections.defaultdict(collections.Counter) for r in rows: a = agg[r["tier"]] a["cases"] += 1 a["in"] += r["usage"]["input_tokens"] a["out"] += r["usage"]["output_tokens"] a["total"] += r["usage"]["input_tokens"] + r["usage"]["output_tokens"] a["rounds"] += r["rounds"] a["compiled"] += int(r["compiled"]) a["clean"] += int(r["clean_fix"]) header = f"{'tier':<10}{'cases':>7}{'in':>10}{'out':>10}{'total':>10}{'clean':>7}" print(header) for tier, a in sorted(agg.items()): print(f"{tier:<10}{a['cases']:>7}{a['in']:>10}{a['out']:>10}{a['total']:>10}{a['clean']:>7}")5. Token 消耗表:三档的量级差异来自哪里
下表是一次子集运行的示例口径(单条 issue 平均值,用于说明统计方式;实际数值随子集难度和仓库规模变化,请以你自己的run.jsonl为准):
| 档位 | 推理强度 | 平均输入 tokens/条 | 平均输出 tokens/条 | 平均轮数 | 单条合计 | 并发 |
|---|---|---|---|---|---|---|
| fast | low | 6.4k | 0.9k | 1.0 | 7.3k | 4 |
| standard | medium | 11.8k | 2.4k | 2.2 | 14.2k | 2 |
| deep | high | 20.5k | 5.3k | 3.6 | 25.8k | 1 |
几个必须注意的读数细节:
输入 token 的增长不是线性的,而是按轮数叠加的。fast 档只跑 1 轮,不回灌任何编译输出;standard 档平均 2.2 轮,每一轮都要把上一轮的编译日志、测试日志重新塞回上下文;deep 档平均 3.6 轮,日志回灌成了输入 token 的主要来源。所以 deep 档的输入量是 fast 档的三倍多,但其中真正属于“prompt 变长”的部分很小。
输出 token 增长慢于输入。推理强度提升主要消耗在思考过程上,最终补丁文本的长度变化不大。这意味着如果你的场景只需要补丁文本,deep 档的“单位产出成本”会明显更高。
并发与档位要匹配。单 Key 下把 deep 档并发开到 4,收益不是 4 倍,而是更频繁的限流重试——重试本身又会产生新的 token 消耗,把统计表彻底搅乱。表中给的 4 / 2 / 1 是一个保守起点。
统计口径必须写死。上表的口径是:输入含 issue 描述、仓库相关片段、轮间日志回灌;输出含推理与补丁文本。换一套口径,三档的相对关系可能就变了。做对比实验,口径比数值本身更重要。
6. FLAWED 修复结果对比:为什么不能只跑一档
同样是一次子集运行的示例记录(n=30,仅用于说明对比方式),三档在同一批 case 上的表现:
| 档位 | 编译通过 | 目标测试通过 | 干净修复 | 人工复核发现的误判 | 平均耗时/条 |
|---|---|---|---|---|---|
| fast | 71% | 58% | 43% | 6% | 48s |
| standard | 86% | 74% | 66% | 3% | 214s |
| deep | 89% | 78% | 71% | 2% | 512s |
从这张表能读出三件事,每一件都直接关系到你怎么用补丁验证 Agent:
第一,档位差异主要体现在“编译通过 → 目标测试通过”这一跳上,而不是“测试通过 → 干净修复”上。fast 档和 deep 档的编译通过率差了 18 个百分点,但目标测试通过率到干净修复的转化率差距明显更小。这说明低档位的主要问题是补不出能编译的补丁,而不是判定环节出问题。
第二,deep 档相对 standard 档的边际收益在快速衰减。干净修复从 66% 到 71%,耗时却从 214 秒涨到 512 秒,token 消耗接近翻倍。工程上更划算的做法是:先用 standard 档跑全量,只把 fast 档失败的 case 升级到 deep 档重跑,而不是无差别地全量上 deep。
第三,误判率随档位下降,但不会归零。三个档位都出现了人工复核判定为“不干净”但自动判定为“干净”的情况。这就是前面把allow_test_edit钉死为false、并在patch_is_clean里加禁用标记扫描的原因——自动判定只能拦住大部分,最后一层还得靠规则白名单。
把这三条合起来看,就回到本文开头那个判断:只跑一档得到的干净修复率,是一个无法解释的数字。它既可能是模型能力上限,也可能是档位给低了,还可能是判定口径松了。分档跑的价值不在于让数字变好看,而在于让每一个百分比都能对应到一个明确的原因。
如果你想把这个流程固化成流水线,最简单的做法是在 CI 里加一层分档:低档位做快速筛选,高档位做定向复核,两档的日志带上不同的 tag 分别落盘。
# 示意:CI 中的分档调度 verify-fast: script: - export ANTHROPIC_BASE_URL="https://taotoken.net/api" - export ANTHROPIC_AUTH_TOKEN="$TAOTOKEN_API_KEY" - bash run_tier.sh fast artifacts: paths: [out/fast/run.jsonl] verify-deep-on-failed: needs: [verify-fast] script: - bash run_tier.sh deep # verify.py 内部只挑 fast 档 clean_fix=false 的 case artifacts: paths: [out/deep/run.jsonl]7. 排障清单:单 Key 分档最容易出问题的七个点
1. 401 / 鉴权失败。九成是 Key 复制时带了换行或首尾空格。用printf '%s' "$ANTHROPIC_AUTH_TOKEN" | wc -c数一下长度,和 Key 的预期长度对不上就重新导出。
2. 404 / 路径拼接错误。检查 Base URL 是否被写成了带结尾斜杠的形式,或者被手动补了/v1。Base URL 严格写https://taotoken.net/api。
3. 模型不可用。分档配置里换了model_reasoning_effort却顺手改了模型 ID,结果档位变了、模型也变了,日志里的结果无法归因。一次只改一个变量。
4. Codex 报变量未设置。大概率是把ANTHROPIC_*配给了 Codex。Codex 读config.toml里的env_key,本文示例用的是TAOTOKEN_API_KEY。
5. 流式响应中断。通常是单轮输出超过了max_output_tokens。deep 档把它调到 8192 以上,或者让 Agent 在接近上限时主动分段输出。
6. 频繁限流重试。单 Key 下并发开太高。把档位对应的concurrency降下来,重试次数才会降下来,Token 统计也才有意义。
7. 日志无法归因。三档共用一个 Key、一个输出目录,最后分不清哪条是谁的。每档独立的out/<tier>/run.jsonl,每条记录带tag字段,这一步不能省。
另外,整套流程里的编译和测试命令都在本地执行,Agent 只负责生成补丁和读取日志;不要把验证环节指向任何生产环境资源,也不要把仓库凭据写进技能配置文件。
8. 把分档能力固化下来
到这里,一条 Key 跑完三档的链路已经闭环了:配置层用同一组ANTHROPIC_BASE_URL和YOUR_API_KEY,脚本层用tiers.yaml控制推理强度和重试轮数,判定层用固定的白名单规则拦住“看起来干净”的补丁,统计层用带 tag 的 JSONL 出 Token 消耗表和修复结果对比。
下一步的三个动作,按顺序做效率最高:
- 先确认模型侧能跑通:到 模型对话 里快速试一条 prompt,确认当前选的模型 ID 可用;
- 评估成本量级:如果你要长期跑分档验证,先看 Coding Plan,把三档的调用量换算成预算再决定并发;
- 创建正式 Key 并落到配置:在 API Keys 生成 Key,然后按 Claude Code 文档 里的写法填进
settings.json。
如果你在配置过程中卡住了,回到最朴素的一步:先用claude -p "只回复 ok"验证 Key 和 Base URL 通不通,再去动分档参数。绝大多数看起来像“Agent 不行”的问题,最后都落在配置层。
TaoToken 官网入口(含最新接入说明):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=flawed_cta
把三档跑通之后,你会发现分档真正的价值不是省钱,而是让每一个结果都有解释:这个 case 失败,是因为补丁本身有问题,还是因为档位没给够——这两件事在工程上需要完全不同的处理方式。