1. 为什么我要用 Claude 来设计 eval
第一次听到“用 Claude 设计 eval”这个说法,很多人会下意识觉得别扭:eval 不就是评测集吗,写几条测试用例、跑个分、看准确率,跟模型本身有什么关系?我一开始也是这么想的,直到自己在一个真实项目里被评测集坑了整整两周,才彻底改变看法。
事情的起因很简单。我手上有一个基于 Claude API 的文本处理流程,输入是用户提交的原始需求描述,输出是结构化的任务拆解。上线初期效果还行,但随着输入分布越来越杂,badcase 开始成片出现。我当时的做法非常“传统”:手动收集几十条失败样本,写成一个 JSON 数组,跑一遍看通过率,然后改 prompt,再跑一遍。问题在于,这个循环极其低效——我改完 prompt 之后,旧 case 过了,新 case 又挂了,而且我根本不知道到底是哪一类输入在拖后腿。
后来我换了个思路:让 Claude 自己来帮我设计 eval。具体来说,就是把我对“好输出”的判断标准用自然语言描述给 Claude,让它生成一批覆盖不同难度、不同边界的测试用例,再由我人工筛选和补充。这个做法听起来有点“用魔法打败魔法”的味道,但实测下来,它把我从“拍脑袋想 case”的泥潭里拽了出来。
这篇文章想聊的就是这套完整的方法论:怎么用 Claude 设计 eval,怎么把 eval 跑起来,怎么根据分数一轮一轮做 hillclimb(爬山式迭代),把分数从 60 分推到 90 分以上。适合正在做 LLM 应用、需要系统性评测、又不想把大量时间耗在手工造数据上的同学。哪怕你只是用 Claude Code 写点小工具,这套思路同样能迁移过去。
2. 先搞清楚 eval 到底在评什么
2.1 eval 不是测试用例的堆砌
很多人对 eval 的理解停留在“一堆输入 + 期望输出”的层面,这其实只对了一半。真正有价值的 eval,核心不在于用例数量,而在于它能否稳定地反映你的业务目标。举个我踩过的坑:早期我写了一个 eval,里面 80% 的 case 都是“正常输入”,结果模型分数一直很高,但线上还是天天出问题。原因很简单——我的 eval 分布和真实流量分布严重不匹配,它评的是一个“理想世界”,不是我的真实场景。
所以设计 eval 的第一步,不是写 case,而是先回答三个问题:
- 我的系统最怕哪类输入出错?(高风险场景)
- 哪类输入最常见?(高频场景)
- 哪类输入最难?(边界场景)
这三个问题的答案,决定了 eval 的骨架。Claude 在这里的价值,就是它能基于你对场景的自然语言描述,快速生成覆盖这三类的候选用例,而不是让你从零开始憋。
2.2 评分标准比用例本身更重要
我见过太多 eval 死在评分标准上。比如“输出是否合理”这种模糊标准,不同人打分能差出 30 分。我的做法是把评分拆成可判定的维度,每个维度要么是二值(通过/不通过),要么是有限档位(0/1/2)。举个例子,对于一个结构化输出任务,我会拆成:
| 维度 | 判定方式 | 权重 |
|---|---|---|
| 格式合法性 | JSON 能否解析 | 必须通过 |
| 字段完整性 | 必填字段是否齐全 | 0.3 |
| 内容准确性 | 关键信息是否与输入一致 | 0.5 |
| 表达简洁性 | 是否有多余废话 | 0.2 |
这张表不是拍脑袋来的,而是我先用 Claude 生成了一版初稿,然后结合线上 badcase 反复调整出来的。权重一定要反映业务优先级,比如格式合法性我设成“必须通过”,因为格式错了后面全白搭。
2.3 用 Claude 生成 eval 的正确姿势
直接跟 Claude 说“帮我写 50 条测试用例”,出来的东西大概率是泛泛而谈。我的经验是,prompt 里必须包含四样东西:
- 任务定义:系统输入是什么、输出是什么、约束是什么。
- 场景描述:真实用户会怎么用,有哪些典型和极端情况。
- 评分维度:你关心哪些方面,哪些是硬性要求。
- 输出格式:要求 Claude 以 JSON 数组返回,每条包含 input、expected、category、difficulty。
我常用的一个模板长这样:
你是一个评测集设计专家。我有一个任务:[任务描述]。 输入格式:[输入格式],输出格式:[输出格式]。 请生成 30 条测试用例,覆盖以下类别: - 正常场景(10 条) - 边界场景(10 条) - 高风险/易错场景(10 条) 每条用例包含字段:id, category, difficulty(1-3), input, expected_output, rationale。 以 JSON 数组返回,不要额外解释。这个模板的关键在于分类生成,而不是让模型自由发挥。分类能强制它覆盖不同难度,避免全生成简单 case。
3. 从零搭一套可复现的 eval 流程
3.1 环境与工具准备
我现在的标准配置是 Claude Code + 一个轻量的 Python 脚本。Claude Code 负责生成和迭代 eval,Python 脚本负责批量跑分。为什么不用现成的评测框架?因为大多数框架要么太重,要么评分逻辑不够灵活,而我的场景需要频繁改评分维度,自己写反而更快。
基础环境很简单:
python -m venv eval-env source eval-env/bin/activate pip install anthropic pandas tqdmClaude Code 的安装这里不展开,官方文档写得很清楚,核心就是装好之后能在终端里直接调用。我习惯把 eval 相关的文件放在一个独立目录里,结构大概是这样:
eval-project/ cases/ v1_cases.json v2_cases.json scripts/ generate_cases.py run_eval.py score.py results/ v1_result.csv v2_result.csv prompts/ system_prompt_v1.txt system_prompt_v2.txt这个结构的好处是版本可追溯。每次改 prompt 或改 eval,都新建一个版本文件,绝不覆盖旧的。我吃过亏——有一次直接改了 v1 的 case,结果分数涨了,但我根本不知道是 prompt 变好了还是 case 变简单了。
3.2 用 Claude 生成第一版 eval
第一版 eval 不要追求完美,目标是快速拿到一个能跑的基线。我的做法是先用 Claude 生成 30 条,人工过一遍,删掉明显不合理的,补上几条自己知道的 badcase,然后就开始跑。
生成脚本的核心逻辑是这样:
import anthropic import json client = anthropic.Anthropic() def generate_cases(task_desc, n=30): prompt = f"""你是一个评测集设计专家。任务描述:{task_desc} 请生成 {n} 条测试用例,覆盖正常、边界、高风险三类场景。 每条包含 id, category, difficulty, input, expected_output, rationale。 以 JSON 数组返回。""" resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=8000, messages=[{"role": "user", "content": prompt}] ) text = resp.content[0].text return json.loads(text)这里有个细节:max_tokens 一定要给足。我第一次只给了 2000,结果 JSON 被截断,解析直接报错。30 条用例大概需要 6000 到 8000 token,保险起见给 8000。
生成完之后,我会人工做三件事:
- 删掉重复或过于相似的 case
- 把 expected_output 里明显错误的改掉
- 补充 3 到 5 条真实 badcase
这一步不能省。Claude 生成的 case 质量整体不错,但它不了解你的业务细节,有些“期望输出”在业务上其实是不对的。
3.3 跑分脚本怎么写才靠谱
跑分脚本看起来简单,其实坑很多。我第一版脚本犯的错是:没有做并发控制,30 条 case 串行跑,等了快十分钟。后来改成并发,但并发太高又触发限流。最后我的方案是限制并发数为 5,配合重试机制。
核心代码大概是这样:
import asyncio from anthropic import AsyncAnthropic client = AsyncAnthropic() sem = asyncio.Semaphore(5) async def run_one(case, system_prompt): async with sem: for attempt in range(3): try: resp = await client.messages.create( model="claude-sonnet-4-5", max_tokens=2000, system=system_prompt, messages=[{"role": "user", "content": case["input"]}] ) return resp.content[0].text except Exception as e: if attempt == 2: return f"ERROR: {e}" await asyncio.sleep(2 ** attempt)重试机制用的是指数退避,这个很关键。限流报错如果不重试,你的分数会被拉低,而且你根本不知道是模型不行还是网络不行。
跑完之后,结果存成 CSV,字段包括 case_id、category、difficulty、output、score、reason。score 和 reason 一定要分开存,reason 是后面分析问题的关键。
3.4 评分环节的自动化与人工结合
评分我分两层:自动评分 + 人工抽检。
自动评分负责可判定的维度,比如格式合法性、字段完整性。这部分用代码写死,不依赖模型。内容准确性这种需要语义判断的,我用 Claude 来打分,但会加一个约束:要求它给出打分理由。
def score_with_claude(case, output): prompt = f"""请对以下输出打分。 输入:{case['input']} 期望:{case['expected_output']} 实际输出:{output} 评分维度:准确性(0-2)、简洁性(0-1)、格式(0-1)。 返回 JSON:{{"accuracy": x, "conciseness": y, "format": z, "reason": "..."}}""" # 调用 Claude 打分人工抽检我一般抽 20%,重点看那些自动评分给出高分但实际有问题的 case。这一步能发现评分标准的漏洞。我有一次发现 Claude 给一个明显答非所问的输出打了满分,原因是我的评分 prompt 里没强调“必须回答输入的问题”,后来补上这条约束,分数立刻正常了。
4. 一轮轮把分数提上去的 hillclimb 实战
4.1 第一轮:建立基线,别急着优化
第一版 eval 跑完,我的分数是 62 分(满分 100)。这个分数不高不低,正好适合作为基线。第一轮最重要的不是提分,而是搞清楚分数是怎么丢的。
我把结果按 category 和 difficulty 做了交叉统计:
| 类别 | 难度1 | 难度2 | 难度3 | 平均分 |
|---|---|---|---|---|
| 正常 | 95 | 88 | 76 | 86 |
| 边界 | 82 | 65 | 48 | 65 |
| 高风险 | 70 | 52 | 35 | 52 |
这张表一眼就能看出问题:难度越高,分数掉得越狠,高风险场景尤其严重。这说明我的 prompt 在处理复杂和边缘情况时不够稳健。
第一轮我做的优化很克制:只改了一处——在 system prompt 里加了一段“遇到不确定的情况时,优先保证格式正确,并在内容中标注不确定”。这一改,高风险场景的格式分立刻上去了,总分从 62 涨到 68。
4.2 第二轮:针对薄弱类别做定向优化
第二轮我开始针对“高风险 + 难度3”这一类做定向优化。做法是把这一类里所有失败的 case 拿出来,逐条看失败原因,然后归纳出共性问题。
我发现三个高频问题:
- 输入里有多个任务时,模型只处理了第一个
- 输入包含否定条件时,模型容易忽略
- 输入格式不规范时,模型直接崩溃
针对这三个问题,我在 prompt 里加了三条明确的处理规则,并且在 eval 里补充了对应的 case。注意,这里补充 case 不是为了刷分,而是因为原来的 eval 覆盖不够。补完之后,总分从 68 涨到 74。
这一轮我最大的体会是:优化 prompt 和优化 eval 是两件事,但必须同步做。只优化 prompt 不补 case,你会误以为问题解决了;只补 case 不优化 prompt,分数会虚低。
4.3 第三轮:引入 few-shot 和结构化约束
到第三轮,单纯改 prompt 文字已经边际收益递减了。我引入了两个新手段:
第一,few-shot 示例。我从 eval 里挑了 3 条最有代表性的 case,把输入和期望输出作为示例放进 prompt。这一步效果立竿见影,总分从 74 涨到 81。原因是 few-shot 给了模型一个具体的“参照物”,比抽象规则有效得多。
第二,结构化约束。我要求模型在输出前先输出一段“思考过程”,再输出最终结果。这个做法有争议,因为会增加 token 消耗,但实测下来,思考过程能显著降低复杂 case 的错误率。我的做法是让思考过程放在一个特定标签里,最终结果放在另一个标签里,解析时只取结果部分。
请按以下格式输出: <thinking>你的分析过程</thinking> <result>最终结果</result>这一轮之后,高风险难度3的分数从 35 涨到了 62,总分 81。
4.4 第四轮:处理长尾和稳定性问题
81 分之后,我遇到了瓶颈。分数卡在 81 到 83 之间来回波动,怎么改 prompt 都上不去。后来我意识到,问题不在 prompt,而在评分的稳定性。
我做了一个实验:同一个 prompt,同一个 case,跑 5 次,看分数波动。结果发现有些 case 的分数在 0 到 2 之间跳,说明模型输出本身不稳定。针对这个问题,我做了两件事:
- 把 temperature 从默认值调到 0,减少随机性
- 对每个 case 跑 3 次取平均分,而不是跑 1 次
这两件事做完,分数波动明显减小,总分稳定在 85。虽然分数没大涨,但稳定性提升本身就是巨大的进步,因为线上系统最怕的就是时好时坏。
4.5 第五轮:用 Claude 反向生成更难 case
到 85 分,我手里的 eval 已经有点“不够用”了——模型能过的 case 越来越多,剩下的都是硬骨头。这时候我做了一个反向操作:让 Claude 基于当前失败的 case,生成更难、更刁钻的变体。
具体做法是把失败 case 的输入和输出喂给 Claude,让它分析“这个 case 为什么难”,然后生成 5 条同类型但更难的 case。这一步相当于用模型来扩展 eval 的难度边界,效果非常好。新生成的 case 里有几条直接把我打回 78 分,但也正是这几条,让我发现了 prompt 里两个隐藏的逻辑漏洞。
修完漏洞之后,总分到了 89。最后一分多是从细节抠出来的,比如统一输出里的标点、处理空输入、处理超长输入。最终稳定在 91 分。
5. 常见问题与排查技巧实录
5.1 分数忽高忽低怎么办
这是最常见的问题。排查顺序我一般是:
- 先看 temperature。如果不是 0,先调到 0 再跑一遍。
- 再看评分脚本。评分 prompt 里如果有模糊表述,Claude 打分本身就不稳定。
- 最后看 case 本身。有些 case 的期望输出本身就有歧义,这种 case 要么改清楚,要么删掉。
我踩过最深的坑是:评分脚本里用了“大致合理即可”这种表述,导致 Claude 打分完全看心情。后来改成明确的档位描述,问题就解决了。
5.2 Claude 生成的 case 质量参差不齐
这是必然的,别指望一次生成就完美。我的处理流程是:
- 先按 category 分组,每组抽 3 条人工检查
- 如果某组问题率超过 30%,整组重新生成
- 生成时在 prompt 里加“避免生成过于简单或重复的 case”
另外,生成 case 的模型和跑 eval 的模型最好用同一个,这样难度分布更匹配。我用 Sonnet 生成,用 Sonnet 跑,一致性明显好于混用。
5.3 跑分太慢怎么优化
30 条 case 串行跑大概 8 到 10 分钟,并发 5 大概 2 分钟。如果 case 上百条,建议:
- 并发数控制在 5 到 10,太高容易限流
- 用异步而不是多线程,Python 里 asyncio 更省资源
- 结果边跑边存,别等全部跑完再写文件,防止中途崩溃丢数据
5.4 分数上不去但不知道卡在哪
这种情况我一般做失败 case 聚类。把所有失败 case 的输入拿出来,让 Claude 帮我归类,看主要集中在哪几类问题。这个方法比逐条看快得多,而且能发现你自己没意识到的模式。
有一次聚类结果显示,40% 的失败都集中在“输入包含多个并列条件”这一类,我之前完全没注意到。针对这一类优化之后,分数直接涨了 6 分。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 分数波动大 | temperature 非 0 / 评分模糊 | 调 temperature,明确评分档位 |
| 高分但线上差 | eval 分布不匹配 | 补充真实 badcase |
| 分数卡住不动 | 优化方向错误 | 做失败 case 聚类 |
| 跑分报错 | 限流 / token 截断 | 加重试,调大 max_tokens |
| case 质量差 | 生成 prompt 太泛 | 加分类约束和难度要求 |
6. 几个我反复验证过的实操心得
第一,eval 要跟着业务一起演进。我现在的习惯是每两周回顾一次 eval,把线上新出现的 badcase 补进去,把已经稳定通过的简单 case 删掉。eval 不是一次性的产物,它是一个活的资产。
第二,hillclimb 的节奏比幅度重要。我见过有人一次改一大堆 prompt,分数涨了 10 分,但根本不知道是哪处改动起的作用。我的做法是一次只改一个变量,改完跑分,记录,再改下一个。慢是慢了点,但每一步都可追溯。
第三,别迷信分数。91 分不代表系统完美,它只代表在你的 eval 上表现好。eval 本身有盲区,所以人工抽检永远不能省。我到现在还保持着每周抽 20 条线上真实输出人工看的习惯,这个习惯帮我发现了至少 5 个 eval 没覆盖到的问题。
第四,Claude 是助手不是裁判。用 Claude 生成 case、打分、聚类都很香,但最终的判断标准必须由你来定。我见过有人完全信任 Claude 的打分,结果 eval 分数很高,上线一塌糊涂。记住,Claude 帮你提效,但业务判断这件事,它替代不了你。
这套流程我从 62 分跑到 91 分,前后大概花了三周,中间踩的坑基本都写在上面的排查表里了。如果你也在做类似的 eval 迭代,建议先从 30 条 case 的小规模开始,跑通整个流程,再逐步扩大。规模不是关键,闭环才是。