评估


你改了个 prompt,试一次,觉得答案好像变好了,于是就上线了,结果一周后用户碰到的那些情况反而悄悄变差了。评估(evaluation,简称 eval)就是解决这个问题的办法:对你的 AI 功能是否真的有效进行可重复的测试——一组输入,配上"好答案应该是什么样"的标准,每次都用同样的方式打分,这样你才能判断一次改动到底是不是真的有帮助。模型是个概率黑盒,同一个问题每次运行给出的答案都可能不一样,所以只有靠测量才能真正知道结果如何,而这种严谨的态度正是"能用的 demo"和"真正的产品"之间最大的区别。
为什么凭感觉判断行不通
一开始靠手动检查 AI 的答案感觉还挺好。你跑一个 prompt,看看回复,觉得不错,就往下走了。问题是有三件事在悄悄和你作对,而这三件事你其实都已经遇到过了。
这三件事是:
- 输出不稳定。 同一个 prompt 每次运行可能会给出不同的答案,所以这次结果好,不代表下次也一定好。
- 改动会引发回归。 修好了一个 prompt 让某个情况没问题了,往往会悄悄把你没注意到的另一个情况搞坏。
- 规模上撑不住。 五条答案你能一条条看完,但每次改一行代码就要看五百条答案,那是不可能靠人力做到的。
这些都不代表你做错了什么,只是说明凭感觉判断这个工具,用来应付这种规模的工作根本不够用,而这个问题其实有更省心的解法。
构建评估集
评估集就是一份测试案例清单。每个案例包含一个输入,以及"好答案应该是什么样"。先从小规模开始。十到二十个覆盖常见情况、再加几个刁钻边界情况的案例,比什么都没有强太多了,而随着你在实际使用中发现新的失败情况,这个集合可以慢慢扩大。
eval_set = [
{"input": "菜又冷又上得慢。", "expected": "NEGATIVE"},
{"input": "这辈子吃过最棒的一餐!", "expected": "POSITIVE"},
{"input": "还行,没什么特别的。", "expected": "NEUTRAL"},
# ...随着你发现真实案例和边界情况,持续把它们加进来
]当用户反馈一个不好的答案时,把它加入案例集,同时标注它本该给出的答案。这样,你的评估集就会逐渐变成一份不断增长的"记忆",记录着你再也不想犯的每一个错误——就像修好的 bug 会变成一个保留下来的测试一样。
如何打分
不同的任务需要不同的打分方式,所以第一步是把检查方式和你期望的答案类型对应起来。
- 精确匹配适用于只有一个正确答案的情况:分类标签、提取出的字段、是或否。你直接把输出和预期值做比较。
- 基于标准的打分适用于没有唯一正确答案的情况,比如摘要或回复。这时你检查输出是否满足某些条件:摘要有没有提到关键事实?是不是在 50 字以内?语气是否礼貌?
- 以 LLM 作评委用另一次模型调用,按你的标准给输出打分。它能应对那些简单检查无法捕捉的模糊质量问题,规模化起来也很方便,但评委本身也是一个模型,也可能出错,所以要时不时拿它和人工判断做个对照检查。
# 在评估集上做精确匹配打分
def run_evals(eval_set, classify):
passed = 0
for test in eval_set:
output = classify(test["input"])
if output == test["expected"]:
passed += 1
else:
print(f'FAIL: "{test["input"]}" -> got {output}, wanted {test["expected"]}')
print(f"{passed}/{len(eval_set)} passed")精确匹配是最容易上手的方式,而且和前面章节讲的分类、提取任务正好契合。以 LLM 作评委这个思路能奏效,是因为判断一个答案是否达标,往往比生成这个答案本身更容易预测——就像审阅一篇文章比自己写一篇文章要容易一样。但评委也是另一个具有同样缺陷的预测器,所以要把它给出的分数当作一个有用的估计,给它一份清晰的评分标准,并时不时拿它的评分和你自己的判断做一次抽样对照。
在每次改动时都跑一次
评估集只有在你真正去运行它的时候才有用。把你的 prompt 当代码一样对待:在把某个改动(不管是改 prompt、换模型,还是调整检索方式)上线之前,跑一次评估,把分数和之前的分数做个对比。如果分数下降了,那就说明你引入了一次回归,就算你自己手动测的那一次感觉还不错也一样。
正是这一步,把 prompt 工程从瞎猜变成了一件你可以放心迭代的事。改动一点东西,测量一下,保留分数更高的版本,丢掉分数更低的版本。这跟让普通软件变得可靠的循环是一样的,只是现在把它用在了 AI 功能里那些模糊的部分。
如果分数下降了,那就是你引入了一次回归。一次手动测试碰巧看起来不错,并不能推翻整个案例集给出的结论,所以每次都让这个数字来当最终裁判。
实战演练
上一节里的 run_evals 函数,就是一个完整的(虽然很小)评估工具。你把它指向你的分类器和评估集,它就会告诉你有多少案例通过了,还会准确指出哪些案例失败了:
run_evals(eval_set, classify_review)
# FAIL: "还行,没什么特别的。" -> got POSITIVE, wanted NEUTRAL
# 2/3 passed那一条失败的记录,比十几次成功的手动测试都更有价值,因为它指出了一个你现在可以着手修复、然后重新测量的真实弱点。最后一章安全与边界会讲到,在把这些东西真正交给用户使用之前,你需要设计防范哪些风险。
run_evals 这样一个小小的工具就已经很有价值了:它给你一个分数,还给你明确出问题的案例。修一个,再跑一次,看着这个数字慢慢变化。接下来,安全与边界会讲到,在真实用户到来之前你需要防范什么。 
