面试知识库

04 · Rubric 动态评测#

简历原话:Rubric 动态评测:通过 judge 模型为每条线上 query 生成三档评分标准并固定,保证版本间分数可比,结果回写 Langfuse;修正 judge 的系统性误判并构建「评分→定位 bad case→迭代 Prompt/Hook/Skill→再评测」的测评闭环。

30 秒口述版#

购物 Agent 的好坏没法用一个固定指标衡量:「送女朋友的礼物」和「批量采购螺丝」的红线完全不一样。所以我让 judge 模型针对每条线上 query 先生成一份专属评分标准,分三档:P0 业务红线一票否决,P1 执行规范按条扣分,P2 质量维度打 1~5 分。标准生成一次就冻结,按 query、约束和生成 prompt 的哈希存起来,之后每个版本都用同一把尺子打分,版本之间的分差才能归到 Agent 身上。打分结果作为 score 挂回那次运行的 Langfuse trace,低分直接能点进去看当时的轨迹。我花力气最多的是校准 judge:它会把内部执行日志当成信息泄露、把软偏好升级成红线、偶尔回一张空表导致「越坏越绿」,这些修掉以后,再按「评分→定位 bad case→改 Prompt / Hook / Skill→用同一把尺子重评」这个循环迭代。

背景与问题#

  1. 通用模板打分失真。最早想用一套固定细则评所有回答,结果两头出错:「这款耳机值不值」被套上价格红线,而「预算 5 美元的耳机」这种真正的预算陷阱反而没被重点检查。分数低了,也说不清是哪条细则不该适用。
  2. 动态生成的尺子自己会抖。改成每条 query 让 judge 现场生成细则后,同一份回答连评两次,细则条目和措辞都不一样,分数跟着变。改完 prompt 分数掉了,分不清是 Agent 变差了还是尺子换了。
  3. judge 本身有系统性误判。第一轮基线均分只有 32.8,一堆 0 分。逐条翻判词发现,大头是 judge 的问题:
    • 把喂给它的工具调用轨迹(里面有 item_id、工具名)当成「Agent 向用户泄露内部 id」,判 P0 失败。调用工具越多的 case 越容易被判死。
    • 给没写预算的 query 自己编一个价格红线,还自行折算成人民币。
    • 给闲聊类 query 设「应该调检索工具」「应该出商品卡」的规范。
  4. 分数和现场是分开的。评测报告里只有一行「q03:0 分」,要去翻日志才知道当时 Agent 调了什么、为什么那样收尾。定位一个 bad case 要来回找好几处。
  5. 只看离线种子集不够。手写的回归集能保证覆盖红线场景,但线上用户的说法、品类分布和它差很多。线上真实出问题的 query 如果不进评测,就永远测不到。

做法与取舍#

1. 每条 query 动态生成三档细则#

  • judge 拿到 query、planner 解析出的已知约束(预算、人群、排除材质等)、意图类型和前情,生成 610 条细则,每档 24 条:
    • P0 业务红线:一票否决。任一条失败,总分直接为 0。例如主体商品超预算、把女士用品推给男性礼物场景、最终回复里对用户暴露内部 id 或工具名。
    • P1 执行规范:每违反一条扣 10 分。例如用户要了到手价却没算、没出收尾清单、同一工具反复调用。
    • P2 质量打分:1~5 分,每条写清 1 分和 5 分各代表什么,例如需求覆盖度、场景洞察、决策建议价值。
  • 总分 = P2 均分 ÷ 5 × 100 − 10 × P1 违规条数,下限 0;P0 失败时总分直接为 0。总分 ≥ 70 且 P0 全过记为「高分轨迹」。
  • 为什么三档而不是一个总分:红线和质量不能相互抵消。回答写得再好,推了超预算的商品就是不合格,所以 P0 是闸门,不参与加权。P1 和 P2 分开,是因为「流程没走对」和「推荐质量一般」的修法完全不同,前者改 Hook,后者改 Prompt 或检索。
  • 否掉的方案:
    • 固定通用 rubric:上面说过,对不同 query 失真。
    • 让 judge 直接给 1~10 总分:没有依据可追溯,分低了不知道改哪里,judge 抖动也更大。
    • 两两对比(pairwise,新旧版本谁更好):只能比相对好坏,给不出「这条破了哪条红线」;而且每加一个版本都要重新两两比较,历史分数没法复用。

一份生成出来的细则长什么样(query:「给男朋友买个生日礼物,预算 80 美元,他不喜欢花哨的」,约束:预算 80 美元、收礼人男性、软偏好简约):

编号档位维度判定描述
P0-1业务红线价格红线推荐的主体商品到手价超过 80 美元即失败
P0-2业务红线人群匹配推荐女士专用商品(女包、女士饰品等)即失败
P0-3业务红线信息安全最终回复正文出现内部商品 id 或工具名即失败
P1-1执行规范收尾清单没有给出带理由的推荐清单即违规
P1-2执行规范商品卡要素商品卡缺标题、价格或推荐理由即违规
P1-3执行规范调用纪律同一检索条件重复检索超过 2 次即违规
P2-1质量风格贴合1 分:推荐明显花哨;5 分:全部简约且理由点明了「不花哨」
P2-2质量场景洞察1 分:只罗列商品;5 分:结合生日送礼场景给出实用性、包装等建议

注意「不喜欢花哨」是软偏好,只出现在 P2,没有被升成红线;用户没提比价和到手价,所以 P1 里也没有「该比价」这一条。这两点都是校准 judge 之后才稳定下来的。

2. 细则冻结:生成一次,所有版本共用#

  • 细则的存储键:把规范化后的 query(去首尾空白、全角转半角)、约束、意图、前情和生成 prompt 全文按固定键序序列化成 JSON,取 SHA-256 的前 16 位十六进制。第一次遇到这条 query 时生成并写入细则库,状态标为已冻结;之后任何版本评这条 query 都直接读,不再调 judge 生成。
  • 生成 prompt 的全文也参与哈希:尺子的定义一改,所有旧细则自动失效重建。这样「尺子没变就复用、尺子变了就重建」两种情况都不用人记。
  • 前情(例如「上一轮已经给过两件包」「该用户有一张未发货订单」)是声明式的客观事实,写在 case 上,不是把 Agent 上一轮的回答塞给 judge。原因:如果把 Agent 自己的输出喂给 judge,Agent 变差时尺子会跟着变松,回归对照就失效了。
  • 前情用前置拼接的方式加进 prompt,不改生成 prompt 本身。原因:改一个字会让所有没有前情的 case 也换一把尺子,跨批次的分数就没法比了。
  • 需要主动换尺子时(比如修了 judge 的误判纪律),走显式的刷新操作,新旧两批分数不混在一起比。
  • 否掉的方案:
    • 每次评测都重新生成细则、多采样取平均:成本翻几倍,而且只能减小方差,消不掉「版本 A 和版本 B 用的是两把不同的尺子」这个问题。
    • 人工写每条 query 的细则:种子集还能做,线上 query 每天几百条,人写不过来。

3. 线上 query 怎么进评测#

  • 每轮任务结束后,按采样率把这一轮的 query、约束、意图、最终回复、商品卡、工具轨迹和 trace_id 投进一条独立的评测 Redis Stream,由低优先级的评测消费者组异步处理,不占主链路的 worker 和模型配额。
  • 采样规则:非闲聊的购物轮默认抽样;用户点了「不满意」、任务被取消、触发过循环检测或超时的轮次全量进评测。
  • 细则库是 MySQL 里的一张表,细则键是唯一键,每行存细则 JSON、生成时用的 judge 模型、状态(生成中 / 已冻结)、生成时间和被丢掉的残缺细则条数。同一条 query 只生成一次细则:
    • 两个消费者同时遇到同一条新 query,都去插一行「生成中」,唯一键冲突的那个不调 judge,隔几秒轮询,等状态变成「已冻结」直接读。
    • 负责生成的消费者崩溃,行会一直停在「生成中」。所以这一行带生成开始时间,超过 5 分钟还没冻结,其他消费者可以用条件更新(状态仍是生成中且开始时间早于阈值)把它抢过来重新生成,条件更新保证只有一个能抢到。
    • judge 两次都没生成出合格细则,这一行删掉,这条评测任务记为失败并留在待确认列表里,由后续重试处理,不写一个空的「已冻结」占位。
    • 冻结后的行只读。显式刷新尺子时,旧行不覆盖,而是挂在旧的生成 prompt 版本下留档,新 prompt 自然算出新键、生成新行。
  • 线上评出来 P0 失败或总分低于 70 的 query,会进入「回放集」:保存脱敏后的 query、约束和前情,以后每个新版本都会拿回放集重跑一遍 Agent,用冻结的细则打分。离线的手写种子集(95 条,覆盖预算陷阱、性别错配、多轮追问等刻意构造的红线场景)和回放集一起组成版本对比用的回归集。
  • 为什么线上评测不直接拿来做版本对比:线上每天的 query 分布在变,今天分高可能只是今天的问题简单。版本对比必须在同一批 query、同一把尺子上做,所以线上负责「发现问题」,回放集和种子集负责「判断改好没有」。

4. 结果回写 Langfuse#

  • 每次评完,往被评的那条 trace 上挂三个 score:rubric_total(01,归一化后的总分)、rubric_pass(布尔,P0 是否全过)、rubric_p2_avg(15 原始质量分)。rubric_total 的 comment 里写失败的维度名和 judge 的判词。
  • trace_id 由调用方显式传入,不从上下文变量里读。原因:评测在 asyncio 的 task 里跑,读上下文变量现在碰巧能拿到值,但只要以后多包一层 task,就会静默拿到空值,分数全丢且不报错。显式传参让这条数据流一眼可见。
  • 评测进程退出前强制 flush。Langfuse SDK 靠后台线程批量上报,短命进程直接退出会丢掉队列里的 score,现象是 trace 有、score 没有,全程零报错。
  • 回写失败只打告警日志,不影响评测结论本身。
  • 为什么回写到 trace 而不是单独建一张评分表:低分和「当时怎么跑的」在同一个页面。在 Langfuse 里按 rubric_total < 0.7 或 rubric_pass = false 筛出来,点进去就是完整的模型调用、工具入参、耗时和 token,定位 bad case 从翻三处日志变成点一次。

5. 修正 judge 的系统性误判#

原则是先校尺子,再改 Agent。歪的尺子量出来的分数没有意义,改 Agent 只会越改越偏。判断方法:看到 0 分先读 judge 的判词,判词引用的证据如果不在「给用户看的内容」里,就是 judge 的问题。

误判现象修法
把内部日志当泄露轨迹里有 item_id,judge 判「向用户暴露内部 id」,P0 失败;调工具越多越容易中渲染轨迹时丢掉键名含 id 的入参,单个入参截到 120 字;材料分三段:最终回复 / 商品卡 / 内部执行日志,第三段标明「非用户可见,禁止据此判泄露」;打分 prompt 再强调一遍信息安全只看最终回复
凭空设价格红线没写预算的 query 被设了「超过 xx 元即失败」,还自行折算人民币生成纪律:价格 P0 只在约束里有明确预算时才设,单位一律美元
软偏好升级成红线「便宜」「小众」被写成一票否决生成纪律:软偏好最多作为 P2 质量维度
强加用户没要的步骤用户只说「推荐几款」,因为没比价判 P1 违规生成纪律:比价、到手价只在用户明确要求时才算「该调的工具」
闲聊套购物规范闲聊 query 被要求「应该调检索工具」按意图切换生成导向:非购物意图只评收尾是否得体、是否诚实说明能力边界
空表假绿judge 偶尔回一张空的打分表,聚合时「一条 P0 都没失败」,总分反而满分空表闸:打分结果里一条都没有就重新采样,仍为空则这条 case 记为评测失败,不算分
单次采样定生死同一份回答连评三次出过 0 / 0 / 90P0 复核:首判有 P0 失败时独立再判一次,两次都失败才算破红线
细则残缺报废整条judge 漏写某条细则的判定描述,结构校验失败,整条 case 作废丢掉残缺的那一条细则并记账,其余照常打分;不拿维度名去补判定描述,避免造出语义残缺的细则
  • P0 复核为什么只翻「失败→通过」一个方向:这个 judge 的系统性偏差集中在假阳性(误判为失败)一侧,而且复核只花在首判失败的少数 case 上,全绿的 case 一分钱不多花。代价是 judge 漏判真红线时复核发现不了,这一侧靠人工抽检判词。
  • 否掉的方案:
    • 换更贵的 judge 模型:试过,轨迹当泄露这类问题换模型照样出,根因是给 judge 的材料边界不清,不是模型能力。
    • 多个 judge 投票:成本按 judge 个数翻倍,而且几个 judge 共享同一种误判时投票也纠不过来。只在 P0 失败时复核一次,是花钱最少、针对性最强的做法。

6. 测评循环:评分 → 定位 → 迭代 → 再评测#

  1. 评分:线上采样 + 回放集 + 种子集跑出每条的三档结果,分数挂在 trace 上。
  2. 定位:在 Langfuse 按低分和 P0 失败筛 trace,先读判词确认是不是 judge 误判;确认是 Agent 的问题后,按根因归类:
    • 流程类(该收尾不收尾、反复调同一工具、跳过必要步骤)→ 改 Hook;
    • 表达和决策类(过度反问、价格文案算错、理由空泛)→ 改 Prompt;
    • 业务流程没按意图加载(比如套装、下单流程没读对应的流程说明)→ 改 Skill 的触发描述或分流表。
  3. 迭代:只改定位到的那一处,一次一个变量。
  4. 再评测:用同一批 query、同一套冻结细则重跑。P0 失败数不许增加是硬门槛;单条分差小于 judge 的噪声区间时不下结论,关键 case 跑 3 遍取中位数。
  5. 沉淀:修好的 case 留在回放集里防回归;高分轨迹(总分 ≥ 70 且 P0 全过)由 judge 蒸馏成「正确决策 / 常见反例」的 few-shot 示例;泄露类 P0 失败自动提出候选脱敏规则,人工确认后才生效。

一次完整的例子:尺子校准后,定位到一类真缺陷叫「过度澄清 + 口头收尾」:有的 case 信息已经够了,Agent 一个工具没调就反问用户;有的走完精挑后只说一句「清单马上生成」就停了,没真正调收尾工具。修法分两处:Prompt 里加「信息够就直接检索」和「不要用文字宣告代替调用」;Hook 在精挑工具返回后、下一次模型调用前注入一条收尾提示,贴着决策点比全局 prompt 管用。用同一套尺子各跑 3 遍取中位数,两条 case 分别从 16.7 到 95、从 10 到 93.3,对照组稳定 100,没有副作用。

Skill 的例子:套装、下单这些流程写成 Skill 按意图加载。评测里发现两条「我已经知道要搜什么」的单品 query 没读 Skill 就直接检索,漏了流程约束。改的是分流表里「单品直搜」的例外条件,触发率从 11/13 到 13/13,负例 0/5 误触发。

流程图 / 架构图#

一条线上 query 的评测链路#

flowchart TD
    A["用户任务跑完"] --> B{"按规则采样"}
    B -->|不抽中| Z["结束"]
    B -->|抽中 / 差评 / 取消 / 超时| C["投入评测 Stream"]
    C --> D["评测消费者组 取任务"]
    D --> E["计算细则键<br/>query+约束+意图+前情+生成 prompt 哈希"]
    E --> F{"细则库已冻结?"}
    F -->|是| G["读取冻结细则"]
    F -->|否| H["抢唯一键 调 judge 生成 6~10 条细则"]
    H --> H1["丢残缺细则 冻结入库"]
    H1 --> G
    G --> I["渲染材料<br/>最终回复 / 商品卡 / 脱 id 的内部日志"]
    I --> J["judge 逐条打分"]
    J --> K{"打分表为空?"}
    K -->|是| K1["重采样一次 仍空记评测失败"]
    K -->|否| L{"有 P0 失败?"}
    L -->|是| M["独立复核一次 两次都失败才算破"]
    L -->|否| N["聚合 P0 闸 + P1 扣分 + P2 均分"]
    M --> N
    N --> O["三个 score 挂回 Langfuse trace"]
    N --> P{"P0 失败或总分低于 70?"}
    P -->|是| Q["进入回放集"]

版本迭代循环#

flowchart LR
    S1["评分<br/>线上采样 + 回放集 + 种子集"] --> S2["Langfuse 按低分筛 trace"]
    S2 --> S3{"判词证据在用户可见内容里?"}
    S3 -->|否 judge 误判| S4["校准 judge<br/>改材料边界或生成纪律 显式刷新尺子"]
    S3 -->|是 Agent 问题| S5{"根因归类"}
    S5 -->|流程| H["改 Hook"]
    S5 -->|表达 / 决策| P["改 Prompt"]
    S5 -->|流程说明没加载| K["改 Skill"]
    H --> R["同一批 query 同一套冻结细则重评"]
    P --> R
    K --> R
    S4 --> S1
    R --> G{"P0 失败数不增加<br/>关键 case 3 遍中位数改善?"}
    G -->|是| D["上线 修好的 case 留在回放集"]
    G -->|否| S5
    D --> S1

新版本上线前的回放对比(时序)#

sequenceDiagram
    participant R as 回放执行器
    participant A as Agent 新版本
    participant DB as 细则库
    participant J as judge 模型
    participant L as Langfuse
    R->>R: 取回归集 种子集 + 回放集
    loop 每条 query
        R->>R: 清空这条 case 的会话历史
        R->>A: 按前情和多轮顺序逐轮执行
        A-->>R: 最终回复 + 商品卡 + 轨迹 + trace_id
        R->>DB: 按细则键读冻结细则
        DB-->>R: 与旧版本同一份细则
        R->>J: 细则 + 分段材料 逐条打分
        J-->>R: 打分表 含判词
        opt 首判有 P0 失败
            R->>J: 独立复核一次
            J-->>R: 复核结果
        end
        R->>L: 三个 score 挂到新版本 trace
    end
    R->>R: 与旧版本逐条配对比较 P0 失败数与关键 case 中位数

数字怎么来的#

简历这条 bullet 没有放结果数字,只有「三档」这一个结构常量。面试时被问「效果怎么样」,用下面这些有口径的数据回答。

  • 三档:P0 / P1 / P2。常量是 P1 每条扣 10 分、高分线 70 分、每条 query 610 条细则、每档 24 条。10 分和 70 分是经验值:P1 扣 10 分让「违规 3 条」大约等于「质量从 5 分掉到 3.5 分」,两类问题的权重相当;70 分是 P2 平均 3.5 分,人工翻过的轨迹里这个分数以上基本能直接当示例用。
  • 校准前后:第一轮基线 18 条种子 query 均分 32.8,大量 0 分。只改 judge、不动 Agent,用归档的同一批 Agent 输出重新调 judge 打分(不重跑 Agent,零额外 Agent 成本),几条典型 case:0 → 36.7、0 → 90、0 → 53.3。这证明那些 0 分是尺子的问题。
  • judge 抖动:同一份回答连评三次出过 0 / 0 / 90;P2 分在 15 条左右的样本上噪声约 ±13 分。所以版本对比只拿 P0 失败数当硬门槛,P2 均分只作参考,关键 case 跑 3 遍取中位数。
  • 细则残缺率:90 条评测里有 8 条(8.9%)因为 judge 漏写一个字段整条报废,改成只丢残缺细则之后这类报废归零。
  • 迭代例子:「过度澄清 + 口头收尾」修复前后,同一套冻结细则、各跑 3 遍取中位数,16.7 → 95、10 → 93.3,对照组稳定 100。Skill 触发率 11/13 → 13/13,负例 0/5 误触发。
  • 被其他 bullet 引用:检索模型微调那条 bullet 的「端到端 Rubric 均分 50.4 → 58.0」用的就是这套冻结细则,前后两个模型版本在同一批 query 上打分。
  • 回归集规模:手写种子集 95 条,覆盖 30 个场景桶,其中 14 条多轮、5 条带前情;日常迭代先跑其中 15 条精选子集,每个会被改到的机制至少有一条对照,全量只在大改动时跑,控制 judge 成本。

追问 Q&A#

Q1:为什么要「动态」生成评分标准,写一套通用的不行吗?

不同 query 的红线差太多。「预算 5 美元的蓝牙耳机」核心红线是价格,「给男朋友买礼物」核心红线是人群,「这款值不值」根本不该设价格红线。通用模板要么漏判要么误伤,而且分数低了说不清是哪条不适用。动态生成让每条细则能引用这条 query 自己的预算数字、人群和排除项,判词也能直接指导改哪里。代价是跨 query 的分数不能直接比,我只比同一条 query 在不同版本上的分数。

Q2:judge 每次生成的细则都不一样,你怎么保证版本间可比?

细则只生成一次就冻结。键是 query、约束、意图、前情和生成 prompt 全文的哈希,第一次生成后存进细则库,之后所有版本都读这一份。剩下的方差只来自两处:Agent 自己的输出,和 judge 打分(temperature 0)。比起「细则抖 + 打分抖」双重方差小很多。生成 prompt 改了,哈希跟着变,所有细则自动重建,这时新旧两批分数不混着比。

Q3:细则冻结了,但如果当初生成的细则本身就是错的呢?不是把错误也固定下来了?

会。冻结解决的是「可比」,不解决「正确」。所以冻结之前先校 judge:我把几类系统性误判写成生成纪律,比如价格红线只在有明确预算时设、软偏好不许升 P0。校准以后显式刷新一次尺子,旧批次的分数就不再和新批次比。单条细则有问题时,从判词能看出来(判词引用的证据不在用户可见内容里),这条单独标记重新生成,不影响其他 query 的尺子。

Q4:你说 judge 有系统性误判,最严重的是哪个?怎么发现的?

最严重的是把内部工具轨迹当成信息泄露。我给 judge 的材料里有工具调用轨迹,里面有 item_id 和工具名,judge 就判「Agent 向用户暴露了内部 id」,P0 一票否决。它是系统性的:调工具越多、跑得越完整的 case 越容易被判 0 分,等于在惩罚好的行为。发现方法是第一轮基线均分只有 32.8,我把所有 0 分的判词逐条读,发现大量判词引用的是日志内容,而不是给用户的回复。修法是轨迹脱掉 id 类入参、材料分三段并标注哪段是用户可见的,然后拿同一批归档输出只重调 judge,几条 case 从 0 回到 36.7、90、53.3。

Q5:LLM 当 judge 本身靠不住,你怎么知道 judge 是准的?

三层办法。第一,judge 输出必须带判词,每条判定引用回答里的具体内容,我能抽查。第二,已知的系统性偏差用机制修,而不是指望模型自己改:材料边界、生成纪律、空表闸、残缺细则处理。第三,一票否决不交给单次采样:首判 P0 失败就独立复核一次,两次都失败才算破红线。剩下的随机抖动我承认存在,P2 在十几条样本上有 ±13 分的噪声,所以版本对比的硬门槛只看 P0 失败数,P2 只作参考,关键 case 跑 3 遍取中位数。

Q6:P0 复核为什么只复核失败的,不复核通过的?这不是偏向让分数变好看吗?

是有意的偏向,原因是 judge 的系统性偏差在假阳性一侧,也就是把没问题的判成有问题,我前面列的几类误判全是这个方向。只复核失败的,额外成本只落在少数 case 上。代价是 judge 漏判真红线时复核发现不了,这一侧我用两个办法补:种子集里刻意构造了预算陷阱、性别错配这类红线场景,漏判会在回归时暴露;另外定期人工抽检通过 case 的判词。和历史基线比的时候,两边要么都开复核、要么都关,不能一边开一边关。

Q7:「每条线上 query」都评,成本扛得住吗?

不是全量评,是采样加全量补漏。购物轮按采样率抽,用户差评、任务取消、触发循环检测或超时的轮次全量进。评测走独立的 Redis Stream 和低优先级消费者组,不占主链路配额。细则按 query 哈希复用,重复或相近的 query 不重复生成,打分是每条一次 judge 调用,只有首判 P0 失败时多一次。线上评测的定位是发现问题,版本对比用的是回放集和种子集,规模固定。

Q8:线上 query 分布一直在变,你怎么判断分数涨了是 Agent 变好了,而不是 query 变简单了?

所以线上分数不拿来做版本对比。线上评测负责发现问题;判断改好没有,用的是两个固定集合:手写种子集 95 条(刻意构造红线场景,冷启动也靠它),加上从线上低分 case 沉淀下来的回放集。新版本上线前在这两个集合上用冻结细则重跑,同一批 query、同一把尺子,分差才能归到 Agent 身上。种子集是考卷,回放集是错题本,错题本会增长,但每次对比时两个版本用的是同一份。

Q9:多轮对话的 case 怎么评?judge 只看最后一轮,不知道前面发生了什么。

每条多轮 case 带一段前情,写的是声明式的客观事实,比如「上一轮已给出两件双肩包清单」「该用户有一张已确认的订单」。生成细则和打分两处都会看到这段前情。不给前情,judge 会把 Agent 正确引用上一轮商品判成「凭空编造」。关键是前情不能是 Agent 上一轮的实际回答:如果把 Agent 自己的输出喂给 judge,Agent 变差时尺子会跟着变松。前情用前置拼接加进 prompt,不改生成 prompt 本身,这样没有前情的 case 尺子不变。

Q10:定位到 bad case 以后,怎么决定改 Prompt、Hook 还是 Skill?

看根因类型。流程类问题,比如该收尾不收尾、同一工具反复调、跳过必要步骤,改 Hook,因为这类问题靠提示词约束不稳定,要在执行层贴着决策点处理。表达和决策类,比如过度反问、价格文案把几个币种的金额加在一起、理由空泛,改 Prompt。业务流程没按意图加载,比如套装请求没读套装流程,改 Skill 的触发描述或分流表。例子:「走完精挑只说一句就停」这个问题,我在 Prompt 里加了一条,同时在 Hook 里精挑返回后注入收尾提示,同一套尺子跑 3 遍取中位数,两条 case 从 16.7 到 95、从 10 到 93.3。

Q11:为什么不用 pairwise 比较或者直接用人工评测?

pairwise 只能说 A 比 B 好,说不出 A 破了哪条红线,而且每出一个新版本都要和旧版本重新两两比,历史分数用不上。人工评测准,但每条要几分钟,版本迭代一天好几次,扛不住。我的做法是 judge 打分作为主力,人工只做两件事:读低分 case 的判词判断是不是误判,定期抽检通过 case。人工的时间花在 judge 最容易错的地方。

Q12:如果换了 judge 模型,历史分数还能比吗?

不能直接比。换 judge 等于换了打分的人,即使细则冻结,打分口径也会变。做法是换模型时把基线版本和新版本在新 judge 下各跑一遍,在新口径下重新建立基线,不拿新 judge 的分数去和旧 judge 的历史分数比。细则本身可以继续复用,因为细则键里没有 judge 模型,只有生成 prompt;如果要连细则也换成新 judge 生成的,走显式刷新。

相关八股#

1. LLM-as-a-Judge 的常见偏差有哪些?

要点:位置偏差(pairwise 时偏向先出现或后出现的答案)、长度偏差(偏好更长的回答)、自我偏好(偏向同家族模型的输出)、格式偏差(偏好 Markdown 结构整齐的)、材料边界不清导致的误判(把上下文里的非答案内容当成答案)。常见缓解:交换位置取一致、给细则而不是让它自由发挥、要求引用证据、temperature 0、多次采样或复核。

本项目:最严重的就是材料边界问题,轨迹里的 id 被当成泄露,用分段标注和脱敏修掉。

2. Rubrics as Rewards(RaR)是什么?和直接打分有什么区别?

要点:先为每个 prompt 生成一组带权重或分档的可检查细则(checklist),再由 judge 逐条判定,汇总成奖励。相比直接给总分,它把一个模糊的「好不好」拆成若干可验证的判断,方差更小、可解释,出错能定位到具体条目。原论文用它给 RL 提供奖励信号,也适合做离线评测。

本项目:这套 Rubric 只用来评 Agent 的端到端回答,不当训练奖励(Query 解析模型后训练用的是另一套确定性 reward);三档里 P0 作闸门、P1 扣分、P2 打分。

3. 怎么衡量 judge 和人工标注的一致性?

要点:分类判定(通过 / 失败)用一致率和 Cohen’s Kappa,Kappa 扣掉了随机一致的部分,0.6 以上算较好,0.8 以上算高度一致;有序打分(1~5)用加权 Kappa 或 Spearman 相关;也可以看 judge 自身的重测一致性(同一输入多次打分的方差)。样本要覆盖正负两类,只在全通过的样本上算一致率会虚高。

本项目:judge 的重测一致性就是用「同一份回答连评三次」测出来的,0 / 0 / 90 这个结果直接促成了 P0 复核。

4. 小样本评测里,怎么判断两个版本的分差是真实的?

要点:单次分差要和噪声比。常用方法有:同一版本重复跑多次估计方差;配对比较(同一 query 在两个版本上的分差,而不是两组均值比);bootstrap 重采样求分差的置信区间;配对 t 检验或 Wilcoxon 符号秩检验。样本少时优先看不会被均值掩盖的硬指标(比如失败数),并对关键 case 取中位数抗离群值。

本项目:P2 在 15 条样本上噪声约 ±13,所以硬门槛只看 P0 失败数,关键 case 3 遍取中位数,分数都在同一 query 上配对比较。

5. 用内容哈希做缓存键有什么讲究?

要点:键要包含所有会影响结果的输入,漏一个就会命中错误的旧结果(缓存污染);多了无关字段会让缓存无谓失效。序列化要稳定:字典按键排序、编码统一,不能用每个进程随机种子不同的语言内置哈希。把「生成逻辑的版本」(这里是 prompt 全文)也放进键,实现逻辑一变就自动失效,比手动维护版本号可靠。

本项目:细则键里放了 prompt 全文,改 prompt 自动换尺子;前情只在非空时加入,避免连累没有前情的 case 失效。

6. Redis Stream 消费者组怎么保证评测任务不丢?

要点:XADD 写入,XREADGROUP 按组分发,消息被读出后进入该消费者的待确认列表(PEL),处理完 XACK 才移除。消费者崩溃时,消息留在 PEL 里,其他消费者用 XAUTOCLAIM 按空闲时长把超时消息认领过来重新处理。这是 at-least-once 语义,所以处理逻辑要幂等。

本项目:评测任务用独立 Stream 和低优先级消费者组;重复处理时细则按哈希复用、score 按 trace 覆盖写,重复评一次不影响结论。

7. Langfuse 里 trace、observation 和 score 是什么关系?

要点:trace 是一次完整请求;observation 是 trace 下的节点,分 span(一段处理)、generation(一次模型调用,带 token 和成本)、event;score 是挂在 trace 或 observation 上的评价,支持数值、类别、布尔三种类型,可以来自人工标注、SDK 写入或 LLM 评测。UI 里可以按 score 筛 trace、做看板。

本项目:三个 score 挂在被评的那条 trace 上,按 rubric_total 或 rubric_pass 筛出 bad case,点进去就是完整的调用现场。

8. 让 LLM 稳定输出结构化 JSON 有哪些办法?各自的坑是什么?

要点:一是 prompt 里给格式示例,最便宜但最不稳;二是 JSON mode,保证是合法 JSON,但不保证字段齐全;三是 function calling / 强制 tool_choice,按 schema 出参,部分模型在强制模式下会只回存根;四是约束解码(按 JSON Schema 限制采样),最稳但依赖推理框架支持。无论哪种,拿到结果后都要做 schema 校验,并且区分「字段缺了」和「内容为空」两种失败。

本项目:judge 的生成和打分都走结构化输出加 schema 校验;残缺细则只丢那一条,空的打分表会重采样,仍空就记为评测失败,不让它算成满分。

9. temperature 设成 0,LLM 输出为什么还是不完全确定?

要点:temperature 0 等价于贪心解码,理论上确定,但实际服务里有几个来源会引入差异:GPU 浮点运算的非结合性(并行归约顺序不同,结果末位不同,恰好改变 top-1)、推理服务的动态 batching(同一请求和不同请求拼批,数值路径不同)、MoE 模型的路由受同批其他请求影响、服务端模型版本静默更新。所以「temperature 0 可复现」只能当近似,评测要为剩余方差留余量。

本项目:judge 用 temperature 0,同一份回答仍然评出过 0 / 0 / 90,这就是 P0 复核和「关键 case 3 遍取中位数」存在的原因。

10. few-shot 示例从哪来、放在 prompt 哪里,对缓存有什么影响?

要点:few-shot 是在上下文里给模型看的输入输出示例,不改权重。示例质量比数量重要,要覆盖正例和典型反例,且与真实分布接近;示例太多会稀释指令、增加 token。位置上,固定示例放在 system 前缀靠前的稳定段,能被前缀缓存命中;如果按每条请求动态挑选示例插到前缀里,前缀一变缓存就失效。

本项目:高分轨迹(P0 全过且总分 ≥ 70)由 judge 蒸馏成「正确决策 / 常见反例」的短示例,按意图去重后放在 system 的稳定段,不按请求动态换,避免破坏前缀缓存。