1. 为什么我要用 Claude 来设计 eval,而不是手写测试用例
做 AI 应用开发的人都有一个共同的痛点:模型输出不稳定,今天跑得好好的 prompt,明天换个输入就崩了。你改了一版 prompt,感觉效果好了,但到底好了多少?说不清楚。更麻烦的是,你改好了 A 场景,结果 B 场景又退化了,而你根本不知道。
这就是 eval 存在的意义。eval 不是单元测试,它更像是一套“评分标准 + 自动化裁判”,让你每次改动之后能快速知道:分数涨了还是跌了,哪个维度涨了,哪个维度跌了。
但问题来了——写 eval 本身就是一件很痛苦的事。你得设计测试集、定义评分维度、写评分逻辑、处理边界情况。传统做法是人工写一堆规则或者正则,费时费力,而且覆盖不全。
我用 Claude 来做这件事,核心思路是:让 Claude 帮我生成 eval 的骨架和评分维度,我再基于实际 badcase 一轮轮迭代,把分数从 60 分推到 90 分以上。这个过程在圈子里叫 hillclimb,就是爬山——每一步都往高处走一点,最终爬到局部最优。
这篇文章适合谁看?如果你正在做 AI 应用(不管是对话、写作、代码生成还是 RAG),并且想让效果可量化、可迭代,那这套方法你可以直接抄作业。如果你还没开始做 eval,看完这篇你至少知道从哪下手。
2. 整体设计思路:先让 Claude 帮你把 eval 框架搭起来
2.1 eval 的本质是什么
很多人一上来就想写一个“完美的评分函数”,结果卡在第一步。我的经验是:eval 不是一次写完的,它是长出来的。
你一开始只需要一个粗糙的评分框架,能跑通就行。然后拿真实数据去跑,看哪些 case 得分低,分析为什么低,再补规则、补维度、补测试用例。每一轮迭代,eval 就更准一点,模型效果也更好一点。
Claude 在这个过程中的角色是:帮你快速生成第一版框架,帮你分析 badcase 的原因,帮你写评分逻辑的代码。它不是替你思考,而是替你省掉那些重复劳动。
2.2 为什么选 Claude 而不是别的模型
我试过几个模型来做这件事,最后选 Claude 的原因很实际:
- 长上下文理解稳:eval 设计经常需要把一堆 badcase、评分标准、历史迭代记录一起丢进去,Claude 在长文本里抓重点的能力比较靠谱。
- 代码生成质量高:评分函数很多时候就是一段 Python,Claude 写出来的代码结构清晰,边界处理也比较到位。
- 指令遵循好:你让它“按这个格式输出评分维度”,它不会给你自由发挥。
当然,这不是说别的模型不行。核心是你要有一个能稳定输出结构化内容的模型来辅助你设计 eval,Claude 在我这里表现最好。
2.3 整体流程长什么样
我把整个流程拆成五步,后面会一步步展开:
- 定义评分维度:让 Claude 根据你的任务描述,生成 3-5 个核心评分维度。
- 生成测试集骨架:让 Claude 生成一批覆盖不同场景的测试输入。
- 写评分逻辑:让 Claude 生成评分函数的代码框架。
- 跑第一轮,收集 badcase:拿真实数据跑,看哪些 case 得分低。
- 迭代 hillclimb:分析 badcase,补规则、补维度、补测试用例,重复第 4 步。
这个流程看起来简单,但每一步都有坑。下面我逐个拆。
3. 核心细节解析:评分维度怎么定,测试集怎么造
3.1 评分维度的设计原则
评分维度是 eval 的灵魂。维度定错了,后面怎么迭代都是白费。
我一般遵循三个原则:
第一,维度要正交。比如你做的是客服对话,那“回答准确性”和“语气友好度”是两个独立维度,不要混在一起。混在一起的结果是,你不知道分数低是因为答错了还是因为语气差。
第二,维度要可判定。“回答得好不好”这种维度没法判定,因为太主观。你要拆成“是否包含关键信息点”、“是否出现事实错误”、“是否超出规定长度”这种可以明确判断的。
第三,维度不要超过 5 个。超过 5 个之后,权重分配会变得很纠结,而且迭代时很难定位问题。3-5 个是最舒服的区间。
我一般会让 Claude 先根据任务描述生成一版维度,然后我自己再调。比如我给它这样的 prompt:
我在做一个技术文档问答系统,用户问技术问题,系统从文档里找答案并生成回答。 请帮我设计 4 个评分维度,要求: 1. 每个维度可以独立打分(1-5 分) 2. 维度之间尽量正交 3. 每个维度给出明确的判定标准 4. 输出格式:维度名称 | 判定标准 | 1分示例 | 5分示例Claude 返回的结果通常可以直接用,我只需要微调判定标准的措辞。
3.2 测试集怎么造才有效
测试集不是越多越好,而是越有代表性越好。我见过有人造了 500 条测试用例,结果 400 条都是同一个场景的变体,跑出来的分数根本没有区分度。
我的做法是:先按场景分类,每个场景造 5-10 条,覆盖正常 case、边界 case、badcase。
让 Claude 生成测试集的时候,我会给它一个场景列表,让它按场景生成。比如:
请为以下 5 个场景各生成 8 条测试输入: 1. 简单事实查询(答案在文档中直接能找到) 2. 需要跨段落推理的问题 3. 文档中没有答案的问题(应该拒答) 4. 问题有歧义,需要澄清 5. 用户输入包含错别字或口语化表达 输出格式:场景编号 | 测试输入 | 预期行为这样造出来的测试集,覆盖度比随机生成高得多。
3.3 评分逻辑的代码框架
评分逻辑我一般分两种:规则评分和模型评分。
规则评分适合那些可以明确判断的维度,比如“是否包含关键信息点”、“是否超出长度限制”。模型评分适合那些需要理解的维度,比如“回答是否流畅”、“逻辑是否连贯”。
Claude 帮我写评分代码的时候,我会让它把两种评分分开:
def rule_based_score(response, expected_keywords, max_length): score = 0 # 关键信息点覆盖 covered = sum(1 for kw in expected_keywords if kw in response) score += (covered / len(expected_keywords)) * 3 # 长度合规 if len(response) <= max_length: score += 2 return score def model_based_score(response, question, client): prompt = f"请对以下回答的流畅度打分(1-5分):\n问题:{question}\n回答:{response}\n只输出分数。" result = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=10, messages=[{"role": "user", "content": prompt}] ) return float(result.content[0].text.strip())这个框架跑通之后,你就可以开始第一轮迭代了。
注意:模型评分会有波动,同一个回答跑两次可能分数不一样。我的做法是跑 3 次取平均,或者用 temperature=0 来降低波动。
4. 实操过程:从 60 分爬到 90 分的完整记录
4.1 第一轮:跑通流程,拿到基线分
第一轮的目标不是高分,而是跑通。我把测试集跑了一遍,得到每个维度的平均分:
| 维度 | 平均分 | 最低分场景 |
|---|---|---|
| 关键信息覆盖 | 3.2 | 跨段落推理 |
| 事实准确性 | 3.8 | 歧义问题 |
| 拒答合理性 | 2.5 | 无答案问题 |
| 回答流畅度 | 4.1 | 口语化输入 |
总分 60 分左右。这个分数很正常,第一轮能跑通就不错了。
关键是要看 badcase。我把得分低于 3 分的 case 全部拉出来,让 Claude 帮我分析原因:
以下是 10 条低分 case,请帮我归类,并指出每一类的主要问题是什么。 输出格式:问题类别 | 涉及 case 编号 | 主要原因 | 改进建议Claude 返回的结果很有用。比如它指出“拒答合理性”低分的主要原因是:模型在文档中没有答案时,倾向于编造一个看起来合理的答案,而不是明确说“文档中没有相关信息”。
4.2 第二轮:针对 badcase 补规则
知道了原因,改起来就有方向了。我做了三件事:
第一,在 prompt 里加拒答指令。明确告诉模型:“如果文档中没有找到答案,必须回答‘根据现有文档无法回答该问题’,不要编造。”
第二,在评分逻辑里加惩罚项。如果模型编造了文档中不存在的信息,直接扣分。
第三,补充测试用例。针对“文档中没有答案”这个场景,我又加了 10 条测试输入,覆盖不同类型的无答案问题。
改完之后再跑一轮,分数变化:
| 维度 | 第一轮 | 第二轮 | 变化 |
|---|---|---|---|
| 关键信息覆盖 | 3.2 | 3.5 | +0.3 |
| 事实准确性 | 3.8 | 4.0 | +0.2 |
| 拒答合理性 | 2.5 | 3.8 | +1.3 |
| 回答流畅度 | 4.1 | 4.1 | 0 |
总分从 60 涨到 72。拒答维度涨得最多,因为问题最明确。
4.3 第三轮:优化跨段落推理
第二轮之后,最低分变成了“跨段落推理”场景。这个场景的问题是:答案分散在文档的不同段落里,模型只找到了一部分。
我的改进方法是:
在 prompt 里加“先检索再回答”的步骤。让模型先把相关段落全部列出来,再基于这些段落生成回答。这样能减少遗漏。
在评分逻辑里加“信息点覆盖完整性”检查。对于跨段落问题,我手动标注了每个问题应该覆盖的信息点,评分时检查覆盖了几个。
这一轮改动比较大,我让 Claude 帮我重新生成了 prompt 模板:
你是一个技术文档问答助手。请按以下步骤回答: 第一步:从文档中找出所有与问题相关的段落,列出来。 第二步:基于这些段落,生成一个完整的回答。 第三步:检查回答是否覆盖了所有相关段落中的关键信息。 如果文档中没有相关段落,直接回答“根据现有文档无法回答该问题”。 问题:{question} 文档:{document}再跑一轮,跨段落推理场景的分数从 2.8 涨到 4.2。总分到了 81。
4.4 第四轮:处理口语化和错别字
第三轮之后,剩下的低分 case 主要集中在口语化输入和错别字上。比如用户输入“这个咋弄啊”,模型有时候会理解偏差。
我的做法是:在预处理阶段加一层输入规范化。让 Claude 帮我把口语化输入转成标准问法,再走后面的流程。
这一步的评分逻辑也要调整:如果模型对口语化输入的回答和对标准问法的回答一致,就给高分。
这一轮涨分不多,从 81 涨到 85。但这一步很关键,因为真实用户输入就是口语化的,不处理的话线上效果会很差。
4.5 第五轮:微调评分权重
到第四轮之后,分数已经比较高了,但我觉得评分权重不太合理。比如“回答流畅度”占了 25% 的权重,但实际上这个维度对用户体验的影响没那么大。
我重新分配了权重:
| 维度 | 原权重 | 新权重 | 理由 |
|---|---|---|---|
| 关键信息覆盖 | 30% | 35% | 最重要 |
| 事实准确性 | 25% | 30% | 第二重要 |
| 拒答合理性 | 20% | 20% | 保持不变 |
| 回答流畅度 | 25% | 15% | 降低权重 |
重新算分之后,总分到了 88。再补了几条边界 case,最终稳定在 91 分左右。
5. 常见问题与排查技巧实录
5.1 评分波动太大怎么办
这是最常见的问题。同一个回答,跑两次分数不一样。原因通常是模型评分那部分不稳定。
解决方法:
- 固定随机种子:如果用的 API 支持,设置 temperature=0。
- 多次取平均:跑 3 次取平均分,成本不高但效果明显。
- 规则优先:能规则判定的维度尽量用规则,减少模型评分的比例。
5.2 分数涨了但线上效果没变好
这种情况通常是 eval 和真实场景脱节了。你的测试集不能代表真实用户输入。
解决方法:定期从线上日志里采样真实 case,补充到测试集里。我一般每周补 20 条,保持测试集的时效性。
5.3 Claude 生成的评分维度不适用
有时候 Claude 生成的维度太抽象,没法落地。比如它给你一个“回答质量”维度,你根本没法打分。
解决方法:在 prompt 里明确要求可判定。加上“每个维度必须能用 1-5 分明确判定,不能有主观模糊空间”这句话,效果会好很多。
5.4 迭代到后面涨不动了
这是 hillclimb 的典型问题:爬到局部最优了。分数卡在 85 左右,怎么改都不涨。
这时候有两个选择:
- 换方向:不是继续优化 prompt,而是换模型、换检索策略、换预处理方式。
- 接受现状:85 分可能已经够用了,继续投入产出比不高。
我的经验是,如果连续三轮迭代涨分不超过 2 分,就该考虑换方向了。
5.5 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 评分波动大 | 模型评分不稳定 | 设 temperature=0,多次取平均 |
| 分数涨线上不涨 | 测试集脱节 | 从线上采样补充测试集 |
| 维度没法判定 | 维度太抽象 | 重新生成,要求可判定 |
| 迭代涨不动 | 局部最优 | 换方向或接受现状 |
| 拒答维度低分 | 模型爱编造 | prompt 加拒答指令,评分加惩罚 |
| 跨段落漏信息 | 检索不完整 | 先检索再回答,分步执行 |
实操心得:每次迭代只改一个变量。如果你同时改了 prompt 和评分逻辑,分数涨了你不知道是哪个起的作用。一次只改一个,跑完看结果,再改下一个。
6. 工具选型与 Claude API 的实操配置
6.1 为什么我用 Claude API 而不是网页版
网页版适合探索,但 eval 迭代需要批量跑、需要程序化调用、需要记录每次的结果。这些网页版都做不了。
Claude API 的配置很简单,核心就是三行:
import anthropic client = anthropic.Anthropic(api_key="your-api-key") response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "你的 prompt"}] )我一般会把评分逻辑封装成一个函数,输入是测试集,输出是每个 case 的分数和总分。这样每次迭代只需要改 prompt 或评分逻辑,然后重新跑一遍就行。
6.2 批量跑 eval 的脚本结构
我的脚本一般长这样:
import json from anthropic import Anthropic client = Anthropic(api_key="your-api-key") def load_test_cases(path): with open(path) as f: return json.load(f) def run_eval(test_cases, prompt_template): results = [] for case in test_cases: # 生成回答 prompt = prompt_template.format(question=case["question"], document=case["document"]) response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, temperature=0, messages=[{"role": "user", "content": prompt}] ) answer = response.content[0].text # 评分 score = score_answer(answer, case) results.append({"case_id": case["id"], "answer": answer, "score": score}) return results def score_answer(answer, case): # 规则评分 + 模型评分 rule_score = rule_based_score(answer, case["expected_keywords"], case["max_length"]) model_score = model_based_score(answer, case["question"]) return rule_score * 0.6 + model_score * 0.4 if __name__ == "__main__": cases = load_test_cases("test_cases.json") results = run_eval(cases, PROMPT_TEMPLATE) total = sum(r["score"] for r in results) / len(results) print(f"总分:{total:.2f}") # 保存结果 with open("eval_results.json", "w") as f: json.dump(results, f, ensure_ascii=False, indent=2)这个脚本跑一次大概几分钟,成本取决于测试集大小和模型选择。我一般用 Sonnet 跑 eval,性价比比较高。
6.3 成本控制的小技巧
eval 跑多了成本会上去。我的控制方法是:
- 测试集控制在 100 条以内:太多没必要,100 条足够有代表性。
- 评分用便宜模型:生成回答用 Sonnet,评分可以用 Haiku,成本低很多。
- 缓存结果:同一个 case 跑过了就缓存,不要重复跑。
注意:评分模型和生成模型最好分开。用同一个模型评分会有偏差,因为它倾向于给自己生成的回答打高分。
7. 迭代过程中的经验与避坑指南
7.1 不要追求一步到位
我见过有人想一次性设计一个完美的 eval,结果花了两个月还没跑通第一轮。正确的做法是:先跑通,再优化。第一版 eval 哪怕只有 60 分,也比没有强。
7.2 badcase 要分类,不要逐个改
低分 case 可能有几十条,你不可能逐个改。正确做法是让 Claude 帮你归类,然后按类别改。改一类,跑一轮,看分数变化。
7.3 评分逻辑要可解释
如果你的评分逻辑是一个黑盒,分数涨了你不知道为什么涨,跌了也不知道为什么跌。我的做法是:每个维度单独输出分数,这样你能看到是哪个维度在变化。
7.4 定期回顾测试集
测试集会过时。三个月前的测试集可能已经不能代表现在的用户输入了。我一般每个月回顾一次,删掉过时的 case,补充新的 case。
7.5 记录每次迭代的变化
这个习惯很重要。我每次迭代都会记录:改了什么、分数变化、badcase 变化。这样回头看的时候,你能清楚地知道哪次改动最有效。
| 迭代轮次 | 改动内容 | 总分变化 | 关键发现 |
|---|---|---|---|
| 第一轮 | 跑通流程 | 60 | 拒答维度最低 |
| 第二轮 | 加拒答指令 | 72 | 拒答涨 1.3 分 |
| 第三轮 | 先检索再回答 | 81 | 跨段落涨 1.4 分 |
| 第四轮 | 输入规范化 | 85 | 口语化场景改善 |
| 第五轮 | 调整权重 | 88 | 分数更合理 |
| 补充 | 边界 case | 91 | 稳定在 91 左右 |
这个表格是我实际迭代的记录,你可以参考这个格式来记录自己的迭代过程。
7.6 一个容易被忽略的细节
评分的时候,不要只看平均分,要看分布。平均分 85 可能是所有 case 都在 85 左右,也可能是 50 条 100 分加 50 条 70 分。后者的问题更大,因为有一半的 case 效果很差。
我一般会看三个数:平均分、最低分、标准差。标准差太大说明效果不稳定,需要重点排查。
8. 后续可以怎么扩展
这套方法跑通之后,可以往几个方向扩展:
第一,自动化迭代。把“分析 badcase → 生成改进方案 → 跑 eval → 看分数”这个流程自动化,让 Claude 自己迭代。当然,关键决策还是要人来把关。
第二,多模型对比。同一套 eval,跑不同模型,看哪个模型在你的场景下效果最好。这个对比结果很有参考价值。
第三,eval 即文档。你的 eval 测试集和评分维度,其实就是你对“好回答”的定义。把它整理成文档,新加入的团队成员一看就懂。
第四,接入 CI/CD。每次改 prompt 或改模型,自动跑一遍 eval,分数跌了就报警。这样能防止退化。
我个人在实际操作中的体会是:eval 的价值不在于分数本身,而在于它强迫你把“什么是好”想清楚。你想不清楚,模型就更想不清楚。用 Claude 来辅助设计 eval,本质上是借它的语言能力帮你把模糊的标准具象化,然后你在这个基础上迭代。这个过程跑一轮下来,你对业务的理解会比之前深很多。