任务类型决定指标选择
第一原则:先分清楚任务类型,再选指标。同一个模型在不同任务上要用不同的尺子。
| 任务类型 | 典型例子 | 推荐主指标 | 辅助指标 |
|---|---|---|---|
| 分类 / 抽取 | 情感分类、意图识别、NER | Accuracy / F1 | 混淆矩阵、每类 Recall |
| 结构化输出 | JSON 生成、函数调用 | Schema 合法率 | 字段准确率、类型正确率 |
| 翻译 / 改写 | 英译中、文风改写 | BLEU / chrF / COMET | 长度比、人工抽检 |
| 摘要 | 新闻摘要、会议纪要 | ROUGE-L / BERTScore | Faithfulness(LLM Judge) |
| 开放问答 | 知识问答、闲聊 | LLM-as-Judge 打分 | 语义相似度、毒性检测 |
| 代码生成 | 函数实现、SQL 生成 | pass@k(执行测试) | 编译通过率、CodeBLEU |
| RAG | 带引用的问答 | Faithfulness + Answer Relevancy | Context Recall/Precision |
| Agent | 多步工具调用 | 任务完成率 | Trajectory match、步数 |
关键观察
只有"分类"和"代码"能用单一数字判定对错。其他所有任务都需要多指标组合——任何声称"一个指标搞定一切"的方案都要警惕。
指标家族一:精确匹配类
Exact Match(EM)
最朴素,最硬。预测字符串与参考完全一致才算对。
def exact_match(pred, ref): return pred.strip().lower() == ref.strip().lower()
适用:短答案抽取(who/when/where)、分类 label、选项题。盲区:空格、大小写、标点都会让它判 0,所以常要配合规范化。
规范化 EM
import re, string def normalize(s): s = s.lower() s = re.sub(r"[%s]" % re.escape(string.punctuation), "", s) s = re.sub(r"\b(a|an|the)\b", " ", s) s = " ".join(s.split()) return s def em_normalized(pred, ref): return normalize(pred) == normalize(ref)
Substring Match / Contains
放宽版:预测包含参考即可。常用于"答案混在解释里"的场景。
def contains(pred, ref): return normalize(ref) in normalize(pred)
坑
Contains 会把 "不是北京" 判为正确答案 "北京"。对否定/反义敏感的任务要慎用。
指标家族二:基于 n-gram 的相似度
BLEU — 机器翻译的老祖宗
BLEU(Bilingual Evaluation Understudy)计算预测中有多少 n-gram 出现在参考里,并做长度惩罚。
计算直觉
1-gram precision + 2-gram precision + 3-gram + 4-gram 的几何平均,再乘以长度惩罚因子(预测太短会被惩罚)。
典型值
翻译任务 30-40 算不错,60+ 很好,90+ 可疑(可能背答案)。
from sacrebleu import corpus_bleu refs = [["the cat is on the mat", "a cat sits on a mat"]] hyps = ["the cat sits on the mat"] bleu = corpus_bleu(hyps, refs) print(bleu.score) # 例如 67.4
ROUGE — 摘要任务的主力
ROUGE(Recall-Oriented Understudy for Gisting Evaluation)和 BLEU 互补——BLEU 看 precision,ROUGE 看 recall:"参考里的 n-gram 有多少被预测命中"。
| 变体 | 计算 | 适用 |
|---|---|---|
| ROUGE-1 | 1-gram overlap | 粗略词覆盖 |
| ROUGE-2 | 2-gram overlap | 短语连贯性 |
| ROUGE-L | 最长公共子序列 | 句子结构 — 摘要最常用 |
| ROUGE-Lsum | 段落级 LCS | 长文摘要 |
from rouge_score import rouge_scorer scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True) scores = scorer.score( target="小明今天去了公园看展览", prediction="小明去公园看了展览", ) print(scores["rougeL"].fmeasure) # 约 0.73
BLEU/ROUGE 的致命盲区
❌ 它们测不出的
- 同义词:"车" vs "汽车" 算不同
- 语序等价:主动/被动句式变换得分下降
- 事实错误:可以通顺胡说而得高分
- 省略但不影响意思的精简
✅ 它们擅长的
- 字面覆盖度(术语、命名实体)
- 快速、便宜、无需 GPU
- 可重复、跨团队可比
- 作为"快筛"指标排除明显差案例
指标家族三:语义相似度
BERTScore
用预训练模型(BERT/RoBERTa)做 token 嵌入,然后对齐预测与参考的 token,计算余弦相似度。解决了 BLEU/ROUGE "字面匹配" 的局限。
from bert_score import score cands = ["猫坐在垫子上"] refs = ["猫躺在毯子上"] P, R, F = score(cands, refs, lang="zh", verbose=False) print(F.item()) # F1 约 0.93 — 语义近即使字面不同
嵌入余弦相似度
更轻量:直接用 embedding 模型算句向量余弦距离。
from openai import OpenAI import numpy as np client = OpenAI() def embed(text): r = client.embeddings.create(model="text-embedding-3-small", input=text) return np.array(r.data[0].embedding) def cosine(a, b): return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b))) sim = cosine(embed("发票开具流程"), embed("如何申请发票")) print(sim) # 约 0.87
语义相似度的陷阱
"北京是中国首都" 和 "北京不是中国首都" 的余弦相似度高达 0.95。嵌入相似度对否定、数字、实体替换极不敏感。只能当粗筛,不能当主指标。
指标家族四:业务约束
这是常被低估的一类,但往往是生产环境最重要的。它不是"评判好坏",而是"确保没有离谱错误"。
Schema 合法率
JSON 是否能被
json.loads 解析;字段是否齐全;类型是否匹配 pydantic model。生产 API 对接这一项失败等于 500。格式规则
是否使用了指定的 XML 标签;是否只输出一个单词;是否包含所需的段落结构。
禁止项检查
是否泄露了 system prompt;是否包含 PII;是否提到竞争对手;是否给出了违禁建议。
长度 / 成本
输出是否超过 token 上限;是否异常短(可能是 refusal)。
引用校验(RAG)
引用的 source_id 是否真的在提供的上下文里;每个断言是否都有引用。
# 硬约束的组合示例 import json from pydantic import BaseModel, ValidationError class TicketReply(BaseModel): category: str urgency: int # 1-5 reply: str def validate_reply(raw: str) -> dict: results = {} # 1. JSON 合法 try: obj = json.loads(raw) results["json_valid"] = True except json.JSONDecodeError: results["json_valid"] = False return results # 2. Schema 合法 try: t = TicketReply.model_validate(obj) results["schema_valid"] = True except ValidationError: results["schema_valid"] = False return results # 3. 业务规则 results["urgency_in_range"] = 1 <= t.urgency <= 5 results["reply_length_ok"] = 20 <= len(t.reply) <= 500 results["no_competitor"] = "竞品名" not in t.reply return results
生产经验
80% 的"奇怪 bug"都是硬约束失败。先把硬约束这张网织密,再去调生成质量。硬约束失败应该让整个 eval 分为 0,而不是被其他指标平均稀释掉。
指标家族五:LLM-as-Judge
用另一个更强的模型给输出打分。第 4 章会单独深讲,这里只给全貌:
- Pointwise — 给一个回答打 1-5 分(或 rubric 各维度分)
- Pairwise — A/B 哪个更好
- Reference-based — 对照黄金答案判对错
- Reference-free — 只看问题和答案,判"好不好"
组合策略:软指标 + 硬约束
一个成熟的 eval 系统通常长这样:
┌───────────────┐
输出 ──▶ │ 硬约束(门禁) │ ─ 任何一条失败 → 总分 0
└───────┬───────┘
│ 全通过
▼
┌───────────────┐
│ 快速软指标 │ BLEU/ROUGE/BERTScore/Embed
│ (全量跑) │ 用于对比迭代、趋势监控
└───────┬───────┘
│
▼
┌───────────────┐
│ LLM-as-Judge │ 贵,采样跑(如 20%)
│ (采样跑) │ rubric 多维度
└───────┬───────┘
│
▼
┌───────────────┐
│ 人工抽检 │ 最贵,3-5% 流量
│ (月度) │ 校准 Judge 是否跑偏
└───────────────┘
一个完整的指标卡例子
客服回复任务的指标卡(Scorecard):
| 维度 | 指标 | 类型 | 阈值 |
|---|---|---|---|
| 格式 | JSON 合法率 | 硬约束 | ≥ 99.5% |
| 格式 | 字段完整率 | 硬约束 | = 100% |
| 安全 | 未泄露系统 prompt | 硬约束 | = 100% |
| 相关性 | Answer Relevancy(Judge) | 软指标 | ≥ 4.2 / 5 |
| 准确性 | Factual Accuracy(Judge) | 软指标 | ≥ 4.5 / 5 |
| 语气 | Tone match(Judge) | 软指标 | ≥ 4.0 / 5 |
| 简洁 | 长度 50-300 字符 | 硬约束 | ≥ 95% |
| 成本 | p95 token | 监控 | < 800 |
指标卡 vs 单一分数
新手喜欢"总分 87 分"式的单一指标,但它隐藏了失败模式。成熟团队一定是多维度独立跟踪——某一维异常下跌会被立刻发现,而不是被平均值掩盖。
本章小结
- 任务类型决定指标选择——没有万能指标
- n-gram 指标(BLEU/ROUGE)便宜、快,但盲区多;语义指标(BERTScore/embed)补全
- 嵌入相似度对否定、数字不敏感,只能当粗筛
- 硬约束是生产系统的救命稻草,常被低估
- Judge 指标最接近人类判断,但最贵,放在采样层
- 用指标卡而非单一分数,才能发现分布级问题
下一章:Golden Dataset 方法论——再好的指标打在错的数据上也是白打。