1. 为什么单点评测在 GPT-5 / Claude 3.7 面前开始失灵
先说一个我最近遇到的真实场景。团队用同一套 200 道题的评测集跑 GPT-5 和 Claude 3.7,结果两个模型的准确率都在 92% 上下,差距不到 1 个百分点。按老规矩,这属于“打平”。但把其中 30 道“答对”的样本抽出来看轨迹,问题就出来了:Claude 3.7 有 7 道题是跳过了工具返回的校验步骤直接给答案,GPT-5 有 5 道题的中间推理引用了不存在的文档编号。答案对,过程是编的。
这就是链式评测要解决的核心问题。传统单点评测(Outcome-level Evaluation)只看最终输出对不对,适合 GPT-3/4 那个“错误主要发生在结果层”的时代。而 GPT-5、Claude 3.7 这类高推理模型,错误已经迁移到过程里了:中间推理链错误、工具调用链错误、状态理解错误、即兴编造步骤、跳步漏步。你只看最终答案,根本发现不了这些致命问题。
链式评测(Chain Evaluation)是什么?简单说,它把一次模型调用拆成“输入 → 推理步骤 → 工具调用 → 观察结果 → 下一步 → 最终输出”的完整轨迹,对轨迹上的每个节点单独打分,而不是只给终点打分。它能做什么?能定位错误发生在第几步、是推理错还是工具错、是幻觉还是跳步。适合谁?适合需要持续对比多个模型表现的开发者、评测团队,以及任何在做 Agent 或复杂推理链产品的团队。
我试过用单点评测去评估一个多步 Agent,结果模型在“查订单 → 校验权限 → 发起退款”这条链上,最终退款金额算对了,但中间跳过了权限校验。单点评测给满分,链式评测直接在第 2 步标红。这就是差别。
下面我会给出可复制的链式评测配置模板,包含评测集分层、链路编排、结果回传字段,并演示一次从单模型打分到多模型链式对比的完整验证动作。你跟着做,能搭出一条可复用的评测流水线。
2. TaoToken 前置准备:把多模型调用统一到一个入口
链式评测的第一个工程难点不是评测逻辑,而是模型调用。你要同时对比 GPT-5、Claude 3.7,甚至还要加一个国产模型做基线,如果每个模型都单独接一套 SDK、单独管一套 Key,评测脚本会变得非常难维护。我的做法是用 TaoToken 作为统一调用入口,它兼容 OpenAI 风格的接口,换模型只需要改一个 model 字段。
先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来。注意这个 Key 只在创建时完整显示一次,丢了就重新建。
然后确认你要用的模型 ID。不同模型在平台上的标识不一样,GPT-5 系列和 Claude 3.7 系列的 ID 可以在模型对话页面里查到:https://taotoken.net/model-chat 。我实测下来,直接在对话页选模型、发一条消息,就能看到实际调用的 model 名称,比翻文档快。
Base URL 统一用 https://taotoken.net/api ,不要加任何路径后缀。这一点很关键,很多人写成 https://taotoken.net/api/v1 反而 404,因为 SDK 自己会拼 /v1/chat/completions。
如果你用的是 Claude Code 这类命令行工具做评测脚本的辅助开发,可以走 Anthropic 兼容入口,配置方式在 https://taotoken.net/doc/claudecode-anthropic 里有完整说明。长期跑评测任务、需要稳定配额的话,Coding Plan 更合适:https://taotoken.net/coding-plan 。
这里给一个最小可用的环境变量配置,后面所有脚本都依赖它:
export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的Key"验证 Key 是否可用,跑一条最简单的请求:
curl -s "$TAOTOKEN_BASE_URL/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5", "messages": [{"role": "user", "content": "回复 OK"}], "max_tokens": 16 }'返回里能看到 choices[0].message.content 就说明通了。如果返回 401,检查 Key 有没有复制完整;如果返回 model not found,去模型对话页确认 model ID 拼写。
注意:评测脚本里不要把 Key 硬编码进代码,用环境变量或 .env 文件,并且把 .env 加进 .gitignore。我见过有人把 Key 提交到公开仓库,几分钟就被刷爆。
3. 可复制的链式评测配置模板
这一节是核心。我给你一套完整的配置结构,包含评测集分层、链路编排、结果回传字段三部分。你可以直接复制成项目骨架。
先看目录结构:
chain-eval/ ├── config/ │ ├── models.yaml │ └── eval_settings.json ├── datasets/ │ ├── layer1_reasoning.jsonl │ ├── layer2_tooluse.jsonl │ └── layer3_agent.jsonl ├── runner/ │ ├── chain_runner.py │ └── step_scorer.py └── results/ └── (输出目录)评测集分层是关键。不要把所有题混在一起,按能力维度分三层:
| 层级 | 名称 | 考察点 | 样本数建议 |
|---|---|---|---|
| L1 | 推理链层 | 中间推理是否有依据、是否跳步 | 50–100 |
| L2 | 工具调用层 | 工具选择、参数、返回解析是否正确 | 50–100 |
| L3 | Agent 协作层 | 多步计划一致性、错误恢复、状态保持 | 20–50 |
L1 的样本格式,每条包含问题和标准推理步骤:
{"id": "l1_001", "question": "一个水池有甲乙两管,甲管单独注水6小时满,乙管单独注水4小时满,两管同时开,几小时满?", "expected_steps": ["求甲管效率1/6", "求乙管效率1/4", "效率和1/6+1/4=5/12", "时间=12/5=2.4小时"], "final_answer": "2.4小时"}L2 的样本要带工具定义和期望调用:
{"id": "l2_001", "question": "查询订单 A123 的物流状态", "tools": [{"name": "query_order", "params": {"order_id": "string"}}], "expected_tool_calls": [{"name": "query_order", "args": {"order_id": "A123"}}], "final_answer": "已发货"}L3 的样本描述一个多步任务:
{"id": "l3_001", "task": "用户申请退款,先校验订单是否在退款期内,再校验用户权限,最后发起退款", "expected_chain": ["check_refund_window", "check_permission", "initiate_refund"], "forbidden_actions": ["initiate_refund_without_permission_check"]}模型配置放在 config/models.yaml:
models: - name: gpt-5 model_id: gpt-5 base_url: https://taotoken.net/api api_key_env: TAOTOKEN_API_KEY - name: claude-3.7 model_id: claude-3.7-sonnet base_url: https://taotoken.net/api api_key_env: TAOTOKEN_API_KEY评测参数放在 config/eval_settings.json,这个文件决定了链式评测的行为:
{ "eval_mode": "chain", "step_scoring": { "enabled": true, "dimensions": ["groundedness", "reasoning_validity", "tool_correctness", "plan_consistency", "workflow_adherence"], "weights": { "groundedness": 0.25, "reasoning_validity": 0.25, "tool_correctness": 0.2, "plan_consistency": 0.15, "workflow_adherence": 0.15 } }, "result_callback": { "fields": ["run_id", "model_name", "sample_id", "layer", "step_index", "step_type", "step_content", "step_score", "step_verdict", "final_score", "error_flag", "latency_ms"] }, "max_steps": 20, "timeout_seconds": 120 }结果回传字段是链式评测和单点评测最大的区别。单点评测只回传 final_score,链式评测要回传每一步的 step_index、step_type(reasoning / tool_call / observation)、step_score、step_verdict(pass / fail / hallucinated / skipped)。这样你才能定位错误发生在哪一步。
链路编排的核心逻辑在 chain_runner.py 里,伪代码结构如下:
def run_chain_eval(model_cfg, sample, settings): trace = [] messages = build_initial_messages(sample) for step in range(settings["max_steps"]): response = call_model(model_cfg, messages) step_record = parse_step(response) step_record["step_index"] = step step_record["step_score"] = score_step(step_record, sample, settings) trace.append(step_record) if is_final(step_record): break messages.append(response) return aggregate(trace, settings)score_step 是链式评测的灵魂。它要判断这一步的推理是否有依据(groundedness)、工具调用参数是否正确(tool_correctness)、是否跳过了期望步骤(plan_consistency)。你可以先用规则打分,再逐步引入模型打分。
提示:第一次跑不要追求打分完全准确,先把轨迹完整记录下来。有了轨迹数据,你才能回头调打分规则。我一开始就是先只记录不评分,跑完 100 条样本后再设计评分维度,效率高很多。
4. 验证请求:从单模型打分到多模型链式对比
配置好了,现在跑一次完整验证。分两步:先单模型链式打分,再多模型对比。
第一步,跑 GPT-5 在 L1 推理链层上的链式评测:
python runner/chain_runner.py \ --config config/models.yaml \ --settings config/eval_settings.json \ --dataset datasets/layer1_reasoning.jsonl \ --model gpt-5 \ --output results/gpt5_l1.jsonl跑完后看结果文件,每条样本会输出完整轨迹。比如 l1_001 这条,GPT-5 的轨迹可能是:
{"run_id": "run_20250101_001", "model_name": "gpt-5", "sample_id": "l1_001", "layer": "L1", "step_index": 0, "step_type": "reasoning", "step_content": "甲管效率是1/6", "step_score": 1.0, "step_verdict": "pass"} {"run_id": "run_20250101_001", "model_name": "gpt-5", "sample_id": "l1_001", "layer": "L1", "step_index": 1, "step_type": "reasoning", "step_content": "乙管效率是1/4", "step_score": 1.0, "step_verdict": "pass"} {"run_id": "run_20250101_001", "model_name": "gpt-5", "sample_id": "l1_001", "layer": "L1", "step_index": 2, "step_type": "reasoning", "step_content": "效率和是5/12", "step_score": 1.0, "step_verdict": "pass"} {"run_id": "run_20250101_001", "model_name": "gpt-5", "sample_id": "l1_001", "layer": "L1", "step_index": 3, "step_type": "final", "step_content": "2.4小时", "step_score": 1.0, "step_verdict": "pass", "final_score": 1.0}如果某一步是跳步,step_verdict 会是 skipped,step_score 记 0。这样你一眼就能看出问题在第几步。
第二步,跑 Claude 3.7 同样的数据集:
python runner/chain_runner.py \ --config config/models.yaml \ --settings config/eval_settings.json \ --dataset datasets/layer1_reasoning.jsonl \ --model claude-3.7 \ --output results/claude37_l1.jsonl然后写一个对比脚本,按 sample_id 对齐两个模型的轨迹,输出对比表:
import json def load_traces(path): traces = {} with open(path) as f: for line in f: rec = json.loads(line) traces.setdefault(rec["sample_id"], []).append(rec) return traces gpt5 = load_traces("results/gpt5_l1.jsonl") claude = load_traces("results/claude37_l1.jsonl") for sid in gpt5: g_final = gpt5[sid][-1]["final_score"] c_final = claude[sid][-1]["final_score"] g_steps = len(gpt5[sid]) c_steps = len(claude[sid]) print(f"{sid}: GPT-5 final={g_final} steps={g_steps} | Claude3.7 final={c_final} steps={c_steps}")实测下来,你会看到很有意思的现象:两个模型 final_score 都是 1.0 的样本里,步数可能差 2–3 步。步数少的那个,往往就是跳步的那个。这就是链式评测的价值——它把“看起来都对”拆成了“过程是否一致”。
再跑 L2 工具调用层,重点看 tool_correctness 维度。我遇到过一个典型 case:模型最终返回了正确的物流状态,但工具调用参数里 order_id 传的是 "A123 "(带空格),工具返回失败后模型自己编了一个状态。单点评测给满分,链式评测在 step_type=tool_call 那一步标了 fail。
多模型对比时,建议按层汇总指标:
| 模型 | L1 平均步分 | L2 工具正确率 | L3 计划一致率 | 跳步率 |
|---|---|---|---|---|
| GPT-5 | 0.94 | 91% | 88% | 3% |
| Claude 3.7 | 0.92 | 94% | 85% | 5% |
这张表比“准确率 92% vs 91%”有用得多。你能看到 GPT-5 推理链更稳,Claude 3.7 工具调用更准,但两者在 L3 计划一致性上都有提升空间。
5. 本篇常见错排查
链式评测跑起来后,报错集中在几个地方。我按真实遇到的频率排一下。
401 Unauthorized。最常见。原因通常是 Key 没读到环境变量,或者 .env 文件没加载。检查方式:在脚本里打印 os.environ.get("TAOTOKEN_API_KEY") 的前 6 位和后 4 位,确认不是 None。另一个原因是 Key 被复制时带了换行或空格,strip 一下。
local proxy failed / connection refused。这个报错一般出现在你本地配了某些网络工具,导致请求没走到 TaoToken。排查方法:先用 curl 直接请求 https://taotoken.net/api/v1/chat/completions,如果 curl 通但 Python 脚本不通,检查脚本里有没有继承系统的 proxy 环境变量。把 HTTP_PROXY 和 HTTPS_PROXY 临时 unset 再跑。
reading choices 报错 / KeyError: 'choices'。说明返回体结构和你预期的不一样。先打印完整 response.text 看看到底返回了什么。常见原因是 model ID 写错,平台返回了错误信息而不是正常 completion。去模型对话页确认 model ID,注意大小写和连字符。
OAuth / authentication_error。如果你用的是 Claude Code 或某些 CLI 工具,它们可能默认走 OAuth 而不是 API Key。需要在配置里显式指定 API Key 模式。Claude Code 的配置方式参考 https://taotoken.net/doc/claudecode-anthropic ,里面写了 Base URL、Key、Model ID 三件套怎么填。
轨迹解析失败 / step_index 乱序。链式评测依赖模型输出结构化的步骤。如果模型返回的是自由文本,解析会失败。解决办法是在 prompt 里强制要求模型按 JSON 格式输出每一步,并在 runner 里加容错解析。我一般会在 system prompt 里写:“每一步推理必须输出为 {"step": "...", "type": "reasoning|tool_call|observation"} 格式。”
评测跑一半超时。L3 Agent 层的任务步数多,容易超过 timeout_seconds。把 max_steps 调到 30,timeout 调到 300。同时给每一步单独设超时,避免某一步卡死拖垮整条链。
多模型对比时 sample_id 对不齐。检查两个结果文件是不是用了同一份数据集。如果数据集更新过,旧结果里的 sample_id 可能已经不存在了。建议每次评测前给数据集打版本号,结果文件里带上 dataset_version 字段。
注意:排查时优先看原始返回,不要只看封装后的报错。我踩过的坑是封装层把 401 包装成了“模型不可用”,白白查了半天模型配置。
6. 把链式评测接进你的日常流水线
跑通一次验证只是开始。真正有价值的是把它变成日常动作。我的做法是每次模型版本更新或 prompt 改动后,自动触发一次链式评测,对比新旧轨迹。
具体操作:把 chain_runner.py 包一层 CI 脚本,每次提交评测集或 prompt 变更时跑 L1+L2,每天定时跑一次 L3。结果写入 results/ 目录,用 run_id 区分。然后写一个简单的 diff 脚本,对比两次 run 的 step_verdict 变化,重点看有没有新增的 hallucinated 或 skipped。
模型调用统一走 TaoToken 的 API 入口,换模型只改 models.yaml 里的 model_id,评测逻辑不用动。需要长期跑、配额稳定的,用 Coding Plan 更省心:https://taotoken.net/coding-plan 。临时验证某个模型的表现,直接去模型对话页手动发几条,比写脚本快:https://taotoken.net/model-chat 。
最后给一个实用技巧:链式评测的轨迹数据本身就是训练数据。你把 step_verdict=fail 的样本挑出来,就是一批高质量的负样本;把全 pass 的轨迹挑出来,就是过程奖励模型的训练素材。评测和训练在链式框架下是同一件事的两面。