当前位置:首页>排行榜>第 12 篇|评测工程化:三层体系 + CI 质量门禁

第 12 篇|评测工程化:三层体系 + CI 质量门禁

  • 更新时间 2026-10-27 14:55:09
第 12 篇|评测工程化:三层体系 + CI 质量门禁

序言 | 本篇要解决什么

前几篇分别讲了埋点、数据集、存储、回灌、Mock、跑批、判分、红队、基线和校准,但工具各自为战,评测还是跑不起来。本篇把它们收拢成一个按成本分层的三层体系,并用四档 CI 流水线让评测自动跑、卡得住、活得久。

学完你能干什么

1. 能说清三层金字塔的分层依据,以及每层为什么选这几个工具

2. 能给自己团队设计分层测试策略(哪些进 PR、哪些只跑 Nightly)

3. 能建立「问题下沉」机制,让同一个 bug 只被逮住一次

4. 能配置四档 CI 触发(PR / Nightly / 版本对比 / 红队),每档有可直接使用的 workflow

5. 能用退出码与阈值做质量门禁,区分「必卡」和「仅告警」

6. 能做好报告归档、失败告警与成本控制,让评测长期活下去

一、方案背景:要解决的两个核心问题

学完前几篇,第一个困惑是:Langfuse、DeepEval、Mock、Inspect、Promptfoo,五个都能测 Agent,到底怎么安排?这是测试怎么组织的问题,答案在第二节的分层策略。

第二个问题更致命:很多团队的评测止步于"测试同学本地跑一遍,发个截图到群里"。三个月后就没人跑了:

问题
后果
靠人记得跑
忙起来就跳过
结果不归档
无法对比趋势
失败无强制
红灯也可以合入
成本不可控
跑几次就超标被叫停

两个问题对应本篇的两条主线:第二~七节用三层金字塔组织测试,第八~十一节用 CI 流水线让评测自动跑、卡得住、活得久。进 CI 的四个价值:自动触发、强制门禁、结果留档、成本可控。

二、总体策略:按成本分层的三层金字塔

答案是按成本分层——越快的测试放越底层,问题尽量在底层拦截:

层
测什么
工具组合
速度
频率
模型
底层:白盒层
循环机制 + 编排决策
Mock + Inspect + pytest
秒级免费
每次 PR
Mock
中层:轨迹层
步骤参数对不对、答案好不好
DeepEval + Inspect
分钟级花钱
每晚
真模型
顶层:效果层
整体表现、安全、成本
Langfuse 跑批 + Promptfoo 红队
30 分钟~小时级
按需 + Nightly 轻量 + 发布前
真模型

金字塔法则:越往下越快越便宜,问题尽量在底层拦截;中顶层发现的每个问题都要下沉成底层用例,防复发。

三、底层白盒层:Mock + Inspect + pytest

3.1 为什么是它们仨(选型推理链)

1. 测点是确定性逻辑(路由、解析、重试、降级、收敛)→ 断言必须确定 → 不能用 judge(裁判有波动,门禁会误报)

2. 不需要模型智力 → 可以 Mock(免费、毫秒级、逐字可复现)

3. 要防死循环和失控 → 需要限额执行引擎 → Inspect(message_limit 卡圈数、cost_limit=0 保证零花费)

4. 剧本回放本质是"给定输入断言输出" → pytest 的 assert 够用,DeepEval 在这层过重

一句话:Mock 出"免费和确定",Inspect 出"安全和限额",pytest 出"断言"。

3.2 具体做法

# ① 剧本    mock = MockLLM(script=[...], fail_on_exhausted=True)    # ② Inspect 挂紧限额    Task(dataset=..., solver=[refund_agent_solver()], scorer=match(), message_limit=8)    # ③ 录像机断言(上下文拼装属「循环机制」层,只有自研 Agent 时才测)    result = agent.run("帮我退掉订单A1029")    assert mock.calls[0]["messages"][0]["role"] == "system"     # 提示词在首位    assert "A1029" in str(mock.calls[0]["messages"])             # 用户输入没丢    assert any(m.get("role") == "tool" for m in mock.calls[1]["messages"])  # 工具结果已回填    # ④ 故障注入测重试    mock.fail_at = 2    result = agent.run("帮我退掉订单A1029")    assert result.retry_count == 2

3.3 能发现的问题与边界

占位符未渲染、工具 schema 与调用格式不一致导致解析崩、工具清单漏传、重试缺失、异常分支没兜住、循环收敛不了。

测不到的:prompt 的语义影响(改措辞让模型行为变了)、答案质量——这些归中层。

四、中层轨迹层:DeepEval + Inspect

4.1 为什么是它们

要真模型了——测"模型 + 编排"合起来走的轨迹和答案质量。

1. 需要丰富判分能力 → DeepEval(对照型 + judge 型 + 六指标)

2. 需要真模型跑批 + 限预算 → 仍用 Inspect 当执行引擎

3. 不用 Langfuse 主判卷:它强在留档对比、弱在断言门禁

Inspect 管"怎么跑",DeepEval 管"怎么判"——引擎与裁判的配合。

4.2 具体做法

# Nightly:真模型 + 预算封顶    inspect eval refund_task.py --model openai/gpt-4o --message-limit 50 --cost-limit 5.0

# 轨迹灌给 DeepEval,混搭判分    tc = LLMTestCase(    input=item.input,    actual_output=run_result.final_answer,    tools_called=[ToolCall(name=t.name, input=t.args) for t in run_result.tool_calls],    expected_tools=item.expected_tools,      # 种子题有,回灌题留空    )    metrics = [    ToolCorrectnessMetric(mode="ordering"),  # 对照型守底线(免费确定)    GEval(name="答案质量", criteria="...", threshold=0.7),  # judge 型判质量    ]    evaluate([tc], metrics)    # 核心题跑 pass^3 测稳定性

4.3 能发现的问题

模型侧漂移、prompt 改动的隐性退化、模型选错工具/传错参数、偶发不稳定(pass^k)、真实工具返回格式变化。

五、顶层效果层:Langfuse + Promptfoo

5.1 为什么是它们

回答管理层的问题:"这版本整体怎么样?敢不敢上?"

1.版本间留档对比 → Langfuse Experiment(分数/成本/延迟自动统计)

2.安全性 → Promptfoo 红队

3. 不用 DeepEval 主判卷:它已在中层干完活,顶层要的是对比视角和安全视角

5.2 具体做法

# 版本对比:同一份 seed-core,两套配置    for cfg in [baseline_cfg, candidate_cfg]:    exp = lf.create_experiment(name=f"compare-{cfg.name}", dataset_name="seed-core")    for item in lf.get_dataset("seed-core").items:        r = agent.run(item.input, model=cfg.model, prompt_version=cfg.prompt_ver)        exp.score(trace_id=r.trace_id, name="quality", value=judge(r))    # 看板:0.71 vs 0.78、成本 $0.021 vs $0.016、延迟 2.3s vs 1.8s

# 红队:发布前深度    npx promptfoo@latest redteam eval -c redteam.yaml -o report.html

5.3 能发现的问题

新版本在真实流量上掉分、成本反升、被打穿越权/泄露、多轮渐进攻破。

六、横切关注点:上下文、记忆、权限、多轮

上下文管理、记忆状态、权限、多轮——这四项是横切关注点,同时存在于每一层:

层
上下文管理
记忆状态
权限
多轮
底层白盒
拼装顺序、截断纯函数、探针保留率
记忆读写/过期接口
权限纯函数矩阵(正反双向)
上下文累积、session 串联
中层轨迹
压缩保真度(judge)
跨会话召回率
拒绝路径是否优雅
降维多轮
(context_turns + 单轮断言)
顶层效果
长对话 token 增长基线
记忆污染/泄露(红队)
越权攻击(红队)
完整多轮
(主战场)

多轮为什么主战场在顶层

理由
说明
需要真实状态
session、记忆只有完整运行环境才真实
成本天然高
5 轮跑 pass^3 = 15 条 trace
判定偏终局
更关心最终完成与体验

降维技巧:用 context_turns + target_turn 结构把多轮变单轮——前 N-1 轮作为上下文灌入,只跑第 N 轮。降维后能进中层甚至 PR,只有"过程体验"(追问时机、话题切换优雅度)必须留在完整多轮里测。

七、问题下沉机制(金字塔的灵魂)

每层中顶层发现的 bug,都要想办法沉到底层变成 Mock 用例。

举例:Nightly 发现某轮询导致剧本耗尽(死循环):

Nightly 报红(跑了很久才失败)    → 定位:agent 停止条件写错    → 下沉:在 Mock 里故意返回"格式略怪的回复",断言"agent 必须在 5 圈内正常结束"    → 结果:此后这类死循环在 PR 门禁就被拦住

步骤
做法
1. 定位
中顶层报错后,找到最小复现条件
2. 构造
用 Mock 剧本复现该条件(不依赖真模型)
3. 断言
写成确定性断言(不用 judge)
4. 入库
加进 PR 门禁用例集

检验标准:同一个 bug 只会被逮住一次。如果一个问题反复出现,说明下沉没做。

八、CI 集成设计:四档触发

三层体系落进 CI,就是四档流水线——每档对应金字塔的一层或一个专项视角:

档
触发
测什么
模型
时长 / 成本
门禁强度
PR 门禁
每次 PR
编排逻辑 + 关键路径
Mock
秒级 / 0
必卡
Nightly 回归
每日定时
轨迹 + 答案质量 + 稳定性
真模型
10~30 分钟 / 几美元
卡(可降级告警)
版本对比
换模型/大改 prompt
新旧版本对比
真模型
30 分钟 / 几美元
不卡(出报告决策)
红队
Nightly 轻量 + 发布前深度
安全攻击
真模型
2~30 分钟 / 几分~几美元
高危必卡

原则:只有免费、快速、确定的测试才配进 PR 必卡档。

九、四档流水线实施

9.1 档一:PR 门禁(免费、秒级、全确定)

# .github/workflows/agent-pr-gate.yml    name: agent-pr-gate    on:    pull_request:    paths: ['agent/**', 'tests/**', 'rules/**']    jobs:    pr-gate:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-python@v5        with: { python-version: '3.11' }      - run: pip install pytest inspect-ai deepeval      # ① 纯函数单测(业务规则,毫秒级)      - name: Unit tests (rules)        run: pytest tests/test_rules.py -q      # ② Mock 白盒跑批(零成本护栏)      - name: Mock orchestration eval        env:          OPENAI_BASE_URL: http://localhost:9000/v1   # 指向本地 Mock server        run: |          python -m mock_server &                     # 起假模型服务          inspect eval tests/refund_task.py \            --model mock/refund-script \            --message-limit 8 \            --cost-limit 0                            # 关键:绝不允许花钱      # ③ 踩坑集回归(确定性断言)      - name: Regression pitfalls        run: pytest tests/test_regression_pitfalls.py -q

三个要点:

1.cost_limit=0 —— 配置错了指向真模型会立刻失败,而不是烧钱

2.message_limit=8 —— 死循环兜底

3.全确定性断言 —— 不用 judge

9.2 档二:Nightly 回归(真模型 + 预算封顶)

# .github/workflows/agent-nightly.yml    name: agent-nightly    on:    schedule:    - cron: '0 2 * * *'    workflow_dispatch:    jobs:    nightly:    runs-on: ubuntu-latest    env:      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}      LANGFUSE_PUBLIC_KEY: ${{ secrets.LF_PK }}      LANGFUSE_SECRET_KEY: ${{ secrets.LF_SK }}    steps:      - uses: actions/checkout@v4      - uses: actions/setup-python@v5        with: { python-version: '3.11' }      - run: pip install inspect-ai deepeval langfuse      # ① 真模型跑批,预算封顶      - name: Run eval        run: |          inspect eval tests/refund_task.py \            --model openai/gpt-4o \            --message-limit 50 \            --cost-limit 5.0                    # 单晚预算上限      # ② DeepEval 判分(对照型 + judge 混搭)      - name: Score with DeepEval        run: python tests/score_nightly.py      # ③ 红线校验(比对基线,跌破即失败)      - name: Check baseline        run: python tests/check_baseline.py --config baselines.yaml      # ④ 报告归档      - name: Upload report        if: always()        uses: actions/upload-artifact@v4        with: { name: nightly-report, path: reports/ }

红线校验脚本(跌破基线即 CI 红):

# check_baseline.py    import sys, yaml, json    cfg = yaml.safe_load(open("baselines.yaml"))    scores = json.load(open("reports/scores.json"))    failed = []    for name, rule in cfg["baselines"]["metrics"].items():    actual = scores[name]    if actual < rule["red_line"]:        failed.append(f"{name}: {actual} < 红线 {rule['red_line']}")    if failed:    print("\n".join(failed))    sys.exit(1)          # 退出码非 0 → CI 红    print("全部指标在红线以上")

9.3 档三:版本对比(不卡,出报告供决策)

# .github/workflows/agent-compare.yml    name: agent-compare    on:    workflow_dispatch:    inputs:      candidate_model: { description: '候选模型', required: true }    jobs:    compare:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - run: pip install langfuse deepeval      - name: Run both configs        run: |          python tests/run_experiment.py --name baseline  --model gpt-4o-0715  --prompt v1.2          python tests/run_experiment.py --name candidate --model ${{ inputs.candidate_model }} --prompt v1.3      - name: Report        run: python tests/compare_report.py    # 输出分数/成本/延迟对比      - uses: actions/upload-artifact@v4        with: { name: compare-report, path: compare.html }

不设门禁——版本对比是决策输入,人来拍板;强行卡住会阻碍升级。

9.4 档四:红队(分层配置)

# PR 冒烟:2 个高危插件 + basic 策略 + numTests 1(几分钱)    promptfoo redteam eval -c redteam.pr.yaml --no-cache    # 发布前深度:全插件 + 越狱/多轮(几块钱)    promptfoo redteam eval -c redteam.deep.yaml -o report.html --maxConcurrency 4

靶子必须 staging:

env:    STAGING_URL: http://staging.internal:8000   # 硬编码,禁止生产地址

# CI 前置校验:防止靶子被误配成生产    def test_target_is_staging():    assert "prod" not in os.environ["STAGING_URL"], "红队靶子不得指向生产!"

十、质量门禁:必卡与告警的分级

不是所有失败都该阻断合入:

失败类型
强度
理由
确定性断言失败(对照型、纯函数)
必卡
100% 是 bug
judge 型连续 2 次跌破红线
必卡
排除波动误报
judge 型单次跌破
告警
可能是波动
成本超基线 20%
告警
需人工判断是否接受
红队高危项被打穿
必卡
安全零容忍
红队中危项
告警 + 记录
排期修

# judge 型防误报:连续 2 次失败才卡    history = load_last_n_runs(2)    if all(run.below_red_line for run in history):    sys.exit(1)      # 连续两次 → 卡    else:    print("警告:单次跌破,可能是波动")   # 只告警

十一、长期运营:报告归档、告警与成本控制

11.1 报告归档与告警

机制
做法
报告归档
每次跑批上传 artifact,保留 90 天
趋势看板
每日分数写入 Langfuse,看 30 天趋势
P0 告警
错误率突增 / 分数跌破红线 → 立即通知
P1 告警
连续 3 天趋势下滑 → 当天排查
P2
单条差评 → 周度归集

11.2 成本控制(让评测长期活下去)

手段
说明
PR 档 Mock 化
成本 0
cost_limit 封顶
每档设预算上限,超了自动停
分层采样
回灌只采 100%/50%/1%
judge 抽样
在线评估 1% 抽样
便宜模型当裁判
攻击者/裁判用 gpt-4o-mini 级别
缓存
相同输入复用结果(注意 --no-cache 只在需要时用)

十二、协作分工与方案边界

12.1 需要开发配合什么

#
配合项
频率
1
测试环境 Agent 常驻可用(副作用打桩版)
持续
2
模型/prompt 版本可查询、可切换配置(顶层对比用)
一次性建设,持续使用
3
模型 key 与预算申请
持续(按档位)
4
新增工具登记(影响各层期望轨迹)
新工具时
5
提供 staging 靶子环境(顶层红队用)
一次性
6
网关支持 Shadow 流量复制 + 一键回滚开关
一次性

12.2 这套方案能做什么、不能做什么

能做
不能做
自动触发、强制门禁、结果留档
用例质量本身
成本可控、长期可持续
判分准确性
失败告警与响应流程
问题根因定位(靠 trace 下钻)

十三、避坑清单

#
坑
后果
正确做法
1
所有测试堆在顶层,PR 档用真模型
慢、贵、反馈晚、不稳定
按成本分层,PR 档 Mock + cost_limit=0
2
用 judge 做 PR 门禁,单次失败就卡
波动导致误报,团队关掉门禁
门禁只用确定性断言;judge 连续 2 次跌破才卡
3
多轮全塞进中层
中层又慢又贵
降维下沉,完整多轮归顶层
4
框架场景还测循环机制
测框架、不测自己
①循环机制归框架、③模型行为归中顶层,只测②编排决策
5
发现问题不下沉
同一个 bug 反复出现
建立下沉流程
6
三层各写各的用例
重复覆盖、遗漏盲区
统一数据集
7
不设超时
流水线挂死
time_limit + job timeout
8
CI 里没锁模型版本
模型更新导致分数突变无法归因
写死版本号(不用 latest)
9
红队靶子配成生产
真调工具、真泄露
硬编码 staging + 前置校验
10
报告不归档
无法看趋势
artifact 保留 90 天
11
沙箱断网导致通信失败
评测自己跑不起来
白名单放行 mock server 与模型 API

十四、落地自检与自测

14.1 分层自检清单

#
检查项
通过标准
1
底层用例是否全免费、全确定
无 judge、无真模型调用
2
底层是否覆盖循环机制与编排决策
自研时两者都测;框架时只测后者
3
中层是否混搭对照型与 judge 型
有标准答案的用对照型
4
顶层是否有版本对比与安全两件事
Langfuse + Promptfoo 都配了
5
横切四项是否每层都有测点
上下文/记忆/权限/多轮
6
是否有问题下沉记录
每个中顶层 bug 都对应一条底层用例
7
成本是否分层控制
PR 档 cost_limit=0

14.2 自测清单(10 题)

1. 三层的分层依据是什么?为什么这么分而不是按工具分?

2. 底层为什么不能用 judge?哪类断言可以进 PR?

3. 框架场景下,循环机制层要不要测?为什么?

4. 多轮测试的主战场在哪层?降维技巧是什么、能下沉到哪?

5. Nightly 抓到一个死循环 bug,怎么把它下沉成 PR 门禁用例?

6. 四档 CI 触发各自测什么?为什么只有 PR 档能"必卡"?

7. PR 档为什么要设 cost_limit=0?它防的是什么?

8. judge 型指标为什么建议"连续 2 次失败才卡"?

9. 红队靶子配成生产会怎样?怎么在 CI 里防止?

10. 评测要长期存活,成本控制有哪六个手段?

十五、小结(系列收官)

1. 三层按成本分层:白盒(秒级免费)/ 轨迹(分钟级)/ 效果(30 分钟~小时级),每层工具组合有明确推理链

2. 上下文/记忆/权限/多轮是横切维度,每层都有测点

3.问题下沉是金字塔的灵魂:同一个 bug 只被逮住一次

4. 四档触发:PR(免费必卡)/ Nightly(真模型)/ 版本对比(决策用)/ 红队(分层)

5. 门禁分级:确定性必卡,judge 连续 2 次才卡,安全高危必卡

6. 报告归档 + 告警分级 + 成本控制,让评测长期存活

7.没有进 CI 的评测等于没做——这是整套方案能否落地的最后一公里

关注我,私信发自测清单

一份可直接拿去评审的自测表 · 关注后私信即领

点击关注

        若有干扰,可在「设置」关闭悬浮球
        👋 Hi,想聊点什么~
        内容由AI生成,仅供参考

        随机文章