跳转到主要内容
你启用了一条 response 执行面 规则——对你的 模型发出的工具调用加 denysanitize——而你的智能体用 "stream": true 调用网关。真正重要的问题是:一个流式响应能在防火墙判定之前泄露一个被 拦截的工具调用吗? 它不能,而本页解释那个让这件事成真的机制,这样你就能 推理延迟以及你客户端收到的块。 这是对 SSE 行为的一次聚焦观察。关于判定本身参见 判定;关于规则语法参见 规则参考

1. 流式防火墙 SSE 问题

一个非流式响应是一个 JSON 体——防火墙看到整个东西、评估 tool_calls、 返回清理后的结果。一个则不同:一个模型把一次工具调用作为数十个 tool_call 增量跨许多 SSE 帧发出,而一旦一个帧被转发,你的智能体就 已经拿到了它——你发出去的 token 无法收回。评估太早,你就没有完整的调用 (名称 + 完整参数)可判断;边走边转发,一个 deny 就已经太晚了。 网关用一个简单的、可观察的约定解决这个问题:

内容实时流式传输

正常的文本和推理增量原封不动、实时通过——你的用户阅读的 token 上零 增加延迟。

工具调用帧被挂起

任何携带一个 tool_call(或遗留 function_call)增量的帧都从实时流中 被扣住,直到该调用完整并被评估。
防火墙是一道安全门,所以它解析每一个帧。 它不会从原始字节猜测一个帧 是仅内容的——一个 JSON 转义的 tool_calls 成员没有可匹配的字面子串,所以 一个子串捷径会转发一个未评估的工具调用。SSE 帧很小;这道门解析每一个。

2. 挂起-组装-评估序列

对于一个 response 执行面策略处于活跃状态的流式 chat-completions 响应,上游 发出的每个帧走两条路径之一:
立即逐字节流向你的客户端。这些从不携带一个工具调用,所以防火墙没有 东西可决定。
从实时流中缓冲出来。一个工具回合的收尾 finish_reason 帧与它一起被 挂起,因为提早发出它会在防火墙作出裁定之前告诉你的客户端该回合已结束。
流结束时,网关把挂起的帧组装成完整的工具调用(拼接每个调用流式传输 的 arguments 片段),在 response 执行面上对照你的策略评估每一个——与 非流式路径相同的判定和规则语义——并只发出幸存者:
挂起调用的判定你的客户端收到什么
allow / audit原始挂起帧,原封不动——一次延迟的通过,而非一个重新批处理的块。
sanitize参数被重写(匹配到的密钥/PII 被替换为一个带类型的令牌)的调用,重新发出。
deny该调用被丢弃。如果它是该回合唯一的调用,则该回合以 finish_reason: "stop" 收尾——该流看起来就像模型没有做出工具调用。
如果没有任何东西匹配,你只为工具调用帧付出缓冲延迟——内容已经实时流式 传输。防火墙只在它实际行动时(一次拒绝或一次脱敏)重建帧;一次干净的 允许转发你上游的确切字节。

3. 一个具体示例

一条带有针对 *.deletedeny 规则的 response 策略(在控制台规则编辑器 中编写它),以及一个其模型决定同时调用 db.querydb.delete 的流式 请求:
SSE timeline (what your agent receives)
───────────────────────────────────────
data: {"choices":[{"delta":{"content":"Looking that up…"}}]}   ← live
data: {"choices":[{"delta":{"content":" one moment."}}]}        ← live
                                                                ← db.query + db.delete
                                                                  tool_call frames HELD
─── end of stream ───
data: {"choices":[{"delta":{"role":"assistant",
        "tool_calls":[{"index":0,"function":{"name":"db.query",…}}]}}]}
data: {"choices":[{"finish_reason":"tool_calls"}]}
你的智能体实时读取 assistant 文本,然后只收到 db.query——db.delete 被组装、评估、拒绝,且从未被发出。幸存的调用从 0 被重新索引,而被拒绝 调用的防火墙事件带着触发的规则落入你的 事件日志
先在 影子模式 下上线一条流式 response 策略。在影子模式下,每个执行性判定都被降级为 audit(原因前缀为 [shadow] would …),且所有工具调用帧都通过——所以你能在它开始丢弃 调用之前,对照真实的流式流量确认该策略匹配你预期的内容。

4. Inbound 拦截在流开始之前短路

挂起帧的舞蹈只针对 response 执行面——模型发出的调用。一个 inbound 拒绝(一个智能体声明的工具) 在上游模型调用之前触发,所以一个触发了一条 inbound 规则的流式请求根本 不会打开一个 SSE 流:它返回一个普通的 HTTP 400,带有错误码 firewall_blocked,标记为 skip-retry。 没有帧,没有挂起窗口——该拦截像任何非流式错误一样落地。

5. 同一个流上的防护栏

一个流式响应可以同时携带一个 防护栏 输出策略一个 防火墙 response 策略。它们作用于不同的东西——防护栏筛查模型流式传输的 文本;防火墙治理工具调用——而它们组合:
  • 输出防护栏拦截(流式): 输出扫描器在一条规则触发那一刻切断流,转发 一个单一的通用替换块——[Response blocked by content policy.],带 finish_reason: "content_filter"——然后停止。该消息有意通用(无规则 类别),这样一个探测者无法枚举你的策略。当这发生时一个进行中的防火墙 挂起会被丢弃,所以一个被扣住的工具调用无法在拦截之后溜出。
  • 输出防护栏 mask(流式): 在模型上线之前对请求做 mask;对流式 输出的实时带内 mask 在路线图上。在一个流上,一条 mask 规则记录该匹配但 当前转发原始块——编写它时要知道脱敏尚未在线上被重写。输出拦截在流上 完全执行。
本页描述 OpenAI chat-completions SSE 形态。 同一个挂起-评估-发出约定按 格式接线——原生 Anthropic Messages、Gemini、xAI 和 OpenAI Responses 流各自 以它们自己的事件形态携带它——所以无论哪个提供商服务该请求,客户可观察的 行为都相同。

6. 这对你的客户端意味着什么

挂起帧模型的几个实际后果:
一个唯一的工具调用被拒绝的回合以 finish_reason: "stop" 而非 "tool_calls" 收尾——对你的智能体来说它读作”模型选择不调用一个工具”。 一个部分调用幸存的回合以 "tool_calls" 收尾,只携带幸存者。
当一个上游把 token usage 捆绑到防火墙挂起的同一个终端块上时,网关把 它重新附加到最终重建的帧——一个请求了流式 usage 的客户端仍然拿到它。
如果模型在同一个帧中发出了内容一个工具调用,即便该工具调用被剥除, 该内容也会被恢复并重新发出——拦截一个调用绝不会丢弃你的 assistant 文本。
你不必让一个流选择加入这其中任何东西。把一条策略绑定到密钥(或设置一个 工作区默认)并像以前一样继续流式传输——执行在网关上。

接下来去哪里

执行面与执行面

inbound、response、mcp、egress——每条规则在哪里评估。

判定

allow、audit、deny、sanitize、pending_approval、cap_cost。

脱敏参数

从一次工具调用的参数中脱敏密钥——仅参数层。

影子模式

在你衡量影响时把执行性判定降级为审计。
关于这部分在请求路径中的位置,参见 OrcaRouter 如何检查执行路径延迟。关于 response 执行面执行所遏制的威胁,参见 危险工具调用数据外泄。关于完整的规则语法, 参见 防火墙规则参考