安全护栏行为
安全护栏会在 AISIX 转发受支持的网关流量之前、上游返回响应之后,或在这两处添加内容策略检查。其共用运行时控制项决定哪些路由和内容会被检查、匹配是否会阻止流量,以及调用方和操作员能够观测到什么。
内置关键词和 PII 安全护栏在网关内部评估内容。语义筛查会调用配置好的向量嵌入模型,自定义脚本则运行运维自定义的逻辑。服务提供方集成会把提取出的内容发送到审核服务,并应用其决策。选择安全护栏服务提供方比较了这些选项。
所有类别都使用本页所述的行为。适用的检查无法完成时,通常由失败设置决定会发生什么;部分流式和解码路径则有更严格或固定的处理方式。
检查位置
安全护栏的检查位置决定 AISIX 在哪里执行检查,以及阻断内容时会发生什么:
| 检查位置 | AISIX 检查内容 | 被阻断时的效果 |
|---|---|---|
input | AISIX 发送到服务提供方之前的调用方请求。 | 不会调用服务提供方。 |
output | AISIX 返回给调用方之前的响应内容。 | 调用方不会收到被阻断的响应。 |
both | 请求和响应内容。 | 在路由支持的两侧都应用同一个安全护栏。 |
输入安全护栏覆盖标准推理路由中包含文本的部分,包括 Chat Completions、Completions、Responses、Messages、Embeddings、图像生成与编辑、视频生成、音频语音及重排请求。AISIX 还会扫描可选的音频转录和翻译提示词、Realtime 文本帧、MCP tools/call 参数以及 A2A 消息文本。 相同的输入检查也覆盖 Batch 和 Fine-tuning 请求载荷,以及透传路由请求体。
输出安全护栏会扫描 Chat Completions、Completions、Responses、Messages、音频转录和翻译返回的文本。它们还会扫描 Realtime 文本帧、MCP tools/call 结果、Batch 和 Fine-tuning 响应载荷,以及透传路由响应体。A2A、Embeddings、图像生成与编辑、视频生成、音频语音、重排和 Count Tokens 只运行输入侧检查。Realtime 二进制帧会直接中继,不执行安全护栏内容检查。
在原生 /v1/responses 上,输出扫描覆盖消息文本,以及每个工具调用的名称、参数和输入。托管工具的结果(例如 mcp_call.output、File Search 结果和 Code Interpreter 日志)不会被扫描或脱敏,refusal 片段和托管工具的代码或操作文本同样不会。
在 /v1/chat/completions、/v1/messages、/v1/messages/count_tokens、/v1/responses 以及承载可识别信封的透传路由上,护栏在输入和输出两个方向都从不扫描工具调用 ID。这里的 ID 指将工具调用与其结果关联起来的 id、call_id、tool_use_id 或 tool_call_id,包括其中携带的 Gemini 思维签名。函数名称、参数和工具结果仍会被扫描。因此,只匹配工具调用 ID 内部文本的规则不会触发。
在透传路由上,安全护栏通过 passthrough_route 挂载作用域生效,并与调用方 API Key、团队和环境作用域并列。扫描文本遵循识别出的正文信封。携带可识别 LLM 信封(Chat、Completions、Responses)的请求会扫描提取出的消息或提示词文本,以及与这些字段一起转发的普通 JSON 字符串。工具调用 ID 以及安全护栏扫描范围列出的其他不透明句柄不会被扫描。提取范围包括工具调用参数以及 Anthropic Messages 流量的内容;各信封读取哪些内容,请参见安全护栏扫描范围。
如果 AISIX 识别出信封但无法解码,AISIX 不会回退到扫描原始正文。这可防止类型化的不透明媒体被重新解释为文本。对于透传路由承载的可识别 Responses 信封,输出扫描仅覆盖可识别的可见文本、refusal 和工具字段。已知媒体、推理和未知输出项仍不透明。格式错误的可识别缓冲输出无法评估,并按已配置的失败策略处理。未知的 Responses 流事件仍不透明。
对于不透明的缓冲流量,如果正文是 JSON,则扫描已解码的 JSON 字符串值;否则扫描文本正文。不透明 SSE 流只能扫描单独的 JSON 字符串。其他承载内容的载荷形态无法评估,因为 AISIX 无法可靠地将其文本与之前的流内容关联。
请求在转发前扫描。阻断判定会返回 422,并且不会接触上游。内置 pii 安全护栏无法把掩码结果写回服务提供方原生正文,因此仅配置 mask 动作时,匹配内容会不经掩码原样转发,并按无法执行脱敏的接入面所述记录。如果匹配内容绝不能到达上游,请使用 block 动作。Presidio、Lakera 等安全护栏类型则会在可掩码的结果没有回写通道时给出阻断判定。有一种情况会覆盖上述全部规则。Anthropic thinking 块内部的掩码命中,对所有安全护栏类型都会原样转发,因为该块是带签名的。参见推理与思考内容。
非流式响应会在中继前完整缓冲,AISIX 会扫描其符合条件的内容。阻断判定会返回 422,并扣下上游响应体。流式响应按护栏链的流式输出策略增量中继。hold-back 护栏会按窗口或全量缓冲帧,只有扫描文本通过后才释放;阻断判定会以一个终止性 SSE error 帧结束流,并丢弃被扣住的帧。各接口面使用各自协议的错误形态,并没有一个统一的帧。Chat Completions 携带 content_filter。/v1/messages 使用嵌套的 Anthropic 形态,错误类型取 Anthropic 合法值,error.code 为 content_filter 或 guardrail_unavailable。透传路由跟随其所中继流的协议:Anthropic Messages 流使用 Anthropic 形态,其他流使用 Chat Completions 形态。/v1/responses 则按上游区分:上游原生提供 Responses API 时,被保留的流会以 422 而不是一个帧被拒绝;AISIX 为不提供该 API 的服务提供方做桥接时,拒绝才是那个扁平的 Responses 错误事件。参见安全护栏拒绝。
无法执行脱敏的接入面
以下接入面中继的内容,AISIX 没有改写通道:
- 透传路由的请求和响应。
/a2a消息。- Batch 和 Fine-tuning 的请求体,以及为其返回的上游正文。
/v1/realtime双向的文本帧。/v1/videos的提示词。
pii 脱敏规则在这些接入面上命中时,内容不经修改放行,AISIX 会在用量记录中记录这次命中:
| 执行模式 | AISIX 记录的内容 |
|---|---|
block | 一条 action 为 mask_unsupported 的执行命中,附带检测器计数。guardrail_blocked 保持为 false;由于没有内容被脱敏,redacted_entity_counts 不计入该命中。 |
monitor | 一条 action 为 would_mask_unsupported 的监控命中,取代 would_mask,因为强制执行该规则同样不会脱敏任何内容。 |
如果匹配内容绝不能通过,请使用 block 动作。Presidio、Lakera、AWS Bedrock、阿里云 AI 安全护栏和自定义脚本不会在这些接入面上放行脱敏结果,而是拒绝该内容。其他运行安全护栏的接入面会改写需要脱敏的内容,本页其他部分所述的例外除外。
Files 路由不做筛查
安全护栏完全不作用于 Files API。POST /v1/files、GET /v1/files、GET /v1/files/{id}、DELETE /v1/files/{id} 和 GET /v1/files/{id}/content 既不运行输入侧检查也不运行输出侧检查,无论请求作用域内挂了什么安全护栏、fail_open 取什么值。上传的文件会按调用方书写的样子原样离开网关,下载的文件也会按服务提供方存储的样子原样到达调用方。同一段文本放在对话消息里会被关键词、PII 或远程安全护栏拒绝,放在这里则根本不会被看到。
遥测不会反映这个缺口。 Files 的用量事件不会为这次缺失的安全护栏检查留下任何标记:安全护栏证据字段全是空的。因此即使在环境上挂了一条关闭式失败的安全护栏,也不会有任何信号提示这些路由没有被覆盖。Files 事件上空白的安全护栏证据部分并不能证明安全护栏覆盖到了它。如果上传内容必须经过筛查,请在它到达网关之前完成。
/v1/batches 和 /v1/fine_tuning/jobs 不受影响。它们的请求体是调用方提交的结构化 JSON,而不是上传的二进制块,这些请求体和对应的响应仍然会被扫描。
推理与思考内容
推理内容按方向划分,两侧并不对称。制定 PII 或关键词策略时请围绕这条边界来规划,不要假定安全护栏覆盖了具备推理能力的模型所接触的一切。
调用方发来的推理内容在扫描范围内。 被回放的推理内容和其他文本一样,都是调用方提供、进入模型的文本,因此会被扫描,并在相同的位置被脱敏:
| 路由 | 扫描并脱敏的位置 |
|---|---|
/v1/responses | reasoning item 的 content[].text 及其 summary[].text |
/v1/chat/completions | assistant 轮次的 reasoning_content |
/v1/messages | thinking 块的文本——会被扫描,但请看下面的例外 |
Anthropic thinking 块永远不会被改写。 thinking 块内部的 mask 动作命中会被原样转发。对所有安全护栏类型都是如此,而不仅仅是内置的 pii——因为改写该块会让服务提供方为它签发的签名失效。
block 动作的规则仍然会在 thinking 块上生效,因此如果这类内容绝不能到达上游,请使用 block 动作。redacted_thinking 块则是另一种情况:它只携带服务提供方的加密数据,完全不向扫描贡献文本,因此没有任何规则能在其中命中,它同样会被原样中继。
模型生成的推理内容不在扫描范围内。 它在任何路由上都不会被扫描,也不会被脱敏——/v1/chat/completions、/v1/messages、/v1/responses 都是如此,流式和缓冲式都是如此。
模型生成的推理内容此前会在三条路径上被顺带扫描到:缓冲式的 /v1/messages、/v1/responses,以及承载这两类信封的透传路由。现在不会了。如果一条 block 规则的关键词只出现在模型生成的推理内容里、而不出现在模型返回的答案中,那么它此前会阻断这些响应,现在不再阻断。调用方可见的答案的扫描范围与此前完全一致。
如果你此前依赖了这一行为,请把该规则移到输入检查位置。否则请接受「模型生成的推理内容位于输出安全护栏范围之外」这一事实。
有一个例外同样不值得依赖。当请求不匹配可识别的信封时,缓冲 JSON 响应会作为原始流量处理,因此其已解码的字符串值可能包含推理内容。这是原始内容扫描,而不是感知推理的处理。格式错误的可识别响应不会回退到该路径。在原始 SSE 流中,只有单独的 JSON 字符串可被扫描。
加密的推理内容不会被脱敏
在 /v1/responses 上,一个 reasoning item 可能携带 encrypted_content 字段。该字段存放的是服务提供方的加密数据。AISIX 会原样转发它,从不扫描,也从不改写。
因此对可读的 summary 做脱敏,并不会把敏感文本从服务提供方收到的那份密文副本中去掉。调用方能看到的明文被脱敏了,服务提供方拿到的仍然是该 item 完整的加密载荷。请把 reasoning item 的 encrypted_content 视为会以未脱敏形式离开你的边界的内容;如果这不可接受,请使用 block 动作而不是 mask 动作。
keyword 和 PII 安全护栏如何读取结构化内容
在 /v1/chat/completions、/v1/completions、/v1/messages、/v1/messages/count_tokens、/v1/responses 和 MCP tools/call 上,内置 keyword 和 pii 安全护栏逐段判断内容,所判断的片段与脱敏改写的片段相同,而不是把整个请求或响应拼接成一段文本来判断。因此,阻断规则、监控模式下的 would_mask 与强制执行的脱敏,对命中内容的判断始终一致。
以下每一项都是单独的片段:
- 一条消息或一个内容块的文本。
- 工具调用参数、工具输入、调用方发送的工具结果,以及 MCP
tools/call结果(包括 MCPstructuredContent)中的每个字符串值。 - 这些结构化载荷中的每个对象键和每个数字。AISIX 会判断它们,但改写它们会改变载荷的契约,因此
pii脱敏规则命中其中之一时会阻断请求或响应,而不是脱敏。 - 工具名称。AISIX 会判断它,但从不改写:阻断规则会拒绝,脱敏命中则原样转发。
这决定了模式能匹配什么。^\d{4}$ 这样的锚定模式匹配一个完整片段,因此能匹配参数 {"pin":1234} 中的数字。模式不能跨片段匹配:不能跨两条消息,不能跨键和值,也不能跨工具名称和参数。请检查那些依赖跨越这些边界匹配的规则。透传路由不适用这一模型,它们把提取出的文本作为一整段扫描。
在原生 /v1/messages 上,keyword 和 pii 不会读取非流式响应中的服务端工具块(server_tool_use、mcp_tool_use 和 *_tool_result)。流式响应会对每个发出 input_json_delta 的块脱敏该内容,但其中的工具结果块不会被扫描或脱敏。
同一条消息中的两个文本块是两个片段,这在 /v1/chat/completions 和 /v1/messages 上相同。被拆分到两个文本块中的违禁词或电话号码(例如 ["call 138001", "38000"])既不会被阻断,也不会被脱敏。当这样的 Chat 请求发往只接收纯消息文本的服务提供方时,AISIX 先逐块脱敏再拼接各块,因此该服务提供方收到的文本与接收内容块的服务提供方相同。用量事件中的 redacted_entity_counts 对每个被脱敏的实体只计一次。
选择输入消息窗口
安全护栏的 input_messages 设置会选择请求中可供输入检查使用的部分。它不决定安全护栏会读取该窗口内的哪些角色。有些类型随后还会应用自己的角色选择器,例如语义筛查和 Azure 文本审核的 text_source。
input_messages | 可供输入检查使用的窗口 |
|---|---|
all | 整个请求:system、developer、user、assistant 和 tool 消息。这是默认值。 |
latest_turn | 最后一条 assistant 消息之后的消息,并排除 system 和 developer 消息:即当前这一轮的 user 消息,以及回应它们的工具结果。 |
IDE 和 Agent 类客户端每次请求都会把整段会话重发一遍。在 all 下,旧消息仍可供安全护栏使用,因此可能在后续请求中继续命中。latest_turn 会在安全护栏应用该 类型特定的文本选择规则之前,移除模型已回答的历史记录。
在 latest_turn 下,窗口内的工具结果是当前这一轮产生的那些:/v1/chat/completions 上的 tool 消息、/v1/messages 上的 tool_result 块,以及 /v1/responses 上的 function_call_output item。
在 Responses API 上,AISIX 将 assistant 消息、function_call、custom_tool_call 和 reasoning item 视为 assistant 轮次,并将 function_call_output 和 custom_tool_call_output item 视为工具结果。当工具调用 item 位于所选窗口内时,其名称、参数或输入可供输入检查使用。会读取该 item 的脱敏安全护栏可以改写参数或自定义工具输入,但不会改写工具名称这一结构字段。
“最后一条 assistant 消息”是在 system 和 developer 以外的消息上判定的。因此结尾处的 assistant 消息属于预填(prefill)——调用方写给模型续写的文本——它属于当前这一轮,而不是这一轮的边界。完全没有 assistant 消息的请求,其窗口是除 system 和 developer 消息外的整个请求。
developer 消息是 OpenAI 对 system 消息的新叫法,所有安全护栏都把它当作 system 消息处理。在 latest_turn 下,无论它出现在哪里都位于窗口之外,因此既不会被检查,也不会被脱敏;在 all 下,它可供输入检查使用,是否会被读取取决于该安全护栏类型的文本选择规则。Responses API 上的 developer 输入 item 同样如此。
该设置对所有安全护栏类型生效,且只作用于输入检查:hook_point: input,以及 both 的输入侧。它不影响输出检查,输出检查始终读取完整响应。在 AISIX Cloud 中,给 hook_point 为 output 的安全护栏设置 latest_turn 会以 400 INVALID_REQUEST 拒绝,而不是存下一个永远不会生效的设置。
安全护栏只能读取和改写自己窗口内的消息,而且其类型可能进一步缩小该集合。在 latest_turn 下,做脱敏的安全护栏会让会话历史保持客户端发送时的原样。如果某个安全护栏的职责就是对整段会话做 PII 脱敏,请让它保持 all。
无论哪种取值,被跳过的历史消息仍然会原样转发给模型。latest_turn 改变的是 AISIX 向输入检查提供什么,而不是 AISIX 发送什么。
有一条边界值得明说:如果客户端把一条被阻断的消息保留在历史里并重新发送,而中间没有 assistant 的回复,那么这条消息仍然位于当前轮次窗口中,读取该角色的安全护栏仍可能检查它。
可以在控制台的安全护栏表单中配置它——输入扫描范围(整个请求 或 仅最新一轮)会在 input 和 both 检查位置下出现。通过 Admin API 时,请在安全护栏上设置 input_messages。在 resources.yaml 中,请更新 guardrails 中对应的条目:
guardrails:
- name: block-secrets
kind: keyword
hook_point: input
input_messages: latest_turn
patterns:
- kind: literal
value: internal-project-codename
安全护栏匹配
选择钩子点后,需要确定安全护栏的作用范围:
- 在 AISIX Cloud 中,创建安全护栏并把它附加到环境或选定的模型别名、MCP 服务器、调用方 API Key 或团队。Attachment 允许一个定义以明确优先级应用到多个 Scope。
- 对于开源 AISIX 网关,在
resources.yaml中声明安全护栏,并为它添加一条guardrail_attachments条目。两种来源判定作用范围的方式一致,因此文件里声明了、但没有任何 Attachment 的安全护栏不会检查任何流量。
安全护栏的作用范围完全由它的 Attachment 决定。没有任何 Attachment 的安全护栏在任何地方都不生效,而这是一个合法状态、不是错误——它原本挂靠的模型、API Key 或团队可能已被删除,删除时会一并移除指向它们的 Attachment。控制台会把这样的安全护栏标记为未附加,网关也会为它记录一条告警。
由此有两点值得提前规划。删除最后一个 Attachment 不会放大安全护栏,而是让它静默——用这个动作让规则下线,而 enabled 用来表示规则是否存在。另外,通过 Admin API 创建的安全护栏在你 POST 一条 Attachment 之前不挂靠任何范围;控制台在创建表单里选定范围时会替你写入,直接调用 API 则不会。
当多个安全护栏同时适用于同一个请求时,AISIX 会把它们作为一条链路进行评估。任意一个安全护栏给出阻断判定,都会阻断请求或响应。
模型别名 Scope 和 MCP 服务器 Scope 选择的是单次请求不会同时具备的两个维度:MCP 工具调用不会解析出模型,模型请求也不会路由到 MCP 服务器。因此模型 Scope 的安全护栏永远不会检查 MCP 工具调用,MCP 服务器 Scope 的安全护栏也永远不会检查模型流量。若要同时覆盖两者,请把安全护栏附加到环境 Scope,或者两种 Attachment 各加一条。
A2A 调用既不解析出模型,也不解析出 MCP 服务器。只有环境、调用方 API Key 和团队 Attachment 能覆盖它的消息文本,模型和 MCP 服务器 Attachment 覆盖不到。
如果同一个安全护栏通过 AISIX Cloud 中配置的多个匹配 Scope 附加到请求链路中,AISIX 只保留一份该安全护栏。优先级最高的 Attachment 生效;优先级相同时,Scope 越具体优先级越高,顺序为:调用方 API Key、团队、模型别名或 MCP 服务器、环境。模型别名与 MCP 服务器同级,因为单次请求不可能同时匹配这两者。
安全护栏作用范围由调用方寻址的模型决定
模型 Attachment 匹配的是调用方寻址的那个模型。AISIX 在挑选目标之前把该模型解析一次,并在整个请求中沿用它。当调用方寻址的模型是多目标路由、语义路由或合议模型时,被寻址的模型就是这个组,因此在模型这一维度上只有该组自身的 Attachment 会运行。附加到其成员模型上的安全护栏不会在该请求上运行,它们只在请求直接寻址该成员时运行。这只收窄模型这一个维度:环境、调用方 API Key 和团队 Attachment 匹配组请求的方式与匹配其他请求完全一致。
举例来说,把一个关键词安全护栏附加到成员模型 m 上,并把 m 放进组 g。发往 m 的请求会被该安全护栏筛查;发往 g 的请求不会,即使 g 把这个请求分发给了 m。若要筛查经由该组进入的流量,请把安全护栏附加到 g 上。
通配符别名不是组,行为也不同。通配符模型本身就是调用方寻址的那个条目,因此附加到 openai/* 上的安全护栏会覆盖该模型服务的每一个请求,包括针对 openai/gpt-4o 的请求。精确别名优先于通配符,因此之后再新建一个 openai/gpt-4o 模型,会把这个名字从通配符的覆盖范围里拿走:此时请求寻址的是这个新别名,只有附加到它上面的安全护栏会运行。
执行模式
安全护栏的 enforcement_mode 决定 AISIX 在安全护栏检测到匹配内容后如何处理。
请根据策略是否已准备好影响流量来选择模式:
| 模式 | 策略命中时 | 适用场景 |
|---|---|---|
block(默认) | 输入侧命中会在模型服务提供方调用前停止请求;输出侧命中会阻止调用方收到响应。脱敏规则会改写受支持的内容。 | 强制执行已经验证的策略。 |
monitor | AISIX 允许请求或响应原样通过,并记录安全护栏本应阻断或脱敏的结果。 | 在策略影响请求前,使用真实流量衡量误报并调优新策略。 |
基于检测的安全护栏可能产生误报和漏报。监控模式可让你先把策略命中与有代表性的流量对照,再允许策略影响请求。
AISIX 会把每次观察作为监控命中记录在该请求的用量记录中。一条命中会携带安全护栏名称、命中的检查位置,以及该安全护栏本应执行的动作:would_block 附带由代码生成的安全护栏种类和结果摘要,would_mask 附带安全的命中次数,would_mask_unsupported 同样附带安全的命中次数,表示该接入面本就无法执行脱敏。参见无法执行脱敏的接入面。如果检查不可用,would_block 摘要还会包含由代码生成且取值范围有限的失败标签,例如 custom_timeout;它不会包含脚本提供的原因或被检查的内容。自定义脚本固定使用计数名称 custom,值为本应发生变化的文本片段数;脚本返回的计数名称和值会被忽略。
AISIX Cloud 会原样存储用量记录详情。因此,由旧版网关创建的记录可能保留脚本提供的旧版原因、计数名称和值,其中也可能包含由请求内容或密钥派生的值。升级网关只会保护新记录,不会改写历史记录;请相应限制对旧版用量数据的访问。
使用这些记录观察命中率并调优策略,然后再切换到 block。对于本应阻断的观察,AISIX 还会在网关日志中以 info 级别记录以下内容:
guardrail in monitor mode observed a violation; not blocking (enforcement_mode=monitor)
流式输出
当强制执行的安全护栏覆盖 output hook 时,AISIX 会先保留流式响应内容,直到相关检查可以执行。根据安全护栏类型,AISIX 可能保留完整响应,也可能检查缓冲窗口。这样可以避免被阻断或未脱敏的内容在安全护 栏判定前到达调用方,也能检测跨越流式分片的文本片段。
监控模式下的安全护栏不会使流式输出延迟。当所有输出安全护栏都以监控模式运行时,AISIX 会在流到达时转发。流结束后,AISIX 对完整的生成输出执行检查并记录所有匹配。该扫描不设大小截断,因此长响应中任何位置的匹配都会被记录,包括流式工具调用参数、/v1/responses 流,以及流式音频转写的转写文本。
下方缓冲区上限不适用于仅包含监控模式的输出链路。AISIX 会交付并观测超大响应,而不是拒绝它。如果任何强制执行的安全护栏也覆盖输出钩子,AISIX 会保留流,以便强制执行链路在交付前检查。
暴露流式缓冲设置的安全护栏类型使用以下默认值:
{
"max_buffer_bytes": 262144,
"on_buffer_exceeded": "fail_closed"
}
对于 AISIX Cloud,请把这些字段添加到安全护栏的 config 对象中。对于开源 AISIX 网关,请直接添加到 resources.yaml 的安全护栏条目中。fail_closed 会阻断过大的输出。fail_open 会释放 AISIX 无法完整检查或改写的输出,因此只有在可用性比严格输出治理更重要时才使用。未暴露这些字段的安全护栏类型使用内置上限,并在溢出时按关闭式失败处理。
max_buffer_bytes 统计被保留的模型生成内容:助手文本、推理内容和工具调用参数,不计入 SSE 和 JSON 帧结构。因此同一响应在 Chat Completions、/v1/messages、/v1/responses 和透传路由上会在同一位置触及上限。在 AISIX 识别不出 Chat、Completions 或 Responses 的透传流上,每一帧的整个 data: 载荷都会计入。
不携带生成内容的帧(例如 ping、keep-alive 和空 delta)不计入上述字节数,但仍会占用内存。因此每个被保留的流还另有原始字节上限,为 max_buffer_bytes 的 128 倍,默认即 32 MiB。超过任一上限都属于同一种溢出,均按 on_buffer_exceeded 处理。普通的逐 Token 流的帧结构小于该比例,会先触及内容上限;例如 OpenAI 输出中文文本的流,其帧结构约为内容的 75 倍。
链路采用保留整个响应的输出安全护栏中最小的 max_buffer_bytes;只要其中任一安全护栏失败关闭,溢出时就会拒绝。哪些安全护栏参与计算取决于其流式处理模式,请参阅 Messages 与 Responses 上的窗口模式。设为 fail_open 时,保留的内容不经扫描和脱敏直接放行,响应的剩余部分不做输出扫描地流式转发,用量记录会携带绕过原因 output_buffer_exceeded。设为 fail_closed 时,拒绝会记录一条 action 为 blocked_buffer_exceeded 的执行命中,并指明触及上限的安全护栏。调大该安全护栏的 max_buffer_bytes 即可放行更大的响应。
Messages 与 Responses 上的窗口模式
Azure 文本审核、阿里云内容审核、阿里云 AI 安全护栏和 custom 安全护栏默认使用 window 模式。在 /v1/chat/completions 和透传路由上,window 模式会在每个窗口扫描通过后逐窗口释放流。/v1/messages 和 /v1/responses 无法逐窗口释放流,因此只要输出链路能够拦截或脱敏,它们就会保留整个响应,并在扫描完成后才交付。调用方因此会比逐 Token 中继更晚收到第一个字节。这一行为同时适用于 window 模式和 buffer_full 模式,也正是它让 custom 输出安全护栏能够判定流式 /v1/messages 响应。仅含监控模式的链路仍会实时中继流,并在流结束时扫描一次。
当流被整体保留,且链路中没有任何输出安全护栏使用 buffer_full 时,由 window 模式安全护栏自身的 max_buffer_bytes 和 on_buffer_exceeded 决定保留行为。多个此类安全护栏的合并方式与 buffer_full 安全护栏相同:取最小的上限;除非每一个都设置为失败开放,否则溢出时失败关闭。这两个字段按安全护栏分别设置:在 AISIX Cloud 中放在 config 内,在 resources.yaml 中直接写在安全护栏条目上。
只要链路中有任何 buffer_full 输出安全护栏,它在所有路由上的上限和溢出策略都只由其 buffer_full 安全护栏决定,同一链路中 window 模式安全护栏的设置既不会收紧也不会放宽它。两种情况下,blocked_buffer_exceeded 都会指明决定链路上限的那个安全护栏。
在 /v1/chat/completions 上,如果链路的输出安全护栏全部使用 window 模式,为扫描而收集的流式工具调用名称和参数同样受该合并后上限约束。收集的内容超过上限即属于溢出,按 on_buffer_exceeded 处理。
AISIX 无法扫描的帧
本节只适用于强制执行的输出安全护栏正在保留流的情况。没有这类安全护栏时,/v1/responses 和 /v1/messages 会原样中继上游帧。
AISIX 会在检查或脱敏前整理缓冲的流,使两项操作收到同一组可读帧。data: 负载为单个 JSON 文档、空值或 [DONE] 结束标记时,该帧可读。
| 上游帧 | AISIX 的处理方式 |
|---|---|
一个帧中有多行 data: | 按照 server-sent events 规范用换行符拼接,再扫描和脱敏完整负载。 |
| 没有终止符的最后一个帧 | 当负载可读或不含可扫描内容时,补全、处理并交付该帧。 |
| 负载不是单个 JSON 文档的已终止帧 | 扫描原始文本中的阻断命中。如果文本通过检查,AISIX 仍会丢弃该帧,因为无法安全写回脱敏结果。 |
| 负载不可读且未终止的最后一个帧 | 在检查运行前丢弃。其文本既不放行也不扫描。 |
注释、保活、空负载或 [DONE] | 保留,因为其中没有可扫描的内容。 |
AISIX 丢弃内容时,调用方会收到缺少帧的流。网关会写入 warn 日志,记录检查位置、原因以及丢弃的帧数和字节数。
如果移除后没有留下任何可扫描内容,AISIX 会拒绝该响应,而不是返回空的 200。/v1/messages 上被保留的流累计超过 1 MiB 仍没有帧终止符时,也会发生同样的拒绝。失败标记为 unscannable_body;触及保留缓冲区上限则使用 output_buffer_exceeded。在 /v1/responses 上,该拒绝为 422:
{
"error": {
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)",
"type": "content_filter",
"code": "guardrail_unavailable"
}
}
在 /v1/messages 上,响应已经作为流开始返回,因此同样的拒绝会以终止性的 SSE error 事件送达,携带相同的消息。该事件使用 Anthropic 信封,因此 error.type 为 invalid_request_error,并携带同样的 guardrail_unavailable 代码:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "guardrail_unavailable",
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)"
}
}
两种情况下该请求都会被记为安全护栏阻断。这些拒绝不受安全护栏失败策略设置影响;参见保留流时的故障。
AISIX 可以保留它没有建模的合法 JSON 事件。该帧能够解析,但扫描或脱敏环节不会从中提取文本,因此这些字节会原样到达调用方。请把缓冲式输出安全护栏理解为对 AISIX 能识别的响应文本进行强制治理,而不是证明每个被中继的字节都经 过了检查。
相邻端点对不可读流式内容的处理方式不同:
| 端点 | 行为 |
|---|---|
/v1/chat/completions | 从已解码事件重建每个输出 chunk,因此解码器无法读取的内容会被丢弃,而不是未经扫描地中继。如果上游对流式请求返回普通 JSON,解码器不会产生事件;配置的流式读取预算也可能触发故障转移并最终返回错误。 |
/v1/responses 和 /v1/messages | 根据上游实际响应类型处理。对流式请求返回的普通 JSON 会进入缓冲扫描和脱敏路径。 |
| 透传路由 | 缓冲响应会遵循可识别的协议字段,对未识别的流量则遵循不透明内容规则。可识别的流会扫描其协议载体。不透明 SSE 流只扫描单独的 JSON 字符串;其他承载内容的载荷形态无法评估。透传安全护栏只返回判定,不会改写响应,因此被放行的内容会原样中继。 |
安全护栏无法完成检查时
有两种情况会导致安全护栏检查无法完成:
| 故障 | 受影响的安全护栏 | 示例 |
|---|---|---|
| 筛查后端没有返回判定 | 调用远程服务的安全护栏、语义筛查和自定义脚本 | 连接或服务提供方错误、限流、超时、脚本异常或脚本判定无法读取。 |
| AISIX 无法把送达的内容交 给安全护栏 | 所有安全护栏类型,包括 keyword 和 pii | 无法读取的 Anthropic 请求、无效的 MCP 工具结果,或包含无效 UTF-8 的纯文本音频转录。 |
远程安全护栏包括 AWS Bedrock Guardrails、Azure AI Content Safety 和文本审核、阿里云内容安全和 AI 安全护栏、Lakera Guard、OpenAI Moderation 以及 Presidio。
适用的失败策略会管辖这两种故障。内置关键词和 PII 安全护栏不会调用后端,但 AISIX 无法向它们提供内容时,其失败策略仍然决定处理结果。
fail_open 现在对 keyword 和 pii 生效早期版本的网关对这两个类型完全忽略 fail_open。如果某条 keyword 或 pii 安全护栏携带 fail_open: true——而设置它的时候该字段还什么都不做——那么网关一升级,它就会停止拒绝 AISIX 读不懂的请求体,无需任何运维操作,也不需要改动控制面。请在升级前而不是升级后检查这些配置行。
各设置分别管哪个检查侧
| 设置 | 检查侧 | 适用类型 | 默认值 |
|---|---|---|---|
fail_open | 输入侧 | 所有类型 | false |
output_fail_open | 输出侧 | 除 keyword 和 pii 外的所有类型 | false |
fail_open | 输出侧 | 仅 keyword 和 pii | false |
会调用外部服务的类型带有自己的 output_fail_open 来管输出侧,于是 fail_open 只管输入侧。keyword 和 pii 没有 output_fail_open——它们从不调用外部服务——因此这一个取值同时管它们的两个检查侧;给这两个类型传 output_fail_open 会被拒绝。
在阻断模式下,true 会放行安全护栏无法检查的流量并记录绕过;false 会拒绝流量。因此两侧默认都是失败关闭。监控模式和保留流存在下文所述的更窄例外。
在 AISIX Cloud 中,fail_open 是安全护栏上的顶层字段,所有类型的创建和编辑表单上都有该选项;output_fail_open 位于安全护栏的 config 对象内,控制台表单和 Admin API 对所有拥有该字段的类型都提供它。在开源 AISIX 网关中,两者都直接写在 resources.yaml 的安全护栏条目上。