前几篇分别讲了埋点、数据集、存储、回灌、Mock、跑批、判分、红队、基线和校准,但工具各自为战,评测还是跑不起来。本篇把它们收拢成一个按成本分层的三层体系,并用四档 CI 流水线让评测自动跑、卡得住、活得久。
1. 能说清三层金字塔的分层依据,以及每层为什么选这几个工具
2. 能给自己团队设计分层测试策略(哪些进 PR、哪些只跑 Nightly)
3. 能建立「问题下沉」机制,让同一个 bug 只被逮住一次
4. 能配置四档 CI 触发(PR / Nightly / 版本对比 / 红队),每档有可直接使用的 workflow
5. 能用退出码与阈值做质量门禁,区分「必卡」和「仅告警」
6. 能做好报告归档、失败告警与成本控制,让评测长期活下去
学完前几篇,第一个困惑是:Langfuse、DeepEval、Mock、Inspect、Promptfoo,五个都能测 Agent,到底怎么安排?这是测试怎么组织的问题,答案在第二节的分层策略。
第二个问题更致命:很多团队的评测止步于"测试同学本地跑一遍,发个截图到群里"。三个月后就没人跑了:
两个问题对应本篇的两条主线:第二~七节用三层金字塔组织测试,第八~十一节用 CI 流水线让评测自动跑、卡得住、活得久。进 CI 的四个价值:自动触发、强制门禁、结果留档、成本可控。
答案是按成本分层——越快的测试放越底层,问题尽量在底层拦截:
| | | | | |
| 底层:白盒层 | | | | | |
| 中层:轨迹层 | | | | | |
| 顶层:效果层 | | Langfuse 跑批 + Promptfoo 红队 | | | |
金字塔法则:越往下越快越便宜,问题尽量在底层拦截;中顶层发现的每个问题都要下沉成底层用例,防复发。
三、底层白盒层:Mock + Inspect + pytest
1. 测点是确定性逻辑(路由、解析、重试、降级、收敛)→ 断言必须确定 → 不能用 judge(裁判有波动,门禁会误报)
2. 不需要模型智力 → 可以 Mock(免费、毫秒级、逐字可复现)
3. 要防死循环和失控 → 需要限额执行引擎 → Inspect(message_limit 卡圈数、cost_limit=0 保证零花费)
4. 剧本回放本质是"给定输入断言输出" → pytest 的 assert 够用,DeepEval 在这层过重
一句话:Mock 出"免费和确定",Inspect 出"安全和限额",pytest 出"断言"。
# ① 剧本 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
占位符未渲染、工具 schema 与调用格式不一致导致解析崩、工具清单漏传、重试缺失、异常分支没兜住、循环收敛不了。
测不到的:prompt 的语义影响(改措辞让模型行为变了)、答案质量——这些归中层。
四、中层轨迹层:DeepEval + Inspect
要真模型了——测"模型 + 编排"合起来走的轨迹和答案质量。
1. 需要丰富判分能力 → DeepEval(对照型 + judge 型 + 六指标)
2. 需要真模型跑批 + 限预算 → 仍用 Inspect 当执行引擎
3. 不用 Langfuse 主判卷:它强在留档对比、弱在断言门禁
Inspect 管"怎么跑",DeepEval 管"怎么判"——引擎与裁判的配合。
# 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 测稳定性
模型侧漂移、prompt 改动的隐性退化、模型选错工具/传错参数、偶发不稳定(pass^k)、真实工具返回格式变化。
五、顶层效果层:Langfuse + Promptfoo
回答管理层的问题:"这版本整体怎么样?敢不敢上?"
1.版本间留档对比 → Langfuse Experiment(分数/成本/延迟自动统计)
3. 不用 DeepEval 主判卷:它已在中层干完活,顶层要的是对比视角和安全视角
# 版本对比:同一份 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
新版本在真实流量上掉分、成本反升、被打穿越权/泄露、多轮渐进攻破。
上下文管理、记忆状态、权限、多轮——这四项是横切关注点,同时存在于每一层:
降维技巧:用 context_turns + target_turn 结构把多轮变单轮——前 N-1 轮作为上下文灌入,只跑第 N 轮。降维后能进中层甚至 PR,只有"过程体验"(追问时机、话题切换优雅度)必须留在完整多轮里测。
每层中顶层发现的 bug,都要想办法沉到底层变成 Mock 用例。
举例:Nightly 发现某轮询导致剧本耗尽(死循环):
Nightly 报红(跑了很久才失败) → 定位:agent 停止条件写错 → 下沉:在 Mock 里故意返回"格式略怪的回复",断言"agent 必须在 5 圈内正常结束" → 结果:此后这类死循环在 PR 门禁就被拦住
检验标准:同一个 bug 只会被逮住一次。如果一个问题反复出现,说明下沉没做。
三层体系落进 CI,就是四档流水线——每档对应金字塔的一层或一个专项视角:
原则:只有免费、快速、确定的测试才配进 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 —— 死循环兜底
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/ }
# 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("全部指标在红线以上")
# .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 }
不设门禁——版本对比是决策输入,人来拍板;强行卡住会阻碍升级。
# 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
env: STAGING_URL: http://staging.internal:8000 # 硬编码,禁止生产地址
# CI 前置校验:防止靶子被误配成生产 def test_target_is_staging(): assert "prod" not in os.environ["STAGING_URL"], "红队靶子不得指向生产!"
# judge 型防误报:连续 2 次失败才卡 history = load_last_n_runs(2) if all(run.below_red_line for run in history): sys.exit(1) # 连续两次 → 卡 else: print("警告:单次跌破,可能是波动") # 只告警
| |
| |
| |
| |
| |
| |
| 相同输入复用结果(注意 --no-cache 只在需要时用) |
| | |
| | |
| 模型/prompt 版本可查询、可切换配置(顶层对比用) | |
| | |
| | |
| | |
| 网关支持 Shadow 流量复制 + 一键回滚开关 | |
| | | |
| | | 按成本分层,PR 档 Mock + cost_limit=0 |
| | | 门禁只用确定性断言;judge 连续 2 次跌破才卡 |
| | | |
| | | ①循环机制归框架、③模型行为归中顶层,只测②编排决策 |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | 白名单放行 mock server 与模型 API |
1. 三层的分层依据是什么?为什么这么分而不是按工具分?
2. 底层为什么不能用 judge?哪类断言可以进 PR?
4. 多轮测试的主战场在哪层?降维技巧是什么、能下沉到哪?
5. Nightly 抓到一个死循环 bug,怎么把它下沉成 PR 门禁用例?
6. 四档 CI 触发各自测什么?为什么只有 PR 档能"必卡"?
7. PR 档为什么要设 cost_limit=0?它防的是什么?
8. judge 型指标为什么建议"连续 2 次失败才卡"?
9. 红队靶子配成生产会怎样?怎么在 CI 里防止?
1. 三层按成本分层:白盒(秒级免费)/ 轨迹(分钟级)/ 效果(30 分钟~小时级),每层工具组合有明确推理链
2. 上下文/记忆/权限/多轮是横切维度,每层都有测点
3.问题下沉是金字塔的灵魂:同一个 bug 只被逮住一次
4. 四档触发:PR(免费必卡)/ Nightly(真模型)/ 版本对比(决策用)/ 红队(分层)
5. 门禁分级:确定性必卡,judge 连续 2 次才卡,安全高危必卡
6. 报告归档 + 告警分级 + 成本控制,让评测长期存活
7.没有进 CI 的评测等于没做——这是整套方案能否落地的最后一公里