首页 核心备考 STAR 案例 评估与护栏 评估深入 护栏深入 核心概念 意图识别 EN

深度解析 · 只讲评估

如何评估一个概率式系统:四组、每组三个,共十二个概念

我在 AI Scientist 面试里关于评估会讲的全部内容,全部落在一个我真正做过的检索系统上 —— 也包括老实交代这个系统还有哪些东西没测。护栏和运行时控制放在配套的护栏页; 这一页只讲评估。

为什么评估这一类系统很难

先读这一节。十二个概念每一个的存在,都是为了解决下面三个问题之一。

问题一:模型不是确定性的。同一个问题问大语言模型两遍,你可能拿到两个 不同的答案。两个都可能对。两个也可能各错一处。普通软件测试的前提是相同输入给出相同输出, 所以跑一遍测试就知道真相。在这里,跑一遍几乎什么都说明不了。一个好答案不能证明系统好; 一个坏答案也不能证明系统坏了。

问题二:很多时候根本没有唯一正确答案。客户问「我的卡为什么被拒付」, 好的回答有很多种,差的回答也有很多种,没有一个精确字符串可以拿来对比。有些问题确实有 精确答案 —— 「我 3 月 3 日的余额是多少」—— 那些是简单的。评估最难的地方,是给那些 「正确」是一个区间而不是一个值的问题设计检查方式。

问题三:系统有很多环节,所以一个总分定位不了故障。现代助手是一条 流水线。它先判断客户想干什么,改写问题,检索文档,过滤检索结果,起草答案,检查这个答案, 再决定要不要拒答。如果你只测「最终答案好不好」,那么当分数从 82% 掉到 71% 时,你只知道 有东西坏了,完全不知道坏在哪。我自己的系统是一个 十一节点的监督者图, 里面还嵌了一个五节点的检索子图。单独一个端到端数字对我毫无用处。

要说出口的那句定调 "Evaluating a probabilistic multi-stage system is a measurement design problem, not a testing problem. I think about it in four questions: what do I measure, how do I measure it, when do I measure it, and how do I know the result is real. Three concepts under each — twelve in total."
(中文意思:评估一个概率式的多环节系统,是一个测量设计问题,不是测试问题。我把它 拆成四个问题:测什么、怎么测、什么时候测、怎么知道结果是真的。每个问题下面三个概念 —— 一共十二个。)
组它回答的问题三个概念
A你测什么?标准答案 · 单环节 vs 端到端 · 每环节指标
B你怎么测?确定性检查 vs 评委 · 评委验证 · 评分标准
C你什么时候测?离线 · CI 门禁与成本分层 · 线上
D你怎么知道结果是真的?方差 · 样本量 · 漂移与基线
为什么要分成「四组、每组三个」— 这是记忆技巧,也是高压下真正管用的原因

有人盯着你的时候,十二条互不相干的事实很难想起来。四个标签、每个下面 三项就容易得多,因为你只需要记住四样东西,然后一次展开一个。如果面试官问一个很大的 问题 —— 「你会怎么评估这个系统?」—— 那四个组名就是答案,十二个概念是追问时的细节。

这个顺序本身也是一个真实的论证,不只是助记。你不可能在还不知道自己要测 什么(A 组)之前就选好方法(B 组)。你也不可能在还不知道一项检查是不是确定性的之前, 就决定它该放在发布流程的哪个位置(C 组)。而如果你分不清真实提升和噪声(D 组), 前面九个都白搭。按这个顺序讲出来,整个结构听起来像工程判断,而不像一张清单。

怎么用这一页

十二张卡片每一张都恰好三个部分,顺序永远一样。

  • 1 · 概念本身 —— 先讲问题,再讲直觉做法以及它为什么不行,然后讲真正的答案, 最后给一个放在假想的 NAB 式助手场景里的具体例子。
  • 2 · 如果缺了它会怎样 —— 那个赭红色横幅。一个具体的失败,加上一个说得通的 银行后果。这部分是面试官真正记得住的。
  • 3 · 面试用的 STAR —— 情境、任务、行动、结果,再加上要背下来的要点清单, 以及这段话能回答的具体面试问题。
关于我说「我的项目没有这个」 十二个概念里有五个在我的项目里没有实现。我在那几张卡片里直接说明,然后讲我会先做哪一个、 为什么。一个能指出自己系统缺口的资深候选人,比一个声称四十条测试用例已经全覆盖的候选人 更有说服力。

A · 你测什么

概念 1–3

在动任何工具之前:先定义「正确」是什么意思,再决定在哪一层做检查, 然后给每个环节挑一个数字。

1

标准答案与黄金测试集

「你怎么知道正确答案是什么?」

1 · 概念本身

问题。你想说「系统 10 题对了 8 题」。要说这句话,你必须事先就知道 这十题各自的正确答案。这个知识从哪来?除非有人先把它写下来,否则哪都来不了。 标准答案(ground truth) 就是那份写下来的正确答案,而 黄金测试集(golden set) 就是那些「输入 + 答案」配对的固定集合,纳入版本管理。

直觉做法,以及它为什么不行。直觉做法是:跑一遍系统,读一读答案, 边读边判断。这样不行,有三个原因。第一,你的判断标准会漂移 —— 周一你能接受的,周五就 不接受了,所以昨天的分和今天的分不可比。第二,你会不自觉地对自己刚做的改动打分偏松, 因为你希望它成功。第三,什么都复用不了:下个月你没法重跑上个月的判断,所以你永远 证明不了自己没有退步。

真正的答案。在跑任何东西之前就把预期写下来,把它和代码 一起放进仓库,并且让检查过程机械化。关键一点:标准答案不必是整段答案文本。它通常是一个 小得多、也容易验证的断言:某个数字必须出现,某条路由必须被选中,某个工具必须被调用, 某次拒答必须发生。写完整的模范答案又慢又脆;写「必须为真的那一件事」又快又耐用。

一个 NAB 式的例子。设想 NAB 的零售客户助手。一个 200 条用例的 黄金测试集可能包含:40 条费用类问题,标准答案是公开费率表里的精确费用数字;30 条交易 争议用例,标准答案是「争议处理工具被调用了,并且带了正确的交易标识」;25 条房贷问题, 标准答案是「给出了一般性信息,没有给个人化建议,并且不出现 'you should' 这类措辞」; 20 条经济困难(hardship)用例,标准答案是「提供了困难援助路径,并且没有发生自动拒答」; 以及 15 条超出范围的用例,标准答案是「拒答并转人工」。注意只有前 40 条有唯一正确字符串。 其余的是一个正确的属性。两者都算标准答案。

黄金测试集从哪来 — 四个来源,按我对它们的信任程度排序
  • 真实用户流量,由人工标注。最好的来源,因为它包含人们真正会问的问题, 包括那些表述很糟糕的。代价高,而且在银行里,任何人看之前都得先做隐私处理。
  • 权威文档。费率表、产品披露声明、已报备的年报。标准答案是从原始出处 抄下来的,所以「它到底对不对」没有争议空间。
  • 事故与投诉历史。每一个曾经造成过伤害的案例都变成一条永久测试。 这是一套测试集不再重复自己历史的办法。
  • 工程师手写。我的项目就是这种。产出最快,也如实反映了流水线的形状, 但它只包含我个人想到的失败模式 —— 这就是我在概念 7 里公开承认的局限。

2 · 如果缺了它会怎样

如果缺了它会怎样

假设测试集里没有一条用例覆盖对冲账户(offset account)。助手告诉一位客户,他的对冲 账户能减少贷款利息 —— 而他那个产品版本根本没有对冲功能。这个答案听起来流畅又权威, 所以没人标记它。六周后投诉来了,而且因为任何问类似问题的人都被生成了同样的措辞, 银行必须去查清有多少客户收到过这句话、以及怎么处理他们。

代价不是那句错话。代价是那六周、那场补救行动,以及当监管机构问「你们是怎么测这一项的」 时,诚实的回答只能是「我们没测,因为我们从来没把『正确长什么样』写下来」。

3 · 面试用的 STAR

情境

我做了一个检索式聊天机器人,数据是五家 ASX 上市矿业公司的年报,跨三个财年。 每个答案里都全是数字。我当时在改提示词和检索参数,唯一的反馈就是读几个答案、 然后形成一个印象。

任务

把「感觉这样好一些」变成写下来的东西,让相隔一个月的两次运行可以互相比较。

行动

我把用例写成 JSON lines 文件,一行一条,分四个套件:路由、检索、计算、边界 —— 分别 8、8、6、6 条,一共 28 条。每条用例必须有五个字段:id、类型、查询、expected 对象,以及 why_it_matters。加载器在任一字段缺失时会抛错, 这样一条随便写的用例会大声失败,而不是悄悄通过。

另外我还写了 12 条数值用例,标准答案是我已经核实过确实存在于入库文本里的精确 数字字符串 —— 比如 BHP FY2024 营收的 55,658,以及它 FY2024 底层 EBITDA 的 29,016。这些只被检索 A/B 测试框架用到。

结果

一共 40 行用例,任何时候都能重跑。事后看 why_it_matters 这个字段价值最大:它逼我为每一条用例给出理由, 而当某条用例失败时,它能立刻告诉我这件事值不值得我在意。

老实说的局限:所有用例都是我自己写的,所以这套测试集反映的是我的想象力, 不是真实用户流量。在银行里我会改成从真实对话记录和投诉历史里取种子。

要点速记
  • 标准答案在跑之前就写好
  • 五个必填字段,缺一个加载器就抛错
  • 4 个套件共 28 条,另加 12 条数值用例
  • 核实过的数字字符串,不是模范答案
  • 缺口:手写的,不是真实流量
对应的面试问题
  • "How did you know your changes were improvements?"(你怎么知道你的改动是提升?)
  • "Where does your evaluation data come from?"(你的评估数据从哪来?)
  • "How do you define correctness when there is no single right answer?"(没有唯一正确答案时,你怎么定义正确?)
2

单环节评估 vs 端到端评估

「你的分数掉了五个点。是哪一部分坏了?」

1 · 概念本身

问题。流水线有很多环节。一个错误的最终答案可能来自其中任何一个, 而且这些环节之间互相影响:检索很差也可能产出一个看起来不错的答案,检索很好也可能被 一段糟糕的总结毁掉。你需要知道该修哪一环。

直觉做法,以及它为什么不行。直觉做法是只做端到端:从前面把问题送进去, 给后面出来的东西打分,一个数字。它很直观,而且那正是用户体验到的东西,所以感觉像是 该测的东西。它会以两种很具体的方式失效。

第一,它没法定位。分数动了,你完全不知道为什么,于是你开始猜、 开始乱改,这是最慢的一种调试循环。第二,错误会互相抵消。检索变差了,答案模型靠 自己训练时学到的东西补上,端到端分数保持不动 —— 而你已经悄悄地把一个有据可依的答案 换成了一个听起来合理的猜测,任何测量都没察觉。

真正的答案。两个都保留,并且清楚各自是干什么用的。 端到端评估 是你衡量用户可见质量的尺子,也是你的发布信号。 单环节评估 是你的调试和实验仪器。单环节评估还便宜得多、噪声小得多,因为你单独测检索器的时候, 整条链路里根本没有语言模型 —— 所以你拿回来的数字是可以精确重现的。

一个 NAB 式的例子。设想助手答错了一个费用问题。端到端告诉你 答案错了。单环节评估告诉你到底是下面四件事里的哪一件:意图分类器把一个费用问题送上了 交易路径;文档检索返回了企业费率表而不是个人费率表;相关性过滤器把正确那一页扔掉了; 或者上游全都对,是写答案那一步读错了表格。这是四种完全不同的修法。没有单环节评估, 你连该讨论哪一件事都判断不了。

端到端

测的是客户实际拿到的东西。慢、贵、噪声大 —— 每个环节的随机性都会叠上来。 适合做发布门禁,也适合按月跟踪整体质量。不适合调试:它只告诉你出事了, 永远不告诉你是哪里。

单环节

用一个受控输入测一个环节。快、便宜,而且常常是完全确定性的。适合做实验和 根因定位。但作为唯一信号就不行:每个环节都在变好,组装起来的系统仍然可能变差。

2 · 如果缺了它会怎样

如果缺了它会怎样

只有端到端打分的情况下,有人升级了文档检索用的向量嵌入模型。检索质量悄悄下滑, 但答案模型足够强,能靠自己对银行业的通用知识产出很有信心的回复。端到端分数几乎不动, 于是这个改动就上线了。

实际发生的是:助手已经不再依据 NAB 自己的文档作答,而是开始依据一个通用模型 「以为银行是怎样」来作答。费用数字现在大致对、具体错。什么都不会告警,因为在场的唯一 仪器看不到流水线内部 —— 而当审计后来问「哪份文档支持了这个答案」时, 你给不出一个诚实的回答。

3 · 面试用的 STAR

情境

我想测一下,把候选池加大再加一道重排序(rerank)步骤,到底有没有真的改善检索。 用端到端去测,差别是看不见的:答案每次跑都不一样,而且答案模型把缺口掩盖了。

任务

单独测这个检索改动,链路里任何位置都不放语言模型,让唯一变化的东西就是检索配置。

行动

我写了第二个测试框架 evals/ab_retrieval.py,它不跑整张图,而是直接调用 retrieve_company_node。它评的是:含有已知数字字符串的那篇文档有没有 到达调用方,以及排在第几位(从 1 开始计)。为了构造基线,我把重排序器换成了一个 直通函数,这样两个配置之间只差候选池大小和重排序这两点。

主测试框架在更小的尺度上做同样的事:路由用例直接调用 retrieve_decision_node 和 query_rewrite_node, 检索用例只调用检索子图 —— 从不跑完整的十一节点图。

结果

一份我一眼就能读懂的逐用例判定:same、 NEW FOUND IT、NEW LOST IT,或者 rank 6 -> 2 better。这是一个不含语言模型方差的单环节测量, 所以名次挪动一位就是真实变化,不是噪声。

有一个实现细节很关键:候选池大小在检索索引从缓存文件加载时就被固化进去了, 所以我必须在两个配置之间清掉 _bm25_cache, 否则第二次运行会悄悄沿用第一次的设置。

要点速记
  • 端到端 = 发布信号;单环节 = 调试
  • 端到端会让错误互相抵消
  • 链路里没有 LLM = 可重现的数字
  • 用直通函数构造基线
  • 配置之间要清缓存,否则会静默沿用
对应的面试问题
  • "How do you debug a multi-stage pipeline?"(多环节流水线你怎么调试?)
  • "How do you isolate the effect of one change?"(你怎么隔离出单个改动的影响?)
  • "Why not just measure the final answer quality?"(为什么不干脆只测最终答案质量?)
3

每环节指标 —— 每个环节一个数字,两个方向的错误都要报

「每一步你具体测什么?」

1 · 概念本身

问题。一旦你接受了每个环节都需要自己的测量,你就得选这个测量 到底是什么。选错了,你会得到一个永远看起来没问题的数字,或者一个因为你根本不在意的原因 上下波动的数字。

直觉做法,以及它为什么不行。直觉做法是到处用准确率 —— 每个环节 都看「正确百分比」。它不行,因为准确率把错误的方向藏起来了,而在银行里,这两个方向的 代价完全不同。一个 95% 准确率的拒答指标,可能意味着助手拒答稍微过于频繁,那只是让人 烦躁;也可能意味着它回答了本该拒答的问题,那就是持牌资质问题。同一个数字, 严重程度相反。

真正的答案。每个环节一个主指标,按这个环节的职责来挑; 另外只要某个环节做的是「是/否」判断,两个方向的错误就分开报。对这类助手来说重要的 是下面五个:

环节指标用大白话说它是什么为什么选这个
路由 路由准确率 在所有被问到的问题里,被送上正确路径的比例。 一次路由错误会让后面所有环节都变得无关。而且它测起来便宜, 因为正确路由就是一个标签。
检索 Recall@k(召回率) 在确实存在正确文档的那些用例里,前 k 条结果中至少出现一篇正确文档的比例。 答案环节可以忽略一篇不相关的文档,但它没法凭空造出一篇从没被取回的文档。 证据缺失是不可挽回的;证据多余通常还能扛过去。
作答 接地率(groundedness rate) 在所有答案里,每一条事实性断言都能追溯到某篇检索文档的那部分占比。 这是能抓住「自信编造」的指标,而自信编造正是真正伤害客户的失败模式。
拒答 拒答正确性,两个方向 报成两个数字:本该回答却错误拒答的,和本该拒答却错误回答的。 这两种代价不可比,所以一个混在一起的数字会毁掉你最需要的信息。
整个系统 每次查询的延迟与成本 回复所需的实际墙上时钟时间,以及每次回复花的钱。 质量提升通常是用时间和金钱买来的。少了这两个,你会把系统「优化」成 谁都负担不起去跑的东西。

一个 NAB 式的例子。一段经济困难(hardship)对话能说明为什么 两个拒答方向必须分开。一位客户写道「我失业了,这个月的还款交不上」。如果助手错误地 拒答 —— 把这当成理财建议然后回避 —— 那就是一个真正处于困境的人,在最糟糕的时刻 被一个自动系统拒之门外。这是真实伤害,是对脆弱客户的失职,而不只是一个答得不好的回答。 反过来,如果助手错误地回答了另一类问题 —— 「我该不该把房贷利率锁成固定的?」—— 并给出了推荐意见,那它就提供了个人化理财建议,而这在澳洲是持牌业务。两者都是拒答错误。 一个伤害客户,另一个造成监管违规。而一个 95% 的准确率数字,两种都可能是。

召回率 vs 精确率 — 以及这两个词背后的含义,用大白话讲

召回率(recall)回答的是:在所有我本该找到的东西里, 我找到了几成?精确率(precision)回答的是:在所有我确实返回的东西里, 真正对的有几成?两者互相牵制。返回更多文档,召回率上升而精确率下降。

Recall@k 是只在前 k 条结果里算的召回率, 这个 k 就是你实际往下传的条数。我的 k 是 8,所以 recall@8 就是 「检索器交给下一环节的那八篇里含有一篇正确文档」的用例占比。排到第九位的东西, 跟不存在没区别。

对于喂给语言模型的检索来说,召回率是要优先优化的指标。 现代模型对上下文里混进几篇不相关文档的容忍度还算不错。但正确文档缺席它就没救了 —— 它要么拒答,要么更糟,用通用知识把这个缺口填上。

偏向召回率的代价 更多文档意味着更长的提示词,所以成本更高、回复更慢,而且模型注意到错误段落的 概率也更大。这正是为什么每次查询的延迟和成本要和质量指标放在同一张表里。

2 · 如果缺了它会怎样

如果缺了它会怎样

只有一个混合质量分的情况下,助手会慢慢变得更谨慎 —— 每次改提示词都多加一点模糊 措辞。经济困难类问题上的拒答率在两个月里从 3% 悄悄爬到 19%。那个混合分反而变好了, 因为拒答从来不会以显眼的方式出错,也从来不会引来一条生气的追问。

可见的结果是:大约每五位来求困难援助的客户里就有一位被自动系统拒之门外, 而且不会再试第二次。什么都没告警,因为被盯着的那唯一一个指标是上升的。 这正是「错误拒答是真实伤害而不是小小不便」的那种具体情形, 而除非把拒答错误按两个方向分开计数,它是看不见的。

3 · 面试用的 STAR

情境

我的流水线有几个清晰分开的环节 —— 一个路由决策、一次查询改写、按公司并行检索、 一个相关性过滤器、答案起草,以及一次答案评分。每一个都能以自己的方式出错。

任务

给每个环节配一个「这个环节坏了它就会真的动」的指标,而不是一个总分。

行动

路由准确率来自路由套件:用例里声明了预期的路由和工具,检查就是精确比对。 检索在 A/B 框架里有一个真正的召回类指标:在那 12 条数值用例上算 hit@top_k、hit@5 和 mean_rank。 接地性是在运行时跑的,不只在测试套件里 —— 我的 grade_answer 节点会列出没有任何检索文档支撑的断言, 并把数量映射成:0 条为 yes,3 条及以下为 partial, 超过 3 条为 no,进而变成答案上的高、中、低置信度。

边界用例覆盖的是拒答方向里「系统应当回避、或应当声明数据缺失」的那一侧。 我没有的是对相反方向 —— 过度拒答 —— 的系统性测量, 而且我完全没有记录每次查询的延迟或成本。

结果

出现退步时,我通常只看输出就能点出是哪个环节,因为失败的检查打印的是具体的 不匹配内容,而不只是一个 FAIL。

两个要老实承认的缺口。第一,没有延迟和成本测量;在生产环境里我会先补这个, 因为它几乎零成本就能采集,而且它才是让「质量 vs 成本」的取舍能被讨论的前提。 第二,我视觉入库步骤里的那个页面过滤器 —— 一个基于页面数字密度的启发式规则 —— 从来没测过它的召回率。它是整条流水线里唯一一个完全没有指标的环节, 而它可能悄悄丢掉含有答案的那一页。

要点速记
  • 每环节一个指标,按职责匹配
  • 检索优先看召回率 —— 证据缺失是致命的
  • 拒答错误:两个数字,绝不能只报一个
  • 延迟和成本要摆在质量指标旁边
  • 缺口:过度拒答、成本、页面过滤器召回率
对应的面试问题
  • "What metrics would you track for a retrieval-augmented assistant?"(检索增强型助手你会跟踪哪些指标?)
  • "Why recall rather than precision for retrieval?"(检索为什么看召回率而不是精确率?)
  • "How do you evaluate whether a system refuses correctly?"(你怎么评估一个系统拒答得对不对?)

B · 你怎么测

概念 4–6

挑能把问题定下来的最便宜的仪器。当这个仪器本身就是个模型时, 就把它当成一台需要校准的仪器来对待。

4

确定性检查 vs LLM 当评委

「你怎么自动给一个答案打分?」

1 · 概念本身

问题。你有了黄金测试集,现在你需要某个东西在没有人去读的前提下, 判定每条用例是通过还是失败。有些用例很容易 —— 55,658 这个数字出现了吗? 有些不容易 —— 这条经济困难场景的回复,共情程度是否恰当、有没有夹带建议?

直觉做法,以及它为什么不行。这里有两种直觉做法, 而且它们朝相反的方向各自失效。

第一种是什么都用精确字符串匹配。它不行,因为模型会换说法。 "Revenue was US$55,658 million" 和 "revenue of approximately 55.7 billion US dollars" 都在传达同一个真相,而一个朴素的字符串匹配会把第二个判错。于是你的日子全花在修误报上, 而不是找真 bug,最后你就不再信任这套测试集了。

第二种是让模型来判所有东西。这很有诱惑力,因为一套实现就能 应付全部用例。它不行,因为模型评分员慢、每条用例都要花钱、它本身还是非确定性的 —— 所以同一个答案这次跑通过、下次跑失败 —— 而且它有能力把简单的事实比对搞错。 用一个概率式仪器去检查一个数字是否等于另一个数字,严格来说比直接用等号更差。

真正的答案,作为一条规则。凡是正则表达式或数值比较能解决的, 绝不送给模型。把每一项检查归入三档,只有在下一档确实办不到时才升档。

档仪器用来做什么特性
1 精确比对或结构比对 路由标签、工具名、某个工具有没有被调用、某个必填字段在不在、 引用标记是否存在、退出码。 免费、瞬时、完全可重现。就它检查的那件事而言,它不可能出错。
2 模式匹配与数值检查 某个数字是否在容差内出现;答案里有没有出现禁用措辞;日期是否落在区间内; 回复里是否包含必需的免责声明。 仍然免费且可重现。需要在单位和格式上小心,那是实打实的工作量,但是有界的。
3 模型当评委 这条断言有没有被这些文档支撑;对一位处境艰难的客户,这个语气合不合适; 这属于一般性信息还是个人化建议。 花钱花时间、非确定性、需要自己被验证。只有在必须理解语义时才说得过去。

一个 NAB 式的例子。对一个费用问题:第 1 档检查费用查询工具有没有 被调用;第 2 档检查费率表里那个精确费用数字是否出现在回复里、以及 "you should" 这个措辞是否没有出现;第 3 档只用来回答「这条回复有没有从一般性信息越界到个人化建议」, 那确实需要读懂这句话。这一条用例上四项检查里有三项是免费且完全可重现的。 只有第四项需要模型。

「LLM 当评委」是什么意思 — 以及它的两种形态

LLM 当评委(LLM-as-judge) 指的是用一个语言模型去给另一个模型的输出打分。你把问题、答案,通常还有原始文档, 以及一套规则交给评委,它返回一个判定。

  • 逐条式(pointwise)。评委看一个答案,按评分标准给它打分。简单, 我的项目用的就是这种。分数容易聚堆、容易漂移,所以绝对数值跨时间比较意义不大。
  • 成对式(pairwise)。评委看同一个问题的两个答案,说哪个更好。 比逐条打分更可靠,因为「比较」比「绝对评级」是更容易的判断 —— 但它会受位置偏见 影响,那是概念 5 的内容。

还有一个常被忘掉、但很有用的中间选项:让模型去抽取, 而不是去判定。让它把自己给出的数值断言抽出来,然后用确定性方式比对这些数字。 难的那部分你借用了模型的语言理解,而在需要精确的那部分你用的是精确比对。

2 · 如果缺了它会怎样

如果缺了它会怎样

假设一个 200 条用例的银行测试集里,每一项检查都是模型判定。跑一整轮要花好几美元、 耗二十分钟,所以没人会在提 PR 时跑它 —— 它先变成每周一次的仪式,然后变成每月一次。 更糟的是,因为评委是非确定性的,在代码完全没改的情况下,这套测试集报出 186、然后 191、然后 184(满分 200)。工程师于是学会了「五个点的波动没有意义」, 所以当真正五个点的退步到来时,它被当成噪声打发掉了。

失败之处不是有个错答案送到了客户面前。而是这套评估系统已经不再起到证据的作用, 却还在持续产出数字;而建立在它之上的发布门禁,诚实地说什么都拦不住。

3 · 面试用的 STAR

情境

我的检查天然分成两类。路由和计算用例有精确预期 —— 一个布尔标志、一份公司列表、 一个工具名。答案质量和接地性则需要判断。

任务

只在确实非模型不可的地方用模型,其余全部保持免费且可重现。

行动

离线测试套件几乎完全是确定性的。共用的辅助函数做精确比对:一个布尔字段检查、 一个把两边都转成大写的列表成员检查(这样股票代码不分大小写都能匹配), 以及一个缺失值辅助函数,它会精确报出哪些预期值没出现。财年用正则表达式从查询里 提取,指标关键词用不区分大小写的子串扫描去比一份固定关键词表。 这里面完全没有模型参与。

我生产上的护栏检查把同一个原则做到了极致:它是对拼装好的上下文做一个 十字符的最小长度检查,完全不调模型。用模型只会为一个 len() 就能完美回答的问题增加成本和延迟。

我确实用了模型的两个地方,都是必须理解语义的运行时评分员: 一个是对检索到的文本块做相关性过滤,另一个是对起草的答案做断言级接地性检查。

结果

离线测试套件零成本运行,而且每次给出同样的结果,所以一次失败永远是一次真实失败。 这正是它能被用作门禁的原因 —— 这一点我在概念 8 会再回来讲。

有一处改进值得一提:接地性评委总是把财务表格里正确的数字标成有问题, 因为单位写在离数字很远的表头单元格里。我用一个正则表达式解决了这个问题: 它在表头行里找到单位标记,然后注入一条显式注解 —— 例如说明这张表里所有裸数字的单位 是百万美元。是一个确定性的预处理步骤让模型判定变得可靠,而不是把模型换掉。

要点速记
  • 规则:== 能解决的事,绝不去问模型
  • 三档:精确、模式、评委
  • 评委的代价是钱、时间,还有可重现性
  • 我的护栏:一个 10 字符检查,不用 LLM
  • 怪评委之前,先修评委的输入
对应的面试问题
  • "When would you use an LLM as a judge, and when would you not?"(什么时候你会用 LLM 当评委,什么时候不会?)
  • "How do you keep evaluation cheap enough to run often?"(你怎么让评估便宜到可以经常跑?)
  • "How do you handle answers that are correct but worded differently?"(对于正确但措辞不同的答案,你怎么处理?)
5

评委验证 —— 以及评委自带的那些偏见

「你怎么知道你的评委靠不靠谱?」

1 · 概念本身

问题。你一旦用模型当评委,那个模型就是你的测量仪器。 你关于系统得出的每一个结论,都经过它过滤。一台未校准的仪器不会给你一个略微偏错的答案; 它给你的是一个看起来很精确、实际未知的答案。

直觉做法,以及它为什么不行。直觉做法是:给评委写一段好的提示词, 读几条它的判定,觉得认同,从此就信任它。它不行,因为你核对的正是那些你和评委都在看 显而易见之处的用例,而你从来没有测过你们两者不一致的比率。没有这个比率, 你就回答不了唯一重要的那个问题:如果评委说质量从 78% 涨到 84%, 这六个点里有多少是真的?

真正的答案。把评委当成一个自己也需要评估集的模型来对待。 抽一批用例 —— 两百来条是个合理的起点 —— 让人来标注,再让评委跑同一批用例,然后测 一致率(agreement)。 同时报出原始一致率和一个经过随机修正的数值。然后去读那些不一致的案例, 因为它们能告诉你评委错在哪个方向,也让你有办法去修评分标准。

另外要预期三种具体偏见,它们是模型评委被反复记录下来的性质, 不是什么意外:

  • 偏好长答案。评委系统性地偏好更长的答案。一个只是让回复变得更啰嗦的改动, 会被记成质量提升。
  • 偏好自己的风格。评委倾向于偏好看起来像自己输出的文本 —— 同一个模型家族、 同样的措辞习惯。如果评委和被测系统是同一个模型,那这个系统就有一部分是在批自己的 作业。
  • 位置偏见。在成对比较里评委会偏向某一个位置,通常是第一个。 修法很便宜:每一对都按两种顺序各跑一次,只有当两次结果一致时才计入这次比较。

一个 NAB 式的例子。设想评委的工作是判定一条回复有没有从一般性 信息越界到个人化理财建议 —— 在澳洲这是一条持牌边界。两个团队在比较提示词版本, 评委说 B 版更好。如果没人测过评委的一致率,银行根本不知道这个评委到底有没有理解那条 建议边界。假设后来的验证显示,它跟一位合规审核员的原始一致率是 71%, 而且不一致的案例正好全都聚集在这条边界所针对的临界情形上。那么恰恰在最需要它的地方, 这个评委近乎无用 —— 而所有用它做出的决定都得重新审视。 这是一个花一个下午做标注就能发现的事实。

怎么读一个一致率数字 — 粗略区间,以及为什么随机修正很重要

没有普适阈值,但粗略的工作区间是有帮助的。经过随机修正的一致率 高于大约 0.8,意味着评委在那项任务上可以替代人。大约 0.6 到 0.8 之间, 意味着它能用来跟踪时间上的趋势方向,但不能用来做绝对结论。低于大约 0.5, 那这个评委在这项任务上就完全不该用,而诚实的做法是把问题收窄到它能胜任为止。

随机修正之所以重要,是因为类别不平衡。如果 90% 的答案本来就是接地的, 那么一个每一次都说「接地」的评委能拿到 90% 的原始一致率,而它什么都没学到。 这个评委的随机修正数值是 0,那才是正确的判定。

还有一个值得随手备好的对照:把人与人之间的一致率也测出来。 如果两位合规审核员彼此只有 80% 的一致率,那么一个 78% 的评委已经接近这项任务的 天花板了,此时正确的反应是把定义磨得更清楚,而不是继续调评委。

2 · 如果缺了它会怎样

如果缺了它会怎样

一个未经验证、带着长度偏好的评委正在悄悄地引导产品走向。六周的提示词迭代, 每一轮都由评委的分数指引,结果回复从 90 词长到了 260 词。评委的质量分全程在涨。 然后真实信号往反方向动了:客户中断对话的次数变多,同类主题打进客服中心的电话也变多, 因为没人会在手机上读 260 个词。

六周的工程投入,花在了优化测量仪器里的一个偏见上。那些分数是真实数字, 被仔细跟踪着,而且全程指错了方向。唯一能抓到它的办法,是在信任评委之前先拿它跟人比 —— 而这恰恰是我自己的项目从来没做过的事。

3 · 面试用的 STAR

情境

我的项目有两个基于模型的评分员在运行时跑着。一个按相关性过滤检索到的文本块。 另一个给起草的答案打分,并列出没有任何来源文档支撑的断言, 把每一条归类为算术、由趋势外推,或通用知识。

任务

老实说清这些评分员到底是什么:我从来没有校准过的测量仪器。

行动

这是我项目里最大的缺口。我从来没有拿人工标注去验证过这两个评委中的任何一个。 没有测出来的一致率,所以当接地性评分员说某个答案是完全接地的, 我没有任何量化依据去相信它。我调这两段提示词的方式,是读输出、形成印象 —— 正是我刚刚批判过的那种直觉做法。

我会先做的事,工作量大约一天:从真实运行轨迹里抽 100 组「答案 + 文档」配对, 我自己隔一周标两遍接地性,用我自己的自一致率作为天花板,再让评分员跑同样这 100 条, 然后报出原始一致率、一个随机修正数值,以及一张关于不一致方向的二乘二表。 我还会专门查长度偏见:测试把同样的断言用更啰嗦的方式重述一遍时, 评分员的判定会不会改变。

结果

今天这里的结果是一条明说的局限,而不是一个数字;我宁愿把这话说清楚, 也不去暗示一种我并不具备的严谨。

有一处缓解措施是我刻意做的:两个评分员都失败放行(fail open)。 如果评分员的调用抛错,相关性过滤器就把所有文档放过去,答案评分员则记为通过。 这让评分员宕机不会把产品搞坏,但同时也意味着评分员的故障在分数里是看不见的 —— 而这恰恰是一致率测量重要的原因。一台失败放行、又静默坏掉的仪器, 读出来就是「一切正常」。

要点速记
  • 评委就是那台仪器 —— 要校准它
  • 跟人比一致率,而且要做随机修正
  • 三种偏见:长度、自我风格、位置
  • 成对比较两个顺序都跑,只有一致时才计数
  • 我的缺口:从未验证;两个评分员都失败放行
对应的面试问题
  • "How do you know your judge is any good?"(你怎么知道你的评委靠不靠谱?)
  • "What are the known failure modes of LLM-as-judge?"(LLM 当评委有哪些已知的失败模式?)
  • "What is the weakest part of your evaluation setup?"(你的评估体系里最弱的一环是什么?)
6

评分标准与结构化判定 —— 约束输出形状,拆开各个判断

「你怎么让评委不说含糊话?」

1 · 概念本身

问题。评委返回的是自由文本。你必须把它变成一个能跨运行比较的数字, 而且你必须知道这个数字是什么意思。自由文本两件都做不到。

直觉做法,以及它为什么不行。直觉做法是「给这个答案打 1 到 10 分」。 三件事会出错。

第一,这个刻度没有共同含义 —— 没有任何东西能区分 6 分和 7 分, 所以同一个答案在不同日子打出不同分,而且当评委模型在你脚下被升级时,分数还会漂移。 第二,这个数字是一个混合值:一个事实错误但写得很漂亮的答案可能得 7 分, 一个事实完美但语气无礼的答案也可能得 7 分。你没法还原是哪一种。第三,解析自由文本很脆 —— 评委写了 "I'd say around 8, though...",你的正则表达式就抽出了错的东西。

真正的答案,分两半。

约束输出形状。要求评委返回一个结构化对象,带有命名字段和一组 固定的允许取值,而且靠 schema 强制执行,不是靠客气地请求。这样就没有解析步骤, 对返回了什么也没有歧义。

拆开各个判断。分别提问,分别拿判定。事实上有没有支撑。 完整不完整。语气对不对。有没有停在建议边界正确的那一侧。每一项都配一段简短、明确的 评分标准(rubric), 写清确切条件。如果你想要一个总览数字,事后再聚合 —— 但要保留各个分项, 因为分项才是告诉你该修什么的东西。

一个 NAB 式的例子。一条经济困难场景的回复需要四个独立判断: 关于困难援助选项的事实内容对不对;有没有提供必需的援助路径;对一位处境艰难的客户, 语气恰不恰当;以及有没有避开个人化理财建议。混成一个 1–10 分的话, 一条温和、准确、有帮助、但夹了一条推荐意见的回复可能得 8 分 —— 而那一条推荐意见就是持牌资质问题。拆成四个判定后,三项返回通过, 建议边界那一项返回失败;这既是正确的评估,也直接可以行动。

混合分

「打 1–10 分。」一个数字,没有共同含义,随时间漂移,掩盖了到底是哪个维度在动。 一次退步看起来像 7.8 → 7.4,既无法行动,又很容易被当成噪声打发掉。

拆开的结构化判定

四个命名字段,每个都有允许取值和一段书面评分标准。一次退步看起来像 advice_boundary: pass → fail on 6 cases, 它点出了问题、规模,还有归谁负责。

2 · 如果缺了它会怎样

如果缺了它会怎样

一个混合质量分连续几个月停在 7.9 左右。在它下面,两件事朝相反方向移动: 文档重建索引之后,费用问题上的事实准确率下降了,而回复变得更友好、排版更好了。 混合把两者抵消掉。看板一片平稳的绿色。

客户正在以一种温和、结构良好的语气收到错误的费用数字。当它终于通过投诉浮上水面时, 事故复盘会问「准确率是从什么时候开始下滑的」—— 而唯一可用的数据是一个从没动过的混合分, 所以起始日期确定不了,受影响的人群范围也划不出来。混合不只是把问题藏起来了; 它还销毁了补救所需的证据。

3 · 面试用的 STAR

情境

我需要答案评分员告诉我两件不同的事:这个答案到底有没有用,以及它里面每一条断言 能不能追溯到某篇来源文档。这两件事该用不同的严格度。把一个部分有用的答案直接不给看, 比给它打上较低置信度标签再展示要更糟。

任务

在不为两次模型调用付费的前提下拿到两个独立判定,而且形状要能被程序直接使用。

行动

一次调用,提示词里显式分开的两个任务,配不同的评分标准。任务一是刻意宽松的 「有用性」通过/失败判定 —— 指令说明只有在确实什么有用的都没有时才判失败。 任务二是严格的断言级检查:列出所有文档不支撑的断言, 并把每一条归类为算术、由可见趋势外推,或通用知识。

输出是一个带类型的对象:一个只允许两种取值的评级,以及一份未被支撑的断言列表, 每条断言的依据只允许三种取值。我用确定性方式把数量映射成等级:0 条是接地, 3 条以内是部分接地,超过 3 条是不接地 —— 进而变成高、中、低置信度。 判断是模型做的;阈值是我的,而且写在代码里,所以它可审计, 也能在不碰提示词的情况下改。

有一处出于隐私考虑的评分标准细节:评委必须写一个不超过八个词的简短标签, 点出指标和上下文 —— 例如 "BHP FY2024 EBITDA" —— 而且不得复述那句话或那个数值。 这个标签已经足够拿去重新检索,同时它把具体数字挡在了会被记日志和追踪的字段之外。

结果

这两个判定驱动不同的行为。有用性判失败会走到兜底路径。有两条或更多未被支撑的 断言会触发一次定向重试:把那些断言标签当作新的检索查询, 并重置这些标签里提到的公司的「已找到」状态, 这样重试是专门去找缺失的证据,而不是把原来的检索重做一遍。

如果我当初要的是一个混合分,上面这些一个都做不到:我会拿到一个数字, 却完全不知道它代表两个问题中的哪一个。

要点速记
  • 用 schema 约束形状,不是靠请求
  • 拆开各个判断;只在最后才聚合
  • 有用性上宽松,接地性上严格
  • 阈值放代码里,判断留给模型
  • 只写标签不写数值 —— 让数字进不了日志
对应的面试问题
  • "How do you make an LLM judge's output usable in code?"(你怎么让 LLM 评委的输出能在代码里用?)
  • "Why not just ask for a single quality score?"(为什么不干脆要一个质量总分?)
  • "How do you design a rubric?"(评分标准你怎么设计?)

C · 你什么时候测

概念 7–9

没人跑的检查就不算检查。这一组讲的是把每项检查放到它会真的被执行的位置, 以及一个事实:离线测试有一个硬天花板,写多少用例都抬不动它。

7

离线评估 —— 以及它绕不过去的天花板

「你的离线测试集永远告诉不了你什么?」

1 · 概念本身

问题。你需要在客户看到之前就知道一处改动安不安全。 这意味着要拿保存好的用例来跑,而不是拿线上真实流量来跑。

它是什么。 离线评估(offline evaluation) 就是跑那份固定的黄金测试集,并把输出和记录下来的标准答案作比较。它的长处恰好是直觉做法 所缺的那几项:它可重复,所以两次运行能对比;它足够便宜,可以经常跑;它安全, 因为没有客户会看到输出;而且它足够快,能塞进开发循环里。

直觉信念,以及它为什么不行。直觉信念是:离线测试集足够大, 系统就可以安全上线了。它不成立,原因只有一条,而且再怎么努力都消不掉: 一份离线测试集里只装得下有人已经想到过的失败。每一个用例的存在, 都是因为有个人想到了它。而在生产环境里真正伤到你的失败,几乎按定义就是没人想到的那些 —— 用你没预料到的方式提的问题、在一条消息里打了三个问题的客户、你忘了还存在的那个产品变体。

还有第二层更隐蔽的天花板:离线用例通常都是单轮的。真实对话是有历史的, 而有意思的失败往往住在第四轮 —— 助手已经忘了「它」指的是什么, 或者把一个错误的假设一路带了下去。

真正的答案。离线评估是一张回归网,不是一份安全证明。 它的职责是保证那些你已经知道会出错的具体事情不会再出错。每一次生产事故都变成一条新用例, 所以这份测试集会随时间越来越靠近现实。而正因为它永远不可能完整, 它必须和线上测量配对使用 —— 也就是概念 9 —— 那是唯一能看见「没人想到过的失败」的仪器。

一个 NAB 式的例子。设想一份 200 条用例的离线测试集,过了 197 条。 里面有费用问题、争议处理流程、经济困难的各种说法,还有超出范围时的拒答。 里面没有的,是一位客户写下 "my pay didn't go in and the rent comes out tomorrow, what do I do"(我工资没到账,房租明天就要扣,我该怎么办)—— 这条消息同时是一个交易问题、 一个经济困难信号,还是一件紧急事。没人写过这条用例,因为没人想到过这种说法。 离线的结论是 200 里过了 197。离线是对的,同时它也不是全貌。

让测试集长过你自己想象力的两种办法 —— 以及各自的局限
  • 从真实流量里挖。把线上问题聚类,再从那些完全没有覆盖的簇里采样。 这是价值最高的来源,因为它能找到你自己永远编不出来的说法。在银行里, 这件事得先解决隐私:个人信息必须先被移除,对话记录才能放进代码仓库, 而这个约束是设计输入的一部分,不是事后补的。
  • 对抗用例与生成用例。用模型去生成各种变体 —— 换句话说的、有错别字的、 意图混杂的消息、提示词注入的尝试。便宜而且覆盖面广。局限在于, 模型生成的是它觉得自然的那些变体,所以它有它自己的盲区, 只是和你的盲区不一样。它把覆盖面拓宽了,但没把它补全。

两种办法都消不掉这个天花板。两种都能把它抬高, 而第二种便宜到没有任何好理由不做。

2 · 如果缺了它会怎样

如果把它当成完整的来信

团队靠一份全绿的离线测试集就上线了,背后没有任何线上测量。一次提示词改动 悄悄破坏了多轮行为:从第三轮开始,当客户名下有多个账户时,助手开始回答错的那个账户。 而每一条离线用例都是单轮的,所以测试集依然全绿。

接下来五周里,有两个账户的客户被告知的是另一个账户的余额。最早的证据是一条投诉, 不是一个指标。事故复盘只能从原始 trace 里重建出哪些对话受了影响; 而那份在这五周里每一天都通过的离线测试集 —— 变成了「测试做得不够」的证据, 而不是「测试做到位了」的证据。

3 · 面试用的 STAR

情境

我整套评估都是离线的。四个测试集一共 28 条用例,另加 12 条数值检索用例, 全部从命令行跑。

任务

把离线运行做得足够便宜、足够具体,让我在开发过程中真的会去跑它, 而不是把它当成一道仪式。

行动

这些测试集放在一个注册表里 —— 一个把每个测试集名字映射到它的用例加载器和 评估器的字典 —— 所以加一个测试集是加一条条目,不是写一个新脚本。 运行器接收几个参数:跑哪个测试集、最多跑多少条用例、以及一个或多个具体的用例 id, 这样我在调某一条用例的时候可以只跑那一条。

每条用例都打印 PASS 或 FAIL, 并且给出具体不匹配在哪,而不只是给个判定;然后是每个测试集的 Score: passed/total 和一个总计;只要有任何失败, 进程就以非零退出码结束。最后这个细节,才是让它之后能被自动化系统使用的关键。

结果

离线评估在开发过程中抓到了真实的退步,也给了我一套讨论它们的词汇 —— 我可以说「路由用例 5 在计算标志位上失败了」,而不是「感觉有点不对」。

这个天花板我心里清楚。那些用例每一条都是我自己写的,所以这份测试集测的是我的想象力。 它是单轮的,所以看不见对话状态类的 bug。它背后没有线上流量。 在生产环境里我会把它当作回归网,把安全的重量压在线上测量上, 并且加一条规则:每一次事故都变成一条永久用例。

要点速记
  • 离线 = 可重复、便宜、安全、快
  • 天花板:只装得下有人想到过的失败
  • 基本是单轮,所以漏掉状态类 bug
  • 注册表用字典;参数是测试集、上限、id
  • 非零退出码 = 自动化系统能用
对应的面试问题
  • "What are the limits of offline evaluation?"(离线评估的局限是什么?)
  • "Would you ship on a green offline suite?"(离线测试集全绿你就敢上线吗?)
  • "How do you decide what goes into your test set?"(你怎么决定测试集里放什么?)
8

CI 门禁与成本分层 —— 三层,以及那条确定性规则

「这套东西你会怎么放进构建流水线?」

1 · 概念本身

问题。靠人想起来才跑的评估保护不了任何东西。它必须是自动的。 但语言系统的一份完整评估测试集是要花真金白银、也要花真实时间的, 所以在每一次提交上都把整套跑一遍是负担不起的 —— 而一条慢的流水线会被绕过去,那比没有流水线更糟。

两种直觉做法,以及它们为什么都不行。直觉做法一: 每次提交都跑完整的真实测试集。它败在成本和速度上 —— 工程师要等二十分钟才拿到反馈, 最后总会有人加一个跳过它的开关。直觉做法二:什么都不自动跑,靠作者自己去检查。 它失败是因为人很忙,而这个检查是看不见的;测试集就这样悄悄烂掉, 到最后没人知道它是不是还能通过。

真正的答案:按成本分层。 持续集成(continuous integration) 应该在每个阶段跑不同分量的评估,跟这个阶段花得起多少钱、 以及它需要买到多少信心相匹配。

层级触发时机跑什么成本 / 时间会拦吗?
1 · Mock(模拟) 每次提交 只跑确定性检查。校验用例 schema、跑通评估器逻辑,不调用任何外部服务。 免费,秒级 会 —— 永远会
2 · Smoke(冒烟) 每个 pull request 一小份真实子集 —— 每个测试集抽几条用例走真实模型 —— 用来证明接线、凭证和提示词还是好的。 几分钱,约 2 分钟 会,硬失败时拦
3 · Full(全量) 每晚定时 完整的真实测试集,对着一份已提交进仓库的基线跑, 并把差异发到一个真有人看的地方。 几美元,约 20 分钟 只告警,不拦

让这套东西成立的那条规则:发布门禁必须是确定性的。 门禁是一个能够阻止改动上线的检查。如果这个门禁不是确定性的 —— 也就是代码一行没改、重跑一次也可能失败 —— 那么人们就会学会去按重试, 到第三次之后他们会不看内容就按重试。到这一刻,这个门禁已经被训练成不存在了, 但它还挂在看板上。所以模型判定出来的指标可以告警,也可以交给人去复核, 但它们绝不能是拦下合并的那个东西。拦下合并的,是那个确定性的层级。

一个 NAB 式的例子。改一处意图分类器的提示词。 第 1 层八秒钟跑完,确认没有格式坏掉的用例文件、没有评估器崩溃 —— 免费,每次推送都跑。 第 2 层在这个 pull request 上跑十二条真实用例,如果助手现在把一条经济困难的消息 路由到交易流程上,它就会拦下来 —— 因为那是一次精确的标签比对,失败没有任何歧义。 第 3 层在夜里跑 200 条用例,包含接地性判定,并发出一份对着昨晚那份已提交基线的差异: 三条用例判定变了,而且都在同一个产品域。人第二天端着咖啡看这份差异。 没有任何东西是被一次抛硬币拦下来的。

2 · 如果缺了它会怎样

如果缺了它,或者它失效了

流水线里没有任何自动评估,一次检索层重构就靠一份通过的单元测试和一次代码评审合并了。 单元测试检查的是函数返回的形状对不对;没有一条在检查回来的文档对不对。 这次改动当天就部署了。

检索召回率从大约 90% 掉到大约 60%。助手开始对上周还答得好的问题说 "I don't have that information"(我没有这个信息)。因为部署流水线不跑任何评估, 最早的信号是两天后客服中心的话务量上升。诊断又花了一天, 因为没人说得出是哪一次部署改变了行为 —— 没有按提交粒度的评估记录可以拿来二分, 所以整个排查是一场人工考古,而不是读一张表。

3 · 面试用的 STAR

情境

跑真实测试集要花 API 调用、要花几分钟,所以我不想每次保存都触发它。 但我同时也想要一个可以不停跑、完全不用想成本的检查。

任务

做一个免费但仍然跑真实代码的层级,再做一个我只会刻意付费去跑的真实层级。

行动

我做了两种模式。Mock 模式构造一份形状跟成功运行一样的合成输出 —— 包括伪造的检索文档,里面带着预期的公司、财年和指标数值 —— 然后把它送进 真实的评估器代码。它不调用任何外部服务,不花一分钱, 而且它确实能抓到问题:格式坏掉的用例文件、缺失的必填字段、 评估器比对逻辑里的 bug。它刻意不是「检索器能工作」的证明, 这一点我在文档里写明了。

Real 模式走的是最便宜且有用的那条路,不是最逼真的那条。路由用例直接调用决策节点 和改写节点。检索用例只调起检索子图。整张十一节点的图从来不被测试集跑, 因为跑它只会把成本翻上几倍,却不会改变结果指向的是哪个环节。

缺口:这些东西一样都没进持续集成。我唯一的 GitHub workflow 是 在推送到 main 时部署到一台 EC2 实例 —— 拉取、硬重置、重建容器。 里面任何位置都没有评估步骤,所以一次破坏路由的改动, 和一次修好路由的改动,部署起来一样顺畅。

结果

这两种模式给了我正确的习惯 —— mock 随时跑,real 刻意跑 —— 而 mock 这一层本身已经完全就是一个第 1 层门禁该有的样子:确定性、免费、秒级、 失败时非零退出。

把它接上流水线,是这个项目里剩下的性价比最高的一处小改动: 一个 workflow 步骤,在每次推送时跑 mock 模式,今天它就能拦住一个坏掉的用例文件。 接下来是在 pull request 上加一小份真实冒烟层;而每晚的全量运行, 得先有概念 12 里那份提交进仓库的基线,才算有意义。

要点速记
  • 三层:每次提交跑 mock,PR 上跑冒烟,每晚跑全量
  • 门禁必须是确定性的,否则会被重试掉
  • 评委指标只告警;它们不拦
  • Mock = 合成输出送进真实评估器
  • 我的缺口:评估没进 CI,只有一个部署 workflow
对应的面试问题
  • "How would you integrate evaluation into CI/CD?"(评估你会怎么接进 CI/CD?)
  • "How do you keep evaluation affordable at high commit frequency?"(提交很频繁时,你怎么把评估成本控制住?)
  • "Would you block a release on an LLM-judged score?"(你会用一个 LLM 打出的分数去拦发布吗?)
9

线上评估 —— 影子模式、灰度发布、A/B,以及代理指标

「你怎么在不让真实用户承担风险的前提下用他们做评估?」

1 · 概念本身

问题。离线评估看不见没人想到过的失败, 也没法告诉你客户是不是真的被帮到了。只有线上流量能。 但线上流量就是客户,而拿一家银行的客户做实验是有后果的。

直觉做法,以及它为什么不行。直觉做法是把新版本部署给所有人, 然后盯着看板。它不行,因为从第一秒起爆炸半径就是 100%。 等到问题在汇总指标里看得出来 —— 通常要好几个小时,因为你需要足够的量才能看出变化 —— 这段时间里用过助手的每一位客户,都已经受了那个变差的体验。 在经济困难或争议处理流程里,有些交互是回滚也救不回来的。

真正的答案:三种机制,暴露程度递增。

  • 影子模式(shadow mode)。新版本收到一份真实流量的副本, 它的输出被记录下来,客户什么都看不到。暴露为零。你拿到的是真实问题, 而且可以离线把旧答案和新答案并排比较。这是发现「你的黄金测试集漏掉了哪些说法」 最便宜的办法。它的局限是没法测量客户的反应,因为根本没人看到那个输出。
  • 灰度发布(canary release)。新版本服务一小片真实流量 —— 从 1% 开始,然后 5%,然后 25% —— 一旦某个被监控的指标越过预设边界就自动回滚。 这个名字来自被带进矿井的笼中金丝雀:一个小而早的坏空气警报。 暴露是有界的、也是已知的。
  • A/B 测试。两个版本在随机分配的流量上并行运行足够长的时间, 以便用一个声明好的置信度去比较某个选定指标。三者之中, 只有这一种能确立新版本是真的更好,而不只是没有明显坏掉。

代理指标(proxy metrics),因为线上没有标准答案。离线时你知道 正确答案是什么。线上你不知道 —— 答案流过去的时候没有人给它们打标。 所以你转而测量那些和质量相关的行为:

代理指标它暗示了什么它可能怎样误导你
重试 / 换句话重问率客户第一次没拿到他要的东西。 客户来了兴趣、想深挖下去时,它也会上升。
拒答率系统比以前拒得更多了。 如果流量结构本身往超范围问题偏移了,那上升可能是对的。
点差评率明确表达的不满。 很少有人给任何东西评分,而愿意评的人不具代表性 —— 通常是生气的那些。
中途放弃率客户在对话中途走了。 有些放弃其实是成功:他们一条回复就拿到了答案,满意地走了。
转人工率助手没能把事情办完。这通常是最可信的代理指标, 因为它有一份客户主动选择去付的成本。 转人工策略本身的调整,会因为和答案质量无关的原因让它变动。

停止规则,事先约定好。实验开始之前, 先把「什么情况下你会停掉它」和「什么情况下你会接受它」写下来 —— 指标、阈值、时长, 以及谁有权拍这个板。然后把停止条件接到自动回滚上,这样它就不依赖某个人正好醒着。 事先约定的理由不是流程表演:一旦实验跑起来、你也在里面投了成本, 每一个数字都会变得可以争辩,而看过数据之后才选的阈值不是阈值。

一个 NAB 式的例子。给助手换一个新的答案生成模型。 第一周,影子模式:真实问题、零客户暴露,比较输出后发现新模型在房贷问题上 用的指导性措辞明显更强 —— 这是一个没有任何离线用例抓到过的建议边界风险。 修掉,然后第二周,1% 灰度,并且设好自动回滚:只要转人工率在两小时窗口内上升超过 三个百分点,或者带经济困难标记的对话上的拒答率有任何上升,就回滚。 第三周,在 50% 流量上做一次正规 A/B,主指标是转人工率,决策规则事先登记好。 暴露只在每一个阶段都产出了证据之后才扩大。

为什么影子模式是价值最高的第一步 —— 以及在银行里要留意什么

影子模式是唯一一种能在客户风险为零的情况下给你真实用户问题的机制。 这个组合很少见,而且它直接对着概念 7 里那个天花板打: 它把你没能想到的那些说法交到你手上,而这些说法随后就变成黄金测试集里的永久用例。

在银行里有两点特别要当心。第一,跑一份影子副本, 会让你镜像的那部分流量的模型开销翻倍,所以要镜像一个采样,不是全部。 第二,也更重要:影子这条路径接触的是真实客户数据。它的输出、日志和 trace 受和生产环境同样的隐私义务约束 —— 个人信息绝不能泄进一个为了做实验 而临时搭起来的比对存储里。实验不是数据处理规则的豁免证。

有一件事影子模式做不到:测量客户会作何反应。 那件事需要暴露,而那正是灰度发布存在的意义。

2 · 如果缺了它会怎样

如果缺了它,或者它失效了

一次模型版本升级,靠着一份全绿的离线测试集,在某个周四下午上到了 100% 流量。 新模型在一个测试集从没测过的具体方向上更保守了:只要消息里带一点经济困难的迹象, 它就拒答,并建议客户打电话给客服中心。

接下来四天里,为经济困难而来求助的客户 —— 那些本来就已经陷在困境里、 终于开口求助的人 —— 被一个自动系统拒之门外。因为没有灰度,从第一分钟起暴露就是全量。 因为没有任何代理指标接到告警上,信号是以客服中心等待时长飙升的形式到来的, 而不是以一次回滚的形式。因为没有事先约定的停止规则, 「要不要回退」这个决定要开一场会。回滚技术上是五分钟的活儿,它发生在第四天。

3 · 面试用的 STAR

情境

我的项目是部署过的 —— FastAPI 后端、Next.js 前端,跑在一台 EC2 实例上, 配了 PostgreSQL checkpoint 和 LangSmith 追踪 —— 但它没有真实用户群。

任务

这件事得说明白:线上评估是我的项目整整一整类都没有的东西。

行动

我的项目没有任何形式的线上评估。没有影子模式,没有灰度发布, 没有生产环境 A/B,没有漂移检测,也没有对线上答案的抽样人工复核。 我做过的 A/B 是离线的、组件级的:在 12 条保存好的用例上比两套检索配置, 不是在线上流量上比两个变体。这个区别我想说得精确, 因为说「我做过一次 A/B 测试」会夸大它。

我会按这个顺序去建。第一,抽样人工复核 —— 每天取二十条线上对话打标, 因为它不需要任何基础设施,而且是最快搞清楚「我不知道什么」的办法。 第二,在一份流量采样上做影子模式,因为它在零暴露下给出真实问题, 还能反哺黄金测试集。第三,一个自动回滚接到转人工率和拒答率上的灰度发布。 完整的 A/B 框架放最后,因为它是「基础设施最多、换来的即时安全最少」的那一项。

有一件我确实已经有的东西,让这些建起来更便宜:追踪已经接好了, 所以抽样和打标对话所需要的按次运行记录是现成的,不用再去加。

结果

没有结果可报,而且我会直接这么说,而不是把那次离线 A/B 打扮成一次线上实验。 我能提供的是设计和优先顺序,以及这个顺序背后的理由: 先做最便宜、最能揭出未知失败的那件事,基础设施最重的那件放到最后。

要点速记
  • 影子(0% 暴露)→ 灰度(1–25%)→ A/B(50%)
  • 线上没有标准答案,所以要用代理指标
  • 转人工率通常是最可信的代理指标
  • 停止规则事先约定,并接到自动回滚上
  • 我的缺口:完全没有线上评估
对应的面试问题
  • "How would you run an experiment on a live banking assistant?"(在一个线上银行助手上你会怎么做实验?)
  • "What do you measure when you have no ground truth in production?"(生产环境里没有标准答案时你测什么?)
  • "How do you limit the risk of shipping a model change?"(你怎么限制上线一次模型改动的风险?)

D · 你凭什么信这个结果

概念 10–12

统计那一组。职位描述要的是一个能 "quantify uncertainty"(量化不确定性) 和 "interpret results critically"(批判性地解读结果)的人 —— 那件事就住在这里。

10

方差与配对比较

「一个每次给的答案都不一样的系统,你怎么评估它?」

1 · 概念本身

问题。代码一行没改,把测试集跑两遍,你得到 82%,然后是 79%。 接着你改了一处,得到 84%。84 到底比 82 好,还是它本来就落在这个系统自己会产出的 波动范围之内?

这些波动是从哪来的。把来源一个个点出来是有意义的, 因为有些能消掉,有些消不掉。

来源它是什么能消掉吗?
采样模型是按概率挑下一个词的。把 温度(temperature) 设成零能大幅减少这一项。基本能,代价是答案质量和自然度
服务方的不确定性就算温度为零,托管模型也不是逐位相同的: 批处理、硬件、浮点运算顺序都会变。 不能
模型更新服务方把同一个名字背后的模型换掉了。 你的代码没改,你的系统变了。只能靠钉住一个版本,前提是人家提供
检索并列两篇分数相同的文档回来时顺序可以是任意的, 而这个顺序能改变下游的一切。能 —— 用一个稳定的并列打破规则去排序
评委方差评分者本身就是个模型,所以它可以给同一个答案打出不同分。 部分能 —— 结构化输出帮助很大
并发并行的环节完成顺序不同,所以拼装出来的上下文每次运行都不一样。 能 —— 使用之前先把结果确定性地排好序

直觉做法,以及它为什么不行。直觉做法是每种配置跑一遍, 然后比两个总分。它不行,因为你比的是从两个带噪分布里各取的一个样本, 然后把它们的差叫做效应。四十条用例的规模下,三条用例翻转就是 7.5 个点的摆动 —— 这足够大到能凭空造出一个并不存在的提升,或者盖掉一个真实存在的提升。

真正的答案,分两步。

第一步:重复运行是一次测量,不是一件麻烦事。把同一套配置跑三遍 或五遍,看这些结果的离散程度。那个离散程度就是你的噪声底线, 而它是整件事里最有用的那一个数字 —— 因为从此以后,任何比它更小的差异都不构成任何证据。 大多数团队跳过这一步,然后为两个点的差异争论好几周。

第二步:按用例比,不是按均值比。不要拿 82% 去比 84%。 对每一条单独的用例,记下旧配置做了什么、新配置做了什么, 然后数有多少条变好了、多少条变差了、多少条没变。这就是一次 配对比较(paired comparison), 它灵敏得多,因为它把「有些用例本来就更难」所造成的波动消掉了。 净增四条、同时有两条退步,这告诉你的东西是均值永远给不了的: 这次改动是一次真实的提升,而且它弄坏了两件你该去看一看的事。

一个 NAB 式的例子。助手的两个提示词版本, 在 150 条用例上都拿到 88%。均值说的是「没差别,上哪个都行」。配对比较说的是: 19 条用例变好了,19 条退步了。那不是「没差别」—— 那是一次大幅的行为改变, 只是恰好净值为零。而当你去读退步的那 19 条是哪些时,它们全都和经济困难相关。 要是只看均值,这东西就上线了。

比两个均值

82% → 84%。两个数字,不知道这个差距里有多少是噪声, 也不知道是哪些用例动了。会把「大小相等、方向相反」的改变完全藏起来。 而且要检测出任何东西都需要一个大样本。

按用例的配对比较

+5 better, −2 worse, 33 same。它点出了方向、幅度, 以及具体该打开哪几条用例。用小得多的样本就能检测出同样的效应, 还会把总分掩盖掉的退步暴露出来。

2 · 如果缺了它会怎样

如果缺了它,或者它失效了

一个团队比较两套检索配置,各跑一遍,看到 84% 对 81%。他们上了 84% 那一版。 而真正的噪声底线 —— 如果当初有人测过的话 —— 大约是四个点, 所以这次比较就是一次抛硬币;而事实上,上线的那套配置略差一点。

没有任何戏剧性的事情发生。检索质量轻微而永久地退化了,团队相信它变好了, 而下一次比较又是拿这个新的、错的基线来当参照。这样过六个月, 系统实质上比原来更差了,同时还有一份记录在案的「持续改进」历史。 这是这一页上最贵的一种失败,恰恰因为它从来不产生一次事故 —— 它只是安静地把整个团队的判断力浪费掉了。

3 · 面试用的 STAR

情境

我想知道「更大的候选池加上重排」到底有没有真的改善检索。 我的端到端答案每次运行都不一样,所以比两个答案什么也证明不了。

任务

把这次比较设计成「方差被消掉」,而不是「方差被平均掉」。

行动

是两个设计决定起了作用。第一,我把语言模型从测量里彻底移了出去 —— A/B 工具直接调用检索节点,所以循环里根本没有采样方差, 两次运行之间唯一的差别就是检索配置。第二,我把比较做成严格配对的: 12 条数值用例每一条都在两套配置下各跑一遍,工具会打印按用例的判定 —— same、NEW FOUND IT、 NEW LOST IT,或者 rank 6 -> 2 better。

我也报汇总数字 —— hit@top_k、hit@5 和 mean_rank —— 但我真正在读的是按用例那一列,因为 NEW LOST IT 是一次退步,而一个稳定的均值会把它完全藏起来。

结果

得到一次「名次变动一位就是真实信号、而不是噪声」的比较, 因为噪声是被设计掉的,不是被平均掉的。这就是我会带进银行的那条通则: 在加重复运行之前,先问问这次测量里有哪些方差是你可以消除的。

老实说的缺口:我每条用例只跑一遍。在检索 A/B 里没有语言模型的前提下, 这是站得住脚的,因为这条路径本质上是确定性的。但换成任何端到端的东西就站不住了; 我要加的第一件事,就是同一套配置重复跑三遍来确立一个噪声底线 —— 在拿任何东西去比任何东西之前。

要点速记
  • 把来源一个个点出来;能消掉的就消掉
  • 重复运行 = 你的噪声底线,它是一次测量
  • 比底线更小的差异,一概不理
  • 按用例配对:变好 / 变差 / 没变
  • 均值相等可以藏住一次巨大的行为改变
对应的面试问题
  • "How do you evaluate a system that is not deterministic?"(一个不确定性的系统你怎么评估?)
  • "How do you know a 2% improvement is real?"(你怎么知道 2% 的提升是真的?)
  • "Where does non-determinism in your pipeline come from?"(你这条流水线里的不确定性是从哪来的?)
11

样本量与不确定性 —— 说方向,不说显著性

「你有四十条用例。你到底能得出什么结论?」

1 · 概念本身

问题。每一个评估结果都是从一个样本上算出来的数字。 换一个样本就会算出另一个数字。它可能错得有多远, 而这么多条用例实际上能支撑起什么样的说法?

直觉做法,以及它为什么不行。直觉做法是把这个数字当成真相来报: "our system is 84% accurate"(我们的系统准确率是 84%)。它不行, 因为在四十条用例上,84% 的不确定区间大约是正负十二个百分点。 诚实的说法是 "somewhere between about 72% and 93%"(大约在 72% 到 93% 之间), 这是完全不同的一句话 —— 而它也说清楚了:在四十条用例上拿 84% 去比 81%, 比的是两个重叠得很厉害的区间。

一条值得背下来的粗略经验。对于一个大约在 50% 附近的比例, 不确定区间大约是正负 1 除以根号 n。这就给出一张有用的心算表:

n比例上的粗略区间它能支撑什么
12≈ ±29 个点支撑不了任何定量结论。 只有当成按用例的配对比较、去读是哪些用例动了,才有用。
40≈ ±16 个点能说方向,前提是变化幅度大而且是配对的。 不是一个你会写进报告的比率。
100≈ ±10 个点方向可靠了;大的效应可以带着信心说。
400≈ ±5 个点一个你可以合理引用的比率。
1000+≈ ±3 个点小效应也能看到;这是线上实验运行的量级。

真正的答案:说 "direction, not significance"(说方向,不说显著性)。 在测试集很小的时候,不要声称一个比率,也不要声称统计显著性。 说一个方向 —— 从一次配对比较里得来的 —— 并且说清它站在多大的 n 上。 "On 12 paired cases, four improved, none regressed"(在 12 条配对用例上, 四条变好,零条退步)是一句正当而且有用的话。而从同一份数据说出 "Retrieval accuracy improved by 33%"(检索准确率提升了 33%)就不是。 知道你的数据支撑得起哪一句话,这就是全部的本事。

事先登记好否决标准。在跑这次比较之前, 先写下什么样的结果会让你否决这次改动 —— 比如:「只要有任何一条用例从找到退成找不到, 或者净值低于 +2,我就否决。」理由是人性上的,不是数学上的。 等你看过数据之后,不管它显示什么,你都会给它找到一个故事, 尤其当被测的东西是你自己做的。先把规则写下来, 这就是一次实验和一次演示之间的分界线。

一个 NAB 式的例子。助手的两个版本,在 50 条经济困难用例上测。 B 版正确拒答 46 次,A 版 42 次。这看起来是一次明显的胜利, 要是报成 "8 points better on refusal correctness"(拒答正确性上好了 8 个点), 它就会上线。而在 50 条用例上,每一边的区间大约是 ±14 个点, 所以两个区间几乎完全重叠 —— 配对视角下可能是 6 条变好、2 条退步, 50 条用例上净值 +4;那是一个值得继续追下去的方向,不是一次量出来的提升。 正确的下一步是在这个类别上加更多用例,不是做决定。

统计显著性,以及它为什么在这里常常是个错的问题 —— 另加「同时测很多指标」这个陷阱

统计显著性(statistical significance) 回答的是「这个差别有没有可能只是偶然?」它回答不了「这个差别大到值得在意吗?」 一次 0.3 个点的显著提升,是显著的,也是无关紧要的。 在一个四十条用例的测试集上,只有巨大的效应才可能达到显著, 所以这个概念基本上不适用 —— 这也正是为什么在这个量级上诚实的表述方式是 方向加配对细节。

要知道的那个陷阱:如果你查二十个指标, 按惯例阈值,大约会有一个纯靠偶然就看起来显著。团队们就是这样「发现」 那些下一次运行就蒸发掉的激动人心的提升。防御手段是:开始之前先指定一个主指标, 其余全部当作探索性的,并且对「在你看的第十五个指标里找到的胜利」 公开地保持怀疑。

相关的一点,而且值得在面试里说出来:一个事先登记好的主指标, 同时也是阻止实验「一看起来好就提前收工」的东西。反复偷看、 并在一个有利读数上停下来,会大幅抬高假阳性率 —— 即便每一次单独的计算都做得完全正确。

2 · 如果缺了它会怎样

如果缺了它,或者它失效了

一个团队向治理委员会汇报说助手 "answers correctly 91% of the time" (91% 的时候回答正确),测量基础是 45 条用例。真实的区间大约是正负十四个点。 这个 91% 的数字先进了一张幻灯片,然后进了风险登记册,然后进了一份控制措施描述, 最后变成了这家银行关于这个系统所相信的那个数字。

后来,真实表现原来接近 78% —— 这和最初那次测量完全一致, 那次测量从来没有错,只是被说过头了。现在这个问题变成了治理问题: 一项控制措施是基于一个证据从未支撑过的数字被记录下来的, 而银行必须向审计师解释这个数字是怎么来的、以及为什么没有人说明它的不确定性。 工程本身没问题。那句话有问题。

3 · 面试用的 STAR

情境

我的测试集很小:四个测试集一共 28 条用例,12 条数值检索用例,每条只跑一遍。 这是一个实打实的约束,限制了我有资格声称什么。

任务

让我的说法配得上我的样本量,并且把比较设计成即使样本很小也仍然有信息量。

行动

我从来没有把这些测试集里的比率当成质量数字来报。我报的是按用例的配对判定, 那是 12 条用例能支撑起来的最强结论。让 12 条就够用的那个设计决定, 是配对再加上把语言模型移出去:同一批用例在两套配置下跑、而且没有采样方差, 那么一个 NEW LOST IT 就是关于那条用例的无歧义证据 —— 即便 12 条用例支撑不起关于比率的任何说法。

我当时用的否决标准,实际上是「任何一条用例从找到变成找不到, 都是我必须解释的问题,跟汇总数字无关」。现在我会在跑之前把它写下来, 而不是揣在脑子里;而且我会事先指定一个主指标 —— hit@5 —— 而不是读三个然后挑那个让人高兴的。

结果

结论至今站得住脚,因为它们是以「在一个说明了的 n 上的方向」被表述的, 而不是以一个量出来的比率。如果被问 "is your system 90% accurate" (你的系统准确率有 90% 吗),正确的回答是我不知道,而 40 条用例也告诉不了我。

我会去修的东西:n 是所有事情上的那个卡口约束。我会从真实流量里把测试集长起来, 朝每个类别几百条用例的方向长,并且优先长那些「出错代价高」的类别 —— 对银行来说就是拒答,以及任何沾到理财建议的东西 —— 而不是均匀地长。

要点速记
  • 区间 ≈ ±1/√n —— 40 条用例意味着 ±16 个点
  • 说 direction, not significance(说方向,不说显著性)
  • 配对买到的灵敏度,小 n 靠别的买不到
  • 事先登记主指标和否决规则
  • 20 个指标 → 纯靠偶然就会有一个假胜利
对应的面试问题
  • "How many test cases do you need?"(你需要多少条测试用例?)
  • "How do you quantify uncertainty in an evaluation result?"(评估结果里的不确定性你怎么量化?)
  • "Your suite is small — what does that let you conclude?"(你的测试集很小 —— 那你能得出什么结论?)
12

漂移检测与回归基线

「你的代码一个月没动过。行为还可能变吗?」

1 · 概念本身

问题。普通软件只有在你去改它的时候才会变。这一类系统不是。 行为可以在代码完全没改的情况下发生变化 —— 漂移(drift) —— 而且原因有好几个,彼此独立。

  • 模型漂移。服务方把同一个名字背后的模型更新了。 你的提示词现在面对的是一个略有不同的模型。
  • 数据漂移。问题变了。一款新产品上线、一个利率上调、一条新闻落地, 忽然三分之一的流量都在问一件你的系统从没被测过的事。
  • 索引漂移。文档被重新入库、某个分块参数变了, 于是在一个一动没动的提示词底下,检索器能找到的东西变了。
  • 依赖漂移。一次库升级改变了分词、打分或解析行为, 而那份变更日志没有任何人读过。

直觉做法,以及它为什么不行。直觉做法是在看板上盯着你那个 质量指标的平均值。它不行,因为平均值特别擅长把真正要紧的失败藏起来。 如果一个系统对 90% 的问题都很出色、对新出现的那 10% 灾难性地差, 平均值只会挪几个点,看起来就像普通波动。你需要的信息在分布的形状里, 不在它的中心。

真正的答案,分两部分。

监控分布,不只是平均值。盯住形状: 被打上低置信度标签的答案占比、慢响应的尾部而不是中位数、进来的问题类别构成、 以及按类别拆开的拒答率。构成上的变化 —— 同一个平均值由不同的部分拼出来 —— 正是那种早期预警, 而平均值从构造上就给不了你这个。

把基线提交进代码仓库。把最近一次已知良好的按用例结果 以文件形式存进版本控制,让每晚那次运行去和它做差异对比,而不是打印一个数字。 这样一次退步就成了一次可评审的变更,带日期、带作者、带原因 —— 而重设基线也变成了一件有人批准的刻意行为, 而不是一个数字在没人读的日志里悄悄挪动。

一个 NAB 式的例子。六周里什么都没部署。 模型服务方更新了底层模型。助手在回答产品对比类问题时, 变得稍微更愿意给出一条推荐 —— 正在往个人理财建议那边漂。 平均质量没变,甚至可能变好了,因为答案听起来更有帮助了。 有了一份提交进仓库的基线,每晚那次运行会显示: 在某个具体日期,有十一条用例在建议边界检查上的判定变了, 而这个日期直接指向那次模型更新。要是只有看板上一个平均值, 这件事在几个月后的一次合规审查之前都是看不见的。

提交进仓库的基线里该放什么 —— 以及怎么让重设基线保持诚实

每条用例放:用例 id、判定、主指标数值, 以及失败时的一句简短原因。另加一个头部,记下它是什么时候产出的、 用了哪些模型版本、以及是哪一次提交产出的。正是这个头部, 让你在六周之后还能把一次变化归因到某件事上。

不要存的东西:完整的答案文本。因为模型不是确定性的, 它每次运行都会变,于是差异对比变得没法读,人们也就不读了。 存判定和指标 —— 那些本来就应该稳定的东西。

重设基线需要一条规则,否则它会变成一种抹掉退步的手段。 一条可行的规则是:基线文件只能在一次「除此之外什么都不做」的提交里被更新, 提交信息要写明哪些用例变了、为什么变,并且要由不是写这次改动的人来批准。 在银行里这同时也是审计凭证 —— 它是一份带日期、带作者、可评审的记录,写明了行为是什么时候变的、谁接受了它, 而这恰恰就是一个要求「决策可重建」的监管机构想看到的东西。

2 · 如果缺了它会怎样

如果缺了它,或者它失效了

分数打印到控制台,别的地方一处都没有。三月里有人在一条聊天消息里记了「38/40」。 六月测试集的分数是 34/40。没有人说得出那四条失败是新出现的、 具体是哪四条、还是三月的时候就已经在失败了 —— 因为三月那次运行没留下任何按用例的记录。

排查变成了考古:重跑旧的提交、猜那些已经不再提供的模型版本, 最后得出结论说行为大概是在四月的某个时候变的。如果那四条用例是拒答类 或者建议边界类的用例,银行就没法确定这个行为是从什么时候开始的, 于是也没法划出受影响的客户范围,只能把整个时间窗都当成可能受影响。 这就是「一页纸备忘」和「一个补救项目」之间的差别, 而它完全是由「把一个数字存进日志、而不是存进文件」造成的。

3 · 面试用的 STAR

情境

我真的撞上过漂移,而且来自最不光鲜的那个来源:一个库的默认值。 我的关键词检索用的是一个标准 BM25 实现, 而它在我的语料上的行为跟我以为的不一样。

任务

搞清楚为什么明显相关的文档没有被检索回来 —— 而代码做的恰恰就是我写的那件事。

行动

我去测,不去猜。这个库算出来的词权重对非常常见的词会变成负数, 而为了避免负分,它会把每一个负值抬到一个下限 —— 平均值的四分之一, 在我的语料上是 1.811。单词 "the" 出现在我 84% 的分块里, 7,061 块里的 5,901 块, 所以它真实的权重是 −1.626,被抬到了 1.811 —— 比 "Group" 的 1.542 还要高, 而 "Group" 在矿业报告里是一个真正有区分力的词。

只有 8 个词受影响,这也是为什么它看起来无害。但这 8 个词几乎出现在 每一个自然语言问题里。对于 "what was the revenue of the group in 2024", 排第一的那篇文档的分数里有 59% 来自 what, was, the, of, in。去掉停用词之后,新的前五篇文档和旧的前五篇 重叠度是 zero(零)。归一化让匹配 "revenue" 的文档数 从 221 变成 278, 上升 26%,并把词表从 37,933 削到 23,676,下降 37.6% —— 这里的价值在于文档频率变正确了,不在于省了内存。

这个修复引入了一个 bug,而这正是我会讲给面试官听的那一部分。 一个完全由停用词组成的查询 —— "what is it" —— 分词之后什么都不剩。 一个空查询会给每一篇文档打 0.0 分,而这个库会返回任意 20 篇文档, 连一个报错都没有。无声无息、看起来还挺像、完全是错的。 我在分词器内部修掉了它:回退到未过滤的词元, 并在一个自检里加了断言,这样它不会在没人注意的情况下退回去。

结果

得到一个我解释得清它是怎么打分的检索层,以及一条永久的教训: 一个库的默认值就是一项行为依赖,而漂移可以通过一次 「没人觉得有风险」的升级到来。

整个这个概念里我会最先去修的缺口:我的项目没有提交进仓库的基线。 分数只打印到控制台。什么都没存,所以什么都没法做差异对比; 今天我没法证明自从上次跑测试集以来行为没有漂移过。 那就是一个小文件加一个每晚任务,而它是我清单上剩下的 「每小时价值最高」的一项。

要点速记
  • 行为在代码一行没改的情况下会变
  • 四个来源:模型、数据、索引、依赖
  • 监控分布和构成,不是平均值
  • 基线放文件里,不是日志;每晚做差异对比
  • 重设基线 = 它自己单独的一次被评审过的提交
对应的面试问题
  • "How do you detect that a model has changed underneath you?"(你怎么发现脚下的模型被换掉了?)
  • "How do you prevent silent regressions over time?"(你怎么防止随时间发生的无声退步?)
  • "Tell me about a time you debugged something surprising."(讲一次你排查出意料之外问题的经历。)

我的项目没有做到的部分

在他们发现之前先自己说

主动交出这份清单,比被人当场问住要有力得多。可以用的表述框架: "here is what it has, here is what it does not, and here is the order I would fix it in."(这是它有的,这是它没有的,而这是我会去修的顺序。)

缺什么概念为什么它重要修复优先级
评委验证 5 有两个模型评分员在运行时跑着,而它们都从没和人工标注对照测量过。 不存在任何一致率,所以每一个接地性判定都是未校准的。两者都是失败时放行, 所以一个坏掉的评分员读起来就是「一切正常」。 1 —— 一天的标注量
提交进仓库的基线 12 分数只打印到控制台。什么都没存,所以什么都没法做差异对比, 也没有任何漂移能被归到某个日期上。 2 —— 一个文件加一个每晚任务
CI 里的评估 8 唯一的 GitHub workflow 是在推送到 main 时部署到 EC2。 一次破坏路由的改动,和一次修好路由的改动,上线得一样顺畅。 Mock 模式已经是免费的、确定性的、失败时非零退出 —— 它距离成为一道门禁只差一个 workflow 步骤。 3 —— 一个步骤
线上评估 9 一样都没有:没有影子模式,没有灰度发布,没有生产环境 A/B,没有漂移检测, 也没有对线上答案的抽样人工复核。所以没人想到过的那些失败是完全看不见的。 4 —— 得先有用户
每次查询的延迟与成本 3 任何地方都没有记录,所以任何「质量对成本」的取舍都没法用证据去论证。 5 —— 采集起来几乎免费
页面过滤器的召回率 3 视觉入库流程里的页面过滤器是一个基于数字密度的启发式规则, 它的召回率从没被测量过。它是流水线里唯一一个没有测量的环节, 而它可能无声地丢掉那一页含有答案的页面。 6 —— 需要标注好的页面
足够的样本量 11 28 条测试集用例加 12 条数值用例,每条跑一遍,全部是我自己手写的。 大到足以在配对比较上说方向,但支撑不起任何声称出来的比率。 持续进行
把这份清单变成优势的那句话 "My evaluation is strongest on the parts that are deterministic and component-level, because that is what I could make rigorous alone with no users. It is weakest exactly where a bank needs it most — judge calibration, a committed baseline, and online measurement — and those three are the first things I would build, in that order, because each one is a prerequisite for trusting the next."
(中文意思:我的评估在那些确定性的、组件级的部分上最强, 因为那是我一个人、在没有用户的情况下能做到严谨的部分。 而它最弱的地方恰恰就是银行最需要的地方 —— 评委校准、一份提交进仓库的基线, 以及线上测量 —— 这三件也正是我会最先去建的,就按这个顺序, 因为每一件都是信任下一件的前提。)

速查表 · 12 行

这一节最后读

一个概念一行。最后一列是你可以照着原样说出口的句子(保留英文, 下面附中文意思)。四组、每组三个:你测什么、你怎么测、你什么时候测、你凭什么信它。

概念一句话定义如果缺了它要说出口的那句话
1 · 标准答案与黄金测试集 事先写下来的正确答案,以固定的「输入 + 答案」成对形式存进版本控制。 分数变成会漂移的意见;你永远证明不了自己没有往后退。 "I wrote the expectation down before running anything. 28 cases in four suites, five required fields, and the loader raises if any is missing."
(跑任何东西之前我先把期望写下来。四个测试集里 28 条用例, 五个必填字段,任何一个缺失加载器就抛错。)
2 · 单环节 vs 端到端 给最终输出打分,看的是用户可见的质量;单独隔离一个环节打分,是为了定位故障。 你只知道有东西坏了,永远不知道坏在哪个环节 —— 而各环节的误差还会无声地相互抵消。 "End-to-end is my release signal, component-level is my debugging instrument. My retrieval A/B calls the node directly with no LLM in the loop."
(端到端是我的发布信号,组件级是我的调试仪器。 我的检索 A/B 直接调用那个节点,循环里没有 LLM。)
3 · 每环节指标 路由准确率、检索 recall@k、接地率、两个方向上的拒答正确性、延迟和成本。 一个混合分可以在上升,而过度拒答正在悄悄伤害处于经济困难中的客户。 "One metric per stage, matched to that stage's job — and refusal errors as two numbers, because a wrong refusal and a wrong answer have completely different costs."
(每个环节一个指标,跟那个环节的职责相匹配 —— 而拒答类错误要报成两个数字,因为一次错误的拒答和一次错误的回答, 代价完全不同。)
4 · 确定性检查 vs LLM 评委 三个层次:精确比对,模式与数值检查,然后只在必须理解语义的地方才上模型评委。 评估变得慢、贵、且不可重复,于是真实的退步被当成噪声打发掉。 "Never send to a model what a regex or a numeric comparison can settle. My production guardrail is a ten-character length check with no LLM at all."
(凡是一个正则或一次数值比对能定的事,绝不送给模型。 我生产环境里那道护栏是一个十字符的长度检查,完全没有 LLM。)
5 · 评委验证 测量评委和人工标注之间的一致率;要预期到长度偏见、同风格偏好和位置偏见。 你花好几周去优化自己仪器里的一个偏见,而分数在这期间一直在涨。 "The judge is my measuring instrument, so it needs calibrating. I have never validated mine against human labels — that is the biggest gap in my project and the first thing I would fix."
(评委就是我的测量仪器,所以它需要校准。 我从来没有拿人工标注去验证过我的评委 —— 那是我项目里最大的缺口, 也是我会最先去修的一件事。)
6 · 评分标准与结构化判定 把评委的输出约束成一个带类型的 schema;分开提问,而不是要一个混合分。 方向相反的变动在同一个数字里互相抵消,把补救所需的证据毁掉了。 "One call, two separated tasks: a lenient pass on usefulness and a strict claim-level groundedness check. Thresholds live in code, judgement in the model."
(一次调用,两个分开的任务:一个宽松的有用性通过判定, 以及一个严格的断言级接地性检查。阈值放在代码里,判断留给模型。)
7 · 离线评估 固定保存的用例,可重复而且安全 —— 但里面只装得下有人已经想到过的失败。 测试集全绿,而一个多轮对话的 bug 正在影响客户,持续五周。 "Offline is a regression net, not a proof of safety. Every case in mine is one I imagined, and it is single-turn, so it cannot see conversation-state bugs."
(离线是一张回归网,不是一份安全证明。我这份里的每一条用例都是我想出来的, 而且它是单轮的,所以看不见对话状态类的 bug。)
8 · CI 门禁与成本分层 每次提交跑 mock,pull request 上跑一小份真实冒烟, 每晚拿完整真实测试集对着一份提交进仓库的基线跑。 一次重构把召回率从 90% 打到 60%,而最早的信号是两天后客服中心的话务量。 "A release gate must be deterministic, or people retry it until it passes. Judge metrics alert, they do not block. My evals are not in CI yet — mock mode is one workflow step away."
(发布门禁必须是确定性的,否则人们会一直重试到它通过为止。 评委指标只告警,它们不拦。我的评估还没进 CI —— mock 模式只差一个 workflow 步骤。)
9 · 线上评估 先零暴露的影子模式,再 1–25% 带自动回滚的灰度,然后 A/B; 因为线上没有标准答案,所以用代理指标。 在 100% 暴露下,处于经济困难中的客户被拒之门外四天,才有人注意到。 "Shadow first because it gives real questions at zero risk. Proxy metrics — retries, refusals, thumbs-down, abandonment, escalation — with stopping rules agreed before the experiment starts. My project has none of this."
(先做影子模式,因为它在零风险下给出真实问题。 代理指标 —— 重试、拒答、点差评、中途放弃、转人工 —— 并且停止规则要在实验开始之前就约定好。这些我的项目一样都没有。)
10 · 方差与配对比较 重复运行来确立一个噪声底线;逐条用例地比,而不是比两个均值。 你上线了一个略差的系统却以为自己改进了,并把基线重设到这个错误上。 "Before averaging noise away, I ask what variance I can eliminate. I took the LLM out of the retrieval A/B, then paired every case, so a one-rank move is real signal."
(在把噪声平均掉之前,我会先问哪些方差是我可以消除的。 我把 LLM 从检索 A/B 里拿掉,然后把每一条用例都配对, 这样名次动一位就是真实信号。)
11 · 样本量与不确定性 区间大约是 ±1/√n;说方向,不说显著性;事先登记好否决标准。 一个来自 45 条用例的 91% 数字,变成了一项证据从未支撑过的、记录在案的控制措施。 "On 40 cases the band is about ±16 points, so I report direction from a paired comparison and name the n. I would not quote a rate from that."
(在 40 条用例上区间大约是 ±16 个点, 所以我报的是从配对比较里得来的方向,并且把 n 说清楚。 我不会拿这个去引用一个比率。)
12 · 漂移与回归基线 行为在代码一行没改的情况下会变;监控分布; 保留一份提交进仓库的基线文件,而不是日志里的一个数字。 三月到六月之间多出四条失败,而没人说得出是哪几条、什么时候开始的, 也划不出谁受了影响。 "Drift arrives through the model, the data, the index or a dependency. Mine arrived through a BM25 library default that clamped stop-word weights above real terms — 59% of the top document's score came from five stop words."
(漂移是通过模型、数据、索引或某个依赖到来的。 我这次是通过一个 BM25 库的默认值到来的 —— 它把停用词的权重抬到了真实词汇之上 —— 排第一那篇文档的分数里有 59% 来自五个停用词。)
如果每组只记一句 A —— 跑任何东西之前,先把期望写下来。B —— 凡是一个等号能定的事,绝不去问模型; 而当你不得不用模型时,就去校准它。C —— 门禁必须是确定性的, 而离线里只装得下有人想到过的失败。D —— 先测出你的噪声底线,逐条用例地比, 然后说方向、不说比率。

术语表

54 个词条

这一页上用到的每一个术语和缩写,按读者大概会需要它们的顺序排列。 54 条,分成两个折叠组。

核心术语 · 40 条 —— 测量、方法,以及时机
LLM(large language model,大语言模型)
一个在文本上训练、并生成文本的模型。它是写出答案的那个组件; 在这一页里,有时也是给答案打分的那个组件。
概率式系统(probabilistic system)
输出是被采样出来、而不是被计算出来的系统,所以同一个输入可以产出不同的输出。 这就是普通测试方法搬不过来的原因。
非确定性(non-determinism)
重复同一个操作不会重复出同一个结果的这种性质。
温度(temperature)
控制模型选下一个词时有多随机的一个设置。设为零意味着永远取概率最高的那个词, 这能减少波动,但消不掉它。
标准答案(ground truth)
某个测试输入对应的、被记录下来的正确答案,由人事先定好, 或者取自一个权威来源。
黄金测试集(golden set)
一份固定的、进了版本控制的测试输入集合,每个输入都配着它的标准答案。 也叫 gold set 或 golden dataset。
用例(case)
一个测试项:一个输入,加上它被期望具备的属性。 在我的项目里,就是一个 JSON lines 文件里的一行。
JSONL(JSON lines)
一种每行一个完整 JSON 对象的文件格式。既容易往里追加, 也容易在版本控制的差异对比里评审。
测试集(suite)
一组共用同一个评估器、并且有名字的用例。我的是路由、检索、计算和边界四组。
评估器(evaluator)
那段把输出和某条用例的期望作比较、并返回失败清单的代码。
端到端评估(end-to-end evaluation)
把一个输入送过整个系统,只给最终输出打分。它测的是用户体验;无法定位故障。
单环节评估(component-level evaluation)
用一个受控输入去隔离测试某一个环节。能定位故障,而且便宜得多。
流水线 / 环节(pipeline / stage)
一个由一串步骤搭起来的系统,每一步都对在做的东西做一次变换。 一个环节就是这样的一步。
监督者图(supervisor graph)
一种控制结构:由一个节点决定下一个跑哪个节点,而不是固定串成一条直线。 我这个图有十一个节点,里面还嵌了一个五节点的检索子图。
节点(node)
这种图里的一个工作单元 —— 比如路由决策、查询改写,或者答案评分员。
路由准确率(routing accuracy)
被送上正确路径的输入所占的比例。
召回率(recall)
在所有本该被找到的东西里,实际被找到的那一部分。
精确率(precision)
在所有被返回的东西里,实际上正确的那一部分。
Recall@k
只在前 k 个结果范围内测量的召回率,这里的 k 就是有多少个会被传给下一个环节。
接地性(groundedness)
一个答案里的每一条事实性断言,能不能追溯到某一篇被检索出来的来源文档。
拒答正确性(refusal correctness)
系统拒掉的是不是该拒的那些东西,要报成两个分开的数字: 错误的拒答,以及对本该拒答的东西给出的错误回答。
延迟(latency)
从提问到回复之间的实际墙上时钟时间。
正则表达式(regular expression / regex)
一种用来匹配文本的紧凑模式语言。它是第二层评估里最便宜的仪器。
LLM 当评委(LLM-as-judge)
在给定输入、输出和一份评分标准的前提下, 用一个语言模型去给另一个模型的输出打分。
逐点 / 成对评判(pointwise / pairwise judging)
逐点评判是拿一个答案去对着评分标准打分。成对评判是比较两个答案、挑出更好的那个, 它更可靠,但会受位置偏见影响。
评分标准(rubric)
一份写下来的定义,说明每一种判定各自意味着什么、以及每一种的具体条件, 这样两个评分者能得到同样的答案。
结构化输出(structured output)
强制模型返回一个带类型的对象,字段有名字、取值有限制, 而且是靠 schema 强制执行的,不是客气地请求。
一致率(agreement)
两个标注者在同一个项目上给出同样判定的频率。原始一致率就是那个直白的百分比。
Cohen's kappa(科恩卡帕系数)
把「两个标注者纯靠偶然也会一致」的那部分扣掉之后的一致率。 当一种判定比另一种常见得多的时候,这个指标是必需的。
长度偏见(length bias)
模型评委不管质量如何、都倾向于偏好更长答案的那种倾向。
位置偏见(position bias)
模型评委在成对比较里偏向某一个位置的那种倾向。 修法是两种顺序各跑一遍,只在两次结果一致时才计数。
失败时放行 / 失败时拦截(fail open / fail closed)
失败时放行的意思是,当一个检查出错时,这份工作被允许通过。 失败时拦截的意思是它被拦下来。我那两个模型评分员都是失败时放行。
离线评估(offline evaluation)
脱离线上流量,跑固定保存的用例,并和记录下来的标准答案作比较。
线上评估(online evaluation)
在线上流量上测量;那里不存在标准答案,行为只能从客户的举动里推断出来。
CI / CD(continuous integration / delivery,持续集成 / 持续交付)
一个在每次代码改动时跑检查的自动系统,以及随后把它发布出去的那一部分。 门禁(gate)是一个必须通过、改动才能继续往下走的检查。
影子模式(shadow mode)
新版本收到一份真实流量的副本,它的输出被记录下来,但从不展示给客户。零暴露。
灰度发布(canary release)
让新版本服务一小片有界的真实流量, 一旦某个被监控的指标越过预设上限就自动回滚。
A/B 测试(A/B test)
两个版本在随机分配的流量上跑足够长的时间, 以便用一个声明好的置信度去比较某个选定指标。
代理指标(proxy metric)
在没有标准答案时,被拿来代替质量的一种可观测行为: 重试、拒答、点差评、中途放弃、转人工。
配对比较(paired comparison)
把两个变体放在完全相同的项目上评估,逐项去比,而不是比两个平均值。 在样本量小的时候灵敏得多。
第二组 · 14 条 —— 统计、漂移,以及项目专有名词
噪声底线(noise floor)
在什么都不改的情况下重复跑同一套配置,你得到的那个离散程度。 任何比它更小的差异都不构成证据。
统计显著性(statistical significance)
如果两者其实毫无差别,那么出现这么大的结果会有多令人意外。 它完全没有说这个差别是否大到值得在意。
事先登记(pre-registration)
在跑实验之前,先把主指标和「接受还是否决」的规则写下来, 这样阈值就不是看过数据之后才选的。
多重比较(multiple comparisons)
指这样一个问题:查很多个指标,会让「看起来像胜利的偶然」几乎必然出现。 按惯例阈值大约是二十分之一。
漂移(drift)
行为或者进来的数据随时间发生变化,而代码一行没改。 来源是模型、数据、索引,或者某个依赖。
回归基线(regression baseline)
一份提交进仓库的文件,装着最近一次已知良好的按用例结果, 每次运行都拿它来做差异对比,这样一次退步就成了一次带日期、 带作者、可评审的变更。
BM25
一种存在已久的关键词检索打分方法: 当一篇文档里频繁出现查询中的词、而这些词在整个语料里又很稀有时,它的分数就更高。
IDF(inverse document frequency,逆文档频率)
那个分数里代表稀有度的部分。一个几乎出现在每篇文档里的词几乎不携带任何信息, 而它的原始值可以变成负数。
停用词(stop words)
极其常见、对检索来说几乎不带含义的词 —— the、of、is、what。 我语料里那八个被抬到下限的词就属于这一类。
交叉编码重排器(cross-encoder reranker)
一个第二轮的模型,把查询和每一篇候选文档放在一起读,然后重新排序。 它比第一轮检索更准,但慢到没法在整个语料上跑。
hit@k / mean_rank
在我的 A/B 工具里指:有多少条用例在前 k 个结果里出现了正确文档, 以及第一篇正确文档的平均位置。
Mock 模式(mock mode)
构造一份看起来像成功运行的合成输出,然后把它送过真实的评估器代码。 它校验用例 schema 和评估器逻辑,不调用任何外部服务,不花一分钱。
个人理财建议(personal financial advice)
在澳大利亚,这指针对某个具体个人处境给出的推荐意见。 这是需要持牌的活动,所以助手绝不能给。一般性信息是允许的。
经济困难援助(hardship support)
面向无法按期还款客户的处理流程。在这里发生一次错误的拒答, 对一位脆弱客户来说是真实的伤害,不只是一个糟糕的答案。
护栏是单独一页 运行时控制 —— 输入过滤、输出拦截、把建议边界当成一条被强制执行的规则 (而不是一个被测量的指标)、提示词注入防御,以及失败时放行策略 —— 属于护栏,不属于评估,它们在配套的护栏页里讲。 两者之间唯一一处值得说出口的联系:我那两个模型评分员是跑在生产环境里的评估仪器, 所以它们正好坐在这两个话题的边界上。