可执行率、参数准确率、任务完成率——这三个词每个人都说得出来,但问「分母是什么」「枚举和自由文本能用同一个口径吗」,多数人答不上来。而口径不清的指标比没有指标更危险:它会让你在错误的数字上做出信心十足的决策。
编辑部注 · 本章的核心是「口径」二字。建议读完把三张表存下来,面试讲到评测时可以直接引用。
办事型 Agent 的评测看起来比洞察型简单——因为它有明确的对错。但恰恰是「明确的对错」让人放松警惕:既然有对错,就一定能算准。事实是,三个团队测同一个系统,可能得出三个差 20 个百分点的答案,而每个团队都认为自己是对的。
一、第一个坑:分母
这是最常见、影响最大的问题。看一个具体的例子:
| 口径设计 | 分子 | 分母 | 算出的「可执行率」 | 问题 |
|---|---|---|---|---|
| A · 只看成功的样本 | 执行成功且结果正确 | 成功产出了 DSL 的样本 | 约 89% | 把「没产出」的样本排除在分母外,等于把失败藏起来了 |
| B · 全样本 | 执行成功且结果正确 | 参与评测的全部样本 | 约 67% | 准确反映端到端表现,适合对外汇报 |
| C · 排除不可解样本 | 执行成功且结果正确 | 全部样本减去「信息不足无法完成」的样本 | 约 78% | 更公平地衡量「在可解问题上做得多好」,但必须公开排除规则 |
三个数字都「对」,但含义完全不同。关键纪律是:报一个数字时必须同时报它的分母定义。 而且在同一份对比报告里,口径不能变——这次用 B、下次用 A 来显示进步,是典型的指标作弊。
与其争论哪个口径对,不如把结果拆开报:
- 端到端完成率(分母 = 全样本):最适合对外、最能反映用户真实体验;
- 可解样本完成率(分母 = 全样本 − 不可解):用来衡量「技术能力的上限有多高」;
- 不可解样本的处理率(分母 = 不可解样本):衡量「该拒绝或追问时有没有正确拒绝/追问」——这是一个独立且非常重要的指标。
第三个指标最容易被漏掉,但它决定了系统「会不会硬猜」。一个把不可解问题也硬答的系统,端到端完成率数字会好看,但用户体验更差。
二、第二个坑:什么叫「参数准确」
「参数准确率」听起来是最客观的指标——毕竟参数有明确的值。但只要细看就会发现问题成堆。
三、第三个坑:任务完成率怎么判
「任务完成」是最难判定的指标,因为它通常需要人来看结果。可行的做法是按任务类型分三档:
| 任务类型 | 判定方式 | 可信度 | 成本 |
|---|---|---|---|
| 结果可程序化验证 | 跑一遍、比对期望结果(如查出的行数、生成的代码能否通过测试) | 最高 | 低(一次执行) |
| 结果可部分验证 | 检查关键断言(字段是否齐全、数值是否在合理范围、引用是否真实存在) | 较高 | 中(写断言) |
| 结果需人工判断 | 抽样人工评审 + 多人交叉打分 | 一般 | 高(每人每条若干分钟) |
把「需人工判断」的任务尽量改造成「可程序化验证」。具体做法是:不判定最终文本好不好,而是判定几个可程序化的副作用——
- 产物文件是否存在、格式是否合法(解析一下就知道);
- 报告里提到的每个引用是否真实存在(拿 URL 去 HEAD 一下);
- 生成的查询是否真的能跑通且返回非空(跑一遍);
- 是否需要澄清时真的澄清了(检查是否发生了澄清动作)。
这四条检查全部可以自动完成,而且它们覆盖了「完成度」最实质的部分。剩下真正需要人看的,往往只有 10%-20%。
四、动手:三档指标的计算实现
from dataclasses import dataclass
from typing import Any
import re, unicodedata
def normalize(v: Any, rules: dict) -> Any:
"""
语义等价的规范化。规则必须是显式的、可审阅的,
而不是「遇到不一致就放宽」——否则指标会失去意义。
"""
if isinstance(v, str):
v = unicodedata.normalize("NFKC", v).strip()
if rules.get("case_insensitive"):
v = v.lower()
for src, dst in rules.get("synonyms", {}).items():
if v == src:
v = dst
return v
def args_match(pred: dict, gold: dict, rules: dict) -> tuple[bool, list[str]]:
"""
参数比对:返回 (是否匹配, 不匹配的字段列表)。
返回字段明细很重要 —— 归因时你需要知道是哪个字段在错。
"""
bad = []
for k, gv in gold.items():
if k not in pred:
bad.append(f"{k}:缺失")
continue
pv = normalize(pred[k], rules.get(k, {}))
if pv != normalize(gv, rules.get(k, {})):
bad.append(f"{k}:{pv!r}≠{gv!r}")
return (len(bad) == 0), bad
@dataclass
class MetricReport:
"""分层报告:任何一个数字都必须带口径说明"""
end_to_end: float # 分母 = 全样本
solvable_only: float # 分母 = 全样本 - 不可解
refuse_handling: float # 分母 = 不可解样本(正确拒绝或澄清的比例)
args_field_level: float # 参数:字段级严格比对
args_semantic: float # 参数:语义等价比对
n_total: int
n_solvable: int
n_unsolvable: int
def render(self) -> str:
return (
f"端到端完成率 {self.end_to_end:.1%}"
f" (分母 {self.n_total} = 全样本)\n"
f"可解样本完成率 {self.solvable_only:.1%}"
f" (分母 {self.n_solvable} = 全样本 − 不可解)\n"
f"不可解处理正确率 {self.refuse_handling:.1%}"
f" (分母 {self.n_unsolvable} = 不可解样本,正确拒绝或追问澄清)\n"
f"参数字段级准确率 {self.args_field_level:.1%} (严格比对)\n"
f"参数语义等价准确率 {self.args_semantic:.1%} (规范化后比对)\n"
f"※ 两行参数指标差值 = {abs(self.args_field_level - self.args_semantic):.1%},"
f"差值过大说明模型偏好非标准表达,可在描述里给出枚举值收敛"
)
def evaluate(results: list[dict], rules: dict) -> MetricReport:
"""
results 的每项形如:
{
"solvable": True,
"pred_args": {...} | None,
"gold_args": {...},
"executed_ok": True, # 真正跑通且结果正确
"correctly_refused": False, # 不可解样本上是否给出了正确回应
}
"""
total = len(results)
unsolvable = [r for r in results if not r["solvable"]]
solvable = [r for r in results if r["solvable"]]
e2e = sum(1 for r in results if r.get("executed_ok")) / total if total else 0
solv = sum(1 for r in solvable if r.get("executed_ok")) / len(solvable) if solvable else 0
refu = (sum(1 for r in unsolvable if r.get("correctly_refused")) / len(unsolvable)
if unsolvable else 0)
has_args = [r for r in results if r.get("pred_args") and r.get("gold_args")]
field_ok = sum(1 for r in has_args if args_match(r["pred_args"], r["gold_args"], {})[0])
sem_ok = sum(1 for r in has_args if args_match(r["pred_args"], r["gold_args"], rules)[0])
return MetricReport(
end_to_end=e2e,
solvable_only=solv,
refuse_handling=refu,
args_field_level=field_ok / len(has_args) if has_args else 0,
args_semantic=sem_ok / len(has_args) if has_args else 0,
n_total=total, n_solvable=len(solvable), n_unsolvable=len(unsolvable),
)
# --- 规范化规则示例(显式、可审阅、可版本化) ---
RULES = {
"case_insensitive": True, # 全局:忽略大小写
"region": {"synonyms": {"华东": "east", "华东区": "east", "East": "east"}},
"op": {"synonyms": {"大于": ">", ">": ">", "gt": ">"}},
"unit": {"synonyms": {"万": "10000", "万元": "10000"}},
}规范化规则一旦修改,历史指标就不可比了。例如把「华东区」加入同义词后,参数准确率会突然上升——但这不代表系统变好了,只是评判标准变了。
做法:把 RULES 当成代码一样纳入版本管理,规则变更时在报告里显式标注「口径已更新」,并且不要在规则变更的同一版里对比新旧数据。这是评测工程的专业性所在,也是「口径不清等于没有指标」这句话的具体表现。
五、自测
六、小结
| 指标 | 关键口径问题 |
|---|---|
| 可执行率 | 分母是「全样本」还是「成功产出样本」?差 20 个百分点 |
| 参数准确率 | 粒度是结构级 / 字段级 / 语义等价级 / 结果级? |
| 任务完成率 | 能程序化验证的优先程序化;人工评审控制在 10-20% |
| 不可解处理率 | 必须独立成指标,否则会激励「硬猜」 |
| 规范化规则 | 必须显式、可审阅、可版本化;变更时口径标注更新 |
| 报告方式 | 分层报告(端到端 / 可解 / 不可解),不选单一数字 |
指标的全部价值在于「能被复现」。一个别人复现不出来的数字,无论多么精确,都只是个人观点。本刊编辑部
下一章处理更棘手的问题:当输出是「洞察」而不是「结果」,怎么评?
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
"参数生成准确性"具体怎么算? 高频
① 枚举类参数(状态、类型、操作符)——精确匹配。生成值必须在合法枚举集合内且与期望一致,错一个就是错。
② 数值类参数(阈值、数量、日期区间)——精确匹配或带明确容差的近似匹配,容差要事前写清楚(例如日期允许 ±0,金额允许 ±0.01)。
③ 自由文本参数(检索关键词、描述字段)——结构化逐项比对:先把期望拆成必含要素集合,再检查生成结果覆盖了哪些要素,得分是覆盖率;若有明确禁止项,命中即扣分。
④ 集合类参数(多选标签、字段列表)——用精确率/召回率或 Jaccard 相似度,而不是"差不多就行"。
还有两个必须明确的口径问题:分母是什么(所有调用,还是只统计"模型认为该调用工具"的那些);部分正确怎么算(4 个参数对 3 个,是算 0 还是 0.75——不同选择会得出完全不同的结论)。
追问链
- 分母你选哪个?为什么?
- 部分正确你怎么处理?
工具调用成功率这个指标有什么陷阱?
① 分母陷阱——如果把"模型压根没调工具"的情况排除在分母外,成功率会虚高,掩盖"该调不调"的问题。分母应该是所有本应调用工具的场景。
② 成功定义陷阱——HTTP 200 不等于任务成功。要区分"调用发出成功""参数正确""业务结果符合预期"三层,混在一起会看不出问题在哪。
③ 难度混同陷阱——把简单查询和复杂多步任务混在一个数字里,任何改动都会被大类淹没。应该按任务类型分层看。
④ 重试归因陷阱——经过重试才成功的算不算成功?我倾向于分开统计:一次成功率(反映模型质量)与最终成功率(反映系统兜底能力),两者同时看。只看最终成功率,会让"重试兜住一切"掩盖模型本身的问题。
任务完成率怎么定义?由谁判定?
① 自动化判定——任务有明确的可验证结果(生成了符合条件的文件、查询返回了非空结果、代码通过了测试),这是最可靠的,应当优先把任务设计成可自动验证的。
② 模型判定——用 LLM 对照 rubric 打分,适用于结果无法程序化验证的场景,必须配人工抽检校准。
③ 人工判定——最准但成本最高,用于校准前两者、以及构建金标准集。
工程上的目标是把尽可能多的任务转化为第 ① 类:给任务设计机器可验证的产出(结构化结果、断言、测试),这比提高打分模型的准确率更根本。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。