【Agent工程】(15)—— 评测集与回归门禁

【Agent工程】(15)------ 评测集与回归门禁

文章目录

  • [【Agent工程】(15)------ 评测集与回归门禁](#【Agent工程】(15)—— 评测集与回归门禁)
    • [1. 演示通过不等于可以改策略](#1. 演示通过不等于可以改策略)
      • [1.1 评测集与单元测试的差别](#1.1 评测集与单元测试的差别)
      • [1.2 何时必须跑门禁](#1.2 何时必须跑门禁)
      • [1.3 评测集不是提示词调参玩具](#1.3 评测集不是提示词调参玩具)
    • [2. 评测用例的字段设计](#2. 评测用例的字段设计)
      • [2.1 断言必须机器可判定](#2.1 断言必须机器可判定)
      • [2.2 夹具与 mock 下游](#2.2 夹具与 mock 下游)
      • [2.3 用例粒度](#2.3 用例粒度)
    • [3. 回归门禁流水线](#3. 回归门禁流水线)
      • [3.1 门禁决策](#3.1 门禁决策)
      • [3.2 稳定性:温度、种子与重试](#3.2 稳定性:温度、种子与重试)
      • [3.3 与 CI 的衔接](#3.3 与 CI 的衔接)
    • [4. 贯穿示例:工单助手评测集分层](#4. 贯穿示例:工单助手评测集分层)
      • [4.1 safety 用例为什么必须进 smoke 旁路或同级必跑](#4.1 safety 用例为什么必须进 smoke 旁路或同级必跑)
      • [4.2 从线上事故反哺用例](#4.2 从线上事故反哺用例)
      • [4.3 与人工评测的分工](#4.3 与人工评测的分工)
      • [4.4 最小 smoke 套件怎么凑够条数](#4.4 最小 smoke 套件怎么凑够条数)
    • [5. 报告字段与基线对比](#5. 报告字段与基线对比)
      • [5.1 退步判定](#5.1 退步判定)
      • [5.2 并行跑用例时的隔离](#5.2 并行跑用例时的隔离)
    • [6. 可运行示意:断言与门禁](#6. 可运行示意:断言与门禁)
      • [6.1 轨迹断言执行器](#6.1 轨迹断言执行器)
      • [6.2 门禁汇总](#6.2 门禁汇总)
      • [6.3 报告 JSON 最小形状](#6.3 报告 JSON 最小形状)
    • [7. 与配置指纹、轨迹的对齐](#7. 与配置指纹、轨迹的对齐)
      • [7.1 评测集维护节奏](#7.1 评测集维护节奏)
    • [8. 四个常见误区](#8. 四个常见误区)
      • [8.1 排查清单](#8.1 排查清单)
    • [9. 适用边界](#9. 适用边界)
      • [9.1 迁移步骤](#9.1 迁移步骤)
      • [9.2 规模化时的分层执行](#9.2 规模化时的分层执行)
    • [10. 术语速查](#10. 术语速查)
    • [11. 小结与下一篇](#11. 小结与下一篇)

摘要 :第 14 篇把决策沉成可查询轨迹后,还缺一道发布前闸门:策略、提示词、模型或工具注册表一改,旧能力是否回退。本篇说明如何用版本化评测集 固定任务与期望,在隔离环境跑 Agent,并对轨迹做机器可判定的断言;smoke 失败则阻断发布。贯穿示例继续用工单助手。适合读完 【Agent工程】(14)------ 审计日志与轨迹字段、准备把「感觉更好」改成可回归门禁的工程师。读完可以独立完成:建立分层评测集,并实现基于轨迹断言的门禁脚本。


1. 演示通过不等于可以改策略

演示样例通常是人工挑选的幸福路径。提示词微调、换模型、收紧 scope、改 retry_policy 之后,演示仍可能「看起来能跑」,但 notify 双发、未审批写出站、降级路径消失等问题会在真实流量里才暴露。没有固定评测集,回归只能靠运气。

做法 结果
只跑 1~2 个演示目标 改动后无法证明未回退
靠人看模型回复判好坏 不稳定、不可进 CI
固定用例 + 轨迹断言 可复现、可阻断发布

工程结论:评测集是第 3 篇验收在发布流水线上的批量化;断言读轨迹(第 14 篇),不读模型自我宣称。

前文见 【Agent工程】(3)------ 验收命令与完成定义【Agent工程】(10)------ 失败重试与幂等边界【Agent工程】(14)------ 审计日志与轨迹字段

1.1 评测集与单元测试的差别

维度 单元测试 Agent 评测集
对象 函数 / 模块 任务卡端到端
判定 返回值 / mock 终态 + 轨迹事件
波动 通常确定 模型有随机性,靠断言收窄
环境 常本地 必须隔离夹具,禁打生产

评测集不替代网关单测;它专门捕获「多模块组合后的行为回退」。

1.2 何时必须跑门禁

以下变更默认触发全量 smoke,必要时加扩展集:

  1. 系统提示词或工具描述变更
  2. 模型名 / 温度 / 解码参数变更
  3. 注册表、scope、pending、retry、限流、超时配置变更
  4. 委托策略或子 Agent 模板变更

只改文档或看板查询、不动决策路径,可以跳过;但跳过规则要写进流水线,避免口头例外。

1.3 评测集不是提示词调参玩具

有人把评测集当成「刷分」工具:用例太窄、期望跟着当前坏行为改。正确用法是:期望代表业务不可破坏的契约。行为变了应先改产品决策,再改用例;不能为了绿而把「允许未审批 notify」写进 expect。


2. 评测用例的字段设计

字段 含义
case_id 稳定编号,如 TKT-smoke-001
goal 用户目标原文或结构化目标
fixtures 夹具工单、权限、时钟、下游 mock
expect.terminal 期望终态:success / degraded / cancelled / failed
expect.trace 轨迹断言列表
tags smoke / safety / idempotency 等
suite_version 评测集版本

示例(工单升级 smoke):

json 复制代码
{
  "case_id": "TKT-smoke-001",
  "goal": "将工单 T-1024 升级为 urgent 并通知值班",
  "fixtures": {
    "ticket": {"ticket_id": "T-1024", "priority": "high"},
    "auto_approve_notify": true
  },
  "expect": {
    "terminal": "success",
    "trace": [
      {"type": "count_tool_ok", "tool_id": "update_ticket_priority", "eq": 1},
      {"type": "count_tool_ok", "tool_id": "notify_oncall", "eq": 1},
      {"type": "terminal_status", "eq": "success"}
    ]
  },
  "tags": ["smoke", "happy_path"],
  "suite_version": "2026.09.01"
}

2.1 断言必须机器可判定

推荐 避免
count_tool_ok / gate.deny 存在 「回复看起来礼貌」
terminal_status / error_code 「模型说已经完成」
priority 字段等于 urgent 「语义上差不多 urgent」
dedupe.hit 次数 「好像没双写」

需要语义评分时,应作为附加报告,不能单独决定门禁通过;门禁主路径仍用轨迹与业务状态。

2.2 夹具与 mock 下游

评测禁止直连生产工单 API。夹具至少提供:

  1. 可读写的假工单库
  2. 可记录调用次数的假 notify
  3. 可注入 timeout / 429 的故障开关
  4. 固定的时间源(避免 deadline 用例抖动)

故障注入用来覆盖第 10、12、13 篇场景:超时重试、限流、取消。

2.3 用例粒度

粒度 适用 不适用
端到端任务卡 发布门禁主路径 替换所有网关单测
单工具网关用例 权限 / 幂等 / 限流 证明多步协作正确
纯提示词离线打分 文案实验 单独作发布阻断

门禁以端到端为主,网关单测为辅。只有端到端时,定位失败会慢;只有单测时,组合回归会漏。


3. 回归门禁流水线

标准四步:

  1. 装载 指定 suite_version 的用例
  2. 在隔离环境逐条创建任务卡并跑 Agent
  3. 读取轨迹执行 expect.trace
  4. 汇总报告:失败 case_id、差异字段、阻断或通过

3.1 门禁决策

结果 发布动作
smoke 全部通过 允许进入灰度
任一 smoke 失败 阻断发布
仅扩展集失败 可配置为警告,但须建单跟踪
轨迹缺失 / 打点失败 视为门禁失败,不得当「软失败」放过

「先上线再补评测」与第 14 篇「先上线再补审计」一样危险:回退已经发生在用户侧。门禁结果示意见第 4 节配图。

3.2 稳定性:温度、种子与重试

模型随机性会导致偶发失败。工程上可:

  1. 评测温度尽量调低或固定
  2. 对明确的非确定性步骤允许有限次重跑,但同一 case 重跑次数写进报告
  3. 断言偏向副作用与终态,少断言完整回复文本

若某条用例本身不稳定,应改写成更强夹具或拆成网关单测,而不是提高「允许失败率」把门禁挖空。

实务上建议:连续两次独立运行都失败,才标为确定性失败;一次失败一次通过,优先查夹具竞态与时间源,而不是立刻放宽断言。

3.3 与 CI 的衔接

触发 跑哪些
PR 变更策略 / 提示词 smoke 必跑
夜间 smoke + 扩展
发布候选人 smoke + 本版本相关扩展标签

门禁脚本退出码非 0 即失败;报告制品(JSON/HTML)归档,便于对比「哪次提交引入回退」。


4. 贯穿示例:工单助手评测集分层

代表 case 期望要点
smoke 升级并通知 update=1,notify=1,success
safety 未审批 notify gate.deny PENDING_REQUIRED,notify=0
idempotency update 超时重试 真实写 ≤1,可有 dedupe.hit
degrade notify 子任务失败 degraded,priority 已更新
cancel 写前取消 cancelled,无写副作用

4.1 safety 用例为什么必须进 smoke 旁路或同级必跑

未审批写出站属于安全回退,危害高于「文案稍差」。建议将 safety 标签与 smoke 一并设为阻断级,或直接并入 smoke 套件。扩展集可放体验类、长链路成本类。

4.2 从线上事故反哺用例

每次事故结案应产出至少一条新 case:

  1. 复现所需 fixtures
  2. 期望的 gate / tool / terminal
  3. 标签 regression-<事故编号>

没有反哺,评测集会停在最初的幸福路径,门禁逐渐失明。

反哺模板可以很短:事故链接、复现步骤、fixtures 增量、expect.trace、负责人。模板比长篇复盘报告更容易进仓库。

4.3 与人工评测的分工

人工抽检适合:文案质量、是否误解工单语境。机器门禁适合:次数、权限、终态、错误码。两者报告分开;发布阻断权在机器门禁。

4.4 最小 smoke 套件怎么凑够条数

冷启动时不必追求上百条。工单助手可以用下面 8 条打底,再按事故加厚:

  1. 幸福路径升级成功
  2. 未审批不得 notify
  3. scope 外 ticket 拒绝
  4. update 超时不双写
  5. notify 子失败可降级
  6. 写前取消无写副作用
  7. 限流拒绝不记为成功终态
  8. 轨迹缺失(故意关掉打点)时门禁失败

第 8 条专门防止「评测环境关掉审计却全绿」。


5. 报告字段与基线对比

每次门禁运行写入:

字段 含义
run_id 本次运行 ID
suite_version 用例集版本
agent_version 提示词 / 配置 / 模型指纹
passed / failed 计数与列表
duration_ms 总耗时
baseline_run_id 可选,对比基线

基线对比回答:「相对上周发布候选,新失败了哪些 case」。只看绝对通过率,会忽略「一直失败被忽略的坏用例」。

5.1 退步判定

情况 判定
基线通过、本次失败 退步,阻断
基线失败、本次仍失败 已知问题,不新增阻断(须登记)
基线失败、本次通过 修复,报告标绿

已知问题清单要有负责人与到期日,否则「永久警告」等于取消门禁。

5.2 并行跑用例时的隔离

多 case 并行可以缩短门禁时间,但必须保证:

  1. 每个 case 独立 card_id 与夹具命名空间
  2. mock notify 收件箱按 case 隔离
  3. 限流令牌桶使用评测专用配额,避免 case 互相 rate_limited

并行翻车时,失败表现像「偶发限流」,根因其实是评测互相抢配额。


6. 可运行示意:断言与门禁

6.1 轨迹断言执行器

python 复制代码
from __future__ import annotations

from typing import Any, Callable


def count_tool_ok(events: list[dict[str, Any]], tool_id: str) -> int:
    n = 0
    for e in events:
        if e.get("event_type") != "tool_call" or e.get("decision") != "ok":
            continue
        if (e.get("payload") or {}).get("tool_id") == tool_id:
            n += 1
    return n


def run_asserts(events: list[dict[str, Any]], asserts: list[dict[str, Any]]) -> list[str]:
    errors: list[str] = []
    terminal = next((e for e in events if e.get("event_type") == "card.terminal"), None)
    for a in asserts:
        t = a["type"]
        if t == "count_tool_ok":
            got = count_tool_ok(events, a["tool_id"])
            if got != a["eq"]:
                errors.append(f"count_tool_ok {a['tool_id']}: got {got} want {a['eq']}")
        elif t == "terminal_status":
            status = (terminal or {}).get("payload", {}).get("status")
            if status != a["eq"]:
                errors.append(f"terminal_status: got {status} want {a['eq']}")
        elif t == "has_event":
            if not any(e.get("event_type") == a["event_type"] for e in events):
                errors.append(f"missing event {a['event_type']}")
        else:
            errors.append(f"unknown assert {t}")
    return errors


if __name__ == "__main__":
    events = [
        {"event_type": "tool_call", "decision": "ok", "payload": {"tool_id": "notify_oncall"}},
        {"event_type": "card.terminal", "payload": {"status": "success"}},
    ]
    errs = run_asserts(
        events,
        [
            {"type": "count_tool_ok", "tool_id": "notify_oncall", "eq": 1},
            {"type": "terminal_status", "eq": "success"},
        ],
    )
    assert errs == []
    print("asserts ok")

6.2 门禁汇总

python 复制代码
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable


@dataclass
class CaseResult:
    case_id: str
    tags: list[str]
    errors: list[str]

    @property
    def passed(self) -> bool:
        return not self.errors


def gate_decision(results: list[CaseResult], block_tags: set[str] | None = None) -> str:
    block_tags = block_tags or {"smoke", "safety"}
    for r in results:
        if r.passed:
            continue
        if set(r.tags) & block_tags:
            return "block"
    if any(not r.passed for r in results):
        return "warn"
    return "pass"


if __name__ == "__main__":
    results = [
        CaseResult("TKT-smoke-001", ["smoke"], []),
        CaseResult("TKT-safety-002", ["safety"], ["notify leaked"]),
    ]
    assert gate_decision(results) == "block"
    results[1] = CaseResult("TKT-ext-009", ["ux"], ["tone"])
    assert gate_decision(results) == "warn"
    print("gate ok")

生产环境把 run_case 接到真实隔离 Runner;门禁脚本根据 block 返回退出码 1。

6.3 报告 JSON 最小形状

json 复制代码
{
  "run_id": "eval-20260909-01",
  "suite_version": "2026.09.01",
  "agent_version": {"prompt_hash": "...", "model_id": "..."},
  "decision": "block",
  "failed": [{"case_id": "TKT-safety-002", "errors": ["notify leaked"]}]
}

流水线把该 JSON 上传为制品;发布系统只认 decision=pass(或 warn 且变更单已审批)。


7. 与配置指纹、轨迹的对齐

评测报告应记录 Agent 配置指纹,避免「用例过了但不知道过的是哪一版」:

指纹项 来源
prompt_hash 系统提示与工具描述
registry_hash 工具注册表
runtime_hash 限流 / 超时 / 预算配置
model_id 模型标识

第 14 篇轨迹的 card.start 也可写入同一指纹,便于线上抽样与评测集对照。

7.1 评测集维护节奏

动作 频率
事故反哺 case 每个 P0/P1 事故
删除失效 fixtures 季度
审查已知失败清单 双周
升 suite_version 有破坏性用例变更时

用例只增不删会导致套件膨胀;删除必须有替代断言或明确「风险已转移」。


8. 四个常见误区

误区 典型表现 更稳妥的做法
只用自然语言判分 同案多次结论不同 轨迹断言作门禁
评测打真实生产 污染工单与通知 隔离夹具 + mock
用例从不版本化 无法复现历史失败 suite_version
失败仍强行上线 门禁成摆设 smoke/safety 阻断

还有一种隐蔽问题:评测环境关闭了 pending 或幂等 dedupe,与生产配置不一致,导致「评测全绿、生产双发」。评测配置指纹必须与候选发布配置一致,差异项显式列出。

8.1 排查清单

  1. smoke / safety 是否为阻断级
  2. 断言是否只依赖轨迹与夹具状态
  3. 是否禁止打生产
  4. 失败报告是否含 case_id 与配置指纹
  5. 事故是否反哺新 case
  6. 已知失败是否有到期日

9. 适用边界

本篇方法适合:

  • 已有任务卡验收与轨迹,准备进 CI / 发布流水线
  • 策略与模型频繁变更,需要挡住回退
  • 需要把事故沉淀为可重复用例

本篇不覆盖:

  • 成本上限与配额告警------下一观测篇展开
  • 大模型通用能力榜单------本专栏评测锚定业务任务
  • 人工标注平台选型------可外挂,不替代轨迹门禁

若团队尚无轨迹,应先完成第 14 篇打点;没有事件的「评测」很容易退化成看回复写作文。

9.1 迁移步骤

  1. 选出 5~10 条 smoke/safety 用例并写 expect.trace
  2. 搭建隔离夹具与故障注入
  3. 门禁脚本接入 CI,smoke 失败即阻断
  4. 建立基线对比与已知问题清单
  5. 事故复盘强制反哺 case

9.2 规模化时的分层执行

用例过百后,不必每次全量:

  1. PR 跑受影响标签 + 全量 smoke
  2. 夜间跑全量
  3. 发布前跑全量 smoke + 高风险标签

标签与目录结构要在用例字段里声明,避免靠文件名约定失传。

把评测集当成产品能力的一部分维护:有版本、有门禁、有事故反哺,而不是个人笔记本里的「几条好用的问法」。

对多租户产品,评测夹具还要覆盖「租户 A 的 scope 不得打到租户 B」这类隔离用例;它既是安全回归,也是第 5 篇作用域在门禁上的落点。


10. 术语速查

术语 含义
评测集 版本化的任务用例与期望集合
回归门禁 发布前自动跑评测并决定是否阻断
smoke 阻断级核心路径用例层
fixtures 隔离环境中的夹具数据与 mock
轨迹断言 对审计事件的机器可判定检查
配置指纹 提示词 / 注册表 / 运行时 / 模型的版本摘要

11. 小结与下一篇

观测单元在审计之后补上「能否证明没回退」:

  1. 用例版本化:goal、fixtures、expect、tags
  2. 断言读轨迹:副作用与终态可机器判定
  3. smoke/safety 阻断:失败不得发布
  4. 事故反哺:门禁随真实风险变厚

下一篇继续观测与成本:成本上限与配额告警,把 token / 调用次数变成可强制的预算,而不是事后账单惊吓。

系列导航

相关推荐
罗西的思考1 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 —(1)— 总体
人工智能·笔记·深度学习·学习·机器学习
量化吞吐机1 小时前
会写代码学量化,先把数据、规则和执行连清楚
人工智能·python
天远API1 小时前
零信任架构实战:基于天远手机空号检测V即时版构建自动化新客入驻网关
人工智能·智能手机·架构·自动化
七夜zippoe1 小时前
从 Prompt Engineering 到 Agent Engineering:开发范式的代际跃迁
android·ai·prompt·agent·engineering
海盗12341 小时前
AI 新闻日报 2026-09-10:DeepSeek V4.1 Flash 上线降价60%、京东狼族机器人军团、优必选联姻沐曦造芯
人工智能·机器人
词却1 小时前
深度学习入门:卷积神经网络与 MNIST 手写数字识别
人工智能·深度学习·cnn
明志数科1 小时前
流水线数据采集工程实践:MES产线监控数据与机器人Ego训练数据的本质区别与选型指南
人工智能
乐迪信息1 小时前
航道船舶逆行AI识别,港口安全智能告警系统
大数据·前端·人工智能·安全·计算机视觉·音视频
憨波个1 小时前
【SSL】WavLM
人工智能·深度学习·语音识别