2024 年我做一个AI 问答功能:用户提问,大模型生成答案。功能跑通之后,我想知道一件事——"它到底答得准不准"。第一版我做得很省事:我自己随手敲了几个问题,看着模型的回答,觉得"嗯,还行",就上线了。后来每次改 prompt、换模型,我还是这么干——自己敲几个问题,肉眼看一看。本地一测——感觉不错:我问的那几个,它都答得挺像样。我心里很踏实:"AI 效果好不好,自己多试几个问题不就知道了。"可等它真正迭代起来,一串问题冒了出来。第一种:我改了一版 prompt,修好了一类答错的问题,满心欢喜上线,结果用户反馈另一类问题反而答崩了——我顾此失彼,却完全不知道。第二种:领导问我"这次换的新模型,到底比旧的好多少",我张口结舌——我说不出一个数,只能说"感觉好一点"。第三种:我和同事对同一个回答,一个说"这答得挺好",一个说"这不行"——我们压根没有一个共同的标准。第四种:我想把评测自动化,可生成式回答每次措辞都不一样、没有唯一正确答案,我没法像传统单元测试那样 assert 一下。最崩溃的一次:我学着用另一个大模型给回答打分,结果发现这个"裁判"本身就有偏见——它偏爱长答案、偏爱排在前面的选项。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"AI 效果好不好,自己多试几个问题就知道了"。这句话把"评测"这件严肃的工程,当成了"随手试试"的手感。可它不是。生成式 AI 的评测,是一套需要专门设计的工程:它不能靠随手试,不能靠肉眼看,不能靠感觉。真正的 AI 评测,核心是:用一个固定的评测集、一套明确的评分标准、一条自动化的评测流程,把"好不好"变成一个可对比、可复现的数字。这篇文章就把 AI 评测梳理一遍:为什么"随手试几个"靠不住、评测集该怎么构建、生成式回答没有标准答案该怎么评分、用大模型当裁判怎么校准它的偏见、版本迭代怎么做回归对比,以及评测集泄漏、评测集腐烂这些把 AI 评测真正做对要避开的坑。
问题背景
先把那次评测翻车的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。
现象:靠"自己敲几个问题、肉眼看"来判断一个 AI 问答功能的效果,迭代时冒出一串问题:改 prompt 修好一类、搞坏另一类却毫不知情;说不出新版到底比旧版好多少;团队成员对同一个回答评价不一致;没法自动化因为生成式回答没有唯一答案;用大模型当裁判,裁判自身有偏见。
我当时的错误认知:"AI 效果好不好,自己多试几个问题、看一看就知道了。"
真相:"随手试几个"有致命的样本偏差(你试的那几个不代表全局)和标准漂移(你今天和昨天的判断尺度会变)。AI 评测真正的工程量,在于:构建一个固定、有覆盖度的评测集、为不同类型的回答设计不同的评分方式、用 LLM 当裁判并校准它的偏见、每次改动都跑回归对比、定期人工抽检并防止评测集泄漏。把功能跑通只是开头,能客观衡量它好不好才是关键。
要把 AI 评测做对,需要几块认知:
- 为什么"随手试几个"靠不住——样本偏差与标准漂移;
- 评测集——一个固定、有覆盖度的题库,是评测的地基;
- 评分——生成式回答没有标准答案,该怎么打出分数;
- LLM 裁判——怎么用大模型评分,怎么校准它的偏见;
- 回归对比、评测集泄漏、人工抽检这些工程坑怎么处理。
一、为什么"随手试几个"靠不住
先把这件最根本的事钉死:"自己敲几个问题看一看"这种评测方式,有两个无法克服的硬伤。一是样本偏差——你随手想到的那几个问题,既不全面、也不随机,它们代表不了真实用户会问的千奇百怪的问题;二是标准漂移——人的判断尺度是会变的,你心情好的时候觉得"还行",较真的时候觉得"不行",你今天的"及格线"和上周的不是同一条。建立在这两个硬伤上的结论,本质上是不可信的。
下面这段代码,就是我那个"迭代起来就翻车"的第一版评测——它全靠肉眼:
# 反面教材:靠随手试几个问题、肉眼看,来判断 AI 效果
def check_quality(model):
questions = ["你们退货政策是什么", "怎么联系客服", "支持哪些支付方式"]
for q in questions:
answer = model.ask(q)
print(f"问:{q}")
print(f"答:{answer}\n")
# 然后我就盯着屏幕看一遍,觉得"还行"就上线了。
# 破绽一:这 3 个问题是我随手想的,代表不了用户真实会问的问题。
# 破绽二:"还行"是个全凭当下感觉的判断,今天明天的尺度都不一样。
# 破绽三:改版后再跑一遍,我没有上一版的结果可比 —— 全靠脑补。
这段代码本身没有 bug,它能跑、能打印。它的问题不在代码,而在这种评测方式从根上就不成立。它默认"我试的这几个问题,能代表整个功能的水平",而且默认"我肉眼给出的'还行',是一个稳定可靠的判断"。这两个默认全都不成立。于是那串问题就有了解释:改 prompt 修好一类、搞坏另一类,是因为我试的那几个问题恰好没覆盖到被搞坏的那类;说不出好多少,是因为我从来没有一个数,只有一个会飘的"感觉";团队评价不一致,是因为每个人心里的标准都不一样,而我们从没把标准写下来。问题的根子清楚了:要评测 AI,你首先得有一个固定不变的"考卷",和一套写死的"评分标准"——而不是每次都临时出题、凭感觉打分。
二、评测集:一个固定、有覆盖度的题库
AI 评测的地基,是一个评测集——一批固定的、精心挑选的测试样本。它一旦定下来,就不再随意改动,每个版本都用同一份考卷去考。这样,不同版本的成绩才可比。评测集的每一条,都该是结构化的:
from dataclasses import dataclass, field
@dataclass
class EvalCase:
"""评测集里的一条样本 —— 一道有明确考点的考题。"""
question: str # 输入给 AI 的问题
category: str # 所属类别,用于分类统计
reference: str = "" # 参考答案(开放题可为空)
key_points: list = field(default_factory=list) # 必须命中的关键点
must_not: list = field(default_factory=list) # 绝不能出现的内容
def build_eval_set() -> list:
"""构建评测集 —— 关键是覆盖度,别只挑简单的。"""
return [
EvalCase("退货政策是几天", "政策", key_points=["7 天"]),
EvalCase("怎么联系人工客服", "操作", key_points=["在线客服", "电话"]),
# 边界 / 易错样本:故意问超纲问题,看它会不会乱编
EvalCase("你们卖不卖飞机", "边界", must_not=["可以", "支持"]),
# ... 几十到几百条,覆盖各类别、各难度、各种刁钻问法
]
构建评测集,最关键的不是数量,而是覆盖度。一个好的评测集,要刻意地覆盖几类样本:高频问题(用户最常问的,占大头);各个功能类别(别让某个类别没被测到);边界和易错问题(故意问超纲的、有歧义的、想诱导它犯错的);还有历史上出过问题的 case(以前答错过的,全部收进评测集,确保不再犯)。注意每条样本里那几个字段:key_points 是"必须答到的点",must_not 是"绝不能说的话"——它们让"这个回答对不对"这件事,从"凭感觉"变成了"对照考点检查"。这就是评测集最大的价值:它把评判标准,从评测者飘忽的心里,挪到了一份白纸黑字的考卷上。有了固定的考卷,下一个问题是:生成式的回答没有唯一答案,这卷子怎么批改?
三、评分:生成式回答没有标准答案,怎么打分
传统软件测试可以 assert result == expected,因为结果是确定的。但生成式 AI 不行——同一个问题,它这次说"我们支持 7 天无理由退货",下次说"退货政策是 7 天内可退",两个都对,但字符串不相等。所以 AI 评测不能用精确匹配,要根据题目类型,用不同的评分方式:
def score_by_keypoints(answer: str, case: EvalCase) -> float:
"""关键点命中评分:看回答覆盖了多少必答点,有没有踩雷。"""
# 先检查"绝不能出现"的内容 —— 踩雷直接 0 分
for bad in case.must_not:
if bad in answer:
return 0.0
if not case.key_points:
return -1.0 # 没有关键点,本函数评不了,交给别的方式
# 命中率:答到了几个必答点
hit = sum(1 for kp in case.key_points if kp in answer)
return hit / len(case.key_points)
def exact_match(answer: str, reference: str) -> float:
"""精确匹配:只适用于答案唯一的题(如"是/否""某个数字")。"""
return 1.0 if answer.strip() == reference.strip() else 0.0
这两个函数,对付的是有明确考点的题。但还有一大类题是开放式的——比如"帮我润色这段话""解释一下这个概念",它没有关键点可对照,好坏要看多个维度。这种题,要做多维度打分:
SCORE_DIMENSIONS = ["相关性", "准确性", "完整性", "简洁性"]
def aggregate_score(dim_scores: dict) -> float:
"""把各维度分数按权重汇总成一个总分。"""
weights = {"相关性": 0.3, "准确性": 0.4, "完整性": 0.2, "简洁性": 0.1}
total = sum(dim_scores.get(d, 0) * weights[d] for d in SCORE_DIMENSIONS)
# 准确性权重最高:一个答案再流畅,信息错了也是不合格的
return round(total, 3)
这里的关键思路是:不要试图用一个笼统的"好不好"去评价一个回答,而要把"好"拆解成几个具体、可分别判断的维度——相关性(答没答到点子上)、准确性(信息对不对)、完整性(该说的说全了没)、简洁性(有没有啰嗦废话)。拆开之后,每个维度单独判断会容易、客观得多,而且权重还能体现业务取向——比如准确性权重最高,因为一个答案再流畅好读,只要信息是错的,它就是不合格的。但问题又回来了:这些维度分数,具体由谁来打?总不能还是人肉吧?这就引出了 AI 评测里最强大、也最需要小心的一招。
四、LLM 当裁判:怎么用,怎么校准它的偏见
对付开放式题目,现在主流的做法是 LLM-as-a-judge——用一个大模型,去给另一个大模型的回答打分。这解决了规模问题:几百条评测集,人工评要几小时,LLM 裁判几分钟就跑完。关键是怎么把"裁判"这件事交代清楚:
JUDGE_PROMPT = """你是一个严格的评分员。请根据以下维度,
给"待评回答"打分,每个维度 0 到 1 分。
问题:{question}
参考答案:{reference}
待评回答:{answer}
请只输出 JSON,格式:{{"相关性": 0.x, "准确性": 0.x, "完整性": 0.x, "简洁性": 0.x}}
"""
def llm_judge(case: EvalCase, answer: str, judge_model) -> dict:
"""用一个大模型给回答打多维分数。"""
prompt = JUDGE_PROMPT.format(question=case.question,
reference=case.reference, answer=answer)
raw = judge_model.ask(prompt)
import json
try:
return json.loads(raw) # 解析裁判输出的 JSON 分数
except json.JSONDecodeError:
return {d: 0.0 for d in SCORE_DIMENSIONS} # 解析失败按 0 分
但 LLM 裁判有一个必须正视的问题:裁判自己是有偏见的。研究和实践都发现,大模型当裁判时,有几种稳定的偏好:长度偏见(倾向于给更长的回答打高分,哪怕它更啰嗦);位置偏见(成对比较两个回答时,倾向于给排在前面的那个打高分);自我偏好(倾向于偏爱和自己风格相近的回答)。这些偏见不校准,评测结果就是歪的。下面是校准位置偏见的办法——做成对比较时,把两个回答的顺序随机交换,跑两次:
import random
def pairwise_compare(case, answer_a, answer_b, judge_model) -> str:
"""成对比较两个回答,通过随机交换位置来抵消位置偏见。"""
# 随机决定谁排前面
swap = random.random() < 0.5
first, second = (answer_b, answer_a) if swap else (answer_a, answer_b)
verdict = judge_model.ask(
f"问题:{case.question}\n回答1:{first}\n回答2:{second}\n"
f"哪个更好?只回答'回答1'或'回答2'。"
)
# 把裁判基于位置的结论,换算回真实的 A / B
if verdict.strip() == "回答1":
return "B" if swap else "A"
return "A" if swap else "B"
用 LLM 当裁判的正确姿势,是把它当成一个"需要被监督的实习评分员":它效率高,但有它的毛病。所以你要给它清晰的评分量规(prompt 里写明每个维度的标准),要用随机化抵消位置偏见,要留意它的长度偏见(在 prompt 里明确"简洁也是优点,别因为长就给高分"),最关键的——你要定期用人工评分去校验裁判的可靠性:抽一批样本同时让人和 LLM 评,如果两者高度一致,这个裁判就可信;如果差很多,就得回头改裁判的 prompt。裁判靠谱了,评测才能真正自动化。自动化之后,评测才能发挥它最大的价值——回归对比。
五、回归对比:每次改动,都跑一遍同一张考卷
评测最大的价值,不是给你一个"当前分数",而是让你在每次改动后,能和上一版精确对比。我开头那个"修好一类、搞坏另一类"的痛苦,根治它的就是回归对比:每次改了 prompt、换了模型,都在同一张考卷上跑一遍,然后逐类别地和上一版比:
def run_eval(model, eval_set: list, judge_model) -> dict:
"""在整个评测集上跑一遍,按类别汇总分数。"""
from collections import defaultdict
cat_scores = defaultdict(list)
for case in eval_set:
answer = model.ask(case.question)
kp = score_by_keypoints(answer, case)
score = kp if kp >= 0 else aggregate_score(llm_judge(case, answer, judge_model))
cat_scores[case.category].append(score)
# 算出每个类别的平均分,以及总平均分
result = {c: sum(v) / len(v) for c, v in cat_scores.items()}
result["_总分"] = sum(result.values()) / len(result)
return result
def compare_versions(old: dict, new: dict) -> bool:
"""对比新旧两版的评测结果 —— 总分涨了还不够,得没有类别退步。"""
print(f"总分:{old['_总分']:.3f} -> {new['_总分']:.3f}")
regressed = []
for cat in old:
if cat == "_总分":
continue
# 任何一个类别掉了 3 个百分点以上,就是一次退步
if old[cat] - new.get(cat, 0) > 0.03:
regressed.append(cat)
if regressed:
print(f"警告:这些类别退步了 {regressed} —— 本次改动不合格")
return False
return True
compare_versions 的精髓,在它不只看总分。这是极其重要的一点:总分涨了,不代表这次改动就是好的。完全可能是一个类别大涨、另一个类别暴跌,平均下来总分还涨了一点——可那个暴跌的类别,正是我开头被用户投诉的"搞坏的另一类"。所以回归对比必须逐类别看:只要有任何一个类别明显退步,这次改动就不合格,哪怕总分是涨的。这就把"改 AI 全凭感觉、改完提心吊胆"这件事,彻底变成了一个有数据、有红线的工程决策。下面这张图,把一次完整的评测流程串起来:
六、工程坑:评测集泄漏、评测集腐烂与人工抽检
五块设计之外,还有几个工程坑,不处理就会让整套评测悄悄失真。坑 1:评测集绝不能泄漏进 prompt 或训练数据。这是最隐蔽、最致命的坑。如果你把评测集里的题目和答案,写进了模型的 prompt,或者拿去微调了模型,那模型就是在"考自己做过的原题"——分数虚高得离谱,却毫无意义。评测集必须和训练/提示数据严格隔离:
import hashlib
def fingerprint_eval_set(eval_set: list) -> set:
"""给评测集每条题算指纹,用于检测它有没有泄漏进别处。"""
return {hashlib.md5(c.question.encode()).hexdigest() for c in eval_set}
def check_leakage(eval_set: list, training_data: list) -> list:
"""检查评测集的题,有没有混进了训练数据里。"""
eval_fp = fingerprint_eval_set(eval_set)
leaked = []
for sample in training_data:
fp = hashlib.md5(sample["question"].encode()).hexdigest()
if fp in eval_fp: # 训练数据里出现了评测题 —— 泄漏!
leaked.append(sample["question"])
return leaked
坑 2:评测集会"腐烂",要定期更新。一份评测集用久了,会慢慢偏离真实情况:用户的提问方式在变、业务的功能在变,而老评测集还停在过去。所以要定期从线上真实流量里,采样新问题补充进评测集(注意脱敏)——尤其是线上新出现的、答错的 case,要第一时间收进评测集。坑 3:再自动化,也要保留人工抽检。LLM 裁判再可靠,也会有它评不准的角落。每个版本上线前,抽一小批样本人工过一遍眼,是必要的最后一道防线——它能发现一些自动评测系统性漏掉的问题。坑 4:评测要算成本。用 LLM 当裁判,每评一条都是一次模型调用,评测集几百条、每天跑几十轮,成本会很可观。可以分层:每次改动跑核心小评测集(快、便宜),大版本才跑完整大评测集。坑 5:别用单一指标。一个回答的好坏是多维的,只盯着一个总分,会掩盖结构性的问题。要同时看分类别得分、看各维度得分、看最差的那批 case——评测报告不该只有一个数字。
关键概念速查
| 概念 / 手段 | 说明 |
|---|---|
| 样本偏差 | 随手试的几个问题代表不了全局,结论不可信 |
| 标准漂移 | 人的判断尺度会变,今天和昨天的及格线不一样 |
| 评测集 | 固定不变的题库,每个版本用同一张考卷成绩才可比 |
| 覆盖度 | 评测集要覆盖高频各类别边界易错和历史出错的 case |
| 关键点命中 | 看回答覆盖了多少必答点,有没有踩到绝不能说的雷 |
| 多维度打分 | 把好不好拆成相关性准确性完整性简洁性分别评 |
| LLM 裁判 | 用大模型给回答打分,解决人工评测规模跟不上的问题 |
| 裁判偏见 | 裁判模型有长度偏见位置偏见自我偏好,必须校准 |
| 回归对比 | 每次改动跑同一考卷,逐类别比,有类别退步就不合格 |
| 评测集泄漏 | 评测题混进 prompt 或训练数据,分数虚高且毫无意义 |
避坑清单
- 别靠随手试几个问题肉眼看判断 AI 效果,有样本偏差和标准漂移。
- 评测要有固定评测集,每个版本用同一张考卷成绩才能横向比。
- 评测集关键看覆盖度,要含高频各类别边界和历史出错的 case。
- 生成式回答没有唯一答案,别用精确匹配,用关键点命中或多维打分。
- 把好不好拆成几个具体维度分别评,比笼统打一个分客观得多。
- 用 LLM 当裁判要校准偏见,成对比较随机交换位置抵消位置偏见。
- 定期用人工评分校验 LLM 裁判可不可靠,差太多要改裁判 prompt。
- 回归对比别只看总分,任一类别明显退步这次改动就不合格。
- 评测集绝不能泄漏进 prompt 或训练数据,否则分数虚高没意义。
- 评测集会腐烂要定期更新,线上答错的 case 第一时间收进去。
总结
回头看那串"改 prompt 修好一类搞坏另一类、说不出新版好多少"的窘境,以及我后来在 AI 评测上接连踩的坑,最该记住的不是某一段评分代码,而是我动手前那个想当然的判断——"AI 效果好不好,自己多试几个问题就知道了"。这句话错在它把"评测"这件需要严谨设计的工程,降格成了"凭手感拍脑袋"。我以为评测是个"试一试、看一看"的轻松动作。可它根本不是。评测的本质,是要把"这个 AI 好不好"这个主观的、模糊的、会飘的判断,变成一个客观的、可复现的、能横向对比的数字。而要做到这一点,你不能临时出题(所以要固定评测集),不能凭感觉打分(所以要明确的评分维度),不能靠人肉跑(所以要 LLM 裁判),不能只看当下(所以要回归对比)。
所以做 AI 评测,真正的工程量不在"跑一下模型看看回答"那几行调用代码上。那几行,任何人都会写。真正的工程量,在于你要为"生成式 AI 没有标准答案、好坏是主观的"这个事实,搭起一整套能产出可信数字的机制:你要构建一个有覆盖度的固定评测集当考卷;你要为不同题型设计不同的评分方式,有考点的对考点、开放题拆维度;你要用 LLM 当裁判提速,同时校准它的偏见;你要每次改动都做逐类别的回归对比,守住"不许有类别退步"这条红线;你还要防住评测集泄漏、防住评测集腐烂。这篇文章的几节,其实就是顺着这条思路展开的:先想清楚"随手试几个"为什么靠不住,再用固定评测集打地基,然后解决"没有标准答案怎么打分",用 LLM 裁判把评测自动化、并校准它的偏见,用回归对比接住版本迭代,最后是泄漏、腐烂、抽检这几个把评测做扎实的工程细节。
你会发现,AI 评测的思路,和现实里一所学校怎么严肃地考核学生完全相通。一个不负责任的老师,会怎么判断一个学生好不好?他随口问两个问题,学生答得顺溜,他就觉得"这孩子不错"——这正是我那个"随手试几个"的第一版。而一所严肃的学校会怎么做?他们有统一的、保密的试卷,所有学生考同一张卷,这样成绩才能横向比较(这是固定评测集);出卷子时,他们会刻意覆盖各个知识点、各种难度,不会只考简单题(这是覆盖度);批卷时,他们有明确的评分标准——这道题答到哪几个点给几分(这是关键点和评分维度),而不是老师凭印象;为了批得快,他们会请助教帮忙批,但会先抽一批让助教和主考一起批、对一对尺度(这是 LLM 裁判加人工校验);他们还死死看守着考题——一旦考题泄漏给了学生,这场考试就彻底作废了(这是防评测集泄漏)。一场考试有没有意义,从来不在于你问了多少问题,而在于你的卷子够不够公平、你的标准够不够明确、你的考题有没有被泄漏。
最后想说,AI 评测做没做扎实,差距永远不会在"功能演示"时暴露——演示时你挑几个你知道它答得好的问题,它对答如流,所有人都觉得"这 AI 真不错"。它只在真实的、长期的、需要不断迭代的产品打磨里才显形。那时候它会用最让人焦虑的方式给你结账:做不好,你会像我一样,陷入一种"蒙着眼睛改 AI"的恐慌——你改了一版 prompt,不知道是真变好了还是错觉,你上了一个新模型,说不清到底强在哪,每次发布都像开盲盒,出了问题还得靠用户投诉才知道;而做对了,你每一次改动,都能在几分钟内拿到一份清清楚楚的评测报告:总分动了多少、哪个类别涨了、哪个类别有没有退步、最差的 case 是哪些。你不再靠感觉,你靠数据;你每一步迭代都是踏实的、可回滚的、有据可依的。所以别等"开盲盒"的焦虑找上门,在你做出一个 AI 功能的那一刻就该想清楚:我凭什么说它好?我怎么证明下一版比这一版强?这两个问题有了答案,你的 AI 才不只是一个"演示时挺惊艳"的功能,而是一个能被持续、可靠、有数据地打磨下去的真正的产品。
—— 别看了 · 2026