大模型应用评测 Eval:从人工到自动化的工程路径
没有 eval 体系的 LLM 应用,每次改 prompt 或换模型都是在赌运气。系统化的评测让你有信心地迭代:知道哪个改动让回答质量提升了 12%,哪个 prompt 版本在边缘用例上退步了。
真实的场景是这样的:你把 prompt 里的一句话从”请回答用户问题”改成”请基于给定资料回答用户问题,禁止编造资料中没有的内容”,测试了 3 个自己想到的例子,效果都不错,就上线了。三天后运营反馈,客服机器人在处理”退货流程”这类问题时开始一本正经地编造步骤。你回去排查,才发现新 prompt 在长文档场景下反而更容易触发幻觉——因为”基于给定资料”这句话让模型在资料检索不完整时更倾向于”脑补补全”,而不是坦白说不知道。如果当时有一个跑着 200 条真实问题的测试集,这个回归在合并前就能被拦下来,根本不用等用户投诉。这就是 eval 体系存在的意义:把”我感觉改好了”变成”有 200 条样本证明改好了”。
Eval 的三个层次
| 层次 | 评测什么 | 成本 | 频率 |
|---|---|---|---|
| 单元级(Unit) | 单次模型输出是否符合预期 | 低 | 每次 PR |
| 端到端(E2E) | 完整用户流程的最终结果 | 中 | 每日 |
| 人工抽查 | 边缘用例、风格、细节 | 高 | 每周/发版前 |
生产建议:单元级 eval 完全自动化跑 CI,人工抽查聚焦在自动化覆盖不到的主观质量维度。
这三层不是平行关系,而是漏斗:单元级负责”每次提交都不能踩的红线”(比如不能输出敏感信息、不能返回空结果、JSON 格式不能错),跑在 PR 阶段,几秒到几十秒出结果,拦不住的问题很少但一旦拦住就是硬伤,必须 block merge。端到端负责”用户视角的完整体验”,比如一次多轮对话下来用户的问题有没有被真正解决,这类 eval 依赖真实调用链路,跑起来慢(几分钟到几十分钟),放在每日定时任务里,出问题第二天早上看报告就行,没必要卡在 PR 上拖慢开发节奏。人工抽查负责”机器判断不了的东西”——语气是不是得体、有没有政治敏感的擦边表达、面对辱骂用户时的应对是否合适,这些交给自动化基本是白费力气,老老实实让人看,每周抽 20~30 条已经能发现大部分问题。三层搭配的关键是别本末倒置:见过团队把大量精力砸在人工抽查上却没有一行自动化 eval,结果就是每次发版前靠人肉过一遍几百条对话,累死人还漏得到处都是。
测试集构建
测试集质量决定 eval 价值上限:
# 测试集结构示例(JSONL 格式)
{
"id": "rag-001",
"input": {"question": "公司的退款政策是什么?", "context": "...相关文档..."},
"expected": "退款需在购买后 7 日内申请,...", # 参考答案(可选)
"criteria": ["包含退款期限", "提到申请方式", "不编造信息"], # 评测标准
"tags": ["rag", "policy", "edge-case"]
}
测试集来源:
- 生产日志中提取真实用户问题(最有价值)
- 人工设计边缘用例(空输入、超长输入、对抗性提问)
- 用 LLM 生成多样化变体(快速扩充,质量需人工审核)
| 来源 | 覆盖真实分布 | 发现边缘问题 | 维护成本 |
|---|---|---|---|
| 生产日志 | 强 | 弱(用户很少故意刁难) | 低(持续产出) |
| 人工设计 | 弱 | 强 | 高(需要专人设计) |
| LLM 生成变体 | 中 | 中 | 中(需人工审核过滤) |
三种来源缺一不可,单靠某一种都会留下盲区:只用生产日志,测试集会严重偏向”常见问题”,那些一年出现一次但一出现就是大事故的边缘输入(比如用户直接粘贴一份 30 页的合同问价格)永远不会被覆盖到;只靠人工设计边缘用例,测试集又会脱离真实分布,你精心设计的 50 个刁钻问题可能一个都没在生产环境出现过,优化它反而是在优化一个不存在的问题。
从生产日志挖测试集,实操上别直接随机抽样,那样抽出来的大概率是重复度很高的”今天天气怎么样”这类高频但没有信息量的问题。更好的做法是先用 embedding 模型把历史问题聚类,再从每个簇里分层抽样:这样既保证了覆盖主要场景(每个大簇都有代表),又不会让长尾问题被高频问题淹没。同时把带有”负反馈标记”(用户点了踩、追问”你说的不对”、转人工客服)的对话优先纳入测试集,这些往往就是模型真实的薄弱环节,标注价值比随机样本高一个数量级。
目标:100 条人工精标 + 500 条自动生成,覆盖主要业务场景和常见失败模式。
LLM-as-Judge
对于无法用规则判断的主观质量,用另一个 LLM 充当裁判:
JUDGE_PROMPT = """
你是一个严格的质量评估员。根据以下标准对回答打分(1-5分):
评估标准:
- 准确性:回答是否与参考资料一致,没有幻觉
- 完整性:是否覆盖了问题的核心要点
- 简洁性:是否没有多余的废话
用户问题:{question}
参考资料:{context}
模型回答:{answer}
以 JSON 格式输出:{{"accuracy": 分数, "completeness": 分数, "conciseness": 分数, "reason": "简短说明"}}
"""
def llm_judge(question: str, context: str, answer: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o", # Judge 用强模型
messages=[{"role": "user", "content": JUDGE_PROMPT.format(
question=question, context=context, answer=answer
)}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
LLM-as-Judge 的局限:对自己生成的内容存在自我偏好偏差,建议用不同于被测模型的 Judge,或用多个 Judge 取平均。
这个偏差不是玄学,是有实打实的机制的:Judge 模型给回答打分时,本质上是在拿”这个回答的表达方式跟我自己会写出来的答案有多像”作为隐性参考,如果被测模型和 Judge 用了同系列模型(比如两个都是 GPT 家族),Judge 会不自觉地给风格相近的回答更高分,哪怕内容准确性其实差不多。实操上有三条经验能缓解:
- 跨厂商配对:被测模型用 DeepSeek,Judge 就换成 Claude 或 GPT-4o,别用同厂商同系列,风格差异越大越能暴露真实的质量差距。
- 打分前先要求 Judge 复述关键事实:在 prompt 里加一句”先列出参考资料中的关键事实点,再逐条核对回答是否覆盖”,比直接让它”打个分”要准得多,因为这逼着 Judge 做真正的核对而不是凭”读感”给分。
- 绝对打分不如相对打分稳:如果条件允许,让 Judge 做 A/B 两个回答的成对比较(pairwise),“哪个更好,为什么”,比让它给单个回答打 1-5 分的绝对分数方差小得多。绝对打分很依赖 Judge 当天的”心情”(同一个回答不同批次跑,4 分和 5 分之间来回跳),相对比较则稳定很多,代价是要多测一次。
踩过的坑:response_format={"type": "json_object"} 不是所有模型都严格支持,遇到过 Judge 返回结果里 JSON 字符串没有闭合花括号,json.loads 直接抛 json.decoder.JSONDecodeError,导致整批 eval 跑到一半崩掉。生产上给这类调用统一包一层 try/except,解析失败就重试一次,两次都失败就把这条样本标记为”待人工复核”而不是让整个批次中断——eval 脚本本身也要有容错,不能比它测试的东西还脆弱。
常用评测指标
| 场景 | 指标 | 工具 |
|---|---|---|
| RAG 问答 | Hit Rate、MRR、Faithfulness | RAGAS |
| 文本生成 | ROUGE-L、BERTScore | evaluate 库 |
| 代码生成 | Pass@K(单测通过率) | 执行沙盒 |
| 对话系统 | LLM-as-Judge(多维打分) | 自定义 |
| 分类/提取 | 精确率、召回率、F1 | scikit-learn |
接入 CI/CD
# GitHub Actions 示例
- name: Run LLM Eval
run: |
python eval/run_eval.py \
--test-set eval/testsets/rag_core.jsonl \
--threshold-accuracy 4.0 \
--threshold-pass-rate 0.85
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Comment Eval Results on PR
uses: actions/github-script@v7
with:
script: |
const results = JSON.parse(fs.readFileSync('eval_results.json'));
github.rest.issues.createComment({
issue_number: context.issue.number,
body: `## Eval Results\n平均准确性: ${results.avg_accuracy}/5\n通过率: ${results.pass_rate}`
});
阈值设置建议:首次上线以当前版本为基准,后续每次 PR 不得低于基准 5%,触发阈值则 block merge。
常见问题
eval 结果不稳定,同一个 prompt 每次跑分数不一样怎么办? LLM 本身有随机性。解决方案:将 temperature 设为 0(确定性输出);或对每条测试样本跑 3~5 次取平均,用统计置信区间而非单次得分做决策。
测试集应该多大才够? 核心场景 50~100 条精标已能发现大多数回归问题。不要追求数量,优先保证覆盖”高频路径”和”已知失败模式”。测试集要和产品一起演进,定期加入新发现的失败案例。
换了新模型后 eval 分数反而下降,要不要换回去? 先分析失分集中在哪类问题。如果是风格变化(语气更正式/简洁)但信息准确,可能是 Judge prompt 的偏好问题而非模型退步。建议结合人工抽查 20 条再做决策。
← 返回 应用模式总览:从 Prompt 到 Agent | 应用模式专题
多模型横向对比评测?力达云聚合 API 统一接口同时调用 GPT-4o、Claude、DeepSeek,eval 脚本只需改一行参数即可跑对比实验。