智能体产生的最重要的数字不是它的答案——而是它有多确定。用独立信号组合出这个数字,检查它是否经过校准,用第二个模型对高风险决策进行评分,并将其余的妥善路由给人类。
这是在生产环境中运行大语言模型系统的六级成熟度模型的第 3 级。第 1 级和第 2 级让你达到系统能正常工作、并且你能看到它在工作的状态。第 3 级是置信度:系统仅在其校准后的置信度较高时才自主行动,用独立评判者对重要决策进行评分,并以真正能捕获错误的方式将所有不确定的事项路由给人类。
这三件事是同一个机制。置信度是决定哪些自动运行的旋钮。评判者是构建这个旋钮的信号之一——也是将决策从自动化路径上拉下来的触发器。人工交接是旋钮将线下的一切发送出去的地方。把这三件事都做对,你就拥有了一个知道自己何时不知道的系统,因此可以被信任承担更多。
问大多数大语言模型系统“你有多确定?”你得不到任何有用的东西——要么没有数字,要么是模型自我报告的置信度,而后者以校准不良著称(模型在错误时也兴高采烈地确定)。所以你必须设计一个置信度信号。生产级评分由独立信号组合而成,绝不从模型自己的声明中提取:
@dataclass
class Signals:
model_self: float # the model's own score — useful but weighted DOWN (overconfident)
verification: float # fraction of deterministic checks passed (0..1)
judge_agreed: bool | None # True/False if this decision was judged; None if not sampled (most aren't)
historical: float # this slice's past accuracy (e.g. 0.99 vs 0.80)
WEIGHTS = {"model_self": 0.25, "verification": 0.35, "judge": 0.25, "historical": 0.15}
def compose(s: Signals) -> float:
# only ~8% of decisions are judged. When judge_agreed is None, DROP the judge term and
# renormalize the rest — an unjudged decision must not be penalized (False) or faked (True).
parts = {"model_self": s.model_self, "verification": s.verification, "historical": s.historical}
if s.judge_agreed is not None:
parts["judge"] = 1.0 if s.judge_agreed else 0.0
return sum(WEIGHTS[k] * v for k, v in parts.items()) / sum(WEIGHTS[k] for k in parts)确切的权重不如原则重要:单一置信度来源就是单一故障点。注意 model_self 被故意赋予最小的权重——一个自信地犯错的模型会被失败的验证或持不同意见的评判者拉低,因此“自信地犯错”无法仅凭自身越过门槛。另外三个信号也是你可以检查的:确定性验证要么通过要么没通过,评判者要么同意要么不同意,历史准确率是关于这个切片的可测量事实。模型对自身的看法是你无法独立验证的唯一输入,这正是它话语权最小的原因。
评分只有在经过校准时才有用——即“0.9”意味着“大约 90% 的情况下是正确的”。检查它:按预测置信度对决策进行分桶,并测量每个桶的真实准确率(可靠性图)。
predicted bucket mean predicted actual accuracy verdict ---------------- -------------- --------------- --------------------------------------- 0.90 – 1.00 0.95 0.71 ⚠ overconfident — recalibrate / raise T 0.80 – 0.90 0.85 0.84 ✓ well-calibrated 0.70 – 0.80 0.75 0.77 ✓ < 0.70 0.55 0.52 ✓ (correctly unsure)
如果你的最高桶只有 71% 的时间是正确的,那么你的自动化阈值太宽松了。生成该表的分桶方式(以及要告警的差距):
def reliability(samples, bins=10): # samples: list[(confidence, was_correct: 0|1)] rows, ece, n = [], 0.0, len(samples) for b in range(bins): lo, hi = b/bins, (b+1)/bins last = (b == bins - 1) bucket = [s for s in samples if lo <= s[0] < hi or (last and s[0] == 1.0)] # top bin includes 1.0 if not bucket: continue conf = sum(c for c, _ in bucket) / len(bucket) acc = sum(ok for _, ok in bucket) / len(bucket) rows.append((round(lo, 1), round(conf, 3), round(acc, 3), len(bucket))) ece += (len(bucket) / n) * abs(conf - acc) return rows, ece
加权和并非天然校准——compose() 返回的是 [0,1] 范围内的数字,而不是概率。闭环:拟合一个从原始组合评分 → 经验准确率的单调映射,并基于此进行路由。
from sklearn.isotonic import IsotonicRegression calibrate = IsotonicRegression(out_of_bounds="clip").fit(raw_scores, correct) p_correct = calibrate.predict([composed])[0] # THIS is what the threshold compares against
在滚动窗口上重新计算——校准会随模型和输入而漂移。告警时优先使用 Brier 分数或每个桶的有符号差距,而不是 ECE(ECE 对分桶敏感,对于校准不良的模型可能读数约为 0)。并注意独立性警告:将历史切片准确率加权到 compose() 中,然后按切片进行校准会重复计算——将切片可靠性编码在一个地方,而不是两个地方。
一旦组合并校准完成,自动化就是一个阈值——而正确的 T 是按切片(per slice)设定的,来自校准数据:
def route(conf: float, slice_key: str, thresholds: dict[str, float]) -> str: T = thresholds.get(slice_key, 0.95) # default conservative if conf < ABSTAIN_FLOOR: return "abstain" # too unsure to even recommend return "auto" if conf >= T else "hitl_recommended"
这里的路由是 Level 1 四路词汇表的精简视图:abstain 是低于人工审核状态的新底线(用于那些不确定到甚至无法预填草稿的输入),而 Level 1 的 reject(验证失败)和 hitl_required`(极低置信度)仍然适用。先将 T 设高(几乎所有内容都交给人工),然后仅当某个切片的校准证明该区间安全时,才为其降低 T。你可以通过指出证明其合理性的准确率数据,来为每一个自动化区间辩护。
这一点的最高形式是弃权——拒绝做出决定。一个说“我不确定;应该由人工查看”的智能体,比一个总是给出答案的智能体更值得信赖。把它作为一等结果,而不是失败路径。这也是你抵御未知未知的最佳防御:你无法枚举生产环境会发送的每一个奇怪输入,但一个校准分数加上弃权底线意味着奇怪的那些会自动落到人工那里。
ABSTAIN_FLOOR = 0.40 # below this: don't even propose; escalate as "needs human"
整个系统围绕三个杠杆进行调优,而校准数据会告诉你每个杠杆该往哪个方向移动:
lever raise it lower it ------------------- ------------------------------------------ --------------------------------------- threshold `T` fewer auto-errors, more human load more automation, more risk `model_self` weight trusts the model more (risky) leans on verification/judge abstain floor fewer bad auto-proposals, more escalations fewer escalations, more noise to humans
有几个反模式会可靠地破坏这一层:使用原始模型置信度作为调节旋钮(未校准——应改为组合);使用单一的全局阈值(不同切片的准确率差异极大);从不重新检查校准(它会腐化——一个季度前的阈值就是一个季度前的风险模型);以及将弃权视为错误(这是系统正确识别其能力边界——要度量它,而不是压制它)。
compose() 中的一个信号是 j udge_agreed `。它来自这里——而且它值得单独一节,因为对于高风险决策,它是自信猜测与经过检查的猜测之间的区别。
单次模型推理是单点故障。它在出错时也很自信,它有特征性的盲点,而且你无法仅通过查看输出来区分好答案和看似合理的答案。廉价而有效的保险是一个评判器:一个独立的第二个模型,在你根据第一个模型的决策采取行动之前对其进行评估。
@dataclass class Verdict: agrees: bool confidence: float failure_mode: str | None # if it disagrees, why def judged_decision(inputs, primary_out, judge) -> Verdict: return judge.evaluate(inputs=inputs, proposed=primary_out) # narrow question: is this right?
三个设计选择让它奏效。
让评判器独立——并且持怀疑态度。如果评判器是同一个模型、同一个提示词,它就会共享盲点并盖章通过。尽可能使用不同的模型家族、不同的框架,并提示它去反驳,而不是确认。
You are a strict reviewer. You will be given INPUTS and a PROPOSED DECISION made by another system.
Your job is to find the strongest reason the PROPOSED DECISION is WRONG, unsafe, or unsupported by
the inputs. Do not be agreeable. If, after genuinely trying to refute it, you cannot, then agree.
Return JSON: {"agrees": bool, "confidence": 0..1, "failure_mode": string|null}
INPUTS: {{inputs}}
PROPOSED DECISION: {{proposed}}一个被要求反驳的评判器,比一个被要求批准的评判器能捕获多得多的东西——这里的框架确实发挥了作用,因为一个随和的评审者被问到“这是对的吗?”时会想办法说是,而一个对抗性的评审者被问到“这哪里错了?”时会揭示出你原本只会在生产环境中发现的失败模式。
采样——不要评判一切。评判会使模型成本翻倍。把它花在风险或不确定性高的地方:
decision class judge policy -------------------------------------- ------------------------------------------------------ highest-impact / irreversible 100% judged borderline confidence (near threshold) always judged everything else random sample, e.g. 5–10% (a continuous quality probe)
def should_judge(d, thresholds, rate=0.08) -> bool: if d.stakes == "high": return True if abs(d.confidence - thresholds.get(d.slice, 0.95)) < 0.05: return True # borderline (per-slice T, safe default) return deterministic_hash(d.decision_id) % 10000 < rate * 10000 # deterministic + sub-1% safe
将分歧视为一种路由信号——同时也是一项指标。当评判者产生分歧时,该决策每次都会被转交给人类处理。而分歧率是你最好的健康信号之一——出现峰值意味着遭受攻击、一次糟糕的部署,或模型回归。
def combine(primary_out, verdict: Verdict) -> tuple:
emit("judge_invocations_total")
if not verdict.agrees and verdict.confidence >= 0.6: # confident refutation → human, every time
emit("judge_disagreements_total")
return route_to_human(primary_out, reason=verdict.failure_mode), False # judge_agreed = False
if not verdict.agrees: # weak/low-confidence refutation: INCONCLUSIVE
return primary_out, None # don't fake agreement — None makes compose() drop the judge term
return primary_out, True # genuine agreement → judge_agreed = True for compose()有一个陷阱会坑到你:测试中的非确定性。如果评判者随机采样,而该路径在你的测试套件中运行,你的测试就会变得不稳定——相同的输入在一次运行中通过评判,下一次却不然,因此断言会间歇性失败。它看起来像一个神秘的 bug;其实是你的采样率泄漏进了确定性测试。在测试中固定采样率,并使用决策 id 的确定性哈希(而非 RNG),这样即使是被采样的行为也可复现:
# production samples; tests pin the rate. Any randomness in an agent must be injectable. judge = Judge(model=secondary, sampling_rate=0.0 if TESTING else 0.08)
你并不是要让模型变得完美;你是在构建一个比任何单次调用都更可靠的系统——对于经过评判的决策,两个独立模型必须达成一致你才能行动,而它们的分歧就是系统在举手示警。对于真正重要的决策,这是一种极其廉价的方式来换取大量的安全性。
调节旋钮和评判者最终都指向同一个地方:一个被路由给人类的决策。但“路由给人类”正是许多原本良好的系统悄然失败的地方——不是败在模型上,而是败在交接上。做得好,人类就是真正的安全层和训练信号的来源。做得差,你就造出了一个人们看都不看就点击的“批准”按钮——比没有人类更糟,因为它制造了虚假的问责。
懒惰的交接会展示建议的答案和批准/拒绝。在高负载下,人类会批准——提案锚定了他们,拒绝需要付出努力,队列又很长。现在你有了一个什么都不检查的人在回路中,外加一份虚假地写着“有人审查过此内容”的审计记录。良好的 HITL UX 让真正的判断变得廉价,并将注意力引向真正重要的地方。
展示带有证据的草稿,而不是一个等待盖章的裁决。对抗橡皮图章式批准的最大杠杆就是展示推理过程和证据——人类无法检查一个他们看不到依据的结论。
{
"proposed": { "decision": "approve", "fields": { "amount": 250 } }, // editable draft, not a verdict
"confidence": 0.62,
"routed_because": "confidence_below_threshold (0.62 < 0.85)", // tell them where to look
"reasoning": ["matched policy 4.2", "no prior flags"], // the WHY, not just the what
"evidence": [ { "label": "policy", "ref": "...", "snippet": "..." } ],
"stakes": "reversible | high_cost | irreversible", // calibrate their attention
"alternatives": [ { "decision": "reject", "would_trigger": "..." } ]
}将每一个操作都捕获为信号。每个人的操作都是一个标签——结构化地将其记录在对应决策上。编辑是最丰富的信号:一个被修正的答案,对你的黄金集(golden set)以及发现智能体系统性偏差之处而言都是金矿。
def on_review(decision_id, review):
ledger.append_outcome(decision_id, review) # audit: who decided, what, when
emit("human_override_rate", 1.0 if review["action"] != "approve" else 0.0,
slice=slice_of(decision_id))
if review["action"] == "edit":
golden_set.add_candidate(decision_id, review["corrected"]) # edits become eval cases将闭环合回调节旋钮。HITL 和置信度阈值是同一个反馈回路,这正是让整个层能够自我改进而非静态不变的原因:
observation on a slice action ---------------------------- -------------------------------------------------------------- humans approve ~unanimously candidate to automate — lower `T` for that slice frequent overrides raise `T` or pull the slice back to human-only many edits of the same field the agent is systematically wrong there — fix the prompt/logic
你可以有据可依地开放自动化——指出为每个区间提供依据的批准率。这就是回路自我闭合:评判者的分歧和人类的编辑都回流到 compose() 所读取的历史信号和切片准确率信号中,因此每一个被路由的决策都会让下一批自动决策的校准稍微更好一些。
而且不要让人类被淹没。把所有东西都路由给一个人不是安全;那是对你审查者的 DoS 攻击,而被淹没的队列只会被橡皮图章式地批准。良好的置信度和路由的意义在于,只有真正不确定的决策才会到达人类手中——少到足以获得真正的关注。如果队列不堪重负,就在上游修复它(更好的置信度、自动化已被证明安全的切片),而不是增加更多审查者。还有两个值得点名的陷阱:让拒绝比批准更难(一次点击 vs 一个五字段表单会偏向批准),以及遗漏 routed_because(人类不知道往哪里看,于是他们只是草草浏览)。
置信度是安全自动化的关键,它是一个由三部分组成的机制。用独立信号来构建这个数值——绝不要用模型自己的声明——并对照真实结果进行校准,这样“0.9”才真正意味着90%。对于重要的决策,让一个独立且持怀疑态度的评判者尝试证明第一个模型是错的,按风险采样并保持确定性,这样你的测试才能保持稳定;它的一致意见会纳入评分,它的分歧则会举手示意。线以下的一切都交给人类,连同草稿、推理过程以及为什么被路由过去一起移交——不同意和批准一样低成本,每个操作都被捕获为信号,用来将阈值向下或向上调整。
以这种方式构建的系统知道自己什么时候不知道。它可以被信任承担更多,因为它不该处理的情况会自行路由出去——而监督的表象,这个最危险的后果,正是它拒绝制造的东西。
系列:在生产环境中运行大语言模型系统——第3级(共6级):置信度。
这些文章采用 Creative Commons Attribution-ShareAlike 4.0 International 许可协议授权。
creativecommons.org/licenses/by-sa/4.0/deed.en
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Stack Overflow Blog
原文作者:Varun Jindal;如有侵权,请联系管理员删除。