2024 年我做一个 AI 功能——用户输入一段商品描述,让大模型帮忙提炼出标题、卖点和适用人群。这件事我没多想,就有了方案:写一个提示词,把用户的描述塞进去,调模型,把结果返回。第一版我做得很顺手——提示词写好,本地拿三五个商品描述一试,模型提炼得有模有样,标题抓人、卖点到位,我心里很笃定:AI 功能嘛,提示词调到自己看着顺眼,就算做好了,这功能稳了。可等它真正开始迭代,一串问题冒了出来。第一种最先把我打懵:有用户反馈某类商品的提炼很差,我改了改提示词,把那个 case 修好了,信心满满地上线——结果第二天,另一批本来好好的商品,提炼全坏了。我修一个,碰坏一片。第二种最难缠:同一段商品描述,我今天测输出是对的,明天再测,模型给出的结果竟然不一样,有时还会犯错,我连"复现 bug"都做不到。第三种最头疼:模型供应商出了个新版本,我想知道换上去到底是变好了还是变差了,可我手里没有任何能拿来对比的东西,只能凭感觉拍脑袋。第四种最莫名其妙:模型的输出是一大段自然语言,我想给它写个测试,却发现根本没法像测普通函数那样写 assert result == expected——它每次的措辞都不一样,可意思可能都对。我盯着这一连串问题想了很久,才彻底想明白:第一版错在一个根本的认知上。我以为测一个 AI 功能,和测一个普通功能差不多——改完提示词,自己拿几个例子点一点,看着输出顺眼,就算测过了。可这个认知是错的。AI 功能有两个普通功能没有的特性:一是它的输出是不确定的,同样的输入,不同时刻可能给出不同结果;二是改提示词是牵一发动全身的,你为了修好 A 场景动的那几个字,可能悄悄改变了 B、C、D 场景的表现。靠"肉眼看几个例子",你既看不全、也看不稳、更没法和过去对比。要把 AI 功能做扎实,根上要明白:你需要的不是"测试",而是一套评估体系——一个固定的数据集,一套能给开放式输出打分的方法,一条每次改动都自动跑一遍的流水线。本文从头梳理:为什么"肉眼看几个例子"不算测试,为什么要先有评估集,开放式输出怎么打分,评估怎么自动化做回归,模型的不确定性怎么应对,以及一些把它做扎实要避开的工程坑。
问题背景
先把"测 AI 功能"和"测普通功能"的区别说清楚。测一个普通函数,你有明确的输入和唯一正确的输出,写 assert 一比就知道对错,而且每次跑结果都一样。AI 功能在这两点上全都不成立。
错误认知是:AI 功能的测试,就是改完提示词,自己点几个例子看看。真相是:AI 功能的输出是开放的、不确定的,改动是全局牵连的,这意味着"点几个例子"这种方式,既无法覆盖、也无法回归、更无法量化。把这一点摊开,第一版的几类问题就都能解释了:
- 修一个碰坏一片:提示词是全局生效的,改它来修 A 场景,会同时影响所有场景,而你只看了 A,没看 B、C、D。
- 结果无法复现:模型输出有随机性,同样输入多次调用结果可能不同,"点一次看一眼"抓不住这种波动。
- 换模型无法对比:你没有一个固定的、可重复运行的评估集,就没有任何基准能拿来做"换之前 vs 换之后"的对比。
- 开放输出难判对错:输出是自然语言,没有唯一标准答案,没法用"全等比较"来判定,需要专门的打分方法。
所以让 AI 功能做对,核心不是"把提示词调到看着顺眼",而是建一套评估体系:固定的数据集、能打分的方法、自动化的回归。下面六节,就从第一版"肉眼看几个例子"的想当然讲起。
一、为什么"肉眼看几个例子"不是测试
第一版调试 AI 功能的方式,是这样的:改一版提示词,打开几个我手头常用的商品描述,一个个发给模型,瞄一眼输出,觉得顺眼,就算这版提示词通过了。这个方式的问题,不在"看例子"——看例子是对的,而在"几个"和"肉眼"这两个词上。
# 反面教材:改完提示词,手动点几个例子,瞄一眼就上线
def tune_prompt_v1():
# 我手头常用的几个测试输入
samples = [
"一款不锈钢保温杯,容量 500ml……",
"纯棉男士短袖 T 恤,夏季新款……",
"蓝牙降噪耳机,续航 30 小时……",
]
for s in samples:
result = call_model(PROMPT, s)
print(result) # 人肉看一眼输出,觉得顺眼就行
# 这种方式的三个致命问题:
# 1. 只有 3 个例子,覆盖不了用户真实输入的千百种情况
# 2. "看着顺眼"是个主观判断,今天的我和明天的我标准都不同
# 3. 这次看的 3 个,和上次改提示词时看的不一定是同一批,
# 根本没法回答"我这次改动,有没有让以前好的变坏"
把这三个问题摊开看。第一,样本太少:你手头那几个例子,是你记得住的几个,而用户的真实输入有成百上千种形态,你那几个例子的"通过",证明不了整体的"通过"。第二,标准主观:"看着顺眼"不是一个稳定的判据,它取决于你当天的心情、注意力、对"好"的理解,无法量化、无法传递给别人。第三,也是最致命的——无法回归:你这次改提示词,看了 3 个例子;上次改,看的可能是另外 3 个。你永远无法回答那个最关键的问题:"我这次的改动,有没有破坏掉以前已经做对的东西?"第一版"修一个碰坏一片",根子就在这。
这一节要建立的认知是:测试的本质,是"可重复地、用统一标准、对一个有代表性的集合做检验"——而"肉眼看几个例子",这三个要素一个都不占。它不可重复(每次看的例子不固定),没有统一标准(全凭主观),集合没有代表性(就手头那几个)。所以它根本不是测试,它只是"调试时的随手一瞥"。随手一瞥在你初次写提示词时是有用的——它帮你快速感知方向。但一旦这个 AI 功能要持续迭代、要长期维护,随手一瞥就远远不够了,它会让你陷入"修一个、坏一片、再修、再坏"的循环。要跳出这个循环,你必须把"随手一瞥"升级成真正的"评估体系"。而这个升级的第一步,也是地基,是下一节要讲的:先有一个固定的评估集。
二、先有评估集:把"感觉对"变成"可量化"
评估体系的地基,是一个评估集(evaluation set,也常叫测试集、golden set)。它是什么?就是一批固定下来的、有代表性的输入,以及——尽可能地——每个输入对应的"期望"。它不是你随手想到的几个例子,而是被郑重收集、写进文件、纳入版本管理、不会随意变动的一份数据。
# 评估集:一批固定的、带标注的输入,落进文件、纳入版本管理
# eval_set.py —— 这个文件要像代码一样被 git 管理
EVAL_SET = [
{
"id": "case_001",
"input": "一款不锈钢保温杯,容量 500ml,12 小时保温",
# expected 不一定是唯一答案,而是"期望满足的要点"
"expected": {
"must_contain": ["保温", "500ml"],
"title_max_len": 20,
},
"tags": ["日用品", "正常输入"],
},
{
"id": "case_002",
"input": "", # 边界:空输入
"expected": {"should_refuse": True},
"tags": ["边界", "空输入"],
},
{
"id": "case_003",
"input": "这商品就是垃圾别买", # 边界:无效/负面输入
"expected": {"should_refuse": True},
"tags": ["边界", "无效输入"],
},
# ……几十上百条,持续积累……
]
构建评估集,有几个原则。其一,要有代表性:正常输入、边界输入(空的、超长的、格式怪的)、无效输入,都要覆盖——别只放那些"模型容易答对"的。其二,每发现一个线上 bad case,就把它加进评估集:这是评估集最重要的成长方式,它让"修过的 bug 不再重犯"。其三,要标注"期望":期望不一定是一个唯一答案(开放式输出往往没有唯一答案),但至少要写清楚"什么样算对"——必须包含哪些要点、不能出现什么、该不该拒答。
这一节的认知是:评估集的真正价值,是把"这个功能好不好"这个飘忽的、主观的问题,锚定成一个固定的、可以反复测量的对象。没有评估集时,"功能好不好"这个问题的答案,藏在你脑子里那几个记得住的例子和当下的主观感受里——它无法被测量,因此也无法被改进、无法被对比。有了评估集,"功能好不好"就变成了"在这 200 个固定 case 上,表现如何"——这是一个具体的、每次都能重新测一遍的、能和上一次结果做对比的东西。这就像没有体重秤时,你对自己胖了瘦了只能靠感觉,有了秤,才有了一个能持续追踪的客观数字。评估集就是 AI 功能的那杆秤。先有这杆秤,后面"打分""自动化""回归"才都有了依托。所以做 AI 功能,第一件正经事,不是调提示词,是建评估集。
三、怎么给"开放式输出"打分:几种评估方法
有了评估集,下一个问题是:模型对每个 case 的输出,怎么判定它"对不对"?这就是第一版那个"没法写 assert result == expected"的难题。开放式输出没有唯一答案,但这不代表它没法判分。判分的方法,按"严格程度"和"成本",有一个从低到高的谱系,你要按 case 的类型选用。
最简单的一类,是精确匹配和规则检查。如果一个 case 的期望是明确的(比如"该拒答""输出必须是合法 JSON""标题不能超过 20 字"),那就用代码规则直接判。这类判分快、稳定、零成本,能判的就尽量用它判。
# 方法一:规则检查 —— 能用代码硬判的,就别麻烦模型
import json
def rule_based_score(output: str, expected: dict) -> bool:
# 期望它拒答的 case
if expected.get("should_refuse"):
return "无法" in output or "不支持" in output
# 期望输出是合法 JSON
if expected.get("must_be_json"):
try:
json.loads(output)
except json.JSONDecodeError:
return False
# 期望包含某些关键词
for kw in expected.get("must_contain", []):
if kw not in output:
return False
# 期望标题不超长
limit = expected.get("title_max_len")
if limit is not None and len(output) > limit:
return False
return True
但很多 case,规则判不了——比如"这个标题抓不抓人""卖点提炼得到不到位",这是关于质量的判断,没法用关键词和长度来衡量。这时候用第二类方法:让另一个大模型来当裁判(LLM-as-judge)。你把"原始输入、模型的输出、评判标准"一起发给一个模型,让它按标准打个分。
# 方法二:LLM-as-judge —— 用模型给开放式质量打分
JUDGE_PROMPT = """你是一个严格的评审。请根据【评判标准】,
为【待评输出】打分,只输出一个 1 到 5 的整数。
【评判标准】
1分:完全跑题或有明显错误
3分:基本可用,但平淡或有小问题
5分:准确、抓住重点、表达出色
【原始输入】
{input}
【待评输出】
{output}
【分数】"""
def llm_judge_score(input_text: str, output: str) -> int:
prompt = JUDGE_PROMPT.format(input=input_text, output=output)
verdict = call_model(prompt, "").strip()
try:
return int(verdict)
except ValueError:
return 0 # 裁判没按格式回,算 0 分,后面会统计到
这两类方法之上,还有一类不能省的:人工评估。模型当裁判很高效,但它自己也会犯错、有偏好。所以要定期抽一部分 case,由人来评,并拿人评的结果去校准"模型裁判"打的分。三类方法是配合用的:能用规则的用规则(占大头),规则判不了的质量项用模型裁判,再用人工抽检来给模型裁判兜底校准。
这一节的认知是:给开放式输出打分,关键不是找到"那一种"完美的判分法,而是认识到判分是分层的——把一个笼统的"输出好不好",拆解成一条条"能用什么方式判"的具体维度。"输出对不对"听起来无从下手,但你把它拆开:它是不是合法 JSON?(规则)它有没有包含必须的要点?(规则)它该拒答时拒答了吗?(规则)它的标题质量如何?(模型裁判)——拆到这个粒度,每一条都有了可操作的判法。判分的难,难在"开放式输出"这个词给人的笼统的无力感;一旦你养成"拆维度"的习惯,会发现大部分维度其实能用便宜又稳定的规则judge掉,真正需要动用模型裁判和人工的,只是其中一小部分。把判分当成一个"分层拆解"的工程,而不是寻找银弹,你才能给 AI 功能建立起真正可用的打分能力。
四、把评估自动化:每次改提示词都跑一遍回归
有了评估集、有了打分方法,就可以拼出评估体系最关键的一环:一条自动化的评估流水线。它做的事很直接——把评估集里每个 case 跑一遍,逐个打分,最后算出一个整体的通过率(或平均分)。这条流水线,要做到"一键运行"。
# 评估流水线:跑完整个评估集,算出通过率
def run_eval(prompt_version: str) -> dict:
results = []
for case in EVAL_SET:
output = call_model(prompt_version, case["input"])
# 按 case 选打分方式,这里以规则打分为例
passed = rule_based_score(output, case["expected"])
results.append({
"id": case["id"],
"tags": case["tags"],
"passed": passed,
"output": output, # 输出要留存,方便事后排查
})
total = len(results)
passed_n = sum(1 for r in results if r["passed"])
return {
"pass_rate": passed_n / total,
"total": total,
"passed": passed_n,
"results": results,
}
有了这条流水线,第一版那个"修一个碰坏一片"的问题,就有了正面的解法。每次你改提示词,不是改完看几个例子,而是把整个评估集重新跑一遍,然后拿这次的结果,和改动前的结果做逐 case 的对比。哪些 case 从"过"变成了"不过",一目了然——这就是回归测试。
# 回归对比:改提示词前后各跑一次,找出"改坏了"的 case
def compare_eval(before: dict, after: dict):
before_map = {r["id"]: r["passed"] for r in before["results"]}
after_map = {r["id"]: r["passed"] for r in after["results"]}
regressed, fixed = [], []
for cid in before_map:
if before_map[cid] and not after_map.get(cid):
regressed.append(cid) # 原来过,现在不过 —— 改坏了
if not before_map[cid] and after_map.get(cid):
fixed.append(cid) # 原来不过,现在过 —— 修好了
print(f"通过率: {before['pass_rate']:.2%} -> {after['pass_rate']:.2%}")
print(f"修好了 {len(fixed)} 个: {fixed}")
print(f"改坏了 {len(regressed)} 个: {regressed}")
# 如果 regressed 不为空,这次改动就要慎重 —— 它有副作用
return regressed
这一节的认知是:评估自动化真正解决的,不是"测得快"这个效率问题,而是"看得全"这个覆盖问题——它让你每一次改动的影响,都暴露在整个评估集面前,而不只是你随手挑的那几个 case 面前。第一版"修一个碰坏一片"的根本原因,是改动的影响是全局的,而你的观察是局部的——你动了对所有 case 都生效的提示词,却只看了一个 case。自动化评估,就是把"观察"也扩展成全局的:你改一次,它把全部 case 替你看一遍。于是"改坏了"这件事,从"上线后被用户发现",提前到了"上线前被流水线发现"。这个提前,是质变。AI 功能的迭代之所以容易陷入混乱,就是因为缺了这道全局回归;补上它,每一次改提示词,你才能心里有数:我修好了 5 个、没改坏任何一个,这次改动是干净的——这才敢上线。
把改了提示词之后,该不该上线的完整决策流程画出来,就是下面这张图:
[mermaid]
flowchart TD
A[改了提示词] --> B[在完整评估集上跑一遍]
B --> C[和改动前的结果逐case对比]
C --> D{有case从过变成不过吗}
D -->|有| E{修好的明显多于改坏的吗}
E -->|否| F[别上线 这次改动副作用太大]
E -->|是| G[评估改坏的case可否接受]
D -->|没有| H{通过率提升了吗}
H -->|是| I[干净的改动 可以上线]
H -->|否| F
五、应对不确定性:同一输入要跑多次
到这里,评估体系的主干已经搭好了:评估集、打分、自动化回归。但还有一个第一版没解决的问题悬在那里——第二种问题,"结果无法复现"。同一段商品描述,我今天测是对的,明天测又错了。这不是 bug,这是大模型的固有特性:它的输出带随机性,同样的输入,两次调用可能给出不一样的结果。这个特性会动摇你刚建好的整个评估体系——如果一个 case 跑一次过、跑一次不过,那你那张"通过率"的数字,本身就是不可靠的。
解决的方向只有一个:不要用"跑一次"的结果下结论,而要用"跑多次"的结果。对每个 case,连续跑若干次,然后看它的通过率,而不是某一次的过或不过。
# 同一个 case 跑一次的结果不可靠,要跑多次看通过率
def eval_case_multi(prompt_version, case, runs=5):
passed_count = 0
outputs = []
for _ in range(runs):
output = call_model(prompt_version, case["input"])
outputs.append(output)
if rule_based_score(output, case["expected"]):
passed_count += 1
return {
"id": case["id"],
"pass_rate": passed_count / runs, # 这个 case 的稳定度
"outputs": outputs,
}
# pass_rate = 1.0 -> 这个 case 稳稳地过
# pass_rate = 0.0 -> 稳稳地不过,是个明确要修的问题
# pass_rate = 0.6 -> 时过时不过 —— 这才是最危险的信号:
# 它说明提示词在这个 case 上是"飘"的
这里 pass_rate = 0.6 这种"时过时不过"的 case,值得特别说一句。它比"稳定不过"更危险——"稳定不过"你一眼能看见、会去修;而"时过时不过"的 case,你某次评估时它恰好过了,你就把它漏掉了,上线后它又恰好不过,就成了线上事故。多次采样的最大价值,就是把这种"飘忽"的 case 揪出来。
另一个和不确定性相关的旋钮,是温度(temperature)——它控制模型输出的随机程度,温度越高,输出越发散。这里有个很容易踩的坑:评估时为了图"结果稳定",把温度调成 0;而线上为了让文案更灵活,用了较高的温度。这样你评估的,根本不是线上真正在跑的那个东西。
# 评估用的温度,必须和线上保持一致
# 错误做法:评估时为了求稳,把温度压成 0
eval_result = call_model(prompt, case_input, temperature=0)
# 线上为了文案灵活,温度用 0.8
online_result = call_model(prompt, user_input, temperature=0.8)
# 你评估过的那个"温度 0 的模型",根本不是线上那个
# 正确做法:温度作为一份共享配置,评估和线上共用
TEMPERATURE = 0.8 # 评估、线上,都从这里取
def call_for_eval(prompt, text):
return call_model(prompt, text, temperature=TEMPERATURE)
这一节的认知是:面对一个有随机性的系统,任何"单次测量"都只是一个噪声样本,只有"多次测量的统计量"才是可信的信号。第一版"结果无法复现"的困惑,根子在于我用测普通函数的思维去测一个随机系统——普通函数跑一次的结果就是它的全部,而随机系统跑一次的结果,只是它无数种可能输出里的一个。你拿一个样本去代表整体,自然时对时错。把"跑一次"换成"跑多次看通过率",本质上是从"测量一个确定值"切换到"估计一个概率分布"。一旦你接受了"AI 功能的表现是一个分布,而不是一个定值"这件事,很多困惑就解开了:你不会再因为某次输出变了而慌张,你会去看它的通过率有没有掉;你也会明白,追求"100% 稳定"对一个随机系统是不现实的,你要做的是把通过率稳稳地推到一个够高的水位。把不确定性当成 AI 功能的常态去度量它、管理它,而不是当成 bug 去消灭它,你的评估体系才立得稳。
六、把 LLM 评估做扎实,要避开的工程坑
前面五节,搭出了"评估集 + 打分 + 自动化回归 + 多次采样"这套评估体系的主体。但要在生产里真正用好,还有几个坑得专门讲。第一个,也是最重要的:评估集不是建一次就完事的,它必须持续生长。最重要的成长方式,就是把每一个线上暴露出来的 bad case,都回流进评估集。
# 坑一:每一个线上 bad case,都要回流进评估集
def add_bad_case_to_eval_set(user_input, correct_expectation):
new_case = {
"id": f"case_prod_{now_str()}",
"input": user_input,
# 由人工标注"这个 case 正确的期望是什么"
"expected": correct_expectation,
"tags": ["线上回流", "bad case"],
}
append_to_eval_set_file(new_case) # 追加进文件并提交 git
# 从此这个 case 每次评估都会被跑到 —— 它再也不会悄悄重犯
第二个坑,是别只盯着那个总的通过率。一个 90% 的总通过率,看着很漂亮,但它是被"正常输入"这类容易答对的 case 拉上来的平均值。真正的问题,往往藏在某一类 case 里。所以评估报告不能只出一个总数,要按 tags 把通过率拆开看。
# 坑二:别只盯着总通过率,要按 tag 拆开看分布
from collections import defaultdict
def report_by_tag(results):
tag_stat = defaultdict(lambda: [0, 0]) # tag -> [通过数, 总数]
for r in results:
for tag in r["tags"]:
tag_stat[tag][1] += 1
if r["passed"]:
tag_stat[tag][0] += 1
for tag, (passed, total) in tag_stat.items():
print(f"{tag}: {passed / total:.0%} ({passed}/{total})")
# 总通过率 90% 看着很漂亮,但按 tag 一拆,
# 可能"边界输入"那一类只有 40% —— 那才是真问题所在
第三个坑,是别把"模型裁判"当成绝对可信。第三节说过,LLM-as-judge 自己也会犯错、有偏好。如果你完全信任它打的分,那它的偏差就会被你当成事实。所以要定期抽一批 case,既让模型裁判打分、也让人打分,算一算两者的一致率,用这个一致率来判断"这个裁判到底可不可信"。
# 坑三:LLM-as-judge 自己会犯错,要定期用人工评分校准它
def calibrate_judge(sample_cases):
# sample_cases:抽一批 case,既让模型裁判打分,也让人打分
agree, total = 0, len(sample_cases)
for case in sample_cases:
judge_score = llm_judge_score(case["input"], case["output"])
human_score = case["human_score"] # 人工打的分
# 模型与人的分差在 1 分以内,算一致
if abs(judge_score - human_score) <= 1:
agree += 1
print(f"模型裁判与人工的一致率: {agree / total:.0%}")
# 一致率太低(比如低于 80%),说明这个裁判不可信,
# 要回去改 JUDGE_PROMPT,或者换一个更强的裁判模型
还有几个坑值得点一下。其一,离线评估集再全,也只是你"想得到"的输入,真实用户的输入会漂移——所以要做线上抽样评估,定期捞一批真实流量来评,补离线集的盲区。其二,多次采样、LLM-as-judge 都会让评估变慢变贵,要平衡评估的频率和成本:小改动跑核心子集,要上线才跑全量。其三,评估集本身要像代码一样纳入版本管理、被 review,它和提示词同等重要。下面把这套评估手段集中对照一下:
LLM 评估的几种手段对照
手段 判什么 成本 作用
--------------------------------------------------------------
规则检查 格式 关键词 长度 极低 能判的尽量用它
LLM-as-judge 开放式的质量好坏 中 判规则判不了的
人工评估 最终质量 兜底裁定 高 校准模型裁判
多次采样 同一输入的稳定度 成本翻N倍 揪出时好时坏的
回归对比 改动前后的得失 低 拦住改坏的改动
线上抽样评估 真实输入下的表现 中 补离线集的盲区
原则:能用规则别用模型,判分方法之间互相校准,
评估集持续回流生长,永远别让它一成不变。
这一节这几个坑,串起来是同一个意思:评估体系不是"搭一次就用一辈子"的静态设施,它是一个要和你的 AI 功能一起持续生长、持续维护的活的东西。评估集要不断把线上 bad case 回流进来,通过率要按 tag 拆开看分布,模型裁判要用人工评分定期校准,离线集的盲区要靠线上抽样来补。这些事没有一件是"一次性"的——它们都要随着功能的迭代反复地做。AI 功能的评估之所以容易做着做着就废掉,就是因为很多人把它当成了一个"上线前搭一次"的工程,搭完就不管了;而模型在变、提示词在变、用户的输入也在变,一个不生长的评估集,很快就会和真实情况脱节,变成一张过期的、骗自己的成绩单。把评估体系当成一个需要持续投入的、活的基础设施,它才能一直替你看住 AI 功能的质量。
关键概念速查
| 概念 | 说明 |
|---|---|
| 评估集 | 固定的、带标注的、有代表性的输入集合,是评估体系的地基 |
| golden set | 评估集的别名,强调它是不轻易变动、纳入版本管理的标准集 |
| 肉眼看几个例子 | 不可重复、无统一标准、无代表性,不是测试只是随手一瞥 |
| 规则检查 | 用代码硬判格式、关键词、长度,快、稳、零成本 |
| LLM-as-judge | 用另一个大模型按标准给开放式输出打分 |
| 人工评估 | 由人抽检评分,用来校准模型裁判、兜底质量判定 |
| 评估流水线 | 一键跑完整个评估集、逐个打分、算出整体通过率 |
| 回归对比 | 改动前后逐 case 比对,找出从过变成不过的 case |
| 多次采样 | 同一输入连续跑多次,看通过率而非单次的过或不过 |
| 温度 | 控制模型输出随机程度的参数,评估温度要与线上一致 |
避坑清单
- 不要把"肉眼看几个例子"当测试:它不可重复、无统一标准、无代表性。
- 不要调好提示词就上线:要先在完整评估集上跑一遍回归对比。
- 不要让评估集一成不变:每个线上 bad case 都要回流进评估集。
- 不要只放模型容易答对的 case:边界、无效、超长输入都要覆盖。
- 不要用"全等比较"判开放式输出:按维度拆解,分层打分。
- 不要全靠 LLM-as-judge:它会犯错,要用人工抽检定期校准。
- 不要只看总通过率:按 tag 拆开看分布,差的那一类才是真问题。
- 不要凭单次运行下结论:模型输出有随机性,要多次采样看通过率。
- 不要用和线上不一致的温度做评估:评估的必须是线上真在跑的东西。
- 不要以为离线评估集能覆盖一切:真实输入会漂移,要做线上抽样评估。
总结
回头看第一版那个"看着输出顺眼就算测过了"的 AI 功能,它的错误很典型。它不在某一行代码,而在一个对"测试 AI 功能"的根本误解:以为它和测一个普通函数差不多,改完提示词,拿几个例子点一点,顺眼就行。真相是,AI 功能有两个普通功能没有的特性——输出是开放、不确定的,改动是全局牵连的。这两个特性,让"肉眼看几个例子"这种方式既看不全、也看不稳、更没法和过去对比。它不是测试,只是调试时的随手一瞥。
而把 AI 功能的评估做对,工程量并不小。它不是把提示词调顺那么简单,而是要先有一个固定的、持续回流生长的评估集,要为开放式输出设计分层的打分方法,要把评估自动化成一条每次改动都跑一遍的回归流水线,要用多次采样去应对模型的不确定性,还要按 tag 看分布、用人工校准模型裁判、用线上抽样补离线集的盲区。一套真正可用的 LLM 评估体系,是这些环节一个不少地拼起来的。
这件事其实很像学校里的标准化考试。一个老师光凭印象说"这个学生学得不错",是不可靠的——他可能只记得这学生课堂上答对的那两次。标准化考试做的,是把"学得好不好"这个模糊的判断,变成一套可操作的东西:一个固定的、覆盖各类知识点的题库(评估集),一份明确的评分标准和参考答案(打分方法),每个学生都用同一套卷子考、考完统一阅卷(自动化回归)。考试里还有些讲究,正对应着 AI 评估的门道:同一个学生不会只凭一次发挥定成绩,要看多次的稳定表现(多次采样);主观题不能只让一个老师批,要多个老师交叉、防止某个老师手松手紧(人工校准模型裁判);题库还要不断把学生新暴露的薄弱点补进去(bad case 回流)。你给 AI 功能建评估体系,做的就是这套"标准化考试"——让"这个功能好不好"这个问题,有一个客观的、可重复的、能和上次比的答案。
这类问题还有一个共同的麻烦:它在开发和测试时几乎暴露不出来。你自己测一个 AI 功能,用的是你最熟、记得最牢的那三五个例子,模型在这几个例子上表现稳定,你就会觉得这功能稳了。真正会把问题撑开的,是上线后的真实环境:成千上万的用户输入着你从没设想过的商品描述,你为修某个 case 改的提示词在别处悄悄失效,模型供应商一次不起眼的版本更新让输出整体漂移——这些,你那三五个例子一个都照不到。所以如果你正在做一个基于大模型的功能,别等用户拿着一堆糟糕的输出来投诉,才回头怀疑提示词。在写下第一版提示词的时候就想清楚:我拿什么评估集来量它、我用什么方法给开放式输出打分、我每次改动后能不能跑一遍回归——把"调出一版看着顺眼的提示词"和"建一套能持续检验它的评估体系"当成两件必须分别去做的事,这是这篇文章最想留给你的一句话。
—— 别看了 · 2026