1. 传统测试为什么不够用

假设你写了一个函数:

def add(a, b):
    return a + b

assert add(1, 2) == 3   # 通过

这个断言永远成立。但换成 LLM 应用:

result = llm.invoke("帮我把这句话改得更专业:这个方案我觉得不太行")
assert result == "???"

你写不出这个断言。因为:

  1. 输出空间是无限的:同一句话有无数种「更专业」的表达
  2. 同一输入两次输出可能不同:采样温度、模型版本更新都会导致变化
  3. 「好」是主观的:什么算「更专业」,取决于业务标准

这不是「测试写不好」的问题,而是评测对象从「输出值」变成了「输出分布」。评测体系必须重新设计。

2. 三层评测体系

一个能落地的 LLM 评测体系,通常分三层,成本从低到高,稳定性从高到低:

层级评测对象判定方式执行时机成本
L1 确定性断言结构化输出、工具调用精确匹配 / Schema 校验每次提交极低
L2 语义评测内容质量、答案正确性LLM-as-a-Judge、规则匹配每次合并中
L3 端到端指标用户价值人工标注、线上指标发版 / 持续高

关键原则:能用 L1 解决的,绝不上升到 L2;能用 L2 解决的,不要天天跑 L3。

很多团队一上来就搭一套全自动 LLM 评测流水线,结果发现又贵又不稳,最后没人看。正确的做法是从确定性断言开始,把能确定的部分先确定下来。

3. L1:确定性断言能覆盖什么

LLM 应用里,其实有相当大一部分是可以确定性验证的:

3.1 结构化输出

如果要求模型输出 JSON,那就能用 Schema 校验:

from pydantic import BaseModel, ValidationError

class Extracted(BaseModel):
    name: str
    email: str
    company: str | None = None

def test_extraction():
    raw = agent.run("从这段简历里提取姓名、邮箱和公司:...")
    try:
        obj = Extracted.model_validate_json(raw)
    except ValidationError as e:
        pytest.fail(f"Schema 校验失败: {e}")
    assert "@" in obj.email

3.2 工具调用序列

Agent 该调什么工具、什么顺序,这是可以断言的:

def test_agent_tool_sequence():
    trace = agent.run_with_trace("查一下北京今天的天气")
    tools = [step.tool for step in trace if step.type == "tool_call"]
    assert "get_weather" in tools
    assert tools.index("get_weather") < len(tools)   # 至少调用过一次
    assert "send_email" not in tools                 # 不该调用的别调

最后那条「不该调用的别调」尤其重要——它是安全回归测试。

3.3 关键词与禁止词

对于内容生成,可以用规则做粗筛:

FORBIDDEN = ["我无法", "作为一个AI", "抱歉"]

def test_no_meta_refusal():
    out = agent.run("帮我写个请假邮件")
    for word in FORBIDDEN:
        assert word not in out, f"出现元话语: {word}"

这类断言很粗糙,但零成本、零延迟、零波动,作为第一道闸门非常划算。

3.4 性能预算

def test_latency_budget():
    start = time.time()
    agent.run("简单问答:今天星期几")
    assert time.time() - start < 5.0

延迟回归同样会毁掉产品体验,但经常被忽略。

4. L2:LLM-as-a-Judge 的正确用法

语义质量无法用规则判定,于是有了「用模型评模型」。这个思路可行,但坑很多。

4.1 基础实现

JUDGE_PROMPT = """你是一个严格的评测员。根据以下标准评估回答质量。

【用户问题】
{question}

【参考答案】
{reference}

【待评回答】
{answer}

请按 1-5 分打分,并说明理由。先输出 <score>分数</score>,再输出 <reason>理由</reason>。
"""

4.2 三个必须知道的偏差

位置偏差(Position Bias)
在成对比较时,评委倾向于选择第一个或第二个选项。缓解方法:双向评测——同一对答案交换顺序评两次,只有两次都一致才认可结果。

长度偏差(Verbosity Bias)
更长的回答往往得分更高,即使内容并没有更好。缓解方法:在评分标准里明确写「简洁性也是评分维度」,或者在评测时对长度做归一化处理。

自偏好(Self-Preference)
用 GPT 评 GPT 的输出,会系统性偏高。缓解方法:评委模型与生成模型不同源,或者用多个评委投票。

4.3 评分校准

LLM 给出的分数不能直接信。上线前必须做一次人工校准:

  1. 人工标注 50~100 条样本,给出真实分数
  2. 让 Judge 评同一批样本
  3. 计算相关性(如 Spearman 相关系数)
  4. 相关性低于 0.7,说明评分标准或 prompt 需要重写

没有校准过的 Judge,本质上是在测一个随机数。

4.4 成对比较优于绝对打分

实践经验:让模型回答「A 和 B 哪个更好」比「给这个回答打几分」要稳定得多。

def pairwise_judge(question, answer_a, answer_b, model):
    # 正向
    r1 = model.invoke(pairwise_prompt(question, answer_a, answer_b))
    # 反向(交换顺序,消除位置偏差)
    r2 = model.invoke(pairwise_prompt(question, answer_b, answer_a))

    if r1.winner == "A" and r2.winner == "B":
        return "A"
    if r1.winner == "B" and r2.winner == "A":
        return "B"
    return "tie"

这是一个非常实用的模式:用一致性换掉偏差。

5. 评测集怎么建

评测集的质量,直接决定评测体系的价值。三个来源:

5.1 黄金集(Golden Set)

人工精心标注的高质量样本,作为基准。规模不需要大——50 到 200 条高质量样本,胜过 1 万条脏数据。

构建要点:

  • 覆盖主要场景,也覆盖边界情况
  • 包含「应该拒绝」的负样本
  • 每条样本带明确的验收标准

5.2 合成集(Synthetic Set)

用强模型批量生成测试用例。适合快速扩大覆盖面,但要小心:合成数据往往比真实数据「干净」,容易高估系统能力。

5.3 生产回流(Production Mining)

从线上日志里挖掘真实 case,尤其是:

  • 用户点了「踩」或重新生成的
  • 用户手动修改了输出的
  • Agent 报错或超时的
  • 人工客服介入的

这一类的价值最高,因为它们反映真实分布。建议建立固定流程:每周从生产日志挖 10 条,人工标注后进黄金集。

6. L3:生产指标

离线评测再好,也要看线上结果。常用的生产指标:

指标含义采集方式
采纳率输出被用户直接使用的比例前端埋点
重试率用户不满意重新生成的比例前端埋点
编辑距离用户对输出做了多少修改前后文本 diff
人工升级率转人工的比例业务系统
任务完成率多步任务最终成功的比例会话级追踪
成本 / 请求单次请求的 token 成本网关统计

其中**「编辑距离」**是一个被低估的指标。用户对输出的修改量,是内容质量的直接体现——不需要用户主动反馈,就能自动采集。

7. CI 集成

把评测接进 CI,才能真正防回归。几个工程细节:

7.1 分层触发

on:
  pull_request:
    paths: ['prompts/**', 'src/agent/**']

jobs:
  l1-assertions:
    # 每次 PR 都跑:快、便宜、必过
    runs-on: ubuntu-latest
    steps:
      - run: pytest tests/l1 -x

  l2-judge:
    # 只对相关 PR 跑,且做采样
    if: contains(github.event.pull_request.changed_files, 'prompts')
    runs-on: ubuntu-latest
    steps:
      - run: python evals/run_l2.py --sample 50

7.2 门禁阈值

不要用「平均分」做门禁,容易被少数高分样本拉平。建议:

gate = {
    "l1_pass_rate": 1.0,        # L1 必须全过
    "l2_win_rate": 0.55,        # 新版本相对基线的胜率
    "regression_max": 3,        # 允许最多 3 条回归
}

特别注意:胜率要用成对比较算,直接用平均分对比是不靠谱的(分数尺度不稳定)。

7.3 成本控制

LLM 评测本身也烧钱。控制手段:

  • L1 全量,L2 采样(按变更范围选样本)
  • 用便宜的模型做初筛,只有边界样本才用强模型
  • 缓存评测结果(同一个回答不重复评)
  • 夜间跑全量,PR 只跑子集

7.4 版本快照

评测必须记录完整上下文,否则结果无法解释:

{
  "run_id": "2026-11-05-001",
  "model": "xxx-v2",
  "prompt_version": "git:abc123",
  "dataset_version": "golden-v7",
  "judge_model": "yyy",
  "results": { "l1_pass": 48, "l1_total": 48, "l2_win_rate": 0.62 }
}

模型、prompt、数据集、评委,四者任一变化,评测结果都不可直接比较。

8. 五个常见误区

误区一:只测「能不能用」,不测「什么时候不该用」
负样本(该拒绝的请求、该触发的安全策略)比正样本更容易被忽略,也更容易出事故。

误区二:把评测当一次性任务
模型会更新、prompt 会改、用户分布会变。评测集必须持续维护,否则半年后就完全失真。

误区三:追求全自动
完全自动的评测会逐渐偏离人类判断。人类标注必须保留——哪怕一周只标 20 条。

误区四:只看平均分
平均分掩盖尾部风险。一个 95% 正确率的系统,如果 5% 的错误是「给了错误的医疗建议」,那它依然不可上线。要看 P95 / 最差样本。

误区五:评测和产品指标脱节
离线涨了,线上没动——这说明评测集不代表真实分布。要定期做离线指标与线上指标的相关性分析。

9. 小结

LLM 评测的本质,是在非确定性系统上重建确定性。

可行的路径是:

  1. 先把能确定的确定下来(L1 断言,零成本高价值)
  2. 再用模型评模型,但必须校准(L2,成对比较 + 双向评测)
  3. 最终回到线上指标验证价值(L3,编辑距离、采纳率)

记住一句话:

没有评测的 AI 应用,不是「上线了」,只是「发出了请求」。

评测体系不是发版前的一次性动作,而是和代码一起演进的长期资产。它决定了你敢不敢改 prompt、敢不敢换模型、敢不敢快速迭代。