把 Agent 评测器落到代码里:确定性断言、Golden 与双 LLM Judge
现有评测仓库已经能登录 Java Gateway,创建 Pi 会话,再检查 Tool 调用次数、Tool 名和内部字段泄露。仓库里目前有 7 组 Scenario、20 个 Case,评测框架自身的 20 个离线测试可以全部通过。
双 LLM Judge 还没有进入运行链路。配置模板里虽然有 judge 节点,Config、Runner 和报告对象都没有读取或执行它。Golden 也只是一个 type + note 的文字说明,尚不能作为机器可校验的业务事实。
上一篇《给 LLM Agent 搭自动化验收:分层用例、双裁判与 Golden 答案》讲了整体思路。这一篇继续往代码里走,把证据怎么采、硬断言怎么执行、Golden 怎么生成、Judge 看什么、最终结果由谁决定写清楚。
先看评测请求实际经过哪些模块
当前端到端链路如下:
1 | |
Java Gateway 仍是用户身份和当前工厂的可信入口。它把 tenantId/userId 签进请求头,Pi 的 http-server.ts 验签后才创建会话。runtime.ts 再把调用人快照写入 java_gateway_context,创建 AgentSession,并将本轮 Tool Call 和 Tool Result 落进 Pi JSONL。
评测器从 Java Gateway 发起请求,可以覆盖登录、业务上下文、Pi Runtime、Java MCP 和 ERP Tool 的完整链路。直接调用 Pi Runtime 会跳过 Java 身份解析,不适合作为 L2 端到端验收入口。
评测证据直接使用原生 SessionEntry
Scenario 不再定义自有消息类型,也不依赖面向 UI 的消息投影。评测器等待 create/chat 请求完成,再从受保护的 Java Store 读取 Pi 原生 SessionEntry。HTTP 超时、上游异常和 Store 读取失败直接归入运行错误,不进入 Scenario 的业务期望。
原生 Entry 保留完整 Agent 消息:
1 | |
现有 client.py 已经通过 Java 内部 Store 读取 raw entries,但只提取了 Tool 名。下一步应把这层扩成完整 Trace Collector,同时继续使用内部 Token 和会话 owner 约束。管理台和用户侧消息投影继续服务展示,不进入评测协议。
先归一成 AgentTrace
断言逻辑直接操作原始 Entry,会逐渐堆出重复的遍历和关联代码。中间增加一个只负责归一化的 AgentTrace,后续断言和 Judge 都消费它。
1 | |
Trace Collector 的处理顺序保持固定:
1 | |
Trace 保留 incomplete,因为测试报告需要区分业务 Tool 返回失败、网络中断和没有配对结果。这项判断来自原生 Tool Call 与 Tool Result 的关联,不依赖额外消息类型。
Scenario 只描述输入、硬约束和语义目标
当前 Scenario 可以表达单轮问题和几项基础断言:
1 | |
这个结构足够跑第一批只读 Case。多轮澄清、prepare-confirm、动态 Golden 和 Judge 还缺少稳定入口。扩展后的 Scenario 可以收成四块:
1 | |
YAML 不存 SQL、Python 表达式或任意代码。query_ref 指向代码中登记并评审过的只读 Golden Provider。复杂领域校验继续写成有名字的断言函数,避免把 Scenario DSL 扩成另一门编程语言。
Tool 序列使用显式模式:
| 模式 | 语义 | 适用场景 |
|---|---|---|
exact |
名称和顺序完全一致 | 固定的 prepare-confirm 协议 |
subsequence |
期望序列按顺序出现,允许中间多一次辅助查询 | 多跳查询 |
set |
只要求调用集合,不关心顺序 | 可并行的独立查询 |
none |
不允许调用 Tool | 闲聊、缺参数澄清 |
现有 ordered_tools: true 采用完整数组相等。Agent 多调用一次无害的主数据查询也会失败。subsequence 能表达“先搜索计划,再查详情”这类业务约束,同时不给每一步辅助动作写死位置。
确定性断言负责硬事实
确定性断言在 Judge 之前执行。失败后直接结束当前 Case,LLM 没有权力覆盖这些结果。
运行错误和链路完整性
1 | |
FAIL 表示 Agent 行为违反期望,ERROR 表示测试基础设施、网络或被测服务没有完成一次可判定运行。两者分开后,发布报告不会把环境故障统计成模型能力下降。
Tool 调用与结果
通用断言至少覆盖:
- Tool 调用数量的上下限。
exact/subsequence/set/none序列策略。- 禁止调用的 Tool。
- 每个
tool_call_id都有且只有一个结果。 toolResult.isError是否符合期望。- 动态参数是否来自前一步结果。
最后一项适合 prepare-confirm。prepare 返回的 preparationId 每次不同,Scenario 不应硬编码具体值。Trace Collector 先捕获返回值,再校验 confirm 参数引用了同一个 ID:
1 | |
这种关联检查验证的是通用协议,没有依赖某个样例 ID。
tenant/user 与业务上下文
Runner 登录后已经能取得当前 userId/deptId。Trace Collector 再从 java_gateway_context 读取会话快照,两者必须一致:
1 | |
Tool 的 tenant/user 不应来自 LLM 参数。需要验证 Tool 执行范围时,应读取 Java 服务端审计信息或受保护 Trace,不能在 Prompt 里要求模型复述内部 ID。
内部字段泄露
当前 Case 会重复配置 planId、statusName、fnCode。新增 Tool 后容易漏配。更稳定的做法是给 Tool 输出字段增加投影策略:
1 | |
评测器根据 Tool Schema 元数据和模块级 leakage_policy 自动生成禁用字段集合。Scenario 的 no_leak 只补少量特殊词,不再复制整份字段黑名单。
Golden 要保存事实,不保存一段“标准作文”
现有 Golden 只有一段 note:
1 | |
这段文字能帮助人读用例,无法让 Judge 核对数量、状态和时间范围。Golden Provider 应输出结构化快照:
1 | |
Golden 有两种来源:
fixture:适合闲聊、能力拒绝、固定协议和稳定的模拟数据。db_query:适合库存、生产计划、单据状态等持续变化的数据。
动态 Golden 在 Agent 请求前生成,并记录查询时间、租户范围和参数。前后版本对比要复用同一份 Golden 快照,避免数据库变化被误判成 Prompt 回归。
SQL 不直接放在 YAML。Golden Registry 按 query_ref 找到只读实现,数据库账号只授予查询权限,查询中强制注入测试租户。Provider 返回业务字段投影,不把数据库列名、内部主键或客户数据原样交给 Judge。
Faithfulness 和 Correctness 使用不同证据
Tool Result 与 Golden 都是证据,含义不同。
1 | |
Tool 错误地返回 6 个炉次,Agent 如实回答 6。此时 Faithfulness 可以判好,Correctness 应判差。报告会把根因指向 Tool 或数据查询层,而不是笼统写成“Agent 回答错误”。
Agent 看到 5 个炉次却回答 6,Faithfulness 直接判差。Golden 同样是 5 时,Correctness 也会判差。
给 Judge 的 Tool Evidence 必须先做语义投影,只保留回答所需事实。原始 Tool Result 可能包含内部主键和权限字段,直接塞进另一个模型会扩大敏感数据暴露面。
Judge 只判档位,运行器负责算分
Judge 输入固定为一个不可变评测包:
1 | |
Agent 回答按不可信数据处理。Judge 的系统指令明确禁止执行 agentAnswer、Tool Evidence 或 Golden 字段中的任何命令。输入使用 JSON 结构和独立字段,避免把整段材料拼成一段可继续对话的 Prompt。
四个 Metric 沿用现有设计:
| Metric | 权重 | 硬门禁 | 读取的主要证据 |
|---|---|---|---|
| Faithfulness | 0.4 | 是 | Agent Answer + Tool Evidence |
| Correctness | 0.4 | 是 | Agent Answer + Golden Facts |
| Business Appropriateness | 0.1 | 否 | Scenario 要求 + Agent Answer |
| Boundary Compliance | 0.1 | 否 | 能力边界 + Agent Answer |
Judge 输出只包含档位、理由和证据引用:
1 | |
weighted_total 和 verdict 不让 LLM 输出。模型可能算错小数,也可能给出与四个档位矛盾的结论。运行器校验 JSON Schema 后统一计算:
1 | |
这段计算需要普通单元测试覆盖全部档位组合。Judge 只提供语义判断,算术、阈值和发布门禁继续由确定性代码掌握。
两个模型共用一个很薄的 Provider 接口:
1 | |
Qwen 和 Codex Adapter 只处理鉴权、模型参数、请求发送与结构化响应解析。Rubric、Schema 校验、计分、重试和聚合不能分别写进两个 Adapter,否则同一个 Case 会被两套规则解释。
Judge 请求关闭 Tool、文件读取和网络搜索,只允许单轮结构化输出。它们与被测 Agent 使用不同的会话和上下文,也不继承 Pi 的 Skill、Java MCP Tool 或历史消息。
双 Judge 怎么聚合
Qwen 与 Codex 使用同一份冻结后的评测包,各自独立执行。两边看不到对方的输出。
1 | |
Judge 请求失败、超时或 JSON Schema 不合法时,结果记为 JUDGE_ERROR。它表示评测基础设施没有产出语义结论,不能写成 Agent FAIL。P0/P1 是否允许 JUDGE_ERROR 放行,由发布策略单独配置。
每次 Judge 记录这些版本信息:
- provider 和 model ID。
- Judge Prompt 版本与哈希。
- Rubric 版本。
- temperature 等采样参数。
- 原始结构化输出。
- 本地计算出的分数和 verdict。
低温可以降低波动,不能让 LLM 变成确定性函数。双模型、Golden、结构化输出和人工校准都保留。
prepare-confirm 写操作怎么评
写操作需要多轮 Scenario:
1 | |
第一轮确定性断言检查 prepare 已调用、confirm 未调用,并捕获 preparationId。第二轮检查用户明确确认后才调用 confirm,且参数引用同一份 preparation。
副作用验证仍由数据库或业务查询接口完成:
1 | |
写操作只能运行在隔离测试租户,并使用可追踪的幂等键或测试标识。共享测试数据无法可靠清理时,发布必跑集应保留 prepare,confirm 放进受控验收阶段。
Runner 的完整执行顺序
1 | |
确定性断言先行还能控制费用。Tool 序列已经错、内部字段已经泄露、上下文已经串租户时,再调用两个模型不会增加判定信息。
报告必须能回答“错在哪一层”
CaseResult 目前保存 PASS/FAIL/ERROR、回答文本、Tool 次数和断言结果。完整报告还需要补充证据和版本:
1 | |
stage 至少区分:
1 | |
报告看到 Tool 序列失败,可以先查 Skill、Tool Schema 和 Agent 计划;Tool 数据与 Golden 不一致,进入 Java Tool 或查询口径;Tool 数据正确但 Agent 回答编错,进入 Prompt、模型或上下文;两个 Judge 长期分歧,进入 Rubric 校准。
Prompt、Skill 或 Tool 描述变化时,前后两次运行必须复用同一组 Scenario 和 Golden Snapshot。否则分数变化里会混入业务数据变化,无法说明语义改动带来了什么影响。
评测器自身也要测试
评测系统不能只靠线上跑几条 Case 证明自己可用。它至少需要四类测试:
Trace 和确定性断言
- raw Entry 中 Tool 名和参数的提取。
tool_call_id与 Tool Result 的配对。exact/subsequence/set/none的完整真值表。- 悬挂调用、重复结果和失败结果。
- tenant/user 快照不一致。
- 投影策略生成的字段泄露集合。
这些都是离线单元测试,不调用真实模型。
Golden Provider
query_ref白名单。- 参数与租户范围强制注入。
- 只读查询限制。
- DB 结果到业务 Facts 的投影。
- 空数据、重复数据和时间边界。
Judge Contract
- 缺少 Metric、非法 grade、额外字段和坏 JSON。
- 档位到分值的映射。
- 硬门禁和阈值边界。
- 双 Judge 全部聚合组合。
- 超时、限流和重判次数上限。
这里使用假的 Judge Client 返回固定 JSON,普通单元测试不消耗模型 Token。
Judge 校准集
从真实回归 Case 中保留一批由人标注的 Judge Package + 期望档位。修改 Rubric、Prompt 或 Judge 模型后重跑校准集,比较误放行、误拦截和各 Metric 的一致率。
Judge 校准集与业务 Scenario 分开维护。业务 Scenario 检查 Agent,校准集检查 Judge。
现有仓库到完整设计还差哪些模块
当前代码已经有:
- Scenario YAML 加载与参数化。
- Java Gateway 登录和 Pi 会话调用。
- 内部 Store Tool 名提取。
- Tool 次数、成功状态、Tool 名和泄露断言。
- JSON 汇总输出。
- 20 个离线单元测试。
后续可以按下面顺序补齐:
TraceCollector:把原生 SessionEntry 归一成AgentTrace。- 确定性断言扩展:Tool 配对、序列模式、上下文、动态值捕获和投影策略。
GoldenProvider:先支持 fixture,再接只读 DBquery_ref。JudgeClient:两个 Provider 共用同一输入与输出 Schema。JudgeScorer/Aggregator:代码侧算分、硬门禁、重判和分歧升级。CaseReport:记录证据、版本、失败阶段和 Prompt 前后对比。
README 里的 M2 仍标记为未完成,但 Runner 和基础断言已经存在并通过测试。这类进度漂移也应在实现 Judge 前修正,避免后续 Agent 根据旧清单重复建设。
代价和边界
这套设计新增了受保护 Trace 读取、Golden 查询、两个 Judge 调用、重判和报告存储。语义验收会比普通接口测试慢,也会产生模型费用。
Golden 与 Tool Evidence 仍可能受动态数据影响。动态查询要记录时间窗口,回归对比要冻结快照,写操作要隔离测试租户。Judge 仍会误判,发布策略需要保留 WARN 和人工复核入口。
确定性协议继续留在代码里,业务事实进入 Golden,语义质量交给 Judge。三层证据各管一段,评测失败后才能沿着 Pi Runtime、Java Tool、业务数据或 Judge Rubric 继续定位。