1. 为什么我要用 Claude 来设计 eval
1.1 从“凭感觉调 prompt”到“用数据说话”
做 AI 应用的人都有一个共同的痛点:改了一版 prompt,感觉效果好像好了,但到底好了多少、有没有在别的场景上变差,心里完全没底。我以前也是这么干的,改完 prompt 随手试几个 case,觉得“嗯,不错”,就上线了。结果上线之后用户反馈一堆问题,回头一查,原来是我改的那版 prompt 在某个边缘场景上直接崩了。
后来我开始认真做 eval,也就是评估集。简单说,eval 就是一组“输入 + 期望输出”的测试用例,每次改完 prompt 或者换模型,都跑一遍 eval,看分数是涨了还是跌了。这个思路跟软件工程里的单元测试一模一样,只不过测的不是代码逻辑,而是模型的输出质量。
那为什么标题里强调“用 Claude 设计 eval”?因为设计 eval 这件事本身就很费脑子。你得想清楚:哪些场景要覆盖?每个场景的期望输出长什么样?评分标准怎么定?这些问题如果全靠自己拍脑袋,很容易漏掉关键 case,或者评分标准定得太模糊,导致分数忽高忽低没有参考价值。而 Claude 在这件事上特别好用——它能帮你快速生成大量候选测试用例,还能帮你把模糊的评分标准拆成可执行的打分维度。
1.2 什么是 hillclimb,为什么它比“一步到位”更靠谱
Hillclimb,中文叫“爬山法”,核心思想特别朴素:你不需要一次性找到最优解,你只需要每一步都比上一步好一点点。就像爬山一样,你不需要知道山顶在哪,你只需要每次往更高的方向走一步。
放到 eval 优化里,流程是这样的:
- 先有一个 baseline,也就是当前版本的 prompt 和对应的 eval 分数。
- 分析 eval 结果,找出得分最低的那几个 case。
- 针对这些 case 改 prompt,只改一个点,不要一次改一堆。
- 重新跑 eval,看分数有没有提升。
- 如果提升了,保留这版改动;如果没提升或者下降了,回滚。
- 重复步骤 2 到 5,直到分数不再明显提升。
这个方法听起来很笨,但它比“一次性重写 prompt”靠谱得多。因为每次只改一个点,你能清楚地知道是哪个改动带来了提升,哪个改动导致了下降。而且它天然避免了“改了一堆东西,结果有的变好有的变坏,最后总分没变但你已经不知道发生了什么”这种灾难。
1.3 这套方法适合谁,不适合谁
这套方法最适合两类人:一是正在做 AI 应用但还没建立 eval 体系的开发者,二是已经有 eval 但分数卡住了不知道怎么继续提升的人。
不太适合的情况也有:如果你的任务非常简单,比如就是一个固定格式的抽取任务,那可能不需要这么复杂的 eval 体系,写几个 case 手动测一下就够了。另外,如果你的任务没有明确的“好”和“坏”的标准,比如开放式创意写作,那 eval 的设计会非常困难,hillclimb 的效果也会打折扣。
2. 用 Claude 设计 eval 的完整流程
2.1 第一步:让 Claude 帮你生成候选测试用例
设计 eval 最耗时的部分就是“想 case”。你得覆盖正常场景、边缘场景、异常输入、多语言、长文本、短文本……全靠自己想,很容易漏。
我的做法是:先把任务描述和几个典型输入丢给 Claude,让它帮我生成 50 到 100 个候选测试用例。Prompt 大概长这样:
我有一个任务:{任务描述} 下面是几个典型输入示例: {示例1} {示例2} {示例3} 请帮我生成 50 个测试用例,要求: 1. 覆盖正常场景、边缘场景、异常输入 2. 每个用例包含输入和期望输出 3. 期望输出要具体,不要写“合理的回答”这种模糊描述 4. 标注每个用例的难度等级(简单/中等/困难)Claude 生成完之后,不要直接全用。我一般会人工过一遍,删掉重复的、不合理的、期望输出写得太模糊的。通常 50 个里面能留下 30 个左右就不错了。
注意:Claude 生成的期望输出有时候会过于“理想化”,比如要求模型输出一段完美格式的 JSON,但实际场景中模型可能会多输出一句话。这种时候你要根据实际需求调整期望输出,不要盲目追求完美。
2.2 第二步:设计评分标准,把“好”拆成可打分的维度
有了测试用例之后,下一步是设计评分标准。最粗糙的做法是“对/错”二值判断,但这样太粗了,很多 case 其实是“部分正确”。更好的做法是把评分拆成多个维度,每个维度单独打分。
比如我最近做的一个信息抽取任务,评分维度是这样的:
| 维度 | 说明 | 分值 |
|---|---|---|
| 字段完整性 | 是否抽取了所有要求的字段 | 0-3 |
| 字段准确性 | 抽取的值是否与原文一致 | 0-3 |
| 格式正确性 | 输出格式是否符合要求 | 0-2 |
| 无冗余信息 | 是否输出了不要求的内容 | 0-2 |
每个 case 的总分是 10 分,最后把所有 case 的分数加起来除以总分,得到一个百分比分数。
这个评分标准也是让 Claude 帮我细化出来的。我一开始只写了“准确性”和“完整性”两个维度,Claude 提醒我说“格式正确性”和“无冗余信息”也很重要,因为实际使用中格式错误会导致下游解析失败,冗余信息会干扰用户阅读。
2.3 第三步:用 Claude API 批量跑 eval
有了测试用例和评分标准,接下来就是批量跑 eval。这一步可以用 Claude API 来做,核心逻辑是:
- 遍历每个测试用例,把输入发给 Claude。
- 拿到输出后,用评分标准打分。
- 汇总所有 case 的分数,得到总分。
打分这一步可以人工做,也可以让 Claude 来做。如果 case 不多(比如 30 个以内),人工打分更准确。如果 case 很多(100 个以上),可以让 Claude 来打分,但需要给它非常明确的打分指令。
import anthropic client = anthropic.Anthropic(api_key="your-api-key") def run_eval(prompt_template, test_cases): results = [] for case in test_cases: # 构造输入 input_text = prompt_template.format(input=case["input"]) # 调用 Claude API response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": input_text}] ) output = response.content[0].text # 打分(这里简化为人工打分,实际可以接自动打分逻辑) score = manual_score(output, case["expected"]) results.append({"case": case, "output": output, "score": score}) total_score = sum(r["score"] for r in results) / len(results) return total_score, results提示:跑 eval 的时候一定要固定模型版本和参数(temperature、max_tokens 等),否则分数波动你会分不清是 prompt 改了还是模型变了。
2.4 第四步:分析失败 case,找出改进方向
跑完 eval 之后,不要只看总分。总分只是一个数字,真正有价值的是失败 case 的分布。我一般会把所有得分低于 60% 的 case 单独拉出来,按失败原因分类:
- 格式错误:输出格式不符合要求
- 字段缺失:漏抽了某些字段
- 字段错误:抽出来的值不对
- 冗余输出:多说了不该说的话
分类完之后,你会发现失败 case 往往集中在某几个类型上。比如我最近一次 eval,80% 的失败 case 都是“格式错误”,具体表现是模型在 JSON 外面多包了一层 markdown 代码块。这个问题很好解决,在 prompt 里加一句“直接输出 JSON,不要用 markdown 代码块包裹”就行了。
3. Hillclimb 实战:一轮轮把分数提上去
3.1 第一轮:建立 baseline,别急着优化
第一轮的目标不是提升分数,而是建立一个可靠的 baseline。你需要确认三件事:
- eval 本身是稳定的:同样的 prompt 跑两次,分数差异不超过 5%。
- 评分标准是合理的:人工抽查几个 case,确认打分符合预期。
- 失败 case 的分布是清晰的:你知道主要问题出在哪里。
如果这三件事有任何一件没做到,先别急着优化 prompt,先把 eval 本身修好。我见过太多人 baseline 还没建好就开始改 prompt,结果分数忽高忽低,完全不知道自己在优化什么。
3.2 第二轮:针对最大失败类型,做最小改动
Baseline 建好之后,看失败 case 的分布,找到占比最大的那个失败类型,针对它做最小改动。
比如我的情况是“格式错误”占比最高,那我就在 prompt 末尾加一句:
输出要求:直接输出 JSON 对象,不要用 markdown 代码块包裹,不要添加任何解释性文字。只改这一句,其他什么都不动。然后重新跑 eval,看分数变化。
这里的关键是“最小改动”。如果你同时改了格式要求、字段定义、示例,那分数提升了你也不知道是哪个改动起了作用。Hillclimb 的核心就是每次只动一个变量。
3.3 第三轮:处理边缘 case,但别过度拟合
第二轮改完之后,格式错误应该大幅减少。这时候失败 case 的分布会变化,可能变成“字段缺失”占比最高。那就针对字段缺失做改动,比如在 prompt 里补充字段定义,或者加一个 few-shot 示例。
但这里有一个坑:不要针对单个 case 做过度拟合。比如你发现某个 case 失败了,就在 prompt 里加一句专门针对这个 case 的规则。这样做的结果是,这个 case 的分数上去了,但其他类似 case 的分数可能反而下降了。
正确的做法是:看失败 case 的共性,针对共性做改动。比如你发现多个 case 都漏抽了“日期”字段,那就在 prompt 里强调“日期字段必须抽取,如果原文没有明确日期,输出 null”。而不是针对某一个具体 case 写一条特殊规则。
3.4 第四轮及以后:分数卡住了怎么办
通常跑到第四轮或第五轮,分数会进入一个平台期,怎么改都提升不明显。这时候有几个策略:
策略一:换模型。如果当前用的是小模型,换大模型跑一遍 eval,看分数上限在哪里。如果大模型分数明显更高,说明当前 prompt 已经接近小模型的能力上限了。
策略二:加 few-shot 示例。如果 prompt 里还没有示例,加 2 到 3 个高质量示例通常能带来明显提升。示例要覆盖不同的场景,不要只放简单 case。
策略三:拆任务。如果一个 prompt 要模型同时做多件事(比如先抽取再分类再格式化),考虑拆成多个 prompt 串行执行。每个 prompt 只做一件事,成功率会高很多。
策略四:检查 eval 本身。有时候分数卡住不是因为 prompt 不够好,而是因为 eval 里有几个 case 的期望输出本身就有问题。重新审查一遍 eval,把不合理的 case 修掉或删掉。
4. 常见问题与排查技巧实录
4.1 分数波动太大,怎么办
分数波动大通常有三个原因:
- 模型参数不固定。temperature 设得太高,每次输出都不一样。建议 eval 时把 temperature 设为 0 或接近 0。
- eval case 太少。如果只有 10 个 case,一个 case 的分数变化就会导致总分波动 10%。建议至少 30 个 case,最好 50 个以上。
- 评分标准太模糊。如果打分靠人工感觉,不同时间打的分数可能不一致。建议把评分标准写成明确的规则,最好能让两个人独立打分,看一致性有多高。
4.2 Claude 生成的测试用例质量不高,怎么提升
Claude 生成的测试用例质量取决于你给的输入。如果你只给一个任务描述,它生成的用例会很泛。如果你给几个具体示例,它生成的用例会贴近你的实际场景。
我的做法是:先手动写 5 个高质量示例,覆盖不同场景,然后把任务描述和这 5 个示例一起给 Claude,让它基于这些示例生成更多用例。这样生成的用例质量会高很多。
另外,生成完之后一定要人工过一遍。我一般会删掉 30% 到 40% 的用例,留下的都是真正有价值的。
4.3 自动打分和人工打分差距很大,怎么校准
自动打分(让 Claude 打分)和人工打分有差距是正常的,但如果差距超过 20%,说明自动打分的指令需要调整。
校准方法是:先人工打 20 个 case 的分数,然后让 Claude 也打一遍,对比两者的差异。如果 Claude 打分普遍偏高,就在指令里加一句“请严格打分,不要因为输出看起来合理就给高分”。如果 Claude 打分普遍偏低,就加一句“只要输出包含了期望的关键信息,即使格式略有不同也给满分”。
4.4 常见问题速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 分数波动大 | temperature 太高 / case 太少 | 固定 temperature=0 / 增加 case 数量 |
| 分数卡住不涨 | prompt 接近模型上限 | 换大模型 / 加 few-shot / 拆任务 |
| 自动打分不准 | 打分指令模糊 | 校准打分指令 / 人工抽查 |
| 某个 case 反复失败 | 过度拟合其他 case | 检查是否有冲突规则 / 单独处理 |
| eval 跑得太慢 | case 太多 / API 限流 | 分批跑 / 加缓存 / 用更快的模型 |
4.5 几个我踩过的坑
坑一:eval 里混入了“不可能完成”的 case。有些 case 的期望输出本身就不合理,比如要求模型从一段没有日期的文本里抽出日期。这种 case 永远不可能得满分,会拉低整体分数。定期审查 eval,把不合理的 case 删掉。
坑二:prompt 越改越长,最后变成一坨。Hillclimb 的过程中,每次改一点,prompt 会越来越长。跑到十几轮之后,prompt 可能已经几千字了,里面有很多重复和矛盾的规则。这时候需要做一次“重构”,把 prompt 重新整理一遍,删掉冗余规则,合并相似规则。
坑三:只看总分,不看分布。总分从 70% 涨到 75% 看起来是好事,但如果涨的那 5% 全来自简单 case,而困难 case 的分数反而下降了,那这个改动其实是有问题的。每次看分数变化时,都要同时看不同难度等级的分数变化。
坑四:忘了记录每次改动。Hillclimb 会跑很多轮,如果不记录每轮改了什么、分数变化是多少,跑到后面你会完全忘记哪版 prompt 是最好的。建议用一个简单的表格记录:
| 轮次 | 改动内容 | 总分 | 简单 case 均分 | 困难 case 均分 |
|---|---|---|---|---|
| 0 | baseline | 68% | 85% | 45% |
| 1 | 加格式要求 | 74% | 88% | 52% |
| 2 | 加日期字段说明 | 77% | 89% | 58% |
| 3 | 加 few-shot 示例 | 82% | 92% | 65% |
这个表格不仅能帮你追踪进展,还能在回滚时快速找到最好的那一版。
5. 把 eval 和 hillclimb 变成日常习惯
5.1 每次改 prompt 之前,先跑一遍 eval
这个习惯听起来很简单,但坚持下来不容易。我一开始也经常忘记,改完 prompt 直接上线,结果出了问题才想起来没跑 eval。后来我把 eval 脚本做成了一个命令行工具,改完 prompt 之后顺手跑一下,几秒钟就能看到分数变化。
python run_eval.py --prompt prompts/v3.txt --output results/v3.json跑完之后,脚本会自动对比上一版的结果,输出分数变化和失败 case 的变化。这样你就能立刻知道这次改动是正向的还是负向的。
5.2 把 eval case 当成代码来维护
Eval case 不是写完就扔的一次性东西,它需要像代码一样维护。我的做法是:
- 把 eval case 存在一个 JSON 或 YAML 文件里,用 git 管理。
- 每次发现新的失败场景,就把它加进 eval。
- 定期审查 eval,删掉过时的 case,补充新的 case。
- 给每个 case 打标签(难度、场景类型),方便分析。
这样做的结果是,eval 会越来越贴近实际使用场景,分数也越来越有参考价值。
5.3 用 Claude 做 eval 的自动化分析
每次跑完 eval,手动分析失败 case 很费时间。我后来写了一个脚本,把失败 case 自动发给 Claude,让它帮我分类和总结:
def analyze_failures(failed_cases): prompt = f""" 下面是一批失败的测试用例,请帮我分析: 1. 这些失败 case 可以分成哪几类? 2. 每类失败的主要原因是什么? 3. 针对每类失败,给出具体的 prompt 改进建议。 失败 case: {failed_cases} """ response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2048, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text这个脚本帮我省了很多时间,而且 Claude 的分析往往能发现我忽略的模式。
5.4 关于 skill 和 agent 的一点想法
最近“skill”这个词很火,各种 agent skill、claude skill 的讨论很多。我的理解是,skill 本质上就是把一套固定的操作流程封装起来,让 agent 可以复用。Eval 和 hillclimb 其实也可以封装成一个 skill:输入是 prompt 和 eval 集,输出是优化后的 prompt 和分数报告。
但我不建议一上来就搞这么复杂。先把 eval 跑起来,把 hillclimb 的手动流程跑通,等你跑了几十轮之后,自然就知道哪些环节可以自动化了。过早追求自动化,往往会在还没搞清楚问题的情况下就搭了一堆没用的基础设施。
我在实际使用中发现,eval 和 hillclimb 最大的价值不是把分数从 70% 提到 90%,而是让你对 prompt 的每一次改动都有信心。你知道这次改动是正向的,你知道失败 case 在哪里,你知道下一步该往哪个方向走。这种确定性,比分数本身重要得多。