本节摘要:第 1.1 节说过,LLM 应用"质量不可断言"——没有错误码可以告警,所以质量观测必须自己制造信号。本节立第一路信号:在线评测探针。它做三件事:按分层抽样从线上流量抽出小样本(错误与点踩强制全采,正文抽样率随数据记录);用一份精简裁判 rubric——相关性、事实性、完整性、格式安全四维、每维三档——对样本打分(规则裁判起步,LLM 裁判进阶);把评分带着 trace_id 回写,让每个低分样本都能一键还原现场。第二路信号是用户反馈:显式的点赞点踩与重新生成、隐式的复述提问、中途放弃、人工接管——后者是"用户已经知道坏了、而你不知道"的静默失败。评测集设计与裁判校准的完整方法论属于《Evals 实战:LLM 评测工程》,本节只讲在线部分。
上线前你大概做过离线评测:一批评测集、一套打分,分数达标才发布。它管住的是"上线前"。上线之后三件事让离线集必然过时:
所以需要在线评测探针:一条持续运行的流水线——抽样 → 裁判 → 回写 → 聚合,让"线上质量"变成一个每天可看的数字。它和离线评测的关系是接力而非替代:离线集守发布门(变更前后跑一遍),探针守运行态(每天都在跑)。
💡 判定一个团队有没有质量观测,就看一个问题:"昨天的输出质量比前天好还是坏?"——答得出数字的是探针在跑,答"应该没变吧"的是在裸奔。
裁判要成本(LLM 裁判本身也花钱、人工抽检更贵),所以只能抽样。策略直接继承第 2.3 节的三原则,再加两条:
| 策略 | 做法 | 为什么 |
|---|---|---|
| 随机打底 | 每条请求按固定比例(如 1%~5%,社区经验值)进样本池 | 保统计代表性,可估全局合格率 |
| 按功能分层 | 每个功能单独定比例,小流量功能调高 | 防止大头功能的样本淹没小功能的劣化 |
| 错误全采 | 失败、超时、被拦截的请求全部进池 | 第 2.3 节"错误层全采",线索稀疏 |
| 点踩强制入库 | 用户点了踩的会话,正文强制留存并插队送裁判 | 负反馈是最贵的样本,一个都不能丢 |
| 抽样率随行 | 每条评分记录 sampled=true, rate=0.02 |
事后估算全局值时还原权重,否则统计全偏 |
样本量的量级感(示意):日调用量百万级的应用,每天每功能抽出 200~500 条送裁判,就足以看出日级合格率的明显变化;再多的边际价值快速衰减,而裁判成本线性上涨。
裁判的骨架是一份 rubric(评分细则)。第一次搭建时坚决从精简版开始——四维、每维三档:
| 维度 | 考什么 | 0 分 | 1 分 | 2 分 |
|---|---|---|---|---|
| 相关性 | 是否回应了用户所问 | 答非所问 | 部分回应 | 正中问题 |
| 事实性 | 关键陈述是否有依据 | 无依据且可疑 | 部分有依据 | 有据可查 |
| 完整性 | 是否漏答关键点 | 漏掉主要诉求 | 有漏但不伤主干 | 覆盖完整 |
| 格式安全 | 拒答是否得当、有无有害内容 | 有害或乱码 | 格式伤可读性 | 干净规范 |
总分 0~8,阈值线自定(如 6 分以下记"不合格",示意)。三条经验:
# online_probe.py —— 在线评测探针:分层抽样 + 规则裁判 + 评分回写(纯标准库,MOCK 模式) import random from collections import defaultdict REFUSAL_MARKS = ("无法回答", "我不能协助") def rule_judge(answer: str, hits: int, grounded: bool) -> tuple[int, list[str]]: """规则裁判(MOCK 示意):返回四维扣分后的总分(满分 8)与扣分原因。""" score, notes = 8, [] if hits == 0: # 事实性:检索零命中 score -= 3; notes.append("检索零命中") if not grounded: # 事实性:陈述无依据 score -= 1; notes.append("关键陈述无依据") if hits > 0 and any(m in answer for m in REFUSAL_MARKS): score -= 2; notes.append("有据仍拒答") # 格式安全/相关性 if len(answer) < 20: # 完整性:疑似截断 score -= 2; notes.append("输出过短") return score, notes class Probe: """抽样器 + 评分账本。负反馈强制入库;抽样率随行记录(第 2.3 节铁律)。""" def __init__(self, rates: dict[str, float], seed: int = 7): self.rates = rates # 按功能分层:{"客服": 0.05, ...} self.rng = random.Random(seed) self.rows: list[dict] = [] def should_sample(self, feature: str, negative: bool) -> bool: return negative or self.rng.random() < self.rates.get(feature, 0.02) def record(self, trace_id: str, feature: str, question: str, answer: str, hits: int, grounded: bool, negative: bool) -> None: score, notes = rule_judge(answer, hits, grounded) self.rows.append({ "trace_id": trace_id, "feature": feature, "score": score, "notes": notes, "negative": negative, "rate": self.rates.get(feature, 0.02), }) def daily_summary(self) -> None: agg = defaultdict(lambda: [0, 0, 0]) for r in self.rows: a = agg[r["feature"]] a[0] += 1 a[1] += (r["score"] >= 6) # 合格阈值 6(示意) a[2] += r["negative"] print("== 质量探针日结 ==") for f, (n, ok, neg) in sorted(agg.items()): print(f" {f:<8s} 样本 {n:3d} | 合格率 {ok / n:5.1%} | 负反馈样本 {neg}") if __name__ == "__main__": probe = Probe(rates={"客服": 0.05, "营销": 0.05}) rng = random.Random(11) for i in range(400): # 模拟 400 次调用(示意流量) feat = "客服" if i % 4 else "营销" hits = rng.choice((0, 3, 3, 5)) # 1/4 的请求检索零命中 answer = "根据知识库第 3 条,您可以在设置页查看账单明细。" if hits else "无法回答该问题" neg = hits == 0 and rng.random() < 0.3 # 坏答案偶发被点踩 if probe.should_sample(feat, negative=neg): probe.record(f"t-{i:04d}", feat, "怎么查账单", answer, hits, grounded=hits > 0, negative=neg) probe.daily_summary()
输出(实测,固定随机种子可复现):
== 质量探针日结 == 客服 样本 35 | 合格率 31.4% | 负反馈样本 22 营销 样本 14 | 合格率 21.4% | 负反馈样本 10
这份模拟流量里合格率只有三成,原因码几乎全是"检索零命中"——这正是探针的价值形态:分数只是入口,原因码与 trace_id 才是 actionable 的部分(拉出低分样本的 span 树,第一眼就能看到 hits=0)。
注意两个设计:每条评分带 trace_id——低分样本可以一键拉出第 2 章的 span 树还原现场(评分不是终点,是排障入口);negative=True 的样本绕过抽样概率直接入库——对应"点踩强制全采"。
用户反馈是唯一"从真实用户视角"来的质量信号,分两路:
| 路数 | 信号 | 怎么读 | 陷阱 |
|---|---|---|---|
| 显式 | 点赞、点踩、重新生成按钮 | 点踩率是最硬的质量指标,点踩样本全采送裁判 | 点踩可能针对格式而非事实,要看裁判归因 |
| 显式 | 人工客服转接、"转人工"点击 | 转接率上升 = 机器人搞不定的份额在变大 | 需与流量结构变化区分 |
| 隐式 | 复述提问(换说法再问一遍) | 典型的"没答上但懒得点踩",静默失败 | 需按会话窗口判重,误报来自好奇追问 |
| 隐式 | 中途放弃(提问后长时间无后续) | 低成本的用户放弃信号 | 与延迟故障混杂,先查第 4.2 节延迟分布 |
| 隐式 | 复制答案行为 | 正向信号,可做质量代理(社区实践) | 埋点噪声大,只做辅助 |
工程上三件事必须做对:反馈事件带 trace_id(第 2.2 节传播检查清单第 5 条);显式反馈不抽样(全量入库,量小价值高);隐式信号只进面板不进告警一线——它们噪声大,用来看趋势与圈样本,不要半夜叫醒人。
⚠️ 最危险的组合是"点踩率没涨、转人工没涨,但复述率悄悄涨了":用户还没愤怒到点踩,但已经在用脚投票。隐式信号要人工周期性看,不能因为不进告警就没人看。
探针给的是"确实坏了"的确认,但它滞后:抽样要等、裁判要排程、分数要攒天数。第 4.2 节补上先行指标——不看内容、只看形状的四个漂移信号,让"正在变坏"早于"已经坏了"被发现。