
1. 为什么“先写 eval 再调模型”是唯一靠谱的提分路径很多人做 Claude 应用的第一反应是先把 prompt 写漂亮跑几个 case 看着不错就上线了。结果用户一多各种边界情况冒出来你根本不知道是 prompt 的问题、检索的问题还是模型本身在这个任务上就是不行。我踩过这个坑后来彻底改了工作流——先设计 eval再一轮轮把分数提上去。这个顺序不能反。eval 的本质是什么说白了就是给你的任务建一套“考试卷”。没有考卷你永远在凭感觉改 prompt改完也不知道是变好了还是变差了。有了 eval每次改动都有一个客观分数你才知道哪一步真正有效。这套方法论在 Claude 上尤其重要因为 Claude 的模型版本迭代快API 参数多temperature、max_tokens、system prompt、tool use 等不量化根本没法做决策。这篇文章适合谁看如果你正在用 Claude API 做任何需要“效果可衡量”的任务——比如信息抽取、分类、摘要、代码生成、客服问答——那这套 eval 驱动的工作流就是为你准备的。哪怕你只是用 Claude Code 做日常开发辅助理解 eval 的思路也能帮你更高效地迭代 prompt。核心思路一句话把“我觉得这样更好”变成“分数从 0.62 提到了 0.81”。下面我从 eval 设计、数据构造、评分机制、迭代策略、常见坑五个维度把整套流程拆开讲。2. eval 设计的第一步把任务拆成可判定的原子单元2.1 从“模糊需求”到“可判定问题”的翻译过程大多数人写 eval 失败不是因为不会写代码而是因为任务本身没定义清楚。比如你说“帮我做一个客服问答系统”这没法 eval。什么叫好什么叫坏你得先把它翻译成可判定的问题。我的做法是拿一张纸把任务拆成三个层次输入是什么用户问题、上下文、历史对话、知识库片段输出是什么一段文本、一个 JSON、一个分类标签、一段代码什么叫“对”完全匹配、语义等价、关键字段正确、还是人工评分举个例子假设你要做一个“从合同文本中抽取关键条款”的任务。输入是合同段落输出是 JSON包含party_a、party_b、effective_date、termination_clause四个字段。那“对”的定义就很明确四个字段全部正确算 1 分错一个扣 0.25。这就是一个可判定的原子单元。注意不要一开始就追求“完美 eval”。先做一个粗糙版本能跑起来、能出分比什么都重要。我见过太多人卡在“设计完美评分标准”上结果一周过去了还没跑过第一个 case。2.2 评分方式的选择精确匹配、语义匹配还是 LLM 裁判评分方式直接决定了你的 eval 能不能反映真实效果。常见的有三种评分方式适用场景优点缺点精确匹配分类、字段抽取、格式校验客观、快、零成本对语义变化不宽容语义匹配摘要、改写、翻译容忍表达差异需要 embedding 模型有成本LLM 裁判开放式生成、对话质量灵活、接近人类判断有偏差、有成本、需要校准我的经验是能用精确匹配就别用 LLM 裁判。LLM 裁判看起来很美但它本身就是一个需要 eval 的模型。你用它来评分等于引入了一个新的不确定性来源。只有在输出完全无法用规则判定时比如“这段回答是否有帮助”才考虑用 LLM 裁判而且必须用人工标注的小样本校准它的评分偏差。如果非要用 LLM 裁判我建议用 Claude 自己来做裁判但要注意几点给裁判模型明确的评分 rubric让它输出结构化分数比如 1-5 分并且要求它给出评分理由。理由不是为了好看是为了让你在分数异常时能快速定位原因。2.3 构建第一批 eval 数据20 条起步覆盖核心场景eval 数据不在多在精。我通常从 20 条开始覆盖 5-8 个核心场景。每条数据包含输入、期望输出、评分标准。构造数据的方式有几种从真实日志里采样这是最靠谱的因为真实分布就在那里手工构造边界 case比如空输入、超长输入、多语言混合、格式异常用 Claude 生成候选再人工筛选让 Claude 根据你的任务描述生成 50 条测试输入你挑 20 条好的我一般混合使用先从日志采样 10 条再手工构造 5 条边界 case再用 Claude 生成 5 条补充。这样既有代表性又有压力测试。提示eval 数据一定要版本化管理。我习惯用 JSONL 格式每行一条字段包括id、input、expected、scenario、difficulty。这样后续增删改查都方便也容易做分层分析。3. 用 Claude 设计 eval 的具体操作从 prompt 到可运行脚本3.1 让 Claude 帮你写 eval 框架的 prompt 模板这里有个反直觉的点你可以用 Claude 来帮你设计 eval。具体做法是给 Claude 一个清晰的元任务描述让它生成 eval 脚本的骨架。我常用的 prompt 模板是这样的你是一个 eval 设计专家。我有一个 Claude API 任务需要你帮我设计一套 eval 方案。 任务描述[你的任务描述] 输入格式[输入格式] 输出格式[输出格式] 成功标准[你认为什么叫成功] 请输出 1. 评分维度和权重 2. 每条 eval 数据的字段结构 3. 一个 Python 评分函数的伪代码 4. 建议的 eval 数据条数和场景分布这个模板的好处是逼你把任务想清楚。很多时候你以为自己想清楚了一写下来发现全是模糊地带。Claude 会帮你把这些模糊地带暴露出来。3.2 评分脚本的核心结构加载数据、调用 API、计算分数、输出报告一个最小可用的 eval 脚本大概长这样import json import anthropic client anthropic.Anthropic() def load_eval_data(path): with open(path) as f: return [json.loads(line) for line in f] def call_claude(input_text, system_prompt): response client.messages.create( modelclaude-sonnet-4-20250514, max_tokens1024, systemsystem_prompt, messages[{role: user, content: input_text}] ) return response.content[0].text def score_exact_match(output, expected): return 1.0 if output.strip() expected.strip() else 0.0 def run_eval(eval_data, system_prompt): results [] for item in eval_data: output call_claude(item[input], system_prompt) score score_exact_match(output, item[expected]) results.append({ id: item[id], scenario: item[scenario], score: score, output: output, expected: item[expected] }) return results def report(results): total sum(r[score] for r in results) / len(results) print(fOverall score: {total:.3f}) by_scenario {} for r in results: by_scenario.setdefault(r[scenario], []).append(r[score]) for scenario, scores in by_scenario.items(): avg sum(scores) / len(scores) print(f {scenario}: {avg:.3f} ({len(scores)} cases))这个脚本很粗糙但能跑。关键是它把整个流程串起来了加载数据、调用 API、计算分数、输出报告。你先跑通这个再逐步加功能。3.3 第一次跑 eval 的心理准备分数一定很难看我第一次跑 eval 的时候信心满满地以为能到 0.8结果出来 0.42。当时就懵了。后来发现是几个原因prompt 里没给输出格式示例、max_tokens 设太小导致截断、评分函数对空格敏感。这里有个重要心态第一次 eval 的分数不重要重要的是你有了一个基线。0.42 就是你的起点接下来每一轮改动你都知道是往上走还是往下走。我建议第一次跑的时候把每条 case 的输入、输出、期望输出都打印出来人工看一遍。你会发现很多问题不是模型的问题是你 eval 设计的问题。比如期望输出里有个多余的空格或者评分函数没处理大小写。这些都要在第一次跑完后修掉。注意修 eval 本身也是迭代的一部分。但修完 eval 后要重新跑一遍所有历史版本确保分数可比。否则你改完评分标准之前的分数就全废了。4. 一轮轮提分的实战策略从 0.42 到 0.85 的完整路径4.1 第一轮修 prompt 的格式问题通常能涨 10-15 个点第一轮提分往往最简单把输出格式说清楚。很多低分是因为模型输出格式不对而不是内容不对。具体做法在 system prompt 里明确说“只输出 JSON不要任何解释文字”给一个完整的输出示例如果输出是 JSON用response_format参数强制 JSON 模式如果 API 支持检查 max_tokens 是否够用我做过一个信息抽取任务第一轮只是加了“只输出 JSON”和示例分数从 0.42 涨到 0.58。这一轮几乎不需要动脑子但收益巨大。4.2 第二轮加 few-shot 示例针对失败 case 补样本第一轮修完格式后剩下的失败 case 通常是内容问题。这时候加 few-shot 示例最有效。关键技巧不要随机加示例要针对失败 case 加。比如你的 eval 里有 5 条 case 是把“甲方”识别成了“乙方”那你就加一条示例输入类似输出正确区分甲乙双方。我一般加 3-5 个示例太多会占用 context 且可能过拟合。示例的选择标准是覆盖失败模式而不是覆盖所有场景。system_prompt 你是一个合同信息抽取助手。请从合同文本中抽取以下字段以 JSON 格式输出。 示例1 输入甲方张三公司。乙方李四公司。生效日期2024年1月1日。 输出{party_a: 张三公司, party_b: 李四公司, effective_date: 2024-01-01} 示例2 输入本合同由王五集团甲方与赵六科技乙方于2024年3月15日签订。 输出{party_a: 王五集团, party_b: 赵六科技, effective_date: 2024-03-15} 现在请处理以下输入 这一轮通常能再涨 10-15 个点。4.3 第三轮拆解失败模式做针对性修复到这一轮分数可能到了 0.7 左右剩下的失败 case 开始变得零散。这时候要做的是失败模式分析。我的做法是把所有失败 case 打印出来人工归类。常见的失败模式有边界模糊输入本身有歧义模型选了一个合理但不符合期望的答案长文本截断输入太长关键信息被截掉多任务冲突一个 prompt 里让模型做太多事顾此失彼领域知识缺失模型对某个专业领域不熟针对不同模式修复策略不同失败模式修复策略边界模糊在 prompt 里明确优先级规则长文本截断分段处理或先做检索再抽取多任务冲突拆成多个 prompt串行调用领域知识缺失在 prompt 里注入领域知识或做 RAG这一轮可能需要改架构不只是改 prompt。但收益也最大通常能再涨 10 个点。4.4 第四轮换模型、调参数、加后处理到 0.8 以上每一分都很难。这时候可以考虑换模型从 Haiku 换到 Sonnet或者从 Sonnet 换到 Opus看分数变化调 temperature抽取任务用 0生成任务用 0.3-0.7加后处理比如用正则修正常见格式错误用规则校验字段合法性我做过一个实验同一个 promptHaiku 跑 0.72Sonnet 跑 0.85Opus 跑 0.87。但 Opus 的成本是 Sonnet 的 5 倍所以最后选了 Sonnet。提分不是唯一目标性价比才是。提示每次只改一个变量。同时改 prompt 和模型你永远不知道是哪个起了作用。这是做 eval 的铁律。5. 那些让我分数卡住不动的坑排查链路与修复方案5.1 eval 数据泄露为什么你的分数虚高这是最隐蔽的坑。如果你用 Claude 生成 eval 数据然后又用同一批数据调 prompt分数会虚高。因为 Claude 生成的测试数据往往带有它自己的“偏好”你的 prompt 可能只是迎合了这种偏好而不是真正解决了任务。排查方法留一个 hold-out 集永远不用来调 prompt只在最后验证时用。如果 hold-out 分数比训练集低很多比如差 15 个点以上说明过拟合了。修复方案eval 数据尽量来自真实日志或者人工构造。如果用 Claude 生成要人工筛选和改写去掉明显的“Claude 风格”。5.2 评分函数的隐性偏差空格、大小写、同义词我踩过最冤的坑是评分函数对空格敏感。模型输出{name: 张三}期望输出{name:张三}就差一个空格判 0 分。这种偏差会让你的分数严重低估真实效果。修复方案评分前先做归一化。JSON 先 parse 再比较文本先 strip 再 lower数字先转 float 再比较。这些预处理能消掉 80% 的假失败。import json def normalize_json(text): try: return json.loads(text) except json.JSONDecodeError: return None def score_json(output, expected): out_obj normalize_json(output) exp_obj normalize_json(expected) if out_obj is None or exp_obj is None: return 0.0 return 1.0 if out_obj exp_obj else 0.05.3 长尾 case 拉低均分分层报告比总分更有用总分 0.85 听起来不错但如果 0.85 是因为简单 case 全对、难 case 全错呢这种模型上线后会在难 case 上崩掉。我的做法是分层报告按场景、按难度、按输入长度分别算分。这样你能看到模型在哪个维度上弱。分层维度示例分层用途场景抽取、分类、摘要看哪个任务弱难度简单、中等、困难看模型上限输入长度短、中、长看是否截断如果发现某个分层分数特别低就针对那个分层做专项优化。这比盲目调 prompt 高效得多。5.4 迭代疲劳什么时候该停手提分到一定程度边际收益会急剧下降。从 0.42 到 0.75 可能只要三天从 0.85 到 0.88 可能要两周。这时候要问自己这 3 个点值不值两周我的经验法则是当提分成本超过业务收益时停手。比如你的客服问答系统0.85 的准确率已经能覆盖大部分场景剩下的 0.15 是长尾人工兜底成本更低那就别死磕了。停手不等于放弃而是把精力转到其他维度延迟、成本、稳定性。这些在 eval 分数上看不到但上线后同样重要。6. 把 eval 变成日常习惯持续迭代的工程化实践6.1 每次改 prompt 都跑一遍 evalCI 化的最小方案eval 最大的价值不是一次性提分而是持续防退化。你今天调好了 prompt明天改了一个词可能就把某个场景搞崩了。没有 eval你根本发现不了。最小方案把 eval 脚本写成一个命令每次改 prompt 后手动跑一遍。进阶方案接入 CI每次提交自动跑 eval分数下降超过阈值就报警。我用的是 GitHub Actions每次 push 到 main 分支就跑 eval结果输出到 PR 评论里。这样 review 的时候能直接看到分数变化。name: eval on: pull_request: branches: [main] jobs: run-eval: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 with: python-version: 3.11 - run: pip install anthropic - run: python eval/run.py env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}6.2 eval 数据的持续积累从线上日志到回归集eval 数据不是一次性的要持续积累。我的做法是每周从线上日志采样 20 条人工标注后加入 eval 集每次发现线上 bad case立即加入 eval 集每季度清理一次 eval 集去掉过时的 case这样 eval 集会越来越大越来越贴近真实分布。但也要注意eval 集太大会导致跑一次成本很高。我的平衡点是保持在 100-200 条超过就做采样。6.3 用 Claude Code 辅助 eval 开发自动化生成测试用例如果你用 Claude Code可以让它帮你做 eval 的脏活累活。比如让它读你的 prompt生成 20 条测试输入让它读失败 case分析失败模式让它写评分函数的单元测试我常用的一个 prompt 是“读一下 eval/failures.jsonl帮我归类失败模式每类给一个修复建议。”Claude Code 会直接读文件、分析、输出报告。这比人工看快得多。注意Claude Code 生成的测试用例要人工过一遍。它有时候会生成一些不切实际的 case或者期望输出本身就有问题。人工筛选这一步不能省。6.4 从 eval 分数到业务指标怎么跟非技术同学解释最后说一个软技能怎么跟产品、运营解释 eval 分数。你不能说“分数从 0.72 涨到 0.85”他们没感觉。你要翻译成业务语言“之前 100 个合同有 28 个抽错现在只有 15 个”“之前需要人工复核的比例是 40%现在降到 20%”“按每天 1000 单算每天少 200 次人工介入”这种翻译能让非技术同学理解 eval 的价值也能帮你争取资源继续优化。我吃过亏早期只顾着调分数没跟业务对齐结果被问“你这两周在干嘛”的时候答不上来。7. 我在实际项目里踩出来的几条硬经验第一条eval 脚本要简单到你能随时改。我见过有人把 eval 框架写得极其复杂结果改一个评分规则要动五个文件最后没人愿意维护。我的 eval 脚本永远是一个文件200 行以内改起来毫无心理负担。第二条每次只改一个变量。这条我说过但值得再说一遍。同时改 prompt 和模型分数涨了你不知道是哪个的功劳分数跌了你也不知道该回滚哪个。一次一个变量慢就是快。第三条保留所有历史版本的分数。我用一个简单的 CSV 记录每次实验日期、版本号、改动内容、总分、各分层分数。这样回头看的时候能清楚看到哪次改动真正有效。有时候你会发现某个你以为很关键的改动其实只涨了 0.5 个点。第四条不要迷信高分。0.95 的 eval 分数不代表线上就好。eval 数据永远无法完全覆盖真实分布。上线后一定要有监控和反馈闭环把线上 bad case 回流到 eval 集。eval 是手段不是目的。第五条Claude 的模型会更新eval 要跟着跑。每次 Claude 发新版本我都会把 eval 跑一遍看分数是涨是跌。有时候新模型在某个任务上反而更差这时候要么调 prompt要么锁旧版本。不跑 eval你永远不知道模型更新对你意味着什么。这套方法我从去年开始用前后迭代了十几个项目从信息抽取到代码生成到客服问答基本都适用。核心就一句话先建考卷再刷题每轮只改一个变量记录所有分数。听起来简单但真正坚持下来的人不多。你要是能坚持跑二十轮 eval效果一定比凭感觉调 prompt 好得多。