可发布文章正文

内容管家 AI领域评论6字数 2546阅读8分29秒阅读模式

评估体系:LLM 交付的质量门禁与漂移检测

如果一次 prompt 改动没有触发任何 eval 失败就能上线,那所谓的"评估"不过是笔记本里的草稿运行。一旦 CI 绿灯通过,真正的慢泄漏才刚开始悄悄侵蚀系统质量。本文解析成熟度模型第二层——评估体系,核心命题只有一句话:不靠希望交付,靠门禁交付,并在部署后持续监控漂移。

为什么"偶尔手动跑一次 eval"不够用 大多数团队对待 eval 的方式,与当年对待单元测试如出一辙:想起来跑一次、在笔记本里跑、等出了问题再补一个。这意味着系统最可能静默损坏的路径——prompt 微调、模型升级、temperature 调低——完全不在掌控之内。一个看似无害的改动,可能悄然让 8% 的输入质量下滑。解法是把评估当作测试来对待:作为 CI 中的门禁,回归时直接阻断构建。

黄金用例集:像测试用例一样精心筛选

黄金用例(golden case)即系统必须通过的代表性样本,其数据结构示例如下:

{
 "id": "case-0412",
 "slice": "tier1/en",
 "input": { "request": "...", "context": {} },
 "expect": { "decision": "approve", "min_confidence": 0.80 },
 "must_not": { "decision": "auto_resolve" },
 "tags": ["edge-case", "previously-broke"]
}
筛选原则与单元测试相同:覆盖常见场景、已知难点场景,以及每一个历史故障的回归用例(回归用例永不下线)。黄金用例与代码同仓库管理,改动本身成为可审核&查验的 diff。

评分器:显式、类型化、可组合

对每个用例,运行当前 prompt 和模型,对输出进行评分。评分逻辑需显式声明类型,支持精确断言与容差范围的混合:

from dataclasses import dataclass
@dataclass
class CaseResult:

id: str
 slice: str
 passed: bool
 checks: list
def score_case(case, output) -> CaseResult:

exp = case.get("expect", {})
 checks = []
 if "decision" in exp:

checks.append(("decision", output.get("decision") == exp["decision"]))
 if "min_confidence" in exp:

checks.append(("min_conf", output.get("confidence", 0) >= exp["min_confidence"]))
 for k, v in case.get("must_not", {}).items:

checks.append((f"not_{k}", output.get(k) != v))
 assert checks, "empty expect + no must_not would vacuously pass"
 return CaseResult(case["id"], case["slice"], all(ok for _, ok in checks), checks)
def test_scorer_catches_regression:

case = {"id": "c1", "slice": "tier1/en", "expect": {"decision": "approve", "min_confidence": 0.8}}
 assert score_case(case, {"decision": "approve", "confidence": 0.9}).passed
 assert not score_case(case, {"decision": "reject", "confidence": 0.9}).passed

门禁:CI 中的一键质量关卡

门禁逻辑接入 CI,一条命令即可触发,指标低于阈值时构建失败:

eval-gate:
 steps:
 - run: eval run --suite golden --gate --min-pass 0.98 --baseline artifacts/baseline.json
运行输出示例:
$ eval run --suite golden --gate --min-pass 0.98
cases: 240 passed: 236 failed: 4 pass-rate: 98.3%
gate(min-pass>=0.98): PASS
failures: case-0412 (decision), case-0511 (min_conf), ...
有了门禁,任何"改善了 A 场景但破坏了 B、C 两个场景"的 prompt 改动,都会在 PR 阶段暴露,而非等三周后在用户侧才发现。

用例自动路由至对应切片

系统扩展后,"黄金用例集"会按请求类型、用户群或语言环境拆分为多个子 suite。不应让人工选择跑哪套——给每个用例打上 slice 标签,由运行器根据运行时属性自动路由:针对某切片的 eval 只运行属于该切片的用例,消除"测试集"与"运行集"之间的漂移。 同时,不建议用单一全局通过率作为门禁阈值:99% 的总体通过率可能掩盖一个仅 70% 的子切片。门禁应按切片分别设置。

两种回归:悬崖式与慢泄漏

上述门禁机制专精于一种场景,对另一种完全盲区:

  • 悬崖式回归(Cliffs):改动导致质量急剧、即时下降。部署前门禁能有效拦截此类问题。
  • 慢泄漏(Slow Leaks):0.995 → 0.99 → 0.985,连续三个版本逐步小幅下滑,每次幅度都不足以触发 0.98 的阈值。门禁逐一放行,等问题明显时已上线五次。

换言之,CI 绿灯并不等于系统没有持续劣化。门禁测试的是部署瞬间、你预设的那些输入用例;而生产环境在持续变化:输入分布迁移、模型版本更新、prompt 改动优化了测试集却损害了非测试集。漂移检测对付的正是第二种退化,需要完全不同的技术底层。

基线:对比"比之前更好/更差",而非仅判断"是否及格"

门禁回答的是"是否达标"。漂移检测回答的是"是否比之前差"。因此需要捕获基线——取最近一次已知良好版本的指标,作为后续每次运行的参照基准:

{
 "release": "2026-01-09",
 "metrics": {
 "exact_match": { "mean": 0.942, "n": 2400, "std": 0.012 },
 "mean_confidence": { "mean": 0.871, "n": 2400, "std": 0.030 },
 "human_override_rate": { "mean": 0.060, "n": 2400 }
 }
}

import math MIN_EFFECT = { "exact_match": 0.01, "human_override_rate": 0.02, "mean_confidence": 0.03 } HIGHER_IS_WORSE = {"human_override_rate", "judge_disagreement_ratio", "abstention_rate"} def compare_to_baseline(metric, cur, base, is_proportion=True): delta = cur["mean"] - base["mean"] if is_proportion: p = (base["mean"] base["n"] + cur["mean"] cur["n"]) / (base["n"] + cur["n"]) se = math.sqrt(p (1 - p) (1 / base["n"] + 1 / cur["n"])) else: se = math.sqrt(base["std"] 2 / base["n"] + cur["std"] 2 / cur["n"]) z = delta / se if se else 0.0 worse = delta > 0 if metric in HIGHER_IS_WORSE else delta < 0 drifted = worse and abs(delta) >= MIN_EFFECT.get(metric, 0.02) and abs(z) > 2 return {"metric": metric, "delta": round(delta, 4), "z": round(z, 1), "drift": drifted} 一个常见错误:用基线标准差除以 √n 来估算标准误,会忽略当前运行自身的样本量和方差。

例如用 50 条实时决策样本与 2400 条基线对比,任何波动都会被误判为漂移。 差异的标准误应同时纳入两次运行的数据:比例指标用合并两样本比例公式,而非直接使用基线存储的标准差。 基线的核心价值在于:某项改动可能高于门禁阈值,却仍显著低于基线——这是门禁无法捕获的信号:

metric baseline current delta
exact-match 0.942 0.913 -0.029 ⚠ regression vs baseline(still > floor, but flagged)

(待续第二段)

当「金标准」遇上真实流量:LLM 评估的工程实践

手动重置基线,而非自动化

当验证得到一个新的已知良好(known-good)结果时,可以重置基线,但绝不能让它自动执行。否则,模型逐渐偏离标准的过程会被悄悄固化为"新常态",问题就此埋下。

影子评估:用真实流量打标

金标准数据集是有限的、人工精选的;而生产环境是无限的、充满意外的。影子评估(Shadow Eval)从真实流量中采样(已脱敏的输入),对实际决策打分,并持续追踪通过率的变化趋势。 代码实现示例:

SHADOW_SAMPLE_RATE = 0.05
def maybe_shadow(decision, sample_rate=SHADOW_SAMPLE_RATE):

![Diagram illustrating "EVALS AS A DEPLOYMENT GATE" with a quality control gate, a strip-chart showing quality drifting downward over releases, and inspection notes.](https://www.wuaishare.cn/wp-content/uploads/2026/10/imported-image-13.webp)

if deterministic_hash(decision["decision_id"]) % 10000 < sample_rate * 10000:

verdict = score_against(rules_or_judge, decision)
 emit("shadow_eval_pass_ratio", 1.0 if verdict.ok else 0.0, slice=decision["slice"])
 if not verdict.ok:

add_to_review_queue(decision)
失败案例会成为新的黄金用例——生产环境的 hard 之处恰好在强化你的测试套件。采样逻辑使用决策 ID 的确定性哈希,而非随机数发生器,以确保采样结果可复现、不会让测试变得不稳定。

早期预警信号:看趋势,别看单点

信号 含义
human_override_rate 上升 人工推翻 AI 决策的频率增加——质量下滑往往先于指标反映出来
置信度分布漂移 置信度趋势性下降(更不确定),或置信度上升而准确率反而下降(校准失效)
judge_disagreement_ratio 上升 第二层模型推翻第一层决策的频率增加
弃权率上升 系统主动放弃(不输出)的比例比以往更高

触发预警后:调查而非自动回滚

  • 确认信号真实:有统计意义(通常 ~2σ 以上),而非噪声。
  • 定位问题范围:是哪个切片(slice)或哪项能力出问题——标签化评估加上分切片指标可以告诉你。
  • 追溯根因:是模型更新?提示词变更?还是输入分布漂移?
  • 通过正常变更流程修复,验证后再重新设置基线。

常见反模式

  • 评估写在 Jupyter Notebook 里:如果评估没有接入 CI、不能让构建失败,它在真正需要时就不会被运行。
  • 对模型置信度下断言而非对正确性下断言:应该衡量「答案是否正确」,而不是「模型有多自信」。
  • 删除已修复的 Bug 用例:它们是回归测试套件,应该永久保留。
  • 只看全局通过率:整体 99% 的通过率可能掩盖某个切片只有 70% 的事实——按切片分别卡控阈值。
  • 只设下限门控:下限只能拦住明显错误,已知良好状态的基线同样必要。
  • 在评估/judge 路径中使用随机采样:导致测试不稳定,用 ID 哈希替代。
  • 自动重置基线:悄悄将漂移固化为新常态。
  • 盯着单点数据而非趋势:单个小时的糟糕数据是噪声;两周的持续下滑才是漂移。

将评估做成门控(Gate),而非 Notebook。具体来说:测试用例作为版本化数据、显式 scorer、CI 命令(回归时让构建失败)、自动路由到对应切片。在此基础上,增加门控无法覆盖的部分——每次运行都和已知良好状态做带统计显著性检验的基线对比;从真实流量中影子采样得到通过率;监控 override/置信度/disagreement 趋势;将生产环境的意外反馈回黄金用例集。

断崖式下跌容易发现;真正的隐患是缓慢泄漏的质量问题——它只有在「关注是否比之前更差」而非「是否低于阈值」时才会暴露。两者都做到,「发布一条提示词变更」就不再是赌博,而是一个绿色的对勾——这是在 LLM 系统上快速迭代而又不悄悄搞坏它的唯一方式。

系列文章:LLM 系统生产级运行实战 — 第 2 篇(共 6 篇)

 
内容管家

发表评论