Stack Overflow Blog

Part 2: Evals as a deployment gate — and how to know when they drift

8.5内容质量

TL;DR · AI 摘要

将评估作为部署门禁可防止模型变更导致质量下降,需结合基线检测与影子评估循环,代码示例展示如何实现自动化评分系统。

核心要点

  • 部署前必须通过评估门禁,否则等同于使用笔记本而非正式测试
  • 评分器需同时校验决策结果和置信度阈值,错误决策必须触发失败
  • 评估数据集应包含历史回归案例,且与代码同存于版本控制中

结构提纲

按章节快速跳转。

  1. §评估门禁的必要性

    传统测试方式无法应对LLM系统的隐性质量下降风险

  2. 需包含常见案例、困难案例和历史回归案例的JSON格式定义

  3. 通过数据类和断言实现决策校验与置信度阈值检测

  4. CI中运行评估命令,当通过率下降时自动阻断部署

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 评估门禁系统
    • 核心组件
      • 结构化数据集
      • 评分器
      • CI集成
    • 检测机制
      • 基线对比
      • 影子评估

金句 / Highlights

值得收藏与分享的关键句。

#LLM#CI/CD#模型评估#部署门禁
打开原文

第2部分:将评估作为部署的门禁 —— 以及如何判断它们何时发生偏移 - Stack Overflow

2026年10月7日

第2部分:将评估作为部署的门禁 —— 以及如何判断它们何时发生偏移

[

如果你能够在评估未触发构建失败的情况下部署提示词变更,那你拥的不是评估系统,而只是一个笔记本。当门禁显示为绿色时,缓慢的泄漏仍然会找上你。这就是门禁、基线和影子评估循环共同发挥作用的机制。

这是成熟度模型的第2层级:评估。核心原则很简单——你不是基于希望进行部署,而是基于门禁进行部署,然后持续监控偏移情况。门禁保护部署的瞬间,偏移检测则保护部署后的数周时间。两者缺一不可,且它们依赖于不同的技术架构。

大多数团队“执行评估”的方式,就像在引入CI之前“执行测试”的方式一样:偶尔、手动、在笔记本中进行,且往往是在问题已经发生之后。对于LLM系统来说,这种方式并不充分,因为最可能悄无声息破坏产品的因素往往不是代码变更,而是提示词调整、模型升级,或看似无害但实际会使8%输入质量下降的温度参数微调。解决方案是将评估视为测试:在CI中运行的门禁,一旦发生回归就触发构建失败。

一个黄金案例是数据

code
{
  "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"]
}

像测试一样维护它:覆盖常见场景、已知困难场景,以及你修复过的每一个历史失败案例(回归案例永远不会被删除)。它与代码一起存放在版本控制系统中,因此对案例集的修改会生成可审查的差异。

评分器

针对每个案例,运行当前提示词/模型并评分结果。保持评分明确且类型化——结合精确断言和容差范围。

code
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:                                                       # symmetric .get on both sides
        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"            # now this CAN actually fire
    return CaseResult(case["id"], case["slice"], all(ok for _, ok in checks), checks)

def test_scorer_catches_regression():                  # sensitivity: it MUST fail a wrong decision
    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中的命令,当通过率(或任何关键指标)低于阈值时会触发构建失败。

code
# .ci/eval-gate.yml  (示例)
eval-gate:
  steps:
    - run: eval run --suite golden --gate --min-pass 0.98 --baseline artifacts/baseline.json
  # 退出码不等于0会失败流水线 → 部署被阻断
code
$ 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), ...

现在,任何改进一个场景但破坏另外两个场景的提示词变更都无法悄无声息地通过审核——作者会在自己的PR中看到问题,而不是三周后由客户发现。

自动将测试用例路由到合适的切片

随着系统规模扩大,“黄金测试集”会演变为针对不同请求类型、用户群体或语言区域的多个子测试套件。不要让人手动选择运行哪个测试套件——为每个测试用例标注所属切片,让执行器根据系统运行时使用的相同属性,自动将测试用例路由到匹配的切片。某个切片的eval命令只会运行属于该切片的测试用例;无需人工筛选,测试内容与实际运行内容不会出现偏差。同时不要使用全局通过率阈值:整体99%的通过率可能掩盖某个切片70%的通过率,因此也要为每个切片单独设置门控。

两种类型的回归

上述门控机制在检测某些问题时非常有效,但对另一类问题却视而不见:

  • 悬崖式下降——变更导致质量急剧且立即恶化。预发布门控可以捕捉这类问题。
  • 缓慢泄漏——三个版本中通过率从0.995逐步降至0.985,每次下降幅度都低于0.98的阈值。门控会放行每个版本,直到问题明显时,该缺陷已经上线五次。

你可能拥有绿色的eval门控,但系统却在悄悄变差。门控只验证你预设的输入条件,在部署时进行检测。而生产环境的变化发生在你身后:输入分布偏移、提供方更新模型、提示词微调可能改善你测试过的用例,却损害未测试的用例。漂移检测正是为应对第二种情况而设计,它需要不同的检测机制。

基线:与已知良好版本对比,而不仅仅是设定下限

门控机制询问的是“是否高于阈值?”漂移检测询问的是“是否比之前更差?”因此需要捕获基线——即最后一次已知良好版本的指标数据,并将每次运行结果与该基线进行对比。

code
// baseline.json — 在确认某个版本为已知良好版本时,需要主动捕获该文件
{
  "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 }
  }
}
code
import math
# 每项指标的最小有意义变化——0.06的指标变化中,0.02的变动具有意义,而对置信度指标则微不足道
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"}  # 对这些指标而言,数值上升表示恶化
python
def compare_to_baseline(metric, cur, base, is_proportion=True):
    # cur/base 是 {"mean":.., "n":.., "std":..}。使用两个运行的 n — 不仅限于基线
    delta = cur["mean"] - base["mean"]
    if is_proportion:                                   # 率指标 → 两比例 z 检验
        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   # 有意义且约2σ朝坏方向
    return {"metric": metric, "delta": round(delta, 4), "z": round(z, 1), "drift": drifted}
python
# 常见错误:用基线标准差除以√n 会忽略当前运行自身的规模和方差 — 用50个实时决策对比2400行基线时,会误将纯噪声标记为异常
# 差值的标准误使用两者数据;比例使用池化两比例形式,而非存储的std

# 改进可能高于阈值但仍显著低于基线 — 这就是门控机制会漏掉的信号:

metric 基线值 当前值 差值 exact-match 0.942 0.913 -0.029 ⚠ 相对于基线的回归(仍高于阈值,但被标记)

code

有意识地重新设定基线(当验证新已知良好模型时),绝不要自动重置,否则漂移会变成新的常态

影子评估:评估实时流量

你的黄金数据集是有限且经过筛选的;而生产环境是无限且充满意外的。影子评估会采样真实的(去标识化)输入,评估实际决策,并跟踪随时间变化的通过率。

code
SHADOW_SAMPLE_RATE = 0.05    # 评估5%的实时决策(配置参数,非硬编码)

def maybe_shadow(decision, sample_rate=SHADOW_SAMPLE_RATE):
    # deterministic_hash = 你的稳定ID哈希;score_against = 你的规则/评委评分入口
    # 使用% 10000(非% 100)使低于1%的采样率不会静默四舍五入为零
    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)                   # 新黄金案例候选
python
# 失败案例会成为新的黄金案例 — 生产环境会在现实最严峻的地方强化你的测试套件。使用ID的确定性哈希而非随机数,确保采样可复现且不会导致测试不稳定

### 早期预警信号(关注趋势而非单点)

信号 漂移征兆 ----------------------------- ---------------------------------------------------------------------------- human_override_rate ↑ 人类更多地推翻代理 — 在指标完全显示之前质量已下滑 置信度分布偏移 趋势向下(更不确定),或向上但准确率下降(校准错误) judge_disagreement_ratio ↑ 第二模型更多地推翻第一模型 弃权率 ↑ 系统比以前更多地回避决策

code

### 触发时 — 调查,不要自动回滚

- 确认是真实问题(有意义且约2σ,而非噪声)
  • 定位问题(哪个切片/功能出现了问题——你的标记评估和每个切片的指标会告诉你答案)。
  • 找出原因(模型更新?提示词变更?输入分布变化?)。
  • 通过正常变更流程修复问题,验证后重新建立基线。

反模式

  • 在笔记本中进行评估——如果不在CI流水线中设置失败构建,关键时刻不会自动运行。
  • 依赖模型置信度而非正确性进行断言——应衡量模型是否真正正确。
  • 删除已修复的缺陷案例——这些是你的回归测试套件,必须永久保留。
  • 使用单一全局通过率——整体99%可能掩盖某个切片70%的低表现,应按切片单独设置门禁。
  • 仅设置下限门禁——你会漏掉缓慢泄露的缺陷,必须同时与已知良好基线进行对比。
  • 在评估/判断路径中使用随机数生成器采样——会导致测试不稳定,应对ID进行哈希处理。
  • 自动重新建立基线——会悄无声息地将数据漂移视为新常态。
  • 关注单点而非趋势——一个坏小时只是噪声,两周的持续下滑才是数据漂移。

核心要点

让评估成为门禁而非笔记本:将案例作为版本化数据,使用显式评分器,设置在CI中触发构建失败的命令,并自动路由到各切片。然后补充门禁无法实现的部分——每次运行都与已知良好基线进行真实显著性检验,将实时流量样本的影子评估纳入通过率指标,监控覆盖/置信度/分歧趋势,并将生产环境的意外情况反馈到黄金数据集。悬崖式下降容易发现;缓慢泄露会损害质量,只有在监控是否比之前更差而非仅仅低于阈值时才会显现。同时进行这两项操作后,“推送提示词变更”将不再是赌博,而是绿色对勾——这是在LLM系统上快速推进而不静默崩溃的唯一方式。

系列:生产环境运行LLM系统——第六级第二部分:评估。

]

这些文章采用知识共享署名-相同方式共享4.0国际许可协议进行授权。

creativecommons.org/licenses/by-sa/4.0/deed.en