前面几篇讲了 Agent 评测的目标、Rubric 结构和评分体系。走到这里,系统已经有了一套清楚的质量协议:意图清单、硬性要求、红线枚举、1-5 分和规则遵循状态。
接下来要解决一个更工程化的问题:怎么让 LLM-as-judge 稳定输出这些结构化结果?
这一步非常关键。只要评测结果要入库、聚类、统计、出报告,就不能接受一段自由文本。模型说「这条回答整体还可以,但缺少一点风险提示」,对人有帮助;对系统来说,它不知道分数是多少、意图是什么、违反了哪条规则、红线类型是否合法、这一行是不是对应上传数据里的 rowId。
生产评测系统不能靠 prompt 祈祷模型听话。正确分工应该是:模型负责语义判断,系统负责协议边界。
这一篇讲如何把 LLM-as-judge 的输出关进结构里。
一、自由文本评测无法进入生产链路
自由文本评测的最大优点是自然,最大缺点也是自然。
比如模型输出:
这条回答基本能理解用户问题,但没有充分说明风险,也没有给出明确下一步建议。我会给 3 分。
人可以读懂,但系统很难继续处理。
它不知道:
- 这是哪个业务意图;
- 3 分是否符合 Rubric;
- 「没有充分说明风险」对应哪条规则;
- 是否命中红线;
- 这条结果对应上传表里的哪一行;
- 是否可以进入问题聚类;
- 报告里该统计到哪个规则类别。
如果再让模型按自然语言生成多条结果,问题会更大:行数可能缺失,rowId 可能重复,字段名可能变化,枚举值可能用别名,数组可能变成字符串。
所以评测结果必须是一份协议,而不是一段作文。
二、JSON 不等于结构化协议
很多人会说,那就让模型输出 JSON。问题是,JSON 只是一种格式,不是协议。
下面这种输出虽然是 JSON,但仍然不可控:
{
"result": "这条不错,四分吧",
"problem": "略微不完整"
}
系统真正需要的是字段、类型、枚举和值域都被约束:
{
"items": [
{
"rowId": 12,
"intentKey": "HEALTH_CONSULTATION",
"score": 2,
"redFlagTypes": [],
"scoreExplanation": "缺少必要追问和风险分流。",
"unmetRequirementIds": ["HEALTH_FOLLOWUP", "HEALTH_TRIAGE"],
"questionsCheck": "NOT_PRESENT",
"questionsIssues": []
}
]
}
这类结构至少要约束几件事:
- 顶层必须是
{ items: [...] }; - 每个 item 必须有
rowId; intentKey必须来自当前 Rubric 的意图清单;score必须是 1-5 的整数;redFlagTypes必须来自红线枚举;questionsCheck必须来自固定状态枚举;- 不允许输出额外字段。
这就是严格 JSON Schema 的价值。它把协议从 prompt 的自然语言要求,搬到请求参数和服务端校验里。
三、Schema 负责形状,Zod 负责语义
严格 JSON Schema 可以约束模型输出的形状,但它通常还不够。
比如它可以限制 intentKey 来自枚举,却不一定能表达「score 为 1 时必须存在红线」;它可以限制 unmetRequirementIds 是字符串数组,却不一定能判断每个 ID 是否属于当前意图可用规则;它可以限制 score 是 1-5,却不能根据规则命中分重新压低分数。
所以服务端还需要第二道校验。可以用 Zod 或类似工具做语义层检查:
rowId必须对应当前批次输入;- 每个输入行最多只能有一个结果;
- 缺失行要进入修复候选;
intentKey必须能在 Rubric 中找到;unmetRequirementIds必须来自通用规则或当前意图规则;score=1必须存在红线;- 红线必须能映射到对应的 critical 规则;
- 命中
violationScore后,最终分数不能高于规则上限。
这两层边界分工清楚:JSON Schema 尽量把模型输出约束在合法形状内,服务端校验负责业务语义和数据一致性。
不要把所有规则都塞进 prompt 里求模型自觉。越靠近数据一致性的要求,越应该由应用代码兜底。
四、为什么输出要尽量扁平
模型输出协议还有一个经验:让模型少写,系统多补。
在 Rubric 评测里,最直观的输出结构是完整 referenceBasis,也就是每条规则都给出遵循状态:
{
"referenceBasis": [
{ "requirementId": "A", "status": "FOLLOWED" },
{ "requirementId": "B", "status": "VIOLATED" },
{ "requirementId": "C", "status": "NOT_APPLICABLE" }
]
}
这个结构对报告很好,但对模型输出不友好。规则多起来后,模型要重复输出大量 FOLLOWED,很容易漏项、编造 ID 或把状态写错。
更好的方式是让模型只输出未满足规则:
{
"unmetRequirementIds": ["B"]
}
服务端再根据当前意图和 Rubric 补齐完整 referenceBasis:
- 模型列出的未满足规则标为
VIOLATED或PARTIAL; - 当前意图下其余可观测规则标为
FOLLOWED; - 不可观测规则标为
NOT_APPLICABLE; - 红线类型反推对应 critical 规则,防止红线漏聚类。
这就是扁平输出的好处。模型只做高信息量判断,系统负责展开协议结构。输出越短,模型越稳;协议越集中在服务端,系统越可控。
五、批量评测里的行级归并
生产评测通常不会一条一条调用模型,而是批量提交多个样本。批量能省请求开销,但也引入一个问题:模型可能返回不完整的批次。
常见坏情况包括:
- 少返回某些 rowId;
- 返回了输入中不存在的 rowId;
- 同一个 rowId 返回两次;
- 某一项字段非法;
- 整个响应不是合法 JSON;
- 响应结构合法,但语义校验失败。
这里不能简单地「整批失败」。如果一个批次 10 条里 9 条是好结果,只有 1 条坏结果,直接丢掉整批会浪费调用成本,也会拉低覆盖率。
更稳的做法是行级归并:
- 解析响应;
- 按
rowId和输入行对齐; - 每一项独立做 schema 和语义校验;
- 校验通过的立刻保存;
- 缺失、重复、非法的行进入修复候选;
- 修复失败只影响对应行,不回滚同批好结果。
这一步让 LLM 像一个不稳定的外部服务,而不是一个可信的本地函数。调用外部服务时,行级隔离比整批事务更符合生产现实。
六、一次修复,不要无限重试
坏行要不要修?要。要不要无限修?不要。
一次定向修复通常就够了。修复 prompt 不需要重新提交整批,只提交那一行输入、原始坏结果、校验错误和要求的 rowId。目标很明确:请只返回这一行的合法结构。
伪流程如下:
初始批量请求
├─ 好行 → 保存
└─ 坏行 → 单行修复
├─ 修复成功 → 保存
└─ 修复失败 → 标记失败
为什么只修一次?
因为无限重试会制造三个问题。
第一,成本不可控。评测任务可能有几千行,如果坏行反复重试,成本会被长尾拖垮。
第二,延迟不可控。报告迟迟不结束,用户不知道任务到底卡在哪。
第三,语义收益递减。如果同一行在明确错误提示下仍然修不好,继续重试往往只是在碰运气。
失败行应该被记录下来,进入 FAILED 状态,并计入覆盖率。报告可以是部分完成,但不能假装全量成功。
七、覆盖率是可信度,不是质量分
当评测任务存在失败行时,系统需要一个指标表达报告可信度:覆盖率。
coverageRate = diagnosedInstances / totalInstances
覆盖率回答的是:这份报告基于多少比例的可评测样本。它不回答 Agent 好不好。
这个边界很重要。覆盖率低,说明报告结论需要谨慎;但覆盖率低不等于 Agent 质量差。反过来,覆盖率 100% 也不等于 Agent 表现好,只说明所有样本都完成了诊断。
任务终态可以分成几类:
| 状态 | 含义 |
|---|---|
COMPLETED | 所有可评测样本完成诊断 |
PARTIAL | 诊断覆盖率达到可报告阈值,但有失败行 |
INSUFFICIENT_DATA | 覆盖率太低,不输出普通优化报告 |
FAILED | 流水线阶段性失败,无法生成报告 |
报告页应该把覆盖率放在显眼位置,并在部分完成时提示读者:当前结论只基于已完成诊断样本。
八、不要静默归一化语义字段
有些模型输出错误可以安全归一化,比如把单个字符串包成数组、去掉多余空格、补齐服务端可推导字段。还有些错误不能静默修。
比如 intentKey 输出了一个不存在的别名,或者 redFlagTypes 写了一个不在枚举里的值。系统不应该猜它是什么意思。猜错一次,后面的聚类和报告都会被污染。
可以把归一化分成两类:
| 类型 | 是否可自动处理 | 示例 |
|---|---|---|
| 形式归一化 | 可以 | 去重、trim、把缺失问题卡片转成 NOT_PRESENT |
| 语义归一化 | 谨慎或拒绝 | 未知意图、未知红线、未知规则 ID |
生产评测系统宁愿让一行失败,也不要把语义猜错后保存成正式结论。失败行会影响覆盖率,但错误结论会影响信任。
九、Prompt 的职责要收窄
严格协议并不意味着 prompt 不重要。它仍然负责讲清楚评测语义。
但 prompt 的职责要收窄:
- 告诉模型它要做 Rubric 评测;
- 列出固定意图清单;
- 注入通用规则和意图规则;
- 解释评分流程;
- 解释红线;
- 说明
reference_output不是标准答案; - 说明不可观测规则如何处理;
- 要求输出字段含义。
不要让 prompt 承担这些职责:
- 保证 JSON 合法;
- 保证字段类型正确;
- 保证枚举值合法;
- 保证 rowId 不重复;
- 保证规则 ID 存在;
- 保证分数服从命中分;
- 保证失败行不影响好行。
后面这些都应该由结构化输出和服务端代码完成。prompt 越像协议文档,系统越脆;prompt 越专注语义判断,工程边界越清楚。
十、一个简化的处理流程
把上面的设计串起来,一次批量评测大概是这样:
构建输入批次
→ 注入 Rubric prompt
→ 请求严格 JSON Schema 输出
→ 解析顶层 items
→ 按 rowId 对齐输入
→ 单项 Zod 校验
→ hydrate:根据 unmetRequirementIds 补齐 referenceBasis
→ apply policy:按红线和命中分修正 score
→ 保存通过项
→ 修复坏行一次
→ 保存修复项或标记失败
→ 更新覆盖率
这里的关键词是 hydrate 和 apply policy。
hydrate 负责把模型的扁平判断扩展成系统完整记录。比如模型只说违反了哪些规则,服务端补齐所有规则的遵循状态。
apply policy 负责让模型判断服从业务规则。比如命中红线就补红线,命中规则上限就压分。
最终入库的不是模型原始输出,而是服务端规范化后的诊断记录。后续聚类、报告和导出都只使用这份记录。
十一、可观测性也要跟上
LLM-as-judge 是长任务里的外部依赖,必须有运行日志。
至少要记录:
- 批次大小;
- 并发数;
- rowId 列表;
- system prompt 字符数;
- user payload 字符数;
- 模型返回字符数;
- 请求耗时;
- transport retry 次数;
- 初始成功行数;
- 修复行数;
- 最终失败行数;
- 校验错误摘要。
这些日志不是为了好看,而是为了排障。当任务覆盖率异常低时,你需要知道是模型输出坏了、接口超时了、prompt 太长了,还是某批数据触发了规则校验问题。
没有可观测性,评测系统会变成另一个黑盒。我们已经在评测黑盒 Agent 了,最好不要把评测平台本身也做成黑盒。
十二、总结
LLM-as-judge 可以做语义判断,但不能被当作可信协议端。
生产评测系统要做的是把模型输出关进一套稳定边界里:严格 JSON Schema 约束形状,服务端校验约束语义,扁平输出降低模型负担,规则补齐保证报告口径,一次修复控制成本,失败隔离保住覆盖率和可信度。
这套分工可以压缩成一句话:模型负责判断语义,系统负责守住协议边界。
只有这样,评测结果才能稳定入库、聚类、统计和导出。否则我们得到的只是一次看起来合理的模型回答,而不是一套可以长期运行的评测系统。