通过系统化Eval体系(类似驾照考试逻辑)解决"凭感觉"评估AI的困境;六篇文章实践总结。
Written by Syed Muhammad Ali Raza
这个系列写到这里,我悄悄跳过了一个本该更早就出现的问题:你怎么知道自己做的这些东西是好的?不是"我试了三次感觉还不错",而是真正地知道,能拿给队友看,能在代码评审时站得住脚。
我之所以能回避这个问题这么久,是因为在个人项目里,"嗯,看着没问题"是一个可以接受的门槛。但当我在一个真实项目中改了一行 prompt,却完全不知道自己是改好了还是悄悄改坏了那些我没盯着看的 case 时,这个门槛就不够用了。从那一刻起,evaluation(通常就叫 evals)就不再是学术意义上的"锦上添花",而是区分一个玩具和一个真正可信赖的东西的关键。
先讲一个真实的例子,再说技术细节
想想驾校是怎么判断一个学员是否准备好考试的。他们不会简单地问学员"你现在开车有信心吗",然后直接采信。他们也不会只看学员平行停车一次、成功了就宣布这人永远是好司机。
他们让学员每次都经历一套一致的特定场景,对每个学员都一样:停车、汇入高速、红灯停车、处理行人过马路,每个场景都按照明确的标准打分。如果学员停车满分但并线时慌乱,这是有用的、具体的信息,而不是"整体还不错"那种模糊感觉。而且关键的是,他们每轮练习后都会再跑一遍同样的测试,这样他们能真正判断某一节课是帮了某个具体技能还是让它变差了。
这整个思路就是 LLM 输出评估的核心。不是靠"感觉对不对"来凭感觉判断,而是构建一套一致的测试用例,用真实标准对输出打分,每次改动都重新跑一遍同样的测试,这样你就能精确地知道哪些变好了、哪些悄悄坏了。

为什么"看着没问题"在这里真的不管用
我要具体解释为什么这种直觉在 LLM 上特别容易失效,因为这点在一开始并不明显。
这些模型在生成文本时非常擅长产出自信、格式规范的文字,不管内容是否真的正确。LLM 的错误答案读起来往往和正确答案一样流畅——没有视觉提示、没有犹豫的语气、没有人类在不确定时通常会流露的线索。粗略扫几个输出然后说"看起来没问题",这真的不可靠,因为你的眼睛训练的是捕捉别扭的措辞,而不是事实或逻辑的正确性。
还有我在系列第二篇文章里提到的随机性问题:同一个 prompt 在不同运行中可能产生不同输出。某样东西只测一次就说完成了,几乎无法告诉你它在现实世界会被问到的各种场景下表现如何。
而且改动会以一种容易忽略的方式叠加。你调整了一个 prompt 来修复一个特定的问题,却可能悄悄让其他五个 case 变差了——那些在你做改动时根本没看到的 case。如果没有一套常设的测试用例每次都跑一遍,你没法在真实用户遇到之前catch 住这个退化。
三种大类的 evals,以及各自的适用场景
我在脑子里把 evals 分成三类,因为每类所需的技术工具和投入精力确实不同。
精确匹配或基于规则的检查,适用于有明确正确答案、可验证的场景。输出是否包含符合 schema 的有效 JSON?是否正确从文档中提取了特定数字?分类任务是否从固定列表中选对了类别?这些检查成本低、速度快、确定性高,只要真正适用就应该用,因为它们是目前为止最可信赖的一类。
人工审查是让一个人真正阅读输出、根据评分标准做出判断:这个回复是否有帮助?语气是否合适?是否遵循了指令?对于任何真正主观的内容,这是最可靠的检查方式,但它慢,无法在你每次改动 prompt 时都检查成千上万个输出。
LLM as judge(用 LLM 做评判)是一种较新、越来越常见的中庸之道,用一个独立的模型调用来根据评分标准自动给输出打分,可以规模化。它比人工审查快,而且能处理基于规则的检查无法处理的主观质量问题,但它有自己的准确性局限,我后面会讲到——它不是一张免费通行证,不能因此停止思考评估质量。
大多数真实项目最终都会三种混合使用:有真实正确答案的地方用基于规则的检查,用 LLM as judge 来规模化主观质量检查,而人工审查作为周期性健全性检查,看看自动化评判者本身是否真的在合理运作。

来构建一个真实的 eval suite,从基于规则的检查开始
我会用一个真正实用的例子:一个 LLM,本应从客户支持消息中提取结构化数据——姓名、问题类别、紧急程度——以 JSON 格式输出。这是一个很好的首个 eval 场景,因为每个测试用例都有一个真实的、可验证的正确答案。
第一步,用已知正确答案定义你的测试用例
test_cases = [
{
"input": "Hi, this is John, my app crashes every time I open it, please help urgently",
"expected": {"name": "John", "category": "bug", "urgency": "high"}
},
{
"input": "Hey it's Sarah, just wondering how to change my email on file, no rush",
"expected": {"name": "Sarah", "category": "account", "urgency": "low"}
},
{
"input": "This is Mike, I was charged twice for my subscription this month",
"expected": {"name": "Mike", "category": "billing", "urgency": "medium"}
}
# a real eval suite would have dozens to hundreds of these,
# covering edge cases, not just the easy obvious ones
]
最后那句注释比看起来重要得多。三个测试用例只能告诉你模型处理了三个简单例子。一个真实的 eval suite 需要有目的地包含边缘情况:模糊的紧急程度、缺失的姓名、不完全符合任何类别的新消息,因为这些恰恰是最可能出问题的 case。
第二步,运行实际提取并与预期答案比较
import anthropic
import json
client = anthropic.Anthropic(api_key="your-api-key-here")
def extract_support_info(message):
system_prompt = """
Extract the customer's name, issue category (bug, account, billing,
or other), and urgency (low, medium, high) from the message. Respond
with ONLY valid JSON in this exact format:
{"name": "...", "category": "...", "urgency": "..."}
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=150,
system=system_prompt,
messages=[{"role": "user", "content": message}]
)
raw_text = response.content[0].text.strip()
try:
return json.loads(raw_text)
except json.JSONDecodeError:
return {"error": "invalid json", "raw": raw_text}
def run_extraction_eval(test_cases):
results = []
correct_count = 0
for case in test_cases:
actual = extract_support_info(case["input"])
expected = case["expected"]
is_correct = actual == expected
if is_correct:
correct_count += 1
results.append({
"input": case["input"],
"expected": expected,
"actual": actual,
"correct": is_correct
})
accuracy = correct_count / len(test_cases)
return accuracy, results
accuracy, results = run_extraction_eval(test_cases)
print(f"Accuracy: {accuracy * 100:.1f}%\n")
for r in results:
status = "PASS" if r["correct"] else "FAIL"
print(f"[{status}] {r['input'][:50]}")
if not r["correct"]:
print(f" expected: {r['expected']}")
print(f" actual: {r['actual']}")
这给你一个真实的数字,而不是一种感觉。在任何 prompt 改动前后跑一遍完全相同的 suite,你就能立即知道准确率是上升了、下降了还是没变——如果下降了,具体是哪些 case 坏了也一清二楚。
第三步,Partial credit 版本,因为精确匹配有时过于严格
精确的字典相等很残酷,如果模型 name 和 category 都对了但 urgency 略微不对,精确匹配会把它判为完全失败,而这可能掩盖关于什么实际在正常工作的有用信号。
def score_partial_credit(actual, expected):
if "error" in actual:
return 0.0
fields = ["name", "category", "urgency"]
correct_fields = sum(1 for f in fields if actual.get(f) == expected.get(f))
return correct_fields / len(fields)
def run_partial_credit_eval(test_cases):
scores = []
for case in test_cases:
actual = extract_support_info(case["input"])
score = score_partial_credit(actual, case["expected"])
scores.append(score)
print(f"{case['input'][:50]}... -> {score * 100:.0f}%")
average_score = sum(scores) / len(scores)
print(f"\nAverage field accuracy: {average_score * 100:.1f}%")
return average_score
这个版本告诉你多得多的信息。如果 urgency 在很多测试用例中持续是出错的字段,这是一个具体的、可操作的信号——也许你的 prompt 需要更清晰的 urgency 判断示例,而不是盯着输出看时那种模糊的"哪里有点不对劲"的感觉。
现在来说更难的情况:用 LLM judge 评估开放性文本
结构化提取有干净的正确答案。很多真实任务没有:摘要质量如何、客户支持回复听起来是否恰当地有同理心、一篇生成的文章是否真的紧扣主题。对于这些场景,LLM as judge 是实用的中庸之道。
def llm_judge(generated_text, criteria):
judge_prompt = f"""
You are evaluating a piece of AI generated text against a specific
criterion. Be strict and critical, do not default to a positive
score just to be agreeable.
Criterion: {criteria}
Text to evaluate:
{generated_text}
Respond with ONLY valid JSON in this format:
{{"score": <1 to 5>, "reasoning": "<brief explanation>"}}
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt}]
)
try:
return json.loads(response.content[0].text.strip())
except json.JSONDecodeError:
return {"score": None, "reasoning": "judge failed to return valid json"}
# example usage
generated_summary = "The article discusses how solar panels convert sunlight into electricity using photovoltaic cells, and covers basic installation costs."
result = llm_judge(
generated_summary,
"The summary should accurately reflect the main points without adding information not present in the original article."
)
print(result)
关于如何正确做这件事,我有一些 hard way 学到的经验。明确告诉 judge 要严格、不要默认给出友好高分,这非常重要——除非你在 prompt 中直接 push back,否则模型确实有过于慷慨打分的倾向。每次调用给一个狭窄、具体的标准,而不是一个模糊的"这好不好"问题,这样能得到更有用、更一致的分数——"这是否在事实层面有源可溯"是一个比"这好不好"好得多的问题。
还有一点是人们会跳过的部分:你必须定期检查你的 judge 是否真的在正确评判。取一 sample 的 judge 打分结果,自己 review 一下,看看是否同意。如果一个不可靠的 judge 悄悄给所有东西打了错误的分,这可以说比没有自动化评估更糟,因为它给你的是虚假的信心。
def spot_check_judge(judge_results, sample_size=10):
import random
sample = random.sample(judge_results, min(sample_size, len(judge_results)))
print("Manually review these judge decisions, do you agree?\n")
for item in sample:
print(f"Text: {item['text'][:80]}...")
print(f"Judge score: {item['score']}, reasoning: {item['reasoning']}")
print("-" * 40)
整合起来,一个你每次都真正运行的回归测试
这里有一个对我影响最大的实践习惯:把 evals 完全当作单元测试来对待——每当改动一个 prompt 时自动运行,而不是偶尔想起来才做。
def run_full_eval_suite():
print("=" * 50)
print("RUNNING FULL EVAL SUITE")
print("=" * 50)
accuracy, extraction_results = run_extraction_eval(test_cases)
print(f"\nExtraction exact match accuracy: {accuracy * 100:.1f}%")
if accuracy < 0.8:
print("WARNING, extraction accuracy below 80% threshold")
partial_score = run_partial_credit_eval(test_cases)
return {
"extraction_exact_match": accuracy,
"extraction_partial_credit": partial_score
}
baseline_scores = run_full_eval_suite()
# save this somewhere, a json file, a spreadsheet, whatever
# then after any prompt change, rerun and compare against it directly
那个 warning threshold 比它单薄地躺在那一行代码里看起来重要得多。提前决定"足够好"实际上意味着什么,并在低于这个标准时让 suite 大声 flag 出来,这就把评估从"瞟一眼的东西"变成了真正能在问题到达真实用户之前 catch 住它的东西。
坦诚地说说这一切的局限性
我想坦率地说明这种方法仍然不足的地方,因为过度推销 evals 本身就是一种错误。
你的测试用例质量取决于你想象"什么可能出错"的能力——真实用户必然会遇到你没有想到去测试的输入。这里的解决办法不是一开始就做到完美,而是把你的 eval suite 当作一个活的东西来对待,每个真实 bug 或用户报告的奇怪输出都应该变成一个新的永久测试用例,这样你的 suite 才能真正随着时间变强,而不是停留在第一天你想的那些东西上冻结不变。
LLM judge 有自己的盲点和偏见,而且 judge model 可能和被评判的 model 共享系统性的弱点——尤其当两者使用同一个模型家族时。对 judge 自己的打分做周期性人工审查不是可选项,而是保持整个自动化系统诚实的关键。
而且在 eval suite 上得了高分是证据,不是证明。它提高了你的信心,但不能保证你的系统在现实世界里对每一个可能遇到的输入都正确。把它当作一个你主动监控和扩展的强信号来对待,而不是一次打勾就忘掉的 checkbox。
回到整个系列
这个系列中的每一种技术——RAG、fine-tuning、agents、multi-agent pipelines——都产生你必须在真正交付给用户之前真正信任的输出。评估就是那把"我建了某个东西,试了几次感觉还行"变成"我可以给你精确展示这个系统表现如何、在哪些具体 case 上有困难,并且证明我上一次改动是否真的有帮助"的关键。这一步区别——感觉和真实证据之间的差别——老实说,就是区分一个有趣的周末项目和 一个你可以放心用在真实产品和真实用户身上的东西的分水岭。
如果你为自己的某个东西建了 eval suite,我真的很想听听你的测试用例 catch 住了哪些你没有预料到的东西——那通常才是真正的教训所在。