[LLM 8/10] Guardrails:真正的安全护栏不是模型,而是阈值
我和团队给客户上线的泰语聊天机器人,遇到的从来不只是彬彬有礼的提问—— 有人来要违法物品的配方,有人变着法子想诱导它去骂别人, 也有那么一天,模型自己把客户的电话号码吐了出来。 这一章我们要在两个方向上都建起防线:拦截危险 prompt 的入站分类器,以及出站的 PII 过滤器。 但本章真正的核心不是模型本身——而是这样一个事实:护栏是一个在成本不对称条件下所做的阈值决策, 而大家最爱拿出来展示的那个"accuracy 94%",其实几乎什么也没说。
Open in Colab08_guardrails.ipynb
1. 问题(Problem statement)
前面几章我们一直在把模型训得"更强",但无论模型多强,一旦面对真实用户, 损害总是会从两个方向找上门来:
| 方向 | 损害举例 | 本章使用的工具 |
|---|---|---|
| 入站(input) | 用户来问怎么伤害别人、违法物品的配方、作弊的办法——而模型真的答了 | 危险 prompt 分类器 |
| 出站(output) | 模型把身份证号、电话号码、银行账号吐了出来,这些内容泄自 context 或训练数据 | deterministic 的 PII 过滤器 |
别忘了第 2 章的 SFT 有一个非常安静的副作用:用窄领域数据做 fine-tuning 会侵蚀模型原本具备的拒答(refusal)行为。所以你自己微调过的模型,往往比原版更不安全。 这正是真实系统必须在模型之外再加一道围栏的原因。
那么,为什么不能直接买一个号称"准确率 94%"的现成护栏产品? 因为这句话根本没有回答最重要的三个问题:
- 94% 是在哪个阈值上测的——同一个数字可以沿着整条曲线来回滑动
- 测试集里 unsafe 占百分之几——在 production 里,真正危险的流量通常连 1% 都不到
- 以及它拦掉了百分之几的无辜用户——这个数字几乎没人愿意报告
一个会拦住正常提问客户的护栏,不是一个安全的系统,它是一个坏掉的产品。
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 |
护栏不是模型——它是一个在成本不对称条件下所做的阈值决策。 模型只负责给出分数 ,而选择切分点 是在回答一个业务问题:"放过一次危险内容,比拦错一个无辜客户,贵多少倍?"
所以诚实的结果报告必须永远带两个数字:抓到的 unsafe 以及被拦掉的 benign。 只有一个孤零零的数字,那是营销,不是工程。
而且我们还会看到,某些最好的护栏根本不是 ML—— 用 regex + 校验位做的 PII 过滤器是 deterministic 的,可以用 unit test 覆盖,耗时在微秒级, 并且永远不会被 jailbreak。
3. 公式(Equation)
3.1 分类器的 loss —— 老朋友
就是普通的 binary cross-entropy( 表示 unsafe),没有任何新东西——而这恰恰是重点: 护栏里属于 ML 的那一部分,是整个系统中最简单的一部分。真正的内容在下面。
3.2 决策规则与期望成本 —— 本章真正的内容
模型的职责在给出分数那一刻就结束了。拦还是放,取决于与 的比较, 而它是我们通过最小化期望成本选出来的:
- = 漏放的 unsafe 所占比例(false negative rate)
- = 被误拦的 benign 所占比例(false positive rate)
- = 两类错误各自的价格
注意这个式子逼着你回答一个 ML 替你答不了的问题:你这个产品的 到底是多少。 这是一个纯粹的产品决策,而且每个产品的答案都不一样:
| 产品 | FN 的代价(放过 unsafe) | FP 的代价(拦住无辜的人) | 合理的 |
|---|---|---|---|
| 健康咨询聊天机器人 | 可能危及生命 + 法律责任 | 用户略感恼火 | 50:1 起 |
| 企业客服助手 | 上新闻的公关事故 | 客户多联系一次 support | 约 10:1 |
| 员工内部工具 | 有限(用户是可实名定位的员工) | 每天都有工作被打断 | 约 2:1 |
如果你从没把这个比值明明白白写进文档,那就意味着一直以来有人在无意中替你选好了 。
3.3 本章最贵的一课:base rate 能把 precision 打垮
假设我们的分类器抓 unsafe 的 TPR = 95%,误拦率只有 FPR = 5%——听起来棒极了。 问题是:在所有被拦下来的消息里,真正 unsafe 的占多少?直接用贝叶斯算。 令 为真实流量中 unsafe 的比例:
代入两种情形的数字:
- 平衡的测试集():precision
- 真实流量():precision
完全一模一样的分类器——但在 production 里,每 6 条被拦下来的消息中就有 5 条来自无辜用户。 因为当 unsafe 很稀有( 很小)时,分母中的 那一项会把一切都吞掉。 这就是为什么"在平衡测试集上评完然后到处说自己 95% 准"是一种自欺欺人。
3.4 多层防御(layered defence)
如果部署 层彼此独立的护栏(blocklist → 分类器 → system prompt → 人工抽查), 那么 unsafe 要溜出去必须骗过每一层,而 benign 只要任意一层判错就会被拦:
FNR 呈几何级数下降(非常好),但 FPR 会不断累积(这是必须付的账单)。
这个式子成立的前提是各层彼此独立地犯错,而现实中这几乎从来不成立—— 一种规避手法(比如在词中间插入 zero-width space)往往会同时骗过所有基于原始文本的层。 所以各层的错误是相关的,真实的 永远比这个公式更差。 请把它看成 best case,而不是承诺。
3.5 Constrained decoding —— 不是 ML、也最被忽视的那种护栏
如果你的 use case 只需要在有限集合里作答(菜单、类别、符合 schema 的 JSON),那就别事后再去检查文本了—— 在 generate 的那一刻就强制约束,方法是在允许的 token 集合 上重新归一化:
之外的 token 概率精确为零,而不是"非常小"。 结果就是一个 deterministic 的护栏,延迟增量为零,而且宇宙中没有任何 prompt 能绕过它—— 因为它并不是在禁止模型"想说什么",而是让集合之外的词从一开始就不存在于名录里。 凡是能用 constrained decoding 的地方,都先用它,剩下真正属于 free text 的部分再交给分类器去看守。
这篇文章大约是本章的前 30%。其余部分——环境准备、数据准备、核心代码、实测结果与总结——都在免费的 LLM Finetuning 课程中,使用 Google 登录即可阅读。
在课程中阅读完整章节 →