LLM 应用评估:告别「感觉不错」


文档摘要

LLM 应用评估:告别「感觉不错」 本节摘要:你绝不会不上测试就部署 Web 应用,绝不会不做回滚预案就发数据库迁移。但今天多数团队交付 LLM 应用的方式是:读 10 条输出,说「嗯,看着行」。那不是评估,那是祈祷,而祈祷不是工程实践。每一次提示改动、每一次换模型、每一次调温度,都在以你读几个样例无法预测的方式改变输出分布。本节带你建立 LLM 应用的评估体系:三类评估方法(自动指标/LLM-as-judge/人工)各自的角色与局限;评估的六步流水线(提示→运行→收集→打分→对比→决策);评估数据集的黄金集+对抗集+分布采样三件套;样本量与置信区间(50 条不够,200 条起步);回归测试如何拦住「改提示降幻觉却悄悄砍了 34% 完整度」的灾难;以及 promptfoo、DeepEval

LLM 应用评估:告别「感觉不错」

本节摘要:你绝不会不上测试就部署 Web 应用,绝不会不做回滚预案就发数据库迁移。但今天多数团队交付 LLM 应用的方式是:读 10 条输出,说「嗯,看着行」。那不是评估,那是祈祷,而祈祷不是工程实践。每一次提示改动、每一次换模型、每一次调温度,都在以你读几个样例无法预测的方式改变输出分布。本节带你建立 LLM 应用的评估体系:三类评估方法(自动指标/LLM-as-judge/人工)各自的角色与局限;评估的六步流水线(提示→运行→收集→打分→对比→决策);评估数据集的黄金集+对抗集+分布采样三件套;样本量与置信区间(50 条不够,200 条起步);回归测试如何拦住「改提示降幻觉却悄悄砍了 34% 完整度」的灾难;以及 promptfoo、DeepEval 等工具如何把评估嵌进 CI/CD。

对应原课程:Phase 11 · Lesson 10 · evaluation(原英文 phases/11-llm-engineering/10-evaluation/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 为你的 LLM 应用构建带输入输出对、评分量规、边界情况的评估数据集
  2. LLM-as-judge、正则匹配、确定性断言实现自动化打分
  3. 设置回归测试,在提示/模型/参数变化时检测质量退化。
  4. 设计能捕捉你用例真正重要之事(正确性、语气、格式合规、延迟)的评估指标

一、问题与直觉

你做了个客服 RAG 聊天机器人,demo 里表现很好,你上线了。两周后有人改了系统提示以减少幻觉——改动奏效,幻觉率降了。但答案完整度也掉了 34%,因为模型现在对任何不是 100% 确定的事都拒答。

整整 11 天没人发现。自助渠道收入下滑,工单飙升。

这就是「凭感觉评估」的默认结局。你看几个样例,看着行,你合并了。但 LLM 输出是随机的:在 5 个测试用例上有效的提示,可能在第 6 个上失败;在你基准上拿 92% 的模型,可能在用户真正碰到的边界情况上只有 71%。

解法不是「更小心」,而是自动化评估:每次改动都跑、按量规打分、算置信区间、质量退化就拦截部署。评估不是锦上添花,是入场券。没评估就上线,就是蒙眼部署。

评估分类法

LLM 评估有三类,各有角色,单独都不够。

自动指标用算法把输出文本和参考答案对比。BLEU 量 n-gram 重叠(最初为机器翻译);ROUGE 量参考 n-gram 的召回(最初为摘要);BERTScore 用 BERT 嵌入量语义相似度。它们快又便宜——几秒打分 1 万条输出——但抓不住微妙差异:两个答案可以零词重叠却都对,一个答案可以 ROUGE 很高却在上下文里完全错。

LLM-as-judge 用强模型(GPT-5、Claude Opus 4.7、Gemini 3 Pro)按量规给输出打分,捕捉字符串指标漏掉的语义质量(相关度、正确性、有用性、安全)。花钱(GPT-5-mini 约 8 美元/千次裁判调用,Claude Opus 4.7 约 25 美元),但与人类判断的相关度达 82~88%(设计良好的量规下)。

人工评估是金标准,但最慢最贵。留给校准自动评估,别在每次提交上跑。

方法 速度 每千次成本 与人类相关度 最适合
BLEU/ROUGE <1 秒 0 美元 40~60% 翻译、摘要基线
BERTScore ~30 秒 0 美元 55~70% 语义相似度初筛
LLM-as-judge(GPT-5-mini) ~3 分 ~8 美元 82~86% 默认 CI 裁判;便宜、快、已校准
LLM-as-judge(Claude Opus 4.7) ~5 分 ~25 美元 85~88% 高风险打分、安全、拒答
RAGAS ~5 分 ~12 美元 85% RAG 专属指标
DeepEval(G-Eval + Pytest) ~4 分 视裁判 80~88% CI 原生、按 PR 回归门禁
人工专家 ~2 小时 ~500 美元 100%(定义上) 校准、边界情况、政策

LLM-as-judge:主力军

这是你 90% 时间会用的评估方法。模式很简单:给强模型输入、输出、可选参考答案、一份量规,让它打分。四个标准覆盖多数用例:

相关度(1~5):输出是否切中所问?1 完全跑题,5 直接且具体地回答了问题。

正确性(1~5):信息是否事实准确?1 含重大事实错误,5 所有断言可验证且准确。

有用性(1~5):用户会觉得有用吗?1 无价值,5 用户能立即据其行动。

安全性(1~5):输出是否免于有害内容、偏见、政策违规?1 含危险内容,5 完全安全。

量规设计

糟糕的量规产生噪声分数。好的量规把每个分数锚定到具体、可观察的行为。

坏量规:「给答案的好坏打 1~5 分。」

好量规:

  • 5:答案事实正确、直接切题、含具体细节或示例、提供可操作信息。
  • 4:事实正确且切题,但缺具体细节或略冗长。
  • 3:大体正确但含小不准确,或部分偏离问题意图。
  • 2:含重大事实错误,或仅擦边相关问题。
  • 1:事实错误、跑题或有害。

锚定描述比无锚定量表减少裁判方差 30~40%。

成对比较是替代方案:给裁判看两个输出,问哪个更好。这消除了量表校准问题——裁判不必纠结「这是 3 还是 4」,只挑赢家。适合两个提示版本正面交锋。

Best-of-N:为每个输入生成 N 个输出,让裁判挑最好的。这测量你系统的天花板。若 best-of-5 持续胜过 best-of-1,你可能受益于采样多个响应再选。

评估流水线

每次评估都遵循同一个六步流水线。

提示:定义测试用例,每个有输入(用户查询+上下文),可选参考答案。

运行:对模型执行提示,收集输出。想测方差就每用例跑 1~3 次。

收集:存输入、输出、元数据(模型、温度、时间戳、提示版本)。

打分:套用评估方法——自动指标、LLM-as-judge,或两者。

对比:与基线比。基线是你上一个已知良好版本,对差值算置信区间。

决策:新版若统计显著更好(或不更差),上线;若退化,拦截。

评估数据集:根基

你的评估数据集好坏取决于里面的用例。三类测试用例要紧:

黄金测试集(50~100 例):精心策划的输入输出对,代表核心用例。这是你的回归测试,每次提示改动必须过。

对抗样本(20~50 例):专为破坏系统设计的输入——提示注入、边界情况、模糊查询、领域外问题、有害内容请求。

分布采样(100~200 例):从真实生产流量随机抽样。这些抓策划测试漏掉的问题,因为它们反映用户真正在问什么。

样本量与置信区间

50 条测试用例不够。

若 50 例上评估分 90%,95% 置信区间是 [78%, 97%],跨度 19 个点——你分不清一个 80% 的系统和一个 96% 的系统。

200 例、90% 准确率,置信区间收紧到 [85%, 94%],这时才能决策。

测试用例数 观测准确率 95% CI 宽度 能检测 5% 退化?
50 90% 19 点
100 90% 12 点 勉强
200 90% 9 点
500 90% 5 点 有把握
1000 90% 3 点 精确

任何要部署决策的评估,至少用 200 条;若比较两个质量接近的系统,用 500+。

回归测试

每次提示改动都需要前后评估,这没得商量。流程:

  1. 在当前(基线)提示上跑评估套件,存分数。
  2. 改提示。
  3. 在新提示上跑同一套件。
  4. 用统计检验(配对 t 检验或 bootstrap)对比。
  5. 若任何标准无统计显著退化,上线。
  6. 若检出退化,查哪些用例退化了、为什么。

评估的成本

用 LLM-as-judge 时评估花钱,要预算。

评估规模 GPT-5-mini 裁判 Claude Opus 4.7 裁判 Gemini 3 Flash 裁判 时间
100 例 × 4 标准 ~2 美元 ~6 美元 ~0.40 美元 ~2 分
200 例 × 4 标准 ~4 美元 ~12 美元 ~0.80 美元 ~4 分
500 例 × 4 标准 ~10 美元 ~30 美元 ~2 美元 ~10 分
1000 例 × 4 标准 ~20 美元 ~60 美元 ~4 美元 ~20 分

200 例套件在每个 PR 上用 GPT-5-mini 跑,约 4 美元/次。团队每周合 10 个 PR,每月 160 美元。对比「上线一个让用户满意度塌方 11 天的回归」的代价,这笔钱花得值。

反模式

凭感觉评估。「我读了 5 条输出,看着行。」你无法靠读样例感知 5% 的质量退化,大脑会挑确认性证据。

在训练例上测试。 评估用例若与提示或微调数据里的示例重叠,你测的是记忆而非泛化。评估数据要分开。

单指标执念。 只优化正确性而忽略有用性,会产出简短、技术上对但没用的答案。永远多标准打分。

无基线评估。 4.2/5 的孤立分数毫无意义——比昨天好还是差?比竞争提示好还是差?永远要对比。

用弱裁判。 GPT-3.5 当裁判产生噪声大、不一致的分数。用 GPT-4o 或 Claude Sonnet。裁判至少要和被评模型一样强

真实工具

你不必一切从头建。这些工具提供评估基础设施:

工具 干什么 定价
promptfoo 开源评估框架,YAML 配置,LLM-as-judge,CI 集成 免费(开源)
Braintrust 评估平台,打分、实验、数据集、日志 免费层,之后按用量
LangSmith LangChain 的评估/可观测平台,追踪、数据集、标注 免费层,39 美元/月起
DeepEval Python 评估框架,14+ 指标,Pytest 集成 免费(开源)
Arize Phoenix 开源可观测 + 评估,追踪,span 级打分 免费(开源)

本节从零建,让你理解每一层;生产里用这些工具之一。

二、从零实现

完整代码见原课程 code/evaluation.py,这里给出关键骨架。

步骤 1:评估数据结构

import json, math, time, hashlib, statistics from dataclasses import dataclass, field from typing import Optional @dataclass class TestCase: input_text: str reference_output: Optional[str] = None category: str = "general" tags: list = field(default_factory=list) id: str = "" def __post_init__(self): if not self.id: self.id = hashlib.md5(self.input_text.encode()).hexdigest()[:8] @dataclass class EvalScore: criterion: str score: int reasoning: str max_score: int = 5 @dataclass class EvalResult: test_case_id: str model_output: str scores: list model: str = "" prompt_version: str = "" timestamp: float = 0.0 def __post_init__(self): if not self.timestamp: self.timestamp = time.time() def average_score(self): return sum(s.score for s in self.scores) / len(self.scores) if self.scores else 0.0

步骤 2:LLM-as-judge 打分器

用锚定量规给输出打分。生产中把模拟换成真实 GPT-4o/Claude 调用。

RUBRICS = { "relevance": { 5: "直接且具体地回答问题,无无关内容", 4: "较好地切题,含少量擦边内容", 3: "部分切题或漏掉关键方面", 2: "仅擦边相关", 1: "完全跑题或根本未回答", }, "correctness": { 5: "所有断言事实准确、可验证", 4: "大体正确,含一处不影响主旨的小不准确", 3: "含明显不准确但核心信息正确", 2: "含削弱响应的重大事实错误", 1: "根本性错误或含危险错误信息", }, # helpfulness / safety 同理…… } def score_with_llm_judge(input_text, model_output, reference_output=None, criteria=None): if criteria is None: criteria = ["relevance", "correctness", "helpfulness", "safety"] scores = [] for c in criteria: val = simulate_judge_score(input_text, model_output, reference_output, c) reasoning = f"[{c.upper()}={val}/5] {RUBRICS[c].get(val,'')}" scores.append(EvalScore(criterion=c, score=val, reasoning=reasoning)) return scores

simulate_judge_score 在本节用启发式模拟(看输出长度、与参考的词重叠、安全模式);真实使用时整段替换成一次 LLM API 调用,把输入、输出、参考、量规喂给强模型,要它返回 1~5 的分数与理由。

步骤 3:自动指标

实现 ROUGE-L 和词重叠,与 LLM 裁判并行。

def rouge_l_score(reference, hypothesis): if not reference or not hypothesis: return 0.0 ref = reference.lower().split() hyp = hypothesis.lower().split() m, n = len(ref), len(hyp) dp = [[0]*(n+1) for _ in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): dp[i][j] = dp[i-1][j-1]+1 if ref[i-1]==hyp[j-1] else max(dp[i-1][j], dp[i][j-1]) lcs = dp[m][n] if lcs == 0: return 0.0 p, r = lcs/n, lcs/m return round(2*p*r/(p+r), 4)

步骤 4:置信区间计算器

统计严谨把真评估和凭感觉分开。

def wilson_confidence_interval(successes, total, z=1.96): """适用于通过/失败率的置信区间,小样本也稳健。""" if total == 0: return (0.0, 0.0) p = successes / total den = 1 + z*z/total center = (p + z*z/(2*total)) / den spread = z * math.sqrt((p*(1-p) + z*z/(4*total))/total) / den return (round(max(0, center-spread), 4), round(min(1, center+spread), 4)) def bootstrap_confidence_interval(scores, n_bootstrap=1000, confidence=0.95): """对任意分布的分数,用重采样估均值置信区间。""" if len(scores) < 2: return (0.0, 0.0, 0.0) import random rng = random.Random(42) n = len(scores) means = [sum(rng.choice(scores) for _ in range(n))/n for _ in range(n_bootstrap)] means.sort() alpha = (1-confidence)/2 mean = sum(scores)/n return (round(means[int(alpha*n_bootstrap)], 4), round(mean, 4), round(means[int((1-alpha)*n_bootstrap)-1], 4))

步骤 5:评估运行器与对比报告

编排层把一切串起来:跑模型、打分、与基线对比、给出上线/拦截决策。

def run_eval_suite(test_suite, model_name, prompt_version, criteria=None): results = [] for tc in test_suite: output = run_model(model_name, tc.input_text) scores = score_with_llm_judge(tc.input_text, output, tc.reference_output, criteria) results.append(EvalResult(test_case_id=tc.id, model_output=output, scores=scores, model=model_name, prompt_version=prompt_version)) return results def compare_eval_runs(baseline, new, criteria=None): if criteria is None: criteria = ["relevance", "correctness", "helpfulness", "safety"] report = {"criteria": {}, "regressions": [], "improvements": []} for c in criteria: b_scores = [s.score for r in baseline for s in r.scores if s.criterion == c] n_scores = [s.score for r in new for s in r.scores if s.criterion == c] if not b_scores or not n_scores: continue diff = statistics.mean(n_scores) - statistics.mean(b_scores) status = "REGRESSION" if diff < -0.3 else "IMPROVED" if diff > 0.3 else "STABLE" if status == "REGRESSION": report["regressions"].append(c) elif status == "IMPROVED": report["improvements"].append(c) report["criteria"][c] = { "baseline_mean": round(statistics.mean(b_scores), 3), "new_mean": round(statistics.mean(n_scores), 3), "diff": round(diff, 3), "baseline_ci": bootstrap_confidence_interval(b_scores), "new_ci": bootstrap_confidence_interval(n_scores), "status": status, } all_b = [s.score for r in baseline for s in r.scores] all_n = [s.score for r in new for s in r.scores] report["overall"] = { "baseline_mean": round(statistics.mean(all_b), 3), "new_mean": round(statistics.mean(all_n), 3), "ship_decision": "SHIP" if not report["regressions"] else "BLOCK", } return report

💡 阈值 -0.3 与 +0.3 是「实质性变化」门限:分数波动在此区间内视为噪声而非真退化/改进,避免统计噪声触发误拦截。你的阈值要根据用例敏感度调。

三、框架对比

promptfoo:最快的从零到评估管线

# promptfooconfig.yaml # prompts: # - "回答以下问题: {{question}}" # providers: # - openai:gpt-4o # - anthropic:messages:claude-sonnet-5 # tests: # - vars: {question: "法国首都是哪?"} # assert: # - type: contains # value: "巴黎" # - type: llm-rubric # value: "答案应事实正确且简洁" # - type: similar # value: "法国的首都是巴黎" # threshold: 0.8 # 运行: promptfoo eval ; 查看: promptfoo view

YAML 配置、内置 LLM-as-judge、网页查看器、CI 友好输出,开箱支持 15+ 厂商,支持 JS/Python 自定义打分函数。

DeepEval:Pytest 原生

# from deepeval import evaluate # from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric # from deepeval.test_case import LLMTestCase # tc = LLMTestCase(input="法国首都是哪?", actual_output="法国的首都是巴黎。", # expected_output="巴黎", retrieval_context=["法国是欧洲国家,首都是巴黎。"]) # evaluate([tc], [AnswerRelevancyMetric(threshold=0.7), FaithfulnessMetric(threshold=0.7)])

DeepEval 与 Pytest 集成,deepeval test run test_evals.py 把评估当测试套件跑,内置 14 项指标含幻觉检测、偏见、毒性。

CI/CD 集成模式

# .github/workflows/eval.yml # name: LLM Eval # on: # pull_request: # paths: ['prompts/**', 'src/llm/**'] # jobs: # eval: # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - run: pip install deepeval # - run: deepeval test run tests/test_evals.py # env: {OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}"} # - uses: actions/upload-artifact@v4 # with: {name: eval-results, path: eval_results/}

在每个触及提示或 LLM 代码的 PR 上触发评估,任何标准退化超阈值就拦截合并,结果作为 artifact 上传供评审。LangSmith、Braintrust、Arize Phoenix 提供托管的可观测 + 评估平台,把追踪、数据集、标注、打分整合,适合不想自建 CI 管道的团队。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-eval-designer.md:一个可复用提示模板,用于设计评估量规。给它你 LLM 应用的描述,它产出量身定制的评估标准与锚定打分量规。
  • skill-eval-patterns.md:一个决策框架,按你的用例、预算、质量要求选择合适的评估策略。

Python 代码(code/evaluation.py)是一套独立的评估框架,把 simulate_judge_score 换成真实 LLM API、把 run_model 换成被测系统,即可投产;数据结构、量规、置信区间、对比报告逻辑均无需修改。

五、练习

  1. 加 BERTScore:用词嵌入余弦相似度实现简化版 BERTScore。建一个 100 常用词到 50 维随机向量的字典,算参考与假设 token 间的余弦相似度矩阵,用贪心匹配(每个假设 token 匹配最相似的参考 token)算 precision、recall、F1。

  2. 成对比较:改裁判,让它并排比较两个模型输出而非单独打分。给定同一输入和两个输出,裁判返回哪个更好及原因。在测试套件上跑 baseline-v1 vs baseline-v2 的成对比较,算带置信区间的胜率。

  3. 分层分析:按类别(事实、技术、安全、编码、摘要)分组测试用例,算每类带置信区间的分数。找出哪些类改进、哪些退化——一个系统可以总体改进却在某类上退化。

  4. 评分者间一致性:对每个测试用例跑 LLM 裁判 3 次(模拟不同裁判「评分者」),算三次间的 Cohen kappa 或 Krippendorff alpha。若一致性低于 0.7,说明量规太模糊,重写。

  5. 成本追踪器:追踪每次裁判调用的 token 用量与成本。每次裁判输入约含原提示、模型输出、量规(约 500 token 输入、100 token 输出)。算测试套件总评估成本,按每周 10 次评估推算月成本。

本节要点回顾

  1. 凭感觉不是评估:读几个样例感知不到 5% 的质量退化,大脑会挑确认性证据。
  2. 三类评估各有角色:自动指标快但浅,LLM-as-judge 准但要钱,人工是金标准但慢且贵。
  3. LLM-as-judge 是主力:相关度/正确性/有用性/安全性四标准,与人类判断相关度 82~88%。
  4. 量规要锚定:每个分数对应具体可观察行为,比无锚定量表减裁判方差 30~40%。
  5. 六步流水线:提示→运行→收集→打分→对比→决策,基线对比 + 置信区间是核心。
  6. 数据集三件套:黄金集(回归)+ 对抗集(边界)+ 分布采样(真实流量),缺一不可。
  7. 样本量要够:50 条置信区间宽 19 点不够,200 条起步,比较接近系统用 500+。
  8. 回归测试拦灾难:每次提示改动前后跑同套件,统计显著退化就拦截。
  9. 评估要进 CI/CD:PR 触发、退化拦截、artifact 上传,把评估当工程实践。
  10. 五大反模式:凭感觉、在训练例上测、单指标执念、无基线、弱裁判——一个都不能犯。

下一节,我们转向缓存、限流与成本——把评估守好的质量,用语义缓存、请求合并、模型路由变成可负担、可持续的生产服务。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U