Skip to main content

08 - 内容护栏:拦不住的那一类

前置06 - 凭证与密钥07 - 记忆与上下文外泄

本篇回答:怎么防止模型被诱导说出违规内容、产出违规产物;以及为什么这件事和前两篇的性质完全不同。

会用到的词

  • 越狱(jailbreak):诱导模型输出它被训练成不该输出的内容。注意它和提示注入不是一回事 —— 注入是让模型执行不该执行的操作,越狱是让模型说不该说的话
  • 精确率 / 召回率:拦截时有多少是拦对的(precision)、该拦的里拦住了多少(recall)。这两个数在护栏里永远互相拉扯
  • 误杀(false positive):正常内容被拦。这是护栏最主要的成本,而且成本落在正常用户身上
  • TTFT:首个 token 送达用户的时间。流式护栏的取舍全部围绕这个指标

一、这一类问题为什么没有确定性方案

前两篇每一处有效的防线,判据都是确定性的:这个域名在不在白名单里、这枚令牌的 aud 是不是我、这个字段是不是身份字段。判据是布尔的,实现是代码,结果可复现。

内容护栏没有这种判据。「这段话算不算教人做危险品」是一个语义判断,而语义判断没有精确边界。

同一个专题里的三类问题,判据的性质不一样06 · 凭证与密钥判据:这个字段是不是凭证这枚令牌的 aud 是不是我布尔 · 可在代码里实现07 · 记忆与外泄判据:这个域名在不在表里这条记忆来自哪个源布尔 · 可在代码里实现08 · 内容违规判据:这段话算不算教唆算不算歧视、算不算医疗建议语义 · 只能逼近做对了就是零漏洞做对了就是零漏洞没有「做对了」这个状态失败原因是实现有 bug失败原因是漏了某条通路失败是设计的一部分前两类的目标是「补完」,最后一类的目标是「把误杀和漏放调到业务能接受的比例」——所以做之前必须先回答一个前两类不需要回答的问题:这个场景里,误杀一条正常请求和漏放一条违规内容,哪个更贵。
把内容护栏当成前两类那样「查漏补缺」是最常见的误区 —— 它没有补完的那一天,只有一条不断移动的取舍曲线。

这个差别决定了整篇的组织方式:下面讲的每一种手段都要同时说清它拦住了什么、误杀了什么、代价多少。只讲拦截率的护栏方案在工程上是不可评估的。

二、模型自身的对齐不是边界

第一反应通常是「模型自己就会拒绝」。这个判断在大部分普通请求上成立,在被针对的请求上不成立,原因和 04 篇讲六道关卡时给的结论一样:模型的拒绝是它生成出来的一个结果,不是一道执行在它之外的检查。

能被生成的东西就能被条件改变。常见手法按机制分类:

手法机制为什么关键词过滤抓不到
角色扮演给模型一个「在这个身份下这么说是合理的」的框架请求本身用词完全正常
虚构包装把内容包在小说、剧本、教学案例里「写一个小说场景」是绝对正当的请求
分步拆解单独看每一步都无害,拼起来才是完整的每一轮都过检,问题在多轮的累积
编码绕过base64、拼音、火星文、Unicode 变体关键词表匹配的是明文
低资源语言用训练数据稀少的语言提问护栏的词表和分类器往往只覆盖主流语言
多轮爬坡先建立一个无害的上下文,逐轮加码单轮检查看不到轨迹

中文场景额外多一层。 后面第四节会拆的那份开源规则表,识别词全是英文(pretendroleplaydeveloper mode)。同样语义的中文请求根本不会命中任何一条。这不是那个项目的疏忽 —— 中文的分词、同音替换(用拼音或谐音字规避)、以及「同一个意思有十几种说法」的特性,让关键词方案在中文上的性价比比英文低得多。

结论不是「所以不做」,而是:中文场景里,关键词层的定位只能是「拦掉最粗糙的那一批」,主力必须是分类器。

三、两道分类器,各看一半

护栏至少要有两道,位置分别在模型前和模型后。这不是为了保险,是因为两侧能看到的信息本来就不一样。

两侧看到的东西不一样,所以漏的东西也不一样入站分类器看得到:用户想干什么看不到:会生成出什么模型自身对齐是第三道但它是概率性的出站分类器看得到:实际生成了什么看不到:用户为什么要它返回用户两道都过了才走到这里流式下见第七节只做一侧会漏掉什么只做入站「帮我写一个化学实验教学片段」意图完全正常,生成出来的内容却越界了 —— 入站分类器无从判断。只做出站用户在做多轮爬坡试探,每一轮的输出都合规,但整条轨迹是在找边界 —— 出站分类器看不到轨迹。
入站侧还有一个出站侧永远替代不了的作用:它能在花掉推理成本之前就拒绝。对长输出场景,这是实打实的省钱。

两侧的动作也应该不同:入站更适合拒绝,出站更适合改写。 入站拦下来时用户还没等待,直接给拒绝理由体验尚可;出站已经生成完了,整段丢弃会让用户觉得系统坏了,替换成安全版本或者只遮蔽越界片段更合理。

四、拆一份真实的关键词方案

关键词过滤在讨论里常被一句「没用」带过。但真实的生产实现远不是一张词表 —— 拆开看能理解它到底能做到什么程度、代价在哪。

LiteLLM 内置了一套完整的内容过滤,在 litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/:主实现 content_filter.py 有 90KB,规则数据 patterns.json 有 38KB,另有 categories/(三十多份分类规则)、policy_templates/guardrail_benchmarks/

4.1 条件共现,而不是单词命中

看它的越狱检测规则 categories/prompt_injection_jailbreak.yaml

# 注释里第一句就说明了它的设计:
# "Uses conditional logic: BLOCK if an identifier word + block word appear in same sentence"
category_name: "prompt_injection_jailbreak"
default_action: "BLOCK"

# 第一张表:角色扮演类的动作词。单独出现完全正常 ——
# 「pretend you are a translator」是一个正当请求。
identifier_words:
- "pretend"
- "roleplay"
- "act as"
- "imagine you are"
- "you are now"
- "developer mode" # 这一条例外,它本身就足够可疑
- "simulate"
- "impersonate"

# 第二张表:解除限制类的词。同样地,单独出现也可能正常 ——
# 「a story without limits on length」并不是越狱。
additional_block_words:
- "no restrictions"
- "no filters"
- "bypass"
- "unrestricted"
- "uncensored"
- "god mode"
- "without limits"

为什么要用「同一句里共现」而不是任一命中:两张表里的词单独出现时误杀率高得没法用。要求共现,等于要求「既在构造一个身份、又在要求解除限制」这两个特征同时出现 —— 这才接近越狱的实际形态。

代价是它同样可以被拆开绕过:把身份构造放在第一轮、解除限制放在第二轮,同句共现的条件就不成立了。这是所有基于单条消息的规则的共同上限,它看不见轨迹。

4.2 四层匹配和那张「例外表」

同一目录下 guardrail_benchmarks/results/BENCHMARKS.md 描述了另一份规则(拦截投资建议)的完整结构,四层依次是:

  1. 绝对拦截词 —— 「investment advice」「stock tips」这类不可能有正当用法的短语,直接子串匹配
  2. 条件匹配 —— 识别词(stockbitcoin401k)+ 拦截词(buyshould ibest)同句共现
  3. 短语模式 —— 正则,抓那些绕开了金融词汇的说法,文档里给的例子是「put my money to make it grow」「park my cash」
  4. 例外表 —— 命中之后再放行的短语

第四层最值得看,因为它暴露了这类方案的真实工作量。文档里给的例外包括 gold medaltrading cardreturn policyemirates flight

翻译一下:这不是设计出来的,是被误杀报障一条条打回来的补丁。「gold」命中了贵金属识别词,「trading card」命中了交易识别词,「return policy」命中了「return(回报)」。每一条例外的背后都是一次线上误杀。

这条经验可以直接搬走:上关键词规则时,例外表和规则表要同时设计,并且预留一个「误杀反馈直接进例外表」的运维通道。 没有这个通道,规则只会越加越紧,误杀越积越多。

四层依次收窄,最后一层反过来放行① 绝对拦截词investment advicestock tips子串直接命中② 同句共现识别词 stock · 401k+ 拦截词 buy · best同一句里同时出现③ 短语正则抓绕开专业词汇的park my cashspare cash④ 例外表放行gold medaltrading cardreturn policy判定BLOCK 或 MASK或放行全程 <0.1ms误杀风险最低拆成两轮就绕过写一条抓一种说法每条都是一次线上误杀进程内,无额外依赖第 ④ 层不是设计出来的,是被误杀报障一条条打回来的补丁 —— gold 命中贵金属,trading 命中交易,return 命中「回报」。所以上规则时例外表要和规则表同时设计,并预留一条「误杀反馈直接进例外表」的运维通道。
①②③ 收窄、④ 放宽,这个结构本身就说明了关键词方案的运维形态:它不会收敛到一个稳定版本,而是持续在两个方向上被拉扯。

4.3 内置了哪些类别

patterns.json 里有 82 条正则模式,分成几组,而且带 action 字段区分处置方式:

例子处置
PIIus_ssnemailus_phone、十国护照号通常 MASK
支付卡visamastercardamexdiscoverMASK
凭证aws_access_keygithub_tokenslack_token呼应 06 篇的出口脱敏
受保护特征种族、宗教、年龄、残障、婚姻家庭、军人身份用于借贷等合规场景
危险内容weapons_firearms → MASK;explosivesterrorismviolence_threats → BLOCK分级

action 分成 MASK 和 BLOCK 这件事本身是个好设计:枪支相关内容遮蔽即可,爆炸物必须整条拒绝。 一刀切成「命中就拒绝」会把大量可以降级处理的场景变成误杀。

categories/ 下还能看到这套东西的实际用途 —— 除了 harmful_*prompt_injection_* 这类通用项,更多的是行业合规项:claims_medical_adviceclaims_phi_disclosureclaims_prior_auth_gamingdenied_financial_advicedenied_legal_advice,以及 bias_genderbias_racialage_discrimination 这些反歧视规则。policy_templates/ 下直接按法规组织:eu_ai_act_art5_*(欧盟 AI 法案第 5 条的禁止性实践,含法语版)、sg_mas_*(新加坡金管局的模型治理要求)。

这说明了内容护栏在生产里的真实需求分布:绝大部分不是防越狱,是合规。 「不能给出医疗建议」「不能表现出年龄歧视」这类要求的数量远超「不能教人做危险品」,而且它们是有法律后果的。做方案时如果只盯着越狱,会发现上线后需求全在另一边。

五、基准数字要这么读

同一份 BENCHMARKS.md 给了一组完整的对比,测试集是 207 条(85 条应拦,122 条应放):

方案精确率召回率F1误杀数漏放数p50 延迟额外依赖
ContentFilter YAML100.0%100.0%100.0%00<0.1ms
ONNX MiniLM95.3%96.5%95.9%432.4msonnxruntime ~15MB
Embedding MiniLM 80MB98.4%74.1%84.6%122~3mssentence-transformers + torch
NLI DeBERTa-xsmall82.7%100.0%90.5%180~20mstransformers + torch
TF-IDF47.2%100.0%64.2%950<0.1ms
Embedding MPNet 420MB98.3%68.2%80.6%127~5mssentence-transformers + torch

这张表有三处值得停下来。

第一,那个 100% 不能按字面理解。 规则和评测集来自同一个项目、同一批人、大概率是同一段时间。规则作者看着这些用例调规则,达到 100% 是必然的 —— 这是过拟合的教科书形态,不是方案的泛化能力。真正有信息量的是换一份没参与调优的评测集会掉到多少,而这个数没有人公布。

读任何护栏基准都要先问这个问题:评测集是不是规则/模型的作者提供的。是的话,这个数只能证明「在已知用例上不回归」,不能证明拦得住没见过的攻击。

第二,TF-IDF 那一行是这张表里最有价值的。 95 次误杀、47.2% 精确率 —— 意味着每两次拦截里有一次拦错。同时它的召回率是 100%。

这正是护栏最典型的失败形态:一个把所有违规都拦住了的方案,可以同时是一个完全不能上线的方案。 如果只汇报召回率,TF-IDF 看起来和最好的方案一样完美。所以护栏的验收指标必须成对给,只给一个数的评估等于没评估。

第三,两个 embedding 方案的召回率是 74.1% 和 68.2%。 也就是说该拦的漏了四分之一到三分之一,而且 420MB 的那个还不如 80MB 的那个。参数量在这类任务上不构成保证 —— 语义相似度和「是否违反某条具体策略」本来就不是同一个判据,用前者近似后者,模型再大也补不上这个错配。

顺带一个工程结论:<0.1ms~20ms 之间差了两个数量级。护栏跑在每一次请求上,出入两侧各一次。20ms 的方案在高 QPS 下需要独立部署和扩容,0.1ms 的方案可以直接跑在进程内。选型时把延迟和部署形态一起算,不要只比 F1。

六、分类器怎么选

关键词之上是分类器。开源侧值得看的是 Meta 的 meta-llama/PurpleLlama(4,359 star),它的 LlamaFirewall 子项目把护栏拆成了四个可组合的扫描器 —— 这个拆法本身比具体实现更有参考价值:

扫描器做什么对应本专题的哪一篇
PromptGuard 2BERT 规模的分类器,检测直接注入与经典越狱模式本篇第二节
AlignmentCheck审模型的推理过程,检测目标被劫持、行为偏离用户意图01 篇的间接注入
Regex + Custom可配置的正则与轻量 LLM 判断层本篇第四节
CodeShield静态分析模型生成的代码,Semgrep + 正则,覆盖 8 种语言本篇第八节

README 里对 AlignmentCheck 的定位是「chain-of-thought auditing」—— 审的是推理链,不是输出文本。这一条正好补上第三节说的「出站分类器看不到轨迹」:要看轨迹,就得审推理过程,而不是审最终那段话。 代价是它必须跑一次额外的模型调用,延迟和成本都是数量级的差别,只适合高价值决策点。

三层的分工可以这样定:

  • 正则 / 关键词:0.1ms 量级,跑在所有请求上,拦最粗糙的那批,例外表持续维护
  • 小分类器(PromptGuard、Llama Guard 一类):毫秒量级,跑在所有请求上,是主力
  • LLM 判断 / AlignmentCheck:百毫秒量级、有 API 成本,只跑在高危动作前
三层按延迟量级分工,触发条件的依据各不相同正则 · 关键词延迟 0.1ms 量级,无额外依赖跑在所有请求上定位:拦掉最粗糙的那一批小分类器延迟毫秒量级,需独立部署跑在所有请求上定位:主力,中文场景尤其LLM 判断 · 审推理链延迟百毫秒量级,有 API 成本只跑在高危动作前定位:唯一看得到轨迹的一层例外表要持续维护性价比最高的一层按动作危险度触发拆成多轮就绕过仍然看不到多轮轨迹不按内容可疑度触发第三层的触发条件是这张图最容易做错的地方:按「内容看起来可疑」触发,成本会随流量线性上涨且拦不住关键路径;按「这个动作危不危险」触发,成本被钉死在高危动作的数量上,而且保护的正好是损失最大的那几条路径。
转账、删除、对外发送这些动作前跑一次 LLM 判断,普通问答不跑 —— 同样的预算下,这样分配比均摊到所有请求上有效得多。

第三层的触发条件应该是动作的危险度,不是内容的可疑度 —— 转账、删除、对外发送这些动作前跑一次,普通问答不跑。这样成本可控,而且它保护的正好是损失最大的那些路径。

七、流式输出的取舍

出站护栏和流式输出天然冲突:要审完整内容就得等生成完,等生成完就没有流式了。

三种策略,本质上是在拿首字延迟换泄漏窗口A · 全缓冲后审生成完整段再审再发首字延迟 = 整段生成时间泄漏窗口 = 0B · 分块审攒够一段就审一段再放首字延迟 = 一个块的时间泄漏窗口 = 0,但会跨块漏判C · 先放后撤边发边审,违规则撤回首字延迟 = 最优泄漏窗口 > 0长回答体验不可接受多数场景的默认选择内容已经到过用户屏幕适合短回答、高危场景块要带重叠,否则跨块漏截图和日志撤不回来C 的「撤回」只在 UI 层成立。对合规场景要按「已经发生了一次违规展示」计,不能算作拦住了。
B 的跨块漏判是个具体坑:一句违规内容被切在两个块的边界上,两个块单独看都无害。分块时让相邻块重叠一部分能缓解,代价是重复审的开销。

选择规则很简单:看这个场景里「用户看到过」算不算事故。 算,就只能用 A 或 B;不算,C 的体验最好。金融、医疗、面向未成年人的产品通常属于前者。

八、Agent 要审的不只是回复文本

前面七节讲的都是聊天场景的护栏。Agent 场景多一层:它的输出不止是给人看的文本,还有会被执行的东西。 只审回复文本等于只审了出口中的一个。

一次 Agent 回合产生五类出站产物,内容护栏通常只审了第一类① 回复文本给人看的那段常规护栏覆盖② 工具调用参数发给谁 · 发什么要审收件人白名单③ 生成的代码会被执行或提交要走静态分析④ 写入的文件文档 · 配置 · 提交下游会再读回来⑤ 对外消息邮件 · IM · 工单发出去撤不回② 到 ⑤ 的共同点:它们不经过「渲染给用户看」这一步,所以任何挂在回复渲染链路上的护栏都碰不到它们。④ 尤其隐蔽 —— 写进文档的一段内容,下一个会话检索回来时就成了「可信的内部资料」,绕一圈变回了输入。对应到 04 篇的六道关卡,这一层要挂在「网关出站」而不是「客户端渲染」,因为只有网关同时看得见这五类。⑤ 是唯一一个「审得再好也要加人工确认」的:不可逆。
第 ④ 类构成一条闭环:模型写出去的内容会作为可信资料被自己读回来。这条闭环让一次未拦住的输出获得了和 07 篇记忆投毒相同的持久性。

其中第 ③ 类有现成的工具可用 —— 前面提到的 CodeShield 就是干这个的,走 Semgrep 规则做静态分析,覆盖 8 种语言。代码不该用文本分类器审,该用静态分析审,这是两种完全不同的判据,后者接近确定性。

第 ⑤ 类的处理方式和其他四类不同:它不可逆。护栏在这里的作用是降低需要人工确认的比例,不是替代人工确认。对外发消息这个动作本身应该保留一道审批,04 篇讲的客户端人工审批就是这一道。

九、代价与什么时候不该上

内容护栏的成本比前两篇讲的任何手段都高,而且大部分是持续性的:

成本项形态会不会随时间下降
延迟出入两侧各一次,0.1ms 到数百 ms 不等不会
算力分类器要部署、扩容,跟主链路同 QPS不会
误杀落在正常用户身上,且用户不知道为什么被拒会,但需要持续运维投入
规则维护例外表、新攻击手法、新合规要求不会,是长期投入
评估每次改规则都要跑回归,评测集要自己攒一次性建设 + 持续补充

有几种情况下它不该上,或者不该先上:

前两类问题还没解决。 内容护栏挡不住数据外泄 —— 一段把用户邮箱编码进图片 URL 的输出,在任何内容分类器眼里都是完全正常的文本。如果 07 篇的出口白名单还没做,先做那个。

输出不面向人,且不进入任何会被人读到的地方。 一个把日志分类成结构化字段的 Agent,输出是枚举值,schema 校验就是它的护栏,加语义分类器没有意义。

没有能力维护例外表。 上了规则却没有误杀反馈通道,结果是误杀持续积累而没有人知道 —— 用户只是默默不再使用这个功能。这比不上护栏更糟,因为你连问题存在都察觉不到。

用护栏替代权限设计。 「靠护栏拦住模型说出别的用户的数据」是把一个确定性问题(谁能读什么)交给了一个概率性手段。正确做法是那些数据一开始就不该进这个会话的上下文。

十、检查清单

做之前(第一节)

  • 明确回答:这个场景里误杀一条正常请求和漏放一条违规内容,哪个更贵
  • 确认 0607 两类确定性防线已经做完
  • 列出真实的需求分布 —— 通常合规类远多于防越狱类

分层(第三、四、六节)

  • 入站和出站两侧都有,不是只做一侧
  • 入站以拒绝为主,出站以改写 / 遮蔽为主
  • 关键词层的例外表和规则表同时设计,并有误杀反馈进例外表的通道
  • 处置分级:能 MASK 的不要 BLOCK
  • 中文场景不把关键词层当主力
  • LLM 判断层按「动作危险度」触发,不按「内容可疑度」触发

评估(第五节)

  • 精确率和召回率成对汇报,不单独给一个数
  • 评测集里有一部分没参与过规则调优
  • 记录误杀的绝对条数,不只看百分比
  • 延迟和部署形态跟 F1 一起进选型表

Agent 特有(第七、八节)

  • 流式策略按「用户看到过算不算事故」来选
  • 分块审时相邻块有重叠,避免跨块漏判
  • 工具调用参数里的收件人 / 目标地址走白名单
  • 生成的代码走静态分析,不是文本分类器
  • 写入文件的内容也审 —— 它下个会话会被读回来
  • 对外发消息保留人工确认,护栏只用来降低确认频次

← 回到 专题索引