护栏与提示注入检测:OpenRouter 在请求链路上加的那道闸

2026-08-18

假设你的应用把用户输入原样塞进 messages,某天有人在输入框里打了一句 ignore all previous instructions。这条请求会在哪一步被拦下?拦下之后你的客户端收到什么?如果没拦住,你事后能从哪里看出来它来过?

这三个问题的答案,OpenRouter 官方文档摊在 openrouter.ai/docs/guides/features/guardrails 这一组页面里。下面沿着文档描述的路径走一遍,把途中的关键字段标出来。

闸门装在转发之前,而且只看请求这一侧

先定位介入点。《Guardrails》页写明,guardrail 是组织用来约束成员和 API key 怎么用 OpenRouter 的一层设置,Security(提示注入与越狱检测)和 Sensitive Info(敏感信息检测)都是这层设置里的选项,与预算上限、模型 allowlist、provider allowlist、ZDR 强制这些并列。

介入的具体位置,两处文档说法一致:

  • 《Guardrails》页讲 custom content filters 时写明,模式是在请求被转发给模型之前、对每一条用户消息在本地求值的;
  • 《Prompt Injection Detection》页写明,正则模式在请求转发到模型 provider 之前于本地求值。

也就是说这道闸在 OpenRouter 侧、在把请求发给上游 provider 之前,不是在模型那边做的。

比介入点更容易被忽略的是扫描范围。《Sensitive Info Guardrail》页有一句写得很直:检测运行在请求的 input(prompt)侧,扫的是 message content、tool call arguments 和 prompt strings,不扫模型的响应。这一条值得单独记住——如果你担心的是模型把内部资料吐回给用户,这道闸管不到;它管的是「什么东西被送出去」。

命中之后:三档动作,各自发生什么

《Prompt Injection Detection》页把命中后的处置写成三档,语义各不相同:

动作请求是否到达模型内容是否被改典型用途(文档写明)
flag到达原样不改只记录检测结果供观测(指标与分析事件),先在自己的真实流量上测真阳性率
redact到达命中片段被替换命中的片段替换为 [PROMPT_INJECTION],把净化后的请求转发给模型
block不到达——整条请求以 403 被拒,不会到模型

这里最容易踩的坑是 flag。文档在「Limitations」一节专门重申:flag 模式不做任何强制,被 flag 的请求会原封不动地转发给模型,检测结果只进仪表盘和分析。有人把 flag 当成「拦了但放行一部分」,那是误解——它一点都不拦。

敏感信息那一侧的替换标签不一样。《Sensitive Info Guardrail》页写明,内置预设命中并选择 redact 时,替换成带类型的占位符,例如 [EMAIL][PHONE];自定义正则命中并选择 redact 时,替换成 [REDACTED]。内置预设的 slug 与替换标签是一一对应的,API 侧用 content_filter_builtins 字段配置:

{
  "name": "PII Protection",
  "content_filter_builtins": [
    { "slug": "email", "action": "redact" },
    { "slug": "phone", "action": "redact" },
    { "slug": "ssn", "action": "block" },
    { "slug": "credit-card", "action": "block" },
    { "slug": "ip-address", "action": "redact" },
    { "slug": "person-name", "action": "redact" },
    { "slug": "address", "action": "redact" }
  ]
}

自定义正则走另一个字段 content_filters,每条支持 patternaction,外加一个可选的 label——文档写明 label 的用途是在 block 时给出更清楚的错误信息。

{
  "name": "Custom Filters",
  "content_filters": [
    { "pattern": "AKIA[0-9A-Z]{16}", "action": "block", "label": "AWS Key" },
    { "pattern": "PROJ-\\d{4,6}", "action": "redact" }
  ]
}

以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。

403 长什么样,以及怎么让它多说几句

被 block 的请求返回 HTTP 403 Forbidden。《Guardrails》页给出的响应体形如:

{
  "error": {
    "code": 403,
    "message": "Request blocked: prompt injection patterns detected",
    "metadata": {
      "patterns": ["ignore all previous instructions"]
    }
  }
}

内容过滤器那一侧的 message 格式不同,是 Request blocked by content filter: [LABEL]。文档写明 [LABEL] 的取值来源:内置预设取预设名(如 Email addressSocial Security number),自定义模式取你填的 label,没填 label 的自定义模式取 [BLOCKED],NLP 检出的实体则取实体类型(文档举的例子是 Blocked PII detected: PERSON)。

这里有个排查上很关键的分界:《Guardrails》页写明,预算上限和 allowlist 限制(指模型、provider 这类可用范围的 allowlist,不是后文那个提示注入豁免用的 allowlist)同样返回 403,但只有运行时的内容检查会带上 openrouter_metadata 的阶段细节。所以光看 403 判断不了是被内容拦了还是被额度/名单拦了——想分清,得把阶段信息要出来。

要阶段信息需要显式 opt in:加上 X-OpenRouter-Experimental-Metadata: enabled 这个 header(header 名里就带着 Experimental,文档把它归在 router metadata 下),403 响应会额外带一个 openrouter_metadata 对象,其中的 pipeline 数组会列出跑过的每一个 guardrail 阶段。文档示例里 pipeline 的一项是这样的:

{
  "type": "guardrail",
  "name": "regex_pi_detection",
  "guardrail_id": "grd_abc123",
  "guardrail_scope": "api-key",
  "summary": "Blocked: prompt injection detected (1 pattern matched)",
  "data": {
    "action": "blocked",
    "detected": true,
    "engines": ["regex"],
    "patterns": ["ignore all previous instructions"]
  }
}

(上面截取的是文档 403 示例里 pipeline 数组的一项;同级还有路由相关字段,与本篇无关就不抄了。示例中的 id 值是文档的示意值。)

guardrail_idguardrail_scope 是这段里最有用的两个字段——多层 guardrail 叠在一起时,它直接告诉你是哪一条、挂在哪一层拦的。完整的响应形状与阶段参考,文档指向 openrouter.ai/docs/guides/features/router-metadata 的 Error Responses 一节和 openrouter.ai/docs/api_reference/errors-and-debugging 的 Guardrail Errors 一节。

多条 guardrail 同时生效时,谁说了算

一个请求可能同时落在工作区默认 guardrail、成员 guardrail 和 API key guardrail 之下。《Guardrails》页把合并规则写死了:provider allowlist 和 model allowlist 取交集;ZDR 按模型分组走 OR(任一条 guardrail 对某个分组启用了就生效);Sensitive Info 取并集(所有适用 guardrail 的过滤器合并),同一实体类型或同一模式出现不同动作时 block 优先于 redact;预算则是各自独立检查。

提示注入这一侧的优先级在《Prompt Injection Detection》页单独写了一遍,顺序是 block > redact > flag——最严格的那档赢。所以排查「为什么这条请求被拦了」时,别只盯着你刚改的那一条 guardrail。

漏网的三个方向,文档都写了

正则拦不住换个写法。文档在正则模式之外补了几类规避检测,命中的判定不走同一套逻辑:

  • Typoglycemia(乱序拼写):打乱关键词中间字母、保留首尾(文档举的例子是 ignroe),系统会检查一组目标词的这类变体;
  • 编码规避:先解码 Base64 和十六进制内容(文档写明包含空格分隔的十六进制对,例如 69 67 6e 6f 72 65),再在解码后的文本里查注入关键词。这一路跑两个编码检测器:base64_encoded_injectionhex_encoded_injection
  • 字符间隔规避:形如 i g n o r e p r e v i o u s 的文本会先折叠空格做归一化,再拿全部模式重扫一遍。

正则那一侧,文档把模式按攻击手法分成九类(从直接指令覆盖、prompt 提取、角色操纵,一直到标签注入与角色伪装、控制 token 注入),每条模式都带名字、正则和说明,例如 ignore_previous_instructionsreveal_promptsystem_tag_injectioncontrol_token_injection。文档写明这些模式派生自 OWASP 的 LLM 提示注入防护 Cheat Sheet 等资料,除特别注明外大小写不敏感(dan_jailbreak 那条对 DAN 大小写敏感)。

误报这一侧:allowlist 有个反直觉的边界

做安全培训、做客服话术的应用,正文里天然就会出现「ignore previous instructions」这类句子。文档给的出口是 allowlist(openrouter.ai/docs/guides/features/guardrails/prompt-injection/allowlist),机制是先遮蔽再检测:先按大小写不敏感的方式在消息里定位 allowlist 短语,把命中片段替换成中性占位文本,然后在遮蔽后的文本上跑检测;如果动作是 redact,最终输出里会把 allowlist 短语还原回去,只有非 allowlist 的注入模式被换成 [PROMPT_INJECTION]

反直觉的地方在于它的适用范围:allowlist 只对正则模式生效。文档明说,typoglycemia 和 Base64/十六进制编码这两类规避检测不受 allowlist 影响,因为它们跑在解码或归一化后的文本上,做不了有意义的按短语豁免;一旦触发这两类检测,整条消息会被 flag 或 redact,你的 allowlist 条目一个都不管用。

另外几条边界一并记下:匹配是精确子串,大小写不敏感,不支持通配符和正则;三种动作都适用;作用范围是按账号实体而非按 guardrail——个人账号是用户自己,组织里是组织本身,且文档写明组织内只有 org admin 能查看和管理 allowlist。条数上限与单条长度上限文档给了具体数字,这类阈值改起来没有成本,这里只说存在上限、具体值以官方文档为准;被停用(toggle off)的条目不计入上限。

标了 beta 的那两个预设,行为要单独记

《Sensitive Info Guardrail》页把检测分成两种方法:正则和 NLP。正则覆盖邮箱、电话、SSN、信用卡号、IP 地址这些格式化程度高的;人名和物理地址这类靠模式匹配不可靠的走 NLP 实体识别(文档写明用的是 Presidio)。

Person Name 和 Address 这两个预设,文档明确标注为 beta。 文档同时写明两件事:一是检测准确度可能有波动,尤其是不常见的人名格式和不完整、不规范的地址;二是——这条对安全设计影响最大——如果检查超时,请求会继续放行(not blocked)。也就是说依赖这两个预设做阻断,存在一条「超时即放行」的通道。文档还写明 NLP 检测会带来与输入文本长度成正比的额外延迟,这两个预设在控制台里带「Adds latency」标记。

自定义正则也有硬约束。custom content filters 用的是 JavaScript 风格正则,字符类、量词、或、非捕获组、命名捕获组、锚点、转义序列都支持;但前瞻 (?=…) / (?!…)、后顾 (?<=…) / (?<!…)、反向引用(\1\k<name>)以及 (a+)+ 这类嵌套量词都不允许,创建和更新时会被 API 以 invalid_regex_pattern 错误拒掉。敏感信息侧的自定义模式同样做两项校验:语法必须是合法 JavaScript 正则,且不能有灾难性回溯风险(ReDoS),(a+)+(a|a)* 这类会被拒。

想用 API 改工作区默认那条

《Guardrails》页写明每个工作区有一条默认 guardrail,不需要显式分配就对该工作区的全部流量生效。文档给的定位方式是先列出来找到它——它的名字形如 Workspace <workspace-id> Default

curl https://openrouter.ai/api/v1/guardrails?workspace_id=YOUR_WORKSPACE_ID \
  -H "Authorization: Bearer YOUR_MANAGEMENT_KEY"

拿到 id 之后用 PATCH /api/v1/guardrails/{id} 更新,文档写明全部 guardrail 设置都能这样配。注意这条命令用的是 Linux/macOS shell 的反斜杠续行写法;Windows 侧,PowerShell 的续行符是反引号、CMD 是 ^,连着反斜杠原样粘进去会报错,最省事的做法是把参数拼成一行再执行(这属于通用的 shell 差异,不是官方文档内容)。示例里的占位值换成你自己的,别把密钥写进代码仓库。

该平台迭代频繁,上面涉及的字段名、错误码与响应形状都随版本变动,以官方文档最新内容为准。

最后:这道闸能挡什么,不能挡什么

文档自己在「Limitations」里把话说得很清楚,这里照实转述,不做加工:

  • 正则检测不是穷尽的,复杂或新型的注入手法可能不被捕获;
  • flag 模式不执行任何强制
  • 误报是可能的,涉及安全测试之类的正常 prompt 也可能命中模式,文档建议先用 flag 在有代表性的流量上跑,确认误报率可接受后再放开强制;
  • 把某次检测标记为 false positive 不会追溯性地放行那条请求——动作是 block 的话,原请求已经被拒了。

再叠加前面那几条:检测只跑在输入侧,不看模型响应;两个 NLP 预设是 beta 且超时会放行;allowlist 对规避检测不生效。合起来只能得出一个结论:开启 guardrail 不等于安全。它是请求链路上一层可配置的、有已知盲区的过滤,能降低已知模式的命中面、把事件收进可观测数据,但不适合当作唯一防线,也替代不了应用侧对用户输入与工具权限的设计。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

真要动手,文档给的顺序是务实的:先用 flag 收集自己流量上的命中情况,从 Logs 页面看 guardrail 事件(文档写明带 guardrail 事件的记录行会显示 shield 图标),把明显的误报短语加进 allowlist 或标记为 false positive,再决定哪些类别升到 redact、哪些升到 block。敏感信息那一侧的建议是同一个口径:先从 Redact 起步。


本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。 该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。 该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单; 价格、额度与限流的具体数值请以官方定价页与用量说明为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。