1. 为什么我放弃了手写测试用例,转而让 Claude 来设计 eval
第一次认真考虑用 Claude 来设计 eval,是因为一个很具体的场景:我手上有一个文本分类的小系统,需要判断用户输入属于「咨询」「投诉」「建议」还是「其他」四类。一开始我老老实实手写测试集,写了大概六十条,跑下来准确率 87%,看着还行。但当我试图把这个数字往上推的时候,问题来了——我根本不知道那 13% 错在哪,也不知道该往哪个方向补测试用例。手写测试集最大的问题是:它反映的是「我以为模型会错的地方」,而不是「模型真正会错的地方」。
这个认知转变很关键。传统软件测试里,我们写单元测试是验证「代码有没有按预期执行」,逻辑是确定的,边界是清晰的。但 eval 面对的是概率模型,它的错误分布不是靠直觉能覆盖的。你拍脑袋想的那些边界情况,模型可能早就处理得很好;而它真正翻车的地方,往往是你压根没想到的角落。所以 eval 的设计本身就是一个需要迭代、需要探索的工程问题,而不是一次性写完就完事的静态资产。
Claude 在这个环节的价值,不是让它「帮你写测试用例」这么简单。如果只是让它生成一堆用例,那和随便找个模板批量填充没区别。真正有价值的是:让 Claude 扮演一个「对抗性测试设计师」的角色,它见过大量的失败模式,能系统性地帮你枚举那些人类容易忽略的边界。再加上它可以在同一轮对话里同时扮演「出题人」和「考生」,这就形成了一个可以自动运转的 eval 生成与评估闭环。
我后来把这套方法固化成了一个流程:先用 Claude 生成一批候选 eval,人工筛一遍,跑分,看失败案例,再让 Claude 针对失败模式生成新的 eval,再跑分。如此循环几轮,分数从 87% 一路推到 96%。这个过程在圈子里有个形象的说法叫hillclimb(爬山),核心思路就是小步快跑、持续爬坡,而不是指望一次性设计出完美的测试集。
这篇文章我会把这套流程完整拆开讲:怎么让 Claude 设计出真正有区分度的 eval、怎么跑分、怎么分析失败、怎么根据失败反推新的 eval、以及在这个过程中我踩过的那些坑。适合已经在用 Claude 做开发、想把手上的 eval 体系从「能跑」提升到「能指导优化」的读者。如果你还在纠结 Claude Code 怎么安装、怎么配置,那这篇可能稍微超前了一点,建议先把基础环境跑通再回来。
2. 让 Claude 设计 eval 的正确姿势:从「出题」到「对抗」
2.1 直接让 Claude 写测试用例,为什么效果很差
我最早的做法很朴素,就是打开 Claude 对话框,输入「帮我为这个文本分类任务写 50 条测试用例」。结果出来的东西看着挺整齐,但跑下来几乎没有区分度——50 条里 48 条模型都答对了,剩下 2 条还是因为标签本身有歧义。这种 eval 等于没写,因为它无法暴露模型的真实弱点。
问题出在提示词的设计上。当你让 Claude「写测试用例」时,它的默认行为是生成「典型、清晰、无争议」的样本,因为这类样本在训练数据里最常见,也最符合「好例子」的定义。但 eval 的目的恰恰相反——它要的是「能区分好坏模型的样本」,也就是那些处于决策边界附近、容易引发混淆的输入。这两者的目标函数是冲突的。
所以第一步要做的,是把提示词从「生成测试用例」改成「设计对抗性测试」。我常用的提示词结构是这样的:
你是一个专门设计对抗性测试集的工程师。你的目标不是生成"典型"样本, 而是生成能暴露分类器弱点的边界样本。 任务背景:将用户输入分类为 [咨询/投诉/建议/其他] 四类。 请生成 30 条测试样本,要求: 1. 每条样本必须处于两个类别的边界上,说明它为什么容易混淆 2. 覆盖以下维度:长度极短(<5字)、长度极长(>200字)、 混合意图(同时包含投诉和建议)、隐含意图(表面是咨询实为投诉)、 口语化表达、错别字、标点缺失 3. 每条样本附带你的"预期标签"和"混淆标签" 4. 不要生成任何明显属于单一类别的样本这个提示词的关键改动有三处:一是明确了「对抗性」的角色定位,二是给出了具体的混淆维度而不是笼统的「边界情况」,三是要求 Claude 标注「混淆标签」,这迫使它显式地思考「这条样本可能被误判成什么」。实测下来,这样生成的 eval 区分度比朴素提示词高出好几倍。
2.2 用「角色分离」让 Claude 同时出题和答题
单轮生成 eval 有个隐患:Claude 出题时可能不自觉地「手下留情」,生成一些它自己就能轻松答对的样本。要打破这个循环,可以用角色分离的技巧——在同一个对话里,先让 Claude 以「出题人」身份生成 eval,再让它以「考生」身份独立作答,最后对比两者。
具体操作是分两轮对话。第一轮只做出题,把生成的 eval 存下来。第二轮开一个全新的对话(这点很重要,避免上下文污染),把 eval 喂进去,让 Claude 只输出预测标签,不要解释。然后你拿预测标签和预期标签对比,就能得到一份「Claude 自评」的分数。
这份自评分数有两个用途。第一,它是你真实模型分数的一个上界参考——如果 Claude 自己都答不对自己出的题,说明这些题确实有难度,值得保留。第二,如果 Claude 自评分数很高(比如 95% 以上),但你的实际模型分数很低,那说明这些 eval 可能偏向 Claude 自身的偏好,你需要补充一些更「中立」的样本。
我一般会保留那些「Claude 自评错误」的样本,因为它们是天然的困难样本。同时也会保留一部分「Claude 自评正确但实际模型错误」的样本,这些是区分度最高的——它们精确地指出了你的模型和 Claude 之间的能力差距。
2.3 用 Claude Code 把 eval 生成流程脚本化
手动在对话框里来回复制粘贴,做几轮就烦了。如果你已经在用 Claude Code,可以把这个流程脚本化。核心思路是写一个 Python 脚本,通过 API 调用 Claude 生成 eval,然后自动跑分、自动记录失败案例。
import anthropic import json client = anthropic.Anthropic() def generate_evals(task_desc, dimensions, n=30): prompt = f"""你是一个对抗性测试设计师。任务:{task_desc} 请生成 {n} 条边界样本,覆盖维度:{dimensions} 输出 JSON 数组,每条包含 text, expected_label, confusion_label, reason""" resp = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=4096, messages=[{"role": "user", "content": prompt}] ) return json.loads(resp.content[0].text) def run_eval(evals, classifier_fn): results = [] for e in evals: pred = classifier_fn(e["text"]) results.append({ "text": e["text"], "expected": e["expected_label"], "predicted": pred, "correct": pred == e["expected_label"], "confusion": e["confusion_label"] }) return results这个脚本的价值在于,它把「生成 eval」和「跑分」串成了一条流水线。你只需要改dimensions参数,就能快速生成不同维度的测试集。跑完分后,把correct=False的样本单独拎出来,就是下一轮迭代的输入。
提示:调用 API 时注意控制并发和重试。我一般用指数退避处理限流,单次生成 30 条样本足够,太多反而质量下降。
2.4 生成 eval 时最容易忽略的三个维度
在反复迭代中,我发现有三个维度是人工设计时最容易漏掉、但 Claude 能系统覆盖的。
第一个是长度极端值。人类写测试用例时,潜意识里会写「正常长度」的句子,比如二三十个字。但模型在极短输入(「退钱」两个字)和极长输入(一段五百字的抱怨)上的表现往往完全不同。极短输入缺乏上下文,模型容易瞎猜;极长输入可能包含多个意图,模型容易只抓到一个。
第二个是意图混合。真实用户的输入很少是「纯咨询」或「纯投诉」,更多是「我想问一下这个功能怎么用,顺便吐槽一下上次的体验太差了」。这种混合意图的样本,模型很容易只识别出其中一个,漏掉另一个。让 Claude 专门生成这类样本,能有效暴露模型在意图分解上的短板。
第三个是表达噪声。错别字、缺标点、中英混杂、口语化缩写,这些在真实场景里极其常见,但在人工测试集里几乎不会出现,因为写测试的人会不自觉地「规范化」自己的输入。Claude 生成这类样本时没有这种心理负担,能自然地写出「这个咋弄啊急」「退款!!!」「wifi连不上什么鬼」这种真实感很强的输入。
3. 跑分之后:从失败案例里挖出真正的优化信号
3.1 分数只是表象,失败分布才是金矿
跑完一轮 eval,你得到一个分数,比如 87%。这个数字本身信息量很低,它只告诉你「有 13% 错了」,但不告诉你「错在哪」「为什么错」「怎么改」。真正有价值的是失败案例的分布——把它们按维度归类,你会看到一些非常清晰的模式。
我一般会做一个简单的失败分析表,按「混淆方向」和「输入特征」两个维度交叉统计。比如:
| 混淆方向 | 短输入 | 长输入 | 混合意图 | 含噪声 | 合计 |
|---|---|---|---|---|---|
| 咨询→投诉 | 3 | 1 | 5 | 2 | 11 |
| 投诉→建议 | 1 | 2 | 4 | 1 | 8 |
| 建议→其他 | 2 | 0 | 1 | 3 | 6 |
| 其他→咨询 | 4 | 1 | 0 | 2 | 7 |
这张表一出来,优化方向就非常明确了。比如「咨询→投诉」在「混合意图」上错了 5 次,说明模型在处理「表面咨询、实为投诉」的样本时,倾向于抓住表面的疑问句式,忽略了背后的不满情绪。这就是一个具体的、可操作的优化点——你可以在提示词里加一条规则,或者在训练数据里补充这类样本。
3.2 用 Claude 做失败归因,而不是自己硬猜
分析失败案例时,人的直觉经常不准。你觉得模型是因为「没看懂否定词」才错的,实际可能是因为「输入太短导致上下文不足」。与其自己猜,不如让 Claude 来做归因。
做法很简单:把失败案例批量喂给 Claude,让它逐条分析「模型可能为什么答错」。提示词可以这样写:
以下是一个文本分类模型的失败案例。请逐条分析: 1. 这条输入的真实意图是什么 2. 模型可能把它误判成了什么类别 3. 导致误判的最可能原因(从:上下文不足、意图混合、 关键词误导、句式干扰、标签边界模糊 中选择) 4. 如果要修复,应该补充什么样的训练样本 失败案例: [粘贴案例]Claude 的归因不一定 100% 准确,但它能帮你快速发现一些你没想到的模式。我印象最深的一次,是 Claude 指出某批失败案例的共同原因是「输入里同时出现了疑问词和负面情绪词,模型被疑问词带偏了」。这个观察我自己看了半天都没总结出来,但 Claude 一眼就点破了。
3.3 区分「模型错误」和「标签错误」
这里有个很容易踩的坑:不是所有失败案例都是模型的错,有一部分其实是你的「预期标签」本身就有问题。尤其是 Claude 生成的边界样本,它标注的expected_label有时候是值得商榷的。
我遇到过好几次这种情况:某条样本模型判成了「建议」,预期标签是「咨询」,我一开始以为是模型错了,仔细一看,这条输入确实更像建议。这时候如果盲目去「修复」模型,反而会把模型带偏。
所以每轮失败分析,我都会做一次「标签复核」:把失败案例里那些「模型预测和预期标签都有道理」的样本挑出来,人工重新判定。如果确认是标签问题,就修正标签而不是改模型。这个步骤看起来繁琐,但能避免你在错误的方向上浪费大量时间。
注意:Claude 生成的 eval 里,大约有 5% 到 10% 的标签是模糊的。这个比例不算高,但如果不处理,会持续污染你的分数信号。
3.4 建立失败案例库,避免重复踩坑
每轮迭代产生的失败案例,我都会存进一个「失败案例库」,按维度打标签。这个库有两个用途:一是作为下一轮 eval 生成的种子,让 Claude 基于这些真实失败模式生成更多类似样本;二是作为回归测试集,每次模型更新后都跑一遍,确保之前修好的问题没有复发。
失败案例库的结构大概是这样:
{ "id": "fail_0042", "text": "你们这个功能到底怎么用啊,上次问客服也没说清楚", "expected": "咨询", "predicted": "投诉", "dimension": ["混合意图", "隐含不满"], "root_cause": "疑问句式+负面情绪词,模型被情绪词带偏", "fixed": true, "fix_round": 3 }这个库积累到一两百条之后,就变成了一个非常有价值的资产。它比任何通用测试集都更贴合你的具体任务,也更能反映你的模型在真实场景下的弱点。
4. 一轮轮爬坡:把分数从 87% 推到 96% 的完整过程
4.1 第一轮:建立基线,别急着优化
第一轮的目标不是提分,而是建立一个可信的基线。我用 Claude 生成了 60 条对抗性 eval,覆盖前面说的所有维度,然后跑了一遍,得到 87%。这个分数比手写测试集的 87% 含金量高得多,因为它是在困难样本上得到的。
第一轮的失败分析显示,最大的问题集中在「混合意图」和「短输入」两个维度。混合意图错了 10 条,短输入错了 9 条,加起来占了总错误数的七成以上。这个分布非常清晰,直接指明了第二轮的方向。
这里有个心态上的建议:第一轮分数低是好事,说明你的 eval 有区分度。如果第一轮就 95%,那大概率是 eval 太简单了,没有暴露真实问题。
4.2 第二轮:针对最大失败簇补充 eval 和调整策略
第二轮我做了两件事。一是让 Claude 针对「混合意图」和「短输入」两个维度,各生成 20 条新 eval,补充进测试集。二是调整了分类器的提示词,明确要求模型「先判断输入是否包含多个意图,如果有,按主要意图分类」。
调整后重跑,分数从 87% 提到了 91%。提升主要来自混合意图维度,错误从 10 条降到 4 条。但短输入维度几乎没动,还是错 8 条。这说明短输入的问题不是提示词能解决的,可能需要更根本的改动。
4.3 第三轮:短输入的根因是上下文不足,不是分类逻辑
短输入为什么难?因为「退钱」这两个字,既可能是咨询(怎么退),也可能是投诉(要求退),也可能是建议(建议简化退款流程)。没有上下文,任何分类器都只能靠猜。
针对这个问题,我的解法是引入「默认类别」策略:当输入长度小于某个阈值且无法明确判断时,统一归到「其他」类,而不是强行猜一个具体类别。这个策略牺牲了一部分「猜对」的可能性,但大幅降低了「猜错」的概率。调整后短输入错误从 8 条降到 3 条,整体分数到了 93%。
这个案例说明一个道理:不是所有失败都能靠「让模型更聪明」解决,有时候需要的是「让系统更诚实」——承认自己判断不了,比强行给一个错误答案更好。
4.4 第四轮到第六轮:处理长尾和噪声
后面几轮提升越来越慢,从 93% 到 94% 到 95% 再到 96%,每轮只涨一个点左右。这个阶段处理的是长尾问题:含错别字的样本、中英混杂的样本、标点缺失的样本。这些样本数量不多,但每个都很难。
这个阶段的策略从「批量优化」转向「逐个击破」。我会把每个失败案例单独拿出来,分析它的具体原因,然后决定是补样本、改提示词、还是加后处理规则。比如有一类失败是「输入里包含英文单词导致模型误判」,我就在预处理阶段加了一个简单的英文检测,把纯英文输入单独路由。
到第六轮,分数稳定在 96%,剩下的 4% 错误基本都是「标签本身有歧义」的样本,属于不可消除的噪声。这时候继续爬坡的收益已经很低了,我就停下来了。
4.5 每轮迭代的检查清单
为了让每轮迭代有章可循,我总结了一个检查清单,每次跑完分后逐项过一遍:
- 本轮分数相比上轮变化多少?变化主要来自哪个维度?
- 新增的失败案例里,有多少是「模型错误」,有多少是「标签错误」?
- 失败案例是否集中在某几个维度?如果是,下轮优先处理。
- 之前修好的问题有没有复发?如果有,说明修复不彻底。
- 当前分数距离「标签噪声上限」还有多少空间?如果接近了,考虑停止。
这个清单能帮你避免两种常见错误:一是盲目优化已经很好的维度,二是忽略了回归问题。
5. 那些让我多花了两天时间的坑
5.1 上下文污染:同一个对话里生成和评估会互相影响
我最早图省事,在同一个 Claude 对话里既生成 eval 又跑评估。结果发现评估分数虚高,因为 Claude 在评估时能「看到」自己刚才生成 eval 时的推理过程,相当于开卷考试。后来改成每次评估都开新对话,分数才回归真实。
这个坑的教训是:生成和评估必须隔离。如果你用 API,就分成两个独立的调用,不要共享上下文。如果你用对话框,就每次评估前清空历史。
5.2 eval 集膨胀:不是越多越好
第二轮我一次性补了 40 条新 eval,测试集从 60 条涨到 100 条。结果跑分时间翻倍不说,分数还变得很不稳定——同样的模型,两次跑分能差 2 个百分点。原因是新补的样本里有不少和旧样本高度相似,相当于给某些维度加了权重,导致分数被这些维度主导。
后来我定了个规矩:每轮新增 eval 不超过 20 条,且必须和现有样本做去重检查。去重不只看文本相似度,还要看「维度组合」是否重复。如果某个维度组合已经有 5 条以上样本,就不再补充。
5.3 过度拟合 eval:分数涨了但实际效果没变
爬到 94% 左右的时候,我一度很得意,觉得模型已经很好了。但拿真实用户数据一测,发现实际准确率只有 89%,和 eval 分数差了 5 个百分点。这说明我的 eval 集已经被「过拟合」了——模型在 eval 上表现好,是因为 eval 的分布和模型优化方向高度一致,而不是因为模型真的变强了。
解决办法是保留一个「留出集」:从真实用户数据里随机抽一批样本,不参与任何迭代,只在最后用来验证。如果留出集分数和 eval 分数差距在 2 个百分点以内,说明 eval 是可信的;如果差距大,说明 eval 需要重新设计。
5.4 忽略推理成本:每轮都全量跑分太贵
早期我每轮迭代都把全部 eval 跑一遍,包括那些已经稳定通过的样本。后来算了一下 API 成本,发现大部分钱花在了「重复验证已知正确」上。优化方案是分层跑分:先用一个「快速子集」(20 条覆盖各维度的代表样本)做初筛,只有初筛通过的模型才跑全量。这样能把每轮的成本降低六七成。
5.5 标签一致性:Claude 自己标注的标签也会前后矛盾
Claude 生成 eval 时,同一条样本在不同轮次可能给出不同的expected_label。我遇到过一条「这个功能能不能退」的样本,第一次标成「咨询」,第二次标成「投诉」。这种不一致会直接污染分数信号。
解决办法是建立一个「标签规范文档」,把每个类别的判定标准写清楚,然后在生成 eval 时把规范一起喂给 Claude。同时,对已经生成的 eval 做一次人工复核,把有歧义的标签统一。这个工作一次性投入,但能长期受益。
6. 把这套方法迁移到其他任务的注意事项
这套「Claude 设计 eval + 迭代爬坡」的方法,不只适用于文本分类。我后来把它迁移到了信息抽取、摘要质量评估、代码生成正确性判断等任务上,核心逻辑是通的,但每个任务有一些特殊注意点。
信息抽取任务的 eval 设计,重点在「边界实体」和「嵌套实体」。让 Claude 生成 eval 时,要明确要求它覆盖「实体边界模糊」(比如「北京市朝阳区」是一个实体还是两个)和「实体嵌套」(比如「苹果公司CEO」里「苹果公司」和「CEO」的关系)这两类情况。这两类是最容易暴露抽取模型弱点的。
摘要质量评估的 eval 设计更麻烦,因为「好摘要」本身是主观的。我的做法是让 Claude 生成「明显好」「明显差」「边界模糊」三档摘要,然后让评估模型做排序而不是打分。排序任务比打分任务更稳定,也更容易发现模型的偏好偏差。
代码生成正确性判断的 eval,关键是「功能等价但写法不同」的样本。比如同一个功能,用循环写和用递归写,都是正确的,但模型可能只认其中一种。让 Claude 生成这类「等价变体」,能有效检验评估模型是否真正理解了功能而不是匹配了表面形式。
不管迁移到哪个任务,有一条原则是不变的:eval 的目的是「区分」,不是「覆盖」。与其生成一百条模型都能答对的样本,不如生成二十条模型会答错的样本。前者给你虚假的安全感,后者给你真实的优化方向。
最后分享一个我最近在用的技巧:让 Claude 在生成 eval 的同时,顺便生成「这条样本的难度评分」(1 到 5 分)。跑分时按难度分层统计,你会发现模型在难度 1-2 的样本上几乎全对,在难度 4-5 的样本上错误率很高。这个分层视图比单一总分有用得多,它能告诉你「模型的能力边界到底在哪」。当难度 4 的样本正确率从 60% 提到 80% 时,你就知道这轮优化真的起作用了,而不是靠运气在简单样本上多对了几条。