为真正烧 token 的高消耗场景构建的逐步路由监督金标。
说明:本文件是
[README.md](README.md)的中文对照;对外与协作以英文 README 为准。
CommonRouterBench 为真正需要路由的场景(如长链路 agent、长上下文 RAG、vibecoding 工具循环)提供 每一步 LLM 调用的能力档位金标。我们不依赖盲区重重的 LLM-as-judge,而是通过对成功轨迹进行降级搜索,找出能通过严苛任务检查的最低成本能力档。
如果你正在为高消耗、多轮次场景构建路由系统,你可以使用本数据集提供的真实前缀(多轮对话 + 工具返回 + diff 等),来 评测你的路由器 或 训练档位预测模型。
1. 安装
# 本地开发可编辑安装:
pip install -e .
# 发布到 PyPI 后:
pip install CommonRouterBench(注:pip 安装包名为 CommonRouterBench,代码中使用 import main)
2. 评测你的路由
from main.eval import FunctionPredictor, run_question_bank_eval
# 示例:一个总是预测金标档位的 oracle 路由
oracle = FunctionPredictor(lambda row: row["target_tier_id"])
summary = run_question_bank_eval(oracle, n=20, seed=1)
# Headline:4 项独立维度分 + 它们的算术平均。
print(summary["scores_v2"])- 开源范围:本目录是仓库中唯一计划开源的部分。对外只发布
main包、data/题库与文档;不包含私有测试脚本。 - 版本:
0.1.0(变更见 CHANGELOG.md)。 - 依赖:核心包依赖
requests(main.router_llm等 HTTP 辅助)、tiktoken(回退 token 计数)以及tokenizers(HuggingFace——各厂商原生 tokenizer,用于main.tokenizer)。 - 本地测试:你可以在本目录下建立
tests/目录运行pytest,该目录已被.gitignore忽略,不会提交。
| 路径 | 作用 |
|---|---|
main/ |
对外发布的 Python 包(import main)。 |
data/ |
question_bank.jsonl、manifest.json(构建 wheel 时若存在则打入包内)。 |
从私有 benchmark 导出重新生成 data/ 不在本发布包职责内;若有需要请在你自己的流水线或私有仓库中维护合并工具。
data/ 下产物:
data/question_bank.jsonl— 全部路由监督步骤,单文件(无按 benchmark 分子目录)。data/manifest.json— 各来源行数与 schema 说明。
每一行含字符串字段 benchmark(如 swebench、mtrag)供过滤。
题库不包含 optimal_model、baseline_model 等字段。监督目标仅为能力档位,使用英文标签与数字 id。
target_tier(字符串) |
target_tier_id(整数) |
中文档位名 |
|---|---|---|
low |
0 | 低 |
mid |
1 | 中 |
mid_high |
2 | 中高 |
high |
3 | 高 |
每行至少包含:id、benchmark、scenario、instance_id、step_index、total_steps、messages、target_tier、target_tier_id。
下列统计与当前仓库中的 data/question_bank.jsonl、data/manifest.json 一致(共 970 条路由监督步骤)。若从私有流水线重新构建题库,数字可能变化。
对 BFCL 而言,公开题库现在同时包含 single-turn 与 multi-turn 路由监督数据。
benchmark |
行数 | 占全库比例 |
|---|---|---|
swebench |
336 | 34.6% |
bfcl |
248 | 25.6% |
mtrag |
193 | 19.9% |
qmsum |
145 | 14.9% |
pinchbench |
48 | 4.9% |
| 合计 | 970 | 100% |
target_tier |
target_tier_id |
行数 | 占比 |
|---|---|---|---|
low |
0 | 689 | 71.0% |
mid |
1 | 62 | 6.4% |
mid_high |
2 | 49 | 5.1% |
high |
3 | 170 | 17.5% |
| 合计 | — | 970 | 100% |
benchmark |
行数 | low |
mid |
mid_high |
high |
|---|---|---|---|---|---|
bfcl |
248 | 239 | 8 | 1 | 0 |
mtrag |
193 | 183 | 8 | 1 | 1 |
pinchbench |
48 | 41 | 3 | 3 | 1 |
qmsum |
145 | 132 | 10 | 3 | 0 |
swebench |
336 | 94 | 33 | 41 | 168 |
以代码为准:main.pricing 中的 TIER_OUTPUT_USD_PER_1M、TIER_INPUT_USD_PER_1M、TIER_CACHE_READ_USD_PER_1M、TIER_CACHE_WRITE_USD_PER_1M。旧版 section_11 / step_nominal_cost_usd 仅使用输出价(TIER_OUTPUT_USD_PER_1M)。下表与发布包内数值一致。
公开 target_tier |
每百万 输出 token(USD) |
|---|---|
low |
0.5 |
mid |
2.0 |
mid_high |
5.0 |
high |
25.0 |
公开 target_tier |
每百万 input(USD) | 每百万 cache read(USD) | 每百万 cache write(USD) |
|---|---|---|---|
low |
0.26 | 0.13 | 0.26 |
mid |
0.30 | 0.059 | 0.30 |
mid_high |
0.50 | 0.05 | 0.08333 |
high |
5.0 | 0.50 | 6.25 |
对于没有公开 cache write 标价的档位(low、mid),我们保守假设 cache write = 基础 input 价格。
在评测管线里根据具体模型端点算成本时,本库会把已知模型 id 映射到上述档位;未知 id 会抛出 ValueError。该映射只存在于代码中,不在开放 JSONL 里。
data/question_bank.jsonl 的每一行对应 一条路由监督步骤:一段对话前缀(messages)以及金标能力档位(target_tier / target_tier_id)。你接入的任意路由器在该步须给出 档位 id ∈ {0,1,2,3}。库按下文规则对预测与金标打分。
- 全量题库 —
run_question_bank_eval(..., n=None):按文件顺序遍历每一行(当前公开构建约 970 步)。 - 固定条数、按来源分层 — API 传
n=N:按data/manifest.json里sources.*.line_count做 最大余数法 配额,再对每个 benchmark 层做 一遍扫描的蓄水池抽样(--seed固定随机数)。使五个逻辑 benchmark(swebench、pinchbench、mtrag、qmsum、bfcl)在全库中的占比与完整语料大致一致。
请在评测 JSON 中报告 sample_mode、benchmark_counts、by_benchmark,以便他人复现你的划分。
若你自行选择通过 OpenAI 兼容 HTTP API 调用聊天模型,本仓库提供 OpenAICompatRouterClassifier 与 LlmDigitClassifierPredictor 所实现的 数字档位 约定。这仅是参考接入方式,不代表我们推荐「必须用 LLM 做路由」优于规则、传统机器学习或其它设计。
约定内容为:
- 将该行的
messages线性拼成一条 user 字符串(question_bank_messages_to_classifier_prompt)。 - 每行一次 chat 补全;助手回复必须能被解析为单个数字
0–3(允许首尾空白;不能多行或多余说明文字——见parse_tier_response_to_id)。 - 在你自己的驱动代码里调用
main.eval的run_question_bank_eval/evaluate_question_bank_rows(加载行、调用预测器、汇总 JSON)。
实现 f(row: dict) -> int,从原始行返回 0..3 的档位 id(可不用 messages,或从中抽特征)。用 FunctionPredictor 包装后传给 run_question_bank_eval 或 evaluate_question_bank_rows。不需要 HTTP,也不需要 chat 模板;汇总 JSON 与 by_benchmark 拆分形式相同。
所有指标由 main.eval 计算。
scores_v2(evaluation summary 顶层字段,由 compute_v2_scores 计算)是推荐的主打分:4 个相互正交的维度分 + 它们的算术平均。section_11(旧版 cost_savings_score,按行逐步、仅用输出价)与 router_accounting(轨迹级,D = Σ(baseline − gold),N 按轨迹 pass/fail)仍保留以兼容旧消费方,但均已被 scores_v2 取代。两条路径都不要求把完整 benchmark 任务跑到结束。
| # | 字段 | 分母 | 定义 |
|---|---|---|---|
| 1 | case_pass_rate_percent |
全部行 | #{pred_tier_id >= gold_tier_id} / 全部行数(error 行按失败计)。 |
| 2 | case_exact_match_percent |
全部行 | #{pred_tier_id == gold_tier_id} / 全部行数。 |
| 3 | trajectory_pass_rate_percent |
全部行 | 某行计入分子当且仅当它所在整条轨迹每步都 pred_tier_id >= gold_tier_id 且无 error。分母按行,与 metric 1 同口径,数学上保证 trajectory_pass_rate ≤ case_pass_rate。 |
| 4 | cost_savings_score_percent |
USD 比值 | full-cost 口径的省钱率,采用轨迹级自然账单(失败轨迹 = 整条链路 router 白烧 + 整条链路 high 重跑一次,见下文"成本节省公式");按 benchmark 总行数做宏观加权。范围 (−∞, 100],正常落在 [0, 100]。 |
| 5 | combined_score_percent |
— | 1–4 的算术平均;任一为 NaN 则整体为 NaN。 |
包含所有金标档位(gold=high 行在 D 上自然贡献 baseline 总额;失败时整条轨迹都按 -pred_cost 累加)。物理模型是 轨迹级自然账单:
- 通过的轨迹(无
error且每一步pred_tier_id >= gold_tier_id):用户账单 =Σ pred_cost,节省 =Σ (baseline − pred)。 - 失败的轨迹(任一步
error或pred_tier_id < gold_tier_id):整条链路的路由规划作废;用户账单 =Σ pred_cost (router 原本规划的整条链路) + Σ baseline_cost (整条链路用 high 重跑一次)。相对始终 high 的节省刚好 =−Σ pred_cost。
每个可评步使用与 router_accounting 相同的 full-cost 四段模型(step_full_cost_usd,见「名义定价」),按轨迹级 pass/fail 累加:
D_b += baseline_cost # 始终 high 的单步账单
if trajectory_passed: # 无 error 且每步 pred >= gold
N_b += baseline_cost - pred_cost # 该步的节省信用
else: # 轨迹失败 —— 整条链路每步都不给信用
N_b -= pred_cost # 隐含:Σ baseline 的 high 重跑由 D 覆盖
关键设计:失败轨迹里侥幸通过的 step 不再计入节省信用——因为整条链路都要用 high 重跑,步级的"对了"对整条轨迹没有挽救意义。也因此不需要额外再扣一次 Σ baseline(惩罚系数 2):物理上只有一次 full-high 重跑,这次重跑已由分母 D = Σ baseline 覆盖。
跨 benchmark 按总行数宏观加权(与 metric 1 同口径):
cost_savings_score_percent = Σ_b (rows_b / total_rows) × (100 × N_b / D_b)
scores_v2.by_benchmark.<b> 块提供每个 benchmark 的 row_count、step_count、failed_trajectory_count、failed_retry_baseline_usd(失败轨迹的 Σ baseline,仅信息性字段)、D_usd、N_usd、cost_savings_score_percent、weight_in_global_cost_savings。
为保证旧消费方不炸仍保留在 eval summary;新接入请优先使用 scores_v2 上方表格。
| 指标 | 定义 |
|---|---|
tier_match_accuracy |
可评行(无 error)中 pred_tier_id == gold_tier_id 的比例;跳过题不计入分母。按步(按行)。 |
valid_response_rate |
得到有效预测(未记录 error)的行占比。 |
通过(passed) |
pred_tier_id >= gold_tier_id。带 error 的行不算通过。与汇总里的 pass_rate 同为按步。 |
pass_rate |
全部行上 passed / sampled。 |
cost_savings_score(section_11) |
基线 = 始终 high(id 3)。对每个 已通过 且金标严格低于 high 的行,仅用输出价,且用统一正数 assumed_completion_tokens_per_routing_step(默认 1_000_000)作为 (T):cost(tier) = T × (该档输出 USD/1M) / 10^6。save_gt = cost(high) − cost(gold),save_test = cost(high) − cost(pred)。在 save_gt > 0 的已通过行上,得分 = 100 × Σ save_test / Σ save_gt。 |
与任务级 benchmark 的关系: 任务通过率需要端到端执行轨迹;本题库是路由监督切片,衡量档位是否够用(pass_rate)及相对「始终最高档」的名义费用(cost_savings_score 与/或 router_accounting)。
相同 instance_id 的多行构成一条 trajectory(多轮监督)。step_index / total_steps 排序各步。单轮行通常 total_steps == 1,同样有 instance_id。
计算 router_accounting 时,evaluate_question_bank_rows 及外部合并脚本(如 ClawRouter score_with_crb.py)应在每条 per_row 中带上 instance_id、step_index、total_steps、messages,以便用与路由时一致的前缀计费。
从每行 messages 用各档位对应的厂商原生 tokenizer 统计 token。每个档位的 tokenizer 由 _load_tier_encoder 加载(按 tier 缓存):
| 档位 | Tokenizer | 来源 |
|---|---|---|
high |
Anthropic 原生 | 内置 JSON(main/tokenizer_data/anthropic_tokenizer.json) |
mid_high |
cl100k_base |
Gemini 无离线 tokenizer;使用 tiktoken 回退 |
mid |
MiniMax 原生 | HuggingFace MiniMaxAI/MiniMax-Text-01 |
low |
DeepSeek 原生 | HuggingFace deepseek-ai/DeepSeek-V3 |
若 tokenizers 包未安装,所有档位均回退到 tiktoken cl100k_base。每条消息固定加 4 token overhead、整段末尾加 2 priming token,用于近似 chat 格式的包装开销。
- 语义前缀: 相邻两步的
messages在role、content(字符串或块列表;块内忽略cache_control)、tool_calls、tool_call_id、name上比较,避免上游日志序列化差异误判缓存。 - prompt 拆分(baseline / gold / pred 各自一条路径): baseline 恒为
high。冷启动(首步、换档、缓存 TTL 超时、或前缀不匹配)时整段 prompt 按 cache write 计价。若档位未变、缓存未过期、且上步messages是当前步的语义前缀,则前缀为 cache read、增量为 cache write。 - 缓存 TTL: 同一档位(即同一模型)距上次调用超过 3 个全局步 即视为缓存过期,触发全量 cache write。这模拟了多步 agent 轨迹中不同档位交替调用时的真实 prompt cache 失效场景。
- 输出 token: 有下一步时,从
messages增量中只计role=assistant(含tool_callsJSON);该步使用金标档位对应的 tokenizer 做计数。轨迹最后一步用前面步估算值的平均,否则用fallback_output_tokens(见router_accounting字段)。
仍保留在 eval summary 以兼容旧消费方;已被 scores_v2 取代(v2 同样保留轨迹级 pass/fail,但改用 D = Σ baseline,并以"失败轨迹 = 整条链路 full-high 重跑"的自然账单口径累加 N,取代 v1 的 step 级 + retry 惩罚公式)。
由 compute_router_accounting_metrics(main.eval.section11)计算。含 error 的步不计入 evaluable_step_count,也不进入 D_usd / N_usd 的逐步累加;但只要 trajectory 中任一步含 error,该 trajectory 在 pass_rate_percent 与 exact_match_rate_percent 上均计为未通过。
- Trajectory 通过: 无
error,且每个可评步满足pred_tier_id >= gold_tier_id。 - Trajectory 全中: trajectory 通过且每个可评步
pred_tier_id == gold_tier_id。
D_usd / N_usd: 仅在无 error 且 pred_tier_id / gold_tier_id 为 int 的步上累加。每步计算 baseline_cost、gold_cost、pred_cost(四段定价之和)。D_usd += baseline_cost − gold_cost。若 trajectory 通过,每可评步 N_usd += baseline_cost − pred_cost;若 未通过(任一步 error 或任一步 pred < gold),该 trajectory 每个可评步均 N_usd -= pred_cost(该 trajectory 上所有预测路由花费均计入 N 的负向)。
| 字段 | 定义 |
|---|---|
total_trajectories |
当前评分行列表中不同 instance_id 的个数。 |
passed_trajectories / exact_match_trajectories |
trajectory 级通过 / 全步精确匹配条数。 |
evaluable_step_count |
无 error 且 tier id 为 int 的步数(参与 D_usd / N_usd)。 |
skipped_step_count |
含 error 的行数。 |
D_usd |
Σ (baseline_cost − gold_cost)(可评步)。 |
N_usd |
按上段 pass / fail trajectory 规则。 |
pass_rate_percent |
100 × passed_trajectories / total_trajectories;无 trajectory 时为 NaN。 |
exact_match_rate_percent |
100 × exact_match_trajectories / total_trajectories;无 trajectory 时为 NaN。注意: 与顶层 tier_match_accuracy(按步完全匹配率)不是同一指标。 |
accounting_savings_score_percent |
100 × N_usd / D_usd(D_usd > 0 时);D_usd == 0 或无 trajectory 时为 NaN。 |
overall_score_percent |
上述三个百分量的算术平均;任一为 NaN 则总分为 NaN。 |
fallback_output_tokens |
无法从增量推断输出 token 时的回退常数。 |
顶层 tier_match_accuracy 与 accuracy_excluding_errors 仍为按步完全匹配率(二者数值相同)。by_benchmark 里的 exact_match 亦为按步计数(match 为真的行数)。
from main import iter_question_bank, iter_routing_supervision
# Full bank (single file data/question_bank.jsonl)
for row in iter_question_bank():
...
# Only rows whose benchmark field is "swebench" (same as iter_routing_supervision("swebench"))
for row in iter_routing_supervision("swebench"):
messages = row["messages"]
tier = row["target_tier"]
tier_id = row["target_tier_id"]from main.metrics import CaseMetrics, aggregate_routerbench_metrics
cases = [
CaseMetrics(
case_id="a",
task_passed=True,
baseline_cost_nominal=1.0,
optimal_cost_nominal=0.4,
test_cost_nominal=0.5,
),
]
summary = aggregate_routerbench_metrics(cases)from main.metrics import routing_supervision_accuracy
acc = routing_supervision_accuracy(gold_rows, predictions_by_id)OpenAICompatRouterClassifier 每次请求一个 case:system(纯字符串,或在 system_prompt_cache 为 on / auto 且为 Claude 时使用 Anthropic 风格缓存块列表)加上 一条 user 消息,其 content 为字符串(完整 case 文本)。模型必须只回复 一个字符 0–3(对应 target_tier_id:low→0,mid→1,mid_high→2,high→3)。若含换行或多余文字,解析时会抛出 ValueError。
from main import OpenAICompatRouterClassifier, question_bank_messages_to_classifier_prompt
clf = OpenAICompatRouterClassifier(
base_url="https://api.example.com/v1",
api_key="...",
model="deepseek/deepseek-v3.2",
system_prompt_cache="auto",
)
prompt = question_bank_messages_to_classifier_prompt(row["messages"])
result = clf.predict_tier_id(prompt)
assert result.tier_id == row["target_tier_id"]更底层辅助:parse_tier_response_to_id、build_system_content、post_chat_completions、chat_completions_url。默认系统说明见 DEFAULT_ROUTER_SYSTEM_INSTRUCTION。
抽样、记分与可插拔预测器(FunctionPredictor、LlmDigitClassifierPredictor 或任意 QuestionBankRouterPredictor)的编程入口。语义见 Benchmark 用法 与 记分规则。
实现 QuestionBankRouterPredictor(方法 predict(row) -> TierPrediction),或使用:
FunctionPredictor:封装任意callable(row: dict) -> int(启发式、sklearnpredict等);无需 chat prompt。LlmDigitClassifierPredictor:可选的 OpenAI 兼容 chat 封装(OpenAICompatRouterClassifier+question_bank_messages_to_classifier_prompt)。
from main.eval import (
FunctionPredictor,
LlmDigitClassifierPredictor,
run_question_bank_eval,
evaluate_question_bank_rows,
build_eval_summary,
select_question_bank_rows,
)
# Rules / sklearn-style: tier_id only from the row (example: always use gold — not a real model)
oracle = FunctionPredictor(lambda row: row["target_tier_id"])
rows, sample_mode, quotas = select_question_bank_rows(n=20, seed=1)
per_row, errors, correct = evaluate_question_bank_rows(
oracle, rows, predictor_label="oracle_gold"
)
summary = build_eval_summary(
per_row=per_row,
errors=errors,
correct=correct,
predictor_label="oracle_gold",
shard="data/question_bank.jsonl",
sample_mode=sample_mode,
seed=1,
proportional_quotas=quotas,
)
# One-shot (loads bank from package data paths):
# summary = run_question_bank_eval(oracle, predictor_label="oracle_gold", n=20, seed=1)公开辅助函数还包括:manifest_proportional_quotas、proportional_reservoir_sample、load_all_question_bank_rows、compute_section11、compute_router_accounting_metrics、compute_v2_scores、aggregate_by_benchmark。
python -m main.cli metrics --cases path/to/cases.json
CommonRouterBench metrics --cases path/to/cases.json在应用里使用 OpenAICompatRouterClassifier 时,请用环境变量或自有配置传入网关信息。.env.example 列出常见变量名(OPENROUTER_* 或 OPENAI_* / API_KEY + BASE_URL);客户端要求 base URL 已含 /v1。
- 上传 PyPI 前在
pyproject.toml中填写[project.urls](Homepage、Repository 等)。 - 若希望 wheel 内含题库,构建前确保存在
data/question_bank.jsonl与data/manifest.json(见pyproject.toml中package-data)。 - 提升
version,并在CHANGELOG.md中追加一节。 - 构建与上传:
pip install build twine
python -m build
twine check dist/*
twine upload dist/*命名提醒: PyPI / pip 名为 CommonRouterBench,顶层 import 包名为 main;勿在示例脚本旁使用会遮蔽 main 的文件名(例如避免与 import main 冲突的 main.py)。
Apache-2.0(见仓库根目录 LICENSE 与 pyproject.toml)。第三方 benchmark 数据可能有单独许可证。