跳到主要内容

[LLM 8/10] Guardrails:真正的安全护栏不是模型,而是阈值

· 阅读需 8 分钟
Kobkrit Viriyayudhakorn
CEO, iApp Technology

我和团队给客户上线的泰语聊天机器人,遇到的从来不只是彬彬有礼的提问—— 有人来要违法物品的配方,有人变着法子想诱导它去骂别人, 也有那么一天,模型自己把客户的电话号码吐了出来。 这一章我们要在两个方向上都建起防线:拦截危险 prompt 的入站分类器,以及出站的 PII 过滤器。 但本章真正的核心不是模型本身——而是这样一个事实:护栏是一个在成本不对称条件下所做的阈值决策, 而大家最爱拿出来展示的那个"accuracy 94%",其实几乎什么也没说。

Open in Colab08_guardrails.ipynb

1. 问题(Problem statement)

前面几章我们一直在把模型训得"更强",但无论模型多强,一旦面对真实用户, 损害总是会从两个方向找上门来:

方向损害举例本章使用的工具
入站(input)用户来问怎么伤害别人、违法物品的配方、作弊的办法——而模型真的答了危险 prompt 分类器
出站(output)模型把身份证号、电话号码、银行账号吐了出来,这些内容泄自 context 或训练数据deterministic 的 PII 过滤器

别忘了第 2 章的 SFT 有一个非常安静的副作用:用窄领域数据做 fine-tuning 会侵蚀模型原本具备的拒答(refusal)行为。所以你自己微调过的模型,往往比原版更不安全。 这正是真实系统必须在模型之外再加一道围栏的原因。

那么,为什么不能直接买一个号称"准确率 94%"的现成护栏产品? 因为这句话根本没有回答最重要的三个问题:

  1. 94% 是在哪个阈值上测的——同一个数字可以沿着整条曲线来回滑动
  2. 测试集里 unsafe 占百分之几——在 production 里,真正危险的流量通常连 1% 都不到
  3. 以及它拦掉了百分之几的无辜用户——这个数字几乎没人愿意报告

一个会拦住正常提问客户的护栏,不是一个安全的系统,它是一个坏掉的产品

2. 我们要做什么(Solution)

我们会在免费 Colab 上做出两个真实可用的护栏,然后老老实实地把它们测一遍:

位置技术大致延迟
Input guardrail在 prompt 送到 LLM 之前Qwen3-0.6B + sequence-classification head + LoRA r=8~15–30 ms
Output guardrail在 LLM 回答之后、送到用户之前regex + mod-11 校验位(完全没有 ML)~0.1 ms
本章的核心观点

护栏不是模型——它是一个在成本不对称条件下所做的阈值决策。 模型只负责给出分数 pϕ(unsafex)p_\phi(\text{unsafe}\mid x),而选择切分点 τ\tau 是在回答一个业务问题:"放过一次危险内容,比拦错一个无辜客户,贵多少倍?"

所以诚实的结果报告必须永远带两个数字:抓到的 unsafe 以及被拦掉的 benign。 只有一个孤零零的数字,那是营销,不是工程。

而且我们还会看到,某些最好的护栏根本不是 ML—— 用 regex + 校验位做的 PII 过滤器是 deterministic 的,可以用 unit test 覆盖,耗时在微秒级, 并且永远不会被 jailbreak。

3. 公式(Equation)

3.1 分类器的 loss —— 老朋友

L(ϕ)=E(x,y)D[ylogpϕ(unsafex)+(1y)log(1pϕ(unsafex))]\mathcal{L}(\phi) = -\mathbb{E}_{(x,y)\sim\mathcal{D}}\Big[\,y\log p_\phi(\text{unsafe}\mid x) + (1-y)\log\big(1-p_\phi(\text{unsafe}\mid x)\big)\Big]

就是普通的 binary cross-entropy(y=1y=1 表示 unsafe),没有任何新东西——而这恰恰是重点: 护栏里属于 ML 的那一部分,是整个系统中最简单的一部分。真正的内容在下面。

3.2 决策规则与期望成本 —— 本章真正的内容

block(x)=1[pϕ(unsafex)>τ]\text{block}(x) = \mathbf{1}\big[\,p_\phi(\text{unsafe}\mid x) > \tau\,\big]

模型的职责在给出分数那一刻就结束了。拦还是放,取决于与 τ\tau 的比较, 而它是我们通过最小化期望成本选出来的:

C(τ)=cFNP(unsafe)FNR(τ)  +  cFPP(safe)FPR(τ)τ=argminτC(τ)C(\tau) = c_{\text{FN}}\,P(\text{unsafe})\,\text{FNR}(\tau) \;+\; c_{\text{FP}}\,P(\text{safe})\,\text{FPR}(\tau) \qquad\qquad \tau^* = \arg\min_\tau C(\tau)
  • FNR(τ)\text{FNR}(\tau) = 漏放的 unsafe 所占比例(false negative rate)
  • FPR(τ)\text{FPR}(\tau) = 被误拦的 benign 所占比例(false positive rate)
  • cFN,cFPc_{\text{FN}}, c_{\text{FP}} = 两类错误各自的价格

注意这个式子逼着你回答一个 ML 替你答不了的问题:你这个产品的 cFN/cFPc_{\text{FN}}/c_{\text{FP}} 到底是多少。 这是一个纯粹的产品决策,而且每个产品的答案都不一样:

产品FN 的代价(放过 unsafe)FP 的代价(拦住无辜的人)合理的 cFN/cFPc_{\text{FN}}/c_{\text{FP}}
健康咨询聊天机器人可能危及生命 + 法律责任用户略感恼火50:1 起
企业客服助手上新闻的公关事故客户多联系一次 support约 10:1
员工内部工具有限(用户是可实名定位的员工)每天都有工作被打断约 2:1

如果你从没把这个比值明明白白写进文档,那就意味着一直以来有人在无意中替你选好了 τ\tau

3.3 本章最贵的一课:base rate 能把 precision 打垮

假设我们的分类器抓 unsafe 的 TPR = 95%,误拦率只有 FPR = 5%——听起来棒极了。 问题是:在所有被拦下来的消息里,真正 unsafe 的占多少?直接用贝叶斯算。 令 π=P(unsafe)\pi = P(\text{unsafe}) 为真实流量中 unsafe 的比例:

precision=P(unsafeblock)=P(blockunsafe)πP(block)=TPRπTPRπ+FPR(1π)\text{precision} = P(\text{unsafe}\mid\text{block}) = \frac{P(\text{block}\mid\text{unsafe})\,\pi}{P(\text{block})} = \frac{\text{TPR}\cdot\pi}{\text{TPR}\cdot\pi + \text{FPR}\cdot(1-\pi)}

代入两种情形的数字:

  • 平衡的测试集π=0.5\pi = 0.5):precision =0.95×0.50.95×0.5+0.05×0.5=95%= \dfrac{0.95 \times 0.5}{0.95 \times 0.5 + 0.05 \times 0.5} = 95\%
  • 真实流量π=0.01\pi = 0.01):precision =0.95×0.010.95×0.01+0.05×0.9916%= \dfrac{0.95 \times 0.01}{0.95 \times 0.01 + 0.05 \times 0.99} \approx 16\%

完全一模一样的分类器——但在 production 里,每 6 条被拦下来的消息中就有 5 条来自无辜用户。 因为当 unsafe 很稀有(π\pi 很小)时,分母中的 FPR(1π)\text{FPR}\cdot(1-\pi) 那一项会把一切都吞掉。 这就是为什么"在平衡测试集上评完然后到处说自己 95% 准"是一种自欺欺人。

3.4 多层防御(layered defence)

如果部署 KK 层彼此独立的护栏(blocklist → 分类器 → system prompt → 人工抽查), 那么 unsafe 要溜出去必须骗过每一层,而 benign 只要任意一层判错就会被拦:

FNRsys=k=1KFNRkFPRsys=1k=1K(1FPRk)\text{FNR}_{\text{sys}} = \prod_{k=1}^{K}\text{FNR}_k \qquad\qquad \text{FPR}_{\text{sys}} = 1 - \prod_{k=1}^{K}\big(1-\text{FPR}_k\big)

FNR 呈几何级数下降(非常好),但 FPR 会不断累积(这是必须付的账单)。

独立性假设是过于乐观的想象

kFNRk\prod_k \text{FNR}_k 这个式子成立的前提是各层彼此独立地犯错,而现实中这几乎从来不成立—— 一种规避手法(比如在词中间插入 zero-width space)往往会同时骗过所有基于原始文本的层。 所以各层的错误是相关的,真实的 FNRsys\text{FNR}_{\text{sys}} 永远比这个公式更差。 请把它看成 best case,而不是承诺。

3.5 Constrained decoding —— 不是 ML、也最被忽视的那种护栏

如果你的 use case 只需要在有限集合里作答(菜单、类别、符合 schema 的 JSON),那就别事后再去检查文本了—— 在 generate 的那一刻就强制约束,方法是在允许的 token 集合 A\mathcal{A} 上重新归一化:

p(tx)=pθ(tx)1[tA]tApθ(tx)p'(t\mid x) = \frac{p_\theta(t\mid x)\,\mathbf{1}[t\in\mathcal{A}]}{\sum_{t'\in\mathcal{A}} p_\theta(t'\mid x)}

A\mathcal{A} 之外的 token 概率精确为零,而不是"非常小"。 结果就是一个 deterministic 的护栏,延迟增量为零,而且宇宙中没有任何 prompt 能绕过它—— 因为它并不是在禁止模型"想说什么",而是让集合之外的词从一开始就不存在于名录里。 凡是能用 constrained decoding 的地方,都先用它,剩下真正属于 free text 的部分再交给分类器去看守。

完整内容在课程中

这篇文章大约是本章的前 30%。其余部分——环境准备、数据准备、核心代码、实测结果与总结——都在免费的 LLM Finetuning 课程中,使用 Google 登录即可阅读。

在课程中阅读完整章节 →