别再拿十道题测模型了:一个能真正比较三家 API 的 200 题评测脚手架
模型选型最常见的“伪严谨”,是准备十几道题,然后把 GPT、Claude、Gemini 各问一遍。
最后得到一句:
A 更聪明,B 更稳,C 更便宜。
这种测试对聊天体验有点用,对企业选型几乎不够。
因为真实系统真正需要比较的是:
任务成功率
结构化输出成功率
失败类型
延迟
输入/输出 Token
重试
人工修改量
单成功任务成本
今天不讲理论,直接做一个轻量脚手架。
目标不是造一个“通用 Benchmark 平台”,而是让你两三个小时内能把公司自己的 200 条任务跑起来。
我建议先准备 200 条,而不是 20 条
一个比较实用的分布:
40 条 RAG / 文档问答
40 条结构化抽取
40 条 Coding / Debug
30 条 Tool Calling
20 条复杂分析
15 条长上下文
10 条拒绝 / 安全边界
5 条极端长尾
为什么是 200?
因为 20 条太容易被偶然结果影响;200 条已经足够看到明显 Slice 差异,同时成本又没有大到离谱。
更重要的是:这些题不要自己现编。
优先从:
- 已脱敏生产问题;
- 历史失败案例;
- 人工客服问题;
- 真实代码 Issue;
- 真实合同和报表任务;
- 用户反复追问的问题;
里面抽。
数据文件别设计得太复杂
先用 JSONL:
{"id":"rag-001","type":"rag","input":"...","must_include":["..."],"must_not_include":["..."],"max_latency_ms":12000}
{"id":"extract-001","type":"extract","input":"...","json_schema":"invoice-v1","expected":{"currency":"CNY"}}
{"id":"code-001","type":"coding","input":"修复...","repo_fixture":"repo-01","required_tests":["test_x"]}
目录:
benchmark/
├── cases.jsonl
├── adapters/
│ ├── openai_adapter.py
│ ├── anthropic_adapter.py
│ └── gemini_adapter.py
├── evaluators/
│ ├── rule_eval.py
│ ├── json_eval.py
│ └── judge_eval.py
├── results/
└── run.py
不要一上来引入数据库、消息队列和 Web UI。
先把评测跑通。
统一一个模型适配接口
from dataclasses import dataclass
from typing import Protocol, Any
@dataclass
class ModelResponse:
text: str
input_tokens: int
output_tokens: int
latency_ms: int
raw: Any
class ModelAdapter(Protocol):
name: str
async def generate(
self,
prompt: str,
*,
max_output_tokens: int = 4096,
) -> ModelResponse:
...
这样以后无论接:
GPT-5.6
Claude Opus 5
Gemini 3.7 Flash
Kimi K3
DeepSeek
Qwen
评测层都不用改。
OpenAI Adapter 示例
import time
from openai import AsyncOpenAI
class OpenAIAdapter:
def __init__(self, model: str):
self.name = model
self.client = AsyncOpenAI()
self.model = model
async def generate(
self,
prompt: str,
*,
max_output_tokens: int = 4096,
) -> ModelResponse:
started = time.perf_counter()
resp = await self.client.responses.create(
model=self.model,
input=prompt,
max_output_tokens=max_output_tokens,
)
latency_ms = int(
(time.perf_counter() - started) * 1000
)
usage = resp.usage
return ModelResponse(
text=resp.output_text,
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
latency_ms=latency_ms,
raw=resp,
)
注意:不同 SDK 版本的 Usage 字段可能有变化,真正运行时以你安装的当前 SDK 为准。
Anthropic 和 Gemini 不要硬凑成完全一样的参数
一个常见错误是:
temperature=0.2
max_tokens=4096
然后三家模型全部用同一套参数。
这叫“参数一样”,不叫“条件公平”。
不同模型的推荐 reasoning / effort / sampling 方式不同。
更合理的是给每个模型一份 Profile:
models:
gpt-5.6-terra:
effort: medium
max_output_tokens: 4096
claude-opus-5:
effort: high
max_output_tokens: 4096
gemini-3.7-flash:
max_output_tokens: 4096
你要比较的是:
在各自合理配置下,谁更适合这个业务。
而不是强迫所有模型使用相同内部机制。
Runner:控制并发,不要把 API 打爆
import asyncio
import json
from pathlib import Path
async def run_case(
sem,
adapter,
case,
):
async with sem:
try:
resp = await adapter.generate(
case["input"]
)
return {
"case_id": case["id"],
"model": adapter.name,
"ok": True,
"text": resp.text,
"input_tokens": resp.input_tokens,
"output_tokens": resp.output_tokens,
"latency_ms": resp.latency_ms,
}
except Exception as e:
return {
"case_id": case["id"],
"model": adapter.name,
"ok": False,
"error": type(e).__name__,
"message": str(e),
}
async def run_model(adapter, cases, concurrency=5):
sem = asyncio.Semaphore(concurrency)
tasks = [
run_case(sem, adapter, case)
for case in cases
]
return await asyncio.gather(*tasks)
为什么默认并发先用 5?
不是因为 5 最科学,而是因为第一次跑评测,先把:
Rate Limit
429
超时
连接池
重试
这些问题看清楚,再逐步提高。
第一层评测一定用确定性规则
例如 must_include:
def eval_must_include(text, keywords):
missing = [
k for k in keywords
if k not in text
]
return {
"passed": len(missing) == 0,
"missing": missing,
}
must_not_include:
def eval_must_not_include(text, keywords):
hit = [
k for k in keywords
if k in text
]
return {
"passed": len(hit) == 0,
"hit": hit,
}
结构化任务直接解析 JSON:
import json
def eval_json(text):
try:
obj = json.loads(text)
return True, obj, None
except Exception as e:
return False, None, str(e)
能用代码判断的,不要先交给 LLM-as-a-Judge。
数字类任务不要只做字符串比较
比如参考答案:
12.50%
模型回答:
0.125
语义一样,字符串不同。
可以给 Case 写数值规则:
{
"field": "gross_margin",
"expected": 0.125,
"tolerance": 0.001
}
Evaluator:
def within_tolerance(actual, expected, tolerance):
return abs(actual - expected) <= tolerance
RAG 任务至少拆成两层
不要只评“最后回答好不好”。
拆:
Retrieval
Answer
记录:
{
"retrieved_doc_ids": ["doc-18", "doc-42"],
"expected_doc_ids": ["doc-42"],
"answer": "..."
}
否则一个模型最终答错了,你根本不知道:
检索没找到
还是
模型拿到证据后仍然答错
Tool Calling 测什么
我会记录四个字段:
tool_name
arguments
call_count
final_outcome
例如:
{
"expected_tool": "get_order_status",
"required_args": ["order_id"],
"maximum_tool_calls": 3,
"forbidden_tools": ["refund_order"]
}
这比让 Judge 看一眼最终文本靠谱得多。
Coding Case 别只看生成文本
Coding 最好在隔离环境里真正跑:
checkout fixture
→应用 patch
→install
→compile
→test
→lint
→安全检查
结果:
{
"build": true,
"tests_total": 28,
"tests_passed": 28,
"forbidden_files_changed": 0,
"security_findings": 0
}
这就是上一篇 GH200 论文最值得借鉴的地方:Artifact 能不能跑,比模型描述自己“已经修复完成”重要。
再加一层 Judge,但只评开放式部分
Judge 适合:
- 完整性;
- 可读性;
- 是否真正回应用户目标;
- 开放式分析质量。
Judge 不适合替代:
- 测试结果;
- 数据库状态;
- JSON Schema;
- 数字;
- Tool 是否真实成功。
一个简单 Judge 输出:
{
"score": 4,
"blocking_issue": false,
"reason": "..."
}
建议同一边界样本跑两三次,尤其是用于模型迁移时。
把成本算到“成功任务”上
先算单请求:
def token_cost(
input_tokens,
output_tokens,
input_price_per_m,
output_price_per_m,
):
return (
input_tokens / 1_000_000
* input_price_per_m
+ output_tokens / 1_000_000
* output_price_per_m
)
但最后报表不要只看平均请求成本。
看:
Cost per Successful Task
例如:
模型A:平均请求 $0.03,成功率 70%
模型B:平均请求 $0.05,成功率 96%
如果失败任务还要人工补救,A 可能反而更贵。
我会加一个“人工修改分钟数”
这个字段很土,但很有价值:
human_edit_minutes
让评测人员完成最终交付后,记录大概修改时间。
有些模型:
Judge评分 4.5
但人工要花 8 分钟重写。
另一个模型:
Judge评分 4.3
人工只改 1 分钟。
生产里我会选后者。
汇总报表不要只有一个 Winner
按 Slice 输出:
RAG
Extraction
Coding
Tool
Analysis
Long Context
Safety
例如:
| Slice | Model A | Model B | Model C |
|---|---|---|---|
| RAG Task Success | 94% | 91% | 95% |
| Coding | 71% | 83% | 76% |
| Tool | 88% | 82% | 90% |
| JSON Valid | 99% | 97% | 99% |
| P95 | 8.1s | 13.4s | 5.8s |
最终架构可能不是:
Model B wins
而是:
RAG → C
Coding → B
默认 Tool → C
高风险 Review → B
这才是多模型路由的基础。
结果文件建议长这样
{
"case_id": "tool-018",
"slice": "tool",
"model": "candidate-c",
"success": true,
"input_tokens": 18120,
"output_tokens": 930,
"latency_ms": 4231,
"tool_calls": 2,
"retry_count": 0,
"rule_score": 1.0,
"judge_score": 4.4,
"human_edit_minutes": 0.5,
"cost_usd": 0.0172
}
保留每条明细。不要只保存最终 Excel 平均数。
以后模型升级时,你会需要逐条 Diff。
最后一个建议:先跑 20 条 Debug,再跑 200 条
第一次脚手架通常会有自己的 Bug:
- Token 字段解析错;
- Provider 超时;
- 某家 SDK 返回结构不同;
- JSON 输出包了 Markdown;
- 并发太高;
- Case 本身答案有问题。
所以执行顺序:
20 条 Debug Set
↓
修评测框架
↓
200 条正式集
↓
查看失败
↓
再抽 20 条人工复核
不要第一次就花几百美元跑完,然后才发现统计脚本写错了。
真正可靠的模型选型,没有“神奇 benchmark”。
它其实就是一件很朴素的工程工作:拿真实任务、固定输入、记录结果、自动验证、人工抽查,然后算成功率和成本。
做到这一步,模型宣传页就只是一份候选名单,而不是采购结论。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/