Author Avatar

Higress 接入 Qwen3Guard:不改一行业务代码,把 AI 内容安全做进网关主链路

阿里云高级工程师
2026-08-11
Category: article
Higress 接入 Qwen3Guard:不改一行业务代码,把 AI 内容安全做进网关主链路

作者:正己

生成式 AI 正在跨越”能不能用”的门槛,走向”如何规模化地安全使用”。

Higress 项目现已以 Wasm 插件形式接入 Qwen3Guard-Gen。三句话说明它做了什么:

  • 业务零改造:不修改应用代码,不改动上游模型服务,应用继续使用熟悉的 Chat Completions 协议;
  • 输入、输出、流式全覆盖:在请求进入模型前审核用户输入,在模型返回后审核非流式 JSON 或 SSE 流式输出;
  • 安全模型自托管:Qwen3Guard 按官方方式独立部署和扩缩容,无需依赖任何云端内容安全服务,风险阈值和拒答文案由网关统一配置。

之所以要把这件事放到网关,是因为风险来源是双向的:既可能来自用户输入,也可能出现在模型输出。如果每个应用各自维护审核 SDK、风险阈值和拒答逻辑,安全策略很快就会散落在多个代码仓库中;一旦模型切换或策略升级,业务还得重复改造。

先用一张图看懂这次集成:

这不是在网关旁边挂一个与流量脱节的安全样例,而是把内容提取、安全外呼、风险决策、流式缓冲和拒答整形,放进请求与响应真正经过的数据面链路。Higress 负责把模型判断转化为网关动作。

1. Qwen3Guard 提供判断,Higress 负责执行

根据 Qwen3Guard 官方仓库官方模型卡技术报告,Qwen3Guard 是基于 Qwen3 构建的安全审核模型系列,使用超过 119 万条带安全标注的提示词和回复数据训练,提供 0.6B、4B、8B 三种规模,分为两条技术路线:

官方模型工作方式本插件是否直接接入
Qwen3Guard-Gen接收完整用户输入或”输入+回复”,以生成方式输出结构化安全判断
Qwen3Guard-Stream使用专用分类头和流状态,逐 token 进行安全监测

本插件默认调用 Qwen/Qwen3Guard-Gen-4B,原因很直接:Qwen3Guard-Gen 可以通过 vLLM 或 SGLang 暴露 OpenAI-compatible POST /v1/chat/completions 接口,与现有 AI 网关协议自然衔接。

Qwen3Guard 输出三级风险:

flowchart LR
Result["Qwen3Guard Safety 结果"]
Result --> Safe["Safe<br/>通常安全"]
Result --> Controversial["Controversial<br/>依赖场景与策略"]
Result --> Unsafe["Unsafe<br/>通常有害"]
Safe --> Pass["放行"]
Controversial --> Policy{"riskLevelBar"}
Policy -->|"riskLevelBar=Unsafe"| Pass
Policy -->|"riskLevelBar=Controversial"| Block["拦截"]
Unsafe --> Block

插件将官方三级结果映射为两档网关策略:

riskLevelBar拦截范围适用含义
Unsafe,默认值仅拦截 UnsafeControversial 按宽松策略放行
Controversial拦截 ControversialUnsafe将争议内容纳入严格策略

Qwen3Guard 官方安全策略覆盖暴力、非暴力违法行为、性内容、个人身份信息、自杀与自伤、不道德行为、政治敏感主题、版权侵权等类别,并在输入审核中包含 Jailbreak 检测。当前插件能够解析 SafetyCategories 和回复审核中的 Refusal,但真正参与放行决策的只有 SafetyriskLevelBar。当前版本尚未实现按类别配置不同动作,本文也不将其宣传为已具备的能力。

官方资料还给出了 119 种语言和方言支持,并报告了英文、中文和多语言安全基准结果。这些是 Qwen3Guard 模型的公开定位,不等于某个具体业务的线上效果承诺;生产接入仍需使用自己的语言分布、真实风险样本和误拦成本完成评估。

2. 一次完整调用是怎样被保护的

网关是调用者与模型服务之间的必经路径。把安全控制放在 Higress,多个应用就能复用同一套审核接入与阈值,而不必把逻辑复制到每个业务中。

下面的时序图对应当前代码的真实调用顺序:

sequenceDiagram
autonumber
participant C as AI 应用
participant H as Higress qwen3guard 插件
participant G as Qwen3Guard-Gen
participant L as 原大模型
C->>H: POST /v1/chat/completions
H->>H: 从 messages 提取最后一条 content
H->>G: 输入内容安全审核
alt 输入达到拦截阈值
G-->>H: Safety: Unsafe / Controversial
H-->>C: OpenAI-compatible 拒答
else 输入放行
G-->>H: Safety: Safe 或未达到阈值
H->>L: 转发原始请求
L-->>H: 普通 JSON 或 SSE 回复
H->>G: user + assistant 累计内容审核
alt 输出达到拦截阈值
G-->>H: Safety 命中
H-->>C: 普通拒答或 SSE 拒答
else 输出放行
G-->>H: Safety 未命中
H-->>C: 返回原模型回复
end
end

请求侧:先审核,再决定是否访问原模型

插件默认启用 checkRequest。收到请求体后,它按 maxBodyBytes 设置缓冲上限,并使用默认 GJSON Path messages.@reverse.0.content 取得最后一条消息的 content

提取成功后,插件按照 Qwen3Guard-Gen 官方 Prompt Moderation 形态构造请求:

{
"model": "Qwen/Qwen3Guard-Gen-4B",
"messages": [
{
"role": "user",
"content": "<待审核输入>"
}
],
"max_tokens": 128
}

风险达到阈值时,插件直接返回拒答,不再调用原大模型;未达到阈值才恢复请求并继续转发。这样既统一了安全执行点,也避免被拦截的请求继续占用原模型推理资源。

响应侧:结合问题与回答共同判断

插件默认也启用 checkResponse。对于 HTTP 200 的非流式响应,它缓冲完整 JSON,通过 choices.0.message.content 提取回复,再按官方 Response Moderation 结构提交:

{
"model": "Qwen/Qwen3Guard-Gen-4B",
"messages": [
{
"role": "user",
"content": "<原始用户输入>"
},
{
"role": "assistant",
"content": "<模型回复>"
}
],
"max_tokens": 128
}

只要请求阶段成功取得用户文本,回复审核就会保留”问题+回答”的对话关系。如果请求文本未提取成功,插件不会编造上下文,而是仅审核 assistant 回复。响应检测只处理 HTTP 200,其他状态码直接放行;启用响应检测时,插件还会移除请求中的 Accept-Encoding,避免压缩响应无法直接提取文本。

3. SSE 流式回复如何被审核

AI 对话普遍使用流式输出。当前插件识别 Content-Type: text/event-stream,解析 SSE 事件边界和 data: 载荷,通过 choices.0.delta.content 收集新增文本。

flowchart TD
subgraph ParseStage["① 解析 SSE"]
direction LR
Chunk["收到网络分片"]
Join["拼接不完整事件"]
Split["切分完整 SSE 事件"]
Extract["提取 delta.content"]
Chunk --> Join --> Split --> Extract
end
subgraph BufferStage["② 更新缓冲"]
direction LR
Accumulate["更新完整累计回复"]
Unchecked["累计尚未检查文本"]
Trigger{"达到字符窗口<br/>或流结束?"}
Accumulate --> Unchecked --> Trigger
end
subgraph CheckStage["③ 安全审核"]
direction LR
Guard["提交完整累计回复<br/>至 Qwen3Guard-Gen"]
Decision{"达到风险阈值?"}
Guard --> Decision
end
subgraph OutputStage["④ 输出处理"]
direction LR
Hold["继续缓冲<br/>等待后续分片"]
Release["释放已缓冲 SSE"]
Drop["丢弃尚未释放内容"]
DenySSE["注入拒答 chunk<br/>并发送 DONE"]
Drop --> DenySSE
end
Extract --> Accumulate
Trigger -->|"否"| Hold
Trigger -->|"是"| Guard
Decision -->|"否:放行"| Release
Decision -->|"是:拦截"| Drop

插件维护两份状态:

  • 完整累计回复:每次检查都把截至当前的完整文本送检;
  • 自上次检查后的新增文本:达到 streamBufferChars 时触发下一次检查。

默认每新增 1000 个 Unicode 字符触发一次检查,字符数按 UTF-8 rune 计算,不是字节数。窗口越小,检查越频繁、待审核内容越少,但 Qwen3Guard 调用次数和重复计算随之增加;窗口越大,调用次数下降,但更多内容会在一次审核前保持缓冲。具体值需要按真实流量、首字延迟和推理资源测试确定。

命中风险后,插件只能丢弃尚未释放的数据并追加拒答 SSE;已经发给客户端的状态码和历史片段无法追回,因此流式拦截时 denyCode 不生效。

这里需要再次强调:

当前实现是”网关分段缓冲 SSE,并重复调用 Qwen3Guard-Gen 审核累计文本”,不是 Qwen3Guard-Stream 的原生逐 token 分类。

官方技术报告指出,Gen 模型反复处理累计文本会产生重复计算;Stream 模型则通过专用分类头和流状态避免重复处理历史 token。两者不能混为同一项性能能力。

4. 四种服务发现方式,模型服务独立扩缩容

Qwen3Guard 推理服务不嵌入网关进程。插件通过 Higress Wasm Go SDK 构建 Envoy 外呼 cluster,当前支持四种 serviceSource

flowchart LR
Plugin["qwen3guard Wasm 插件"]
Plugin --> K8s["k8s<br/>Service + Namespace + Port"]
Plugin --> Nacos["nacos<br/>Service + NamespaceID + Port"]
Plugin --> IP["ip<br/>Static IP Cluster"]
Plugin --> DNS["dns<br/>Logical Service + Domain + Port"]
K8s --> Guard["Qwen3Guard-Gen 服务"]
Nacos --> Guard
IP --> Guard
DNS --> Guard
serviceSource必要信息代码行为
k8sserviceNameservicePortnamespace 默认 default构建 Kubernetes service cluster
nacosserviceNameservicePortnamespace构建 Nacos service cluster
ipserviceNameservicePort构建 Static IP cluster
dnsserviceNameservicePortdomain构建 DNS cluster

这意味着 Qwen3Guard 可以独立部署和扩缩容,网关只依赖一个可访问的 OpenAI-compatible HTTP 服务。但需要注意:“服务已经启动”并不等于”网关数据面已经可达”。DNS、cluster 名称、Kubernetes 命名空间、出口网络、白名单和鉴权,仍需从 Envoy 所在网络验证。

5. 安装与启用:编译、挂载、下发配置

Higress 的开源 Wasm 插件由使用者自行编译并挂载,官方不提供预构建的 qwen3guard 插件镜像。整个过程分三步:编译 Wasm 产物 → 让数据面能取到产物 → 用 WasmPlugin 下发配置

flowchart LR
Src["插件源码<br/>extensions/qwen3guard"]
Build["编译<br/>GOOS=wasip1 GOARCH=wasm"]
Artifact["Wasm 产物"]
Mount["文件挂载 file://<br/>或自建 OCI 镜像 oci://"]
CR["WasmPlugin CR<br/>phase / priority / config"]
Envoy["Higress 数据面"]
Src --> Build --> Artifact --> Mount --> CR --> Envoy

第一步:编译 Wasm 产物

最简单的方式是本地直接编译,产物为 main.wasm

plugins/wasm-go/extensions/qwen3guard/main.wasm
cd plugins/wasm-go
PLUGIN_NAME=qwen3guard make local-build

等价的手工命令:

Terminal window
cd plugins/wasm-go/extensions/qwen3guard
go test ./...
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm .

如果希望固定编译环境,使用仓库提供的容器构建,产物为 plugin.wasm

plugins/wasm-go/extensions/qwen3guard/plugin.wasm
cd plugins/wasm-go
PLUGIN_NAME=qwen3guard make build

生产环境建议构建成自己的 OCI 镜像并推送到自有仓库:

Terminal window
cd plugins/wasm-go
PLUGIN_NAME=qwen3guard \
REGISTRY=<your-registry>/ \
PLUGIN_VERSION=1.0.0 \
make build-push

注意 REGISTRY 需要以 / 结尾,最终镜像地址为 ${REGISTRY}${PLUGIN_NAME}:${PLUGIN_VERSION}。不指定 PLUGIN_VERSION 时,tag 会退化为”构建时间-commit”,不利于回滚和灰度,生产环境应显式指定。

第二步:让数据面能取到产物

两条路径,按场景二选一:

方式url 写法适用场景前提
文件挂载file:///opt/plugins/wasm-go/extensions/qwen3guard/plugin.wasm本地开发、自建验证Helm 安装时开启 global.volumeWasmPlugins=true,把产物放到 gateway 容器对应路径
OCI 镜像oci://<your-registry>/qwen3guard:1.0.0生产部署已执行 make build-push,且数据面能拉取该仓库

仓库自带的 make install-dev-wasmplugin 走的就是文件挂载路径,它会设置 Helm 变量 global.volumeWasmPlugins=true,把本地编译产物挂进网关容器,适合开发期快速迭代。

第三步:用 WasmPlugin 下发配置

全局启用:

apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: qwen3guard
namespace: higress-system
spec:
phase: UNSPECIFIED_PHASE
priority: 300
url: oci://<your-registry>/qwen3guard:1.0.0
defaultConfig:
serviceSource: k8s
serviceName: qwen3guard
servicePort: 8000
namespace: ai-security
requestPath: /v1/chat/completions
model: Qwen/Qwen3Guard-Gen-4B
timeoutMs: 3000
checkRequest: true
checkResponse: true
riskLevelBar: Unsafe

只对特定 AI 路由生效时,关闭默认配置、改用 matchRules,避免非 AI 流量也被缓冲和审核:

spec:
phase: UNSPECIFIED_PHASE
priority: 300
url: oci://<your-registry>/qwen3guard:1.0.0
defaultConfigDisable: true
matchRules:
- configDisable: false
ingress:
- default/my-ai-route
config:
serviceSource: k8s
serviceName: qwen3guard
servicePort: 8000
namespace: ai-security
riskLevelBar: Unsafe

执行阶段与优先级:和 ai-proxy 一起用时怎么排序

phasepriority 由 WasmPlugin 资源决定,插件代码本身不声明,因此必须在 CR 里显式写清,不要依赖默认值。

规则很简单:阶段先比阶段,同一阶段内 priority** 数值越大越先执行**。仓库 e2e 用例里就有依赖这一规则的注释——custom-responsepriority: 200 以确保先于 priority: 100basic-auth 执行。

参考现有 AI 插件的取值:

插件阶段优先级
ai-proxy默认阶段100
ai-statistics默认阶段200
ai-security-guard默认阶段300

因此推荐 qwen3guard 使用**默认阶段 + **priority: 300,与同类安全插件保持一致的相对次序。这个取值同时解决了两侧的内容提取问题:

  • 请求侧:qwen3guard 先于 ai-proxy 执行,看到的是应用发来的原始 OpenAI 格式请求体,默认路径 messages.@reverse.0.content 可以直接命中;
  • 响应侧:Envoy 的响应路径按相反顺序穿过过滤器,qwen3guard 在 ai-proxy 之后执行,看到的是已经被归一化为 OpenAI 格式的响应,默认路径 choices.0.message.contentchoices.0.delta.content 同样可以直接命中。

如果调整了相对顺序(例如让 qwen3guard 在 ai-proxy 之后处理请求),就需要同步调整三个 GJSON Path,使其匹配那一层实际看到的报文结构。

完整配置字段

serviceSource: k8s
serviceName: qwen3guard
servicePort: 8000
namespace: ai-security
requestPath: /v1/chat/completions
apiKey: EMPTY
model: Qwen/Qwen3Guard-Gen-4B
timeoutMs: 3000
checkRequest: true
checkResponse: true
requestContentJsonPath: messages.@reverse.0.content
responseContentJsonPath: choices.0.message.content
streamingResponseContentJsonPath: choices.0.delta.content
streamBufferChars: 1000
riskLevelBar: Unsafe
denyCode: 200
denyMessage: 很抱歉,我无法回答您的问题
maxBodyBytes: 10485760

几点注意:三个名称包含 JsonPath 的字段实际使用 GJSON Path 语法,不要添加 $ 前缀。apiKey 只填写原始值,插件会自动添加 Bearer 前缀。当前实现未配置密钥时仍使用默认值 EMPTY,并发送 Authorization: Bearer EMPTY,尚不支持完全省略 Authorization 请求头。

另外,插件产物导出 Proxy-Wasm ABI 0.2.100,需要使用支持该 ABI 的 Higress 数据面镜像;只支持其他 ABI 版本的标准 Envoy 无法加载。

6. 部署安全服务并验证链路

用 vLLM 启动 Qwen3Guard-Gen

官方模型卡给出的 OpenAI-compatible 部署方式如下:

Terminal window
pip install "vllm>=0.9.0"
vllm serve Qwen/Qwen3Guard-Gen-4B \
--port 8000 \
--max-model-len 32768

建议先直接验证安全服务本身,不要一开始就把所有问题归因于网关:

Terminal window
curl -sS http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "Qwen/Qwen3Guard-Gen-4B",
"messages": [
{
"role": "user",
"content": "请介绍一下杭州西湖。"
}
],
"max_tokens": 128
}'

官方定义的生成内容采用类似以下结构:

Safety: Safe
Categories: None

如果服务实际部署在 Kubernetes、容器或其他机器中,127.0.0.1:8000 只代表当前命令所在主机,不能据此推断 Higress 网关已经可达。

验证安全输入

Terminal window
curl -i 'http://<YOUR_GATEWAY>/v1/chat/completions' \
-H 'Content-Type: application/json' \
-d '{
"model": "your-upstream-model",
"messages": [
{
"role": "user",
"content": "请介绍一下杭州西湖。"
}
],
"stream": false
}'

预期链路:

客户端
→ Higress 提取用户输入
→ Qwen3Guard 返回未达到阈值
→ 请求继续到原大模型
→ 回复再次审核
→ 返回原模型结果

文章不预设原模型会返回什么内容;能被代码确认的是:未命中阈值时,插件继续转发。

验证输入拦截

使用符合组织安全测试规范的风险样本发送相同请求。若 Qwen3Guard 返回 Safety: Unsafe 且阈值为 Unsafe,原大模型不会被调用,客户端收到插件生成的 Chat Completions 风格拒答:

{
"object": "chat.completion",
"model": "from-security-guard",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "很抱歉,我无法回答您的问题"
},
"finish_reason": "stop"
}
]
}

实际响应还包含动态 idcreatedusagelogprobs。默认 denyCode200,所以客户端不能只靠 HTTP 错误码识别拒答。

验证 SSE 流式拦截

Terminal window
curl -N 'http://<YOUR_GATEWAY>/v1/chat/completions' \
-H 'Content-Type: application/json' \
-d '{
"model": "your-upstream-model",
"messages": [
{
"role": "user",
"content": "<符合测试规范的流式审核样本>"
}
],
"stream": true
}'

流式输出命中风险后,插件追加的拒答形态为:

data: {"object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"很抱歉,我无法回答您的问题"}}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]

上面只保留便于阅读的关键字段;真实事件还包含动态 idcreatedmodelindexlogprobs

可选实践:通过 DNS 接入集群外的 Qwen3Guard

如果安全服务不在 Higress 所在 Kubernetes 集群中,可以使用 serviceSource: dns。当前 SDK 生成的 Envoy cluster 名称为:

outbound|<servicePort>||<serviceName>.dns

例如,逻辑服务名使用 qwen3guard-api,对应的 ServiceEntry host 应为 qwen3guard-api.dns。注意 serviceName 中不要再次填写 .dns,否则 cluster 名称会变成 .dns.dns

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: qwen3guard-api
namespace: higress-system
spec:
hosts:
- qwen3guard-api.dns
ports:
- name: http
number: 80
protocol: HTTP
resolution: DNS
endpoints:
- address: qwen3guard.example.com
ports:
http: 80

插件侧对应改为 serviceSource: dnsserviceName: qwen3guard-apiservicePort: 80domain: qwen3guard.example.com。这两个对象需要同时正确:ServiceEntry 负责让 Envoy 拥有目标 cluster,插件配置负责选择同名 cluster 并设置实际 HTTP Host。只有域名能在开发机解析,并不能证明网关 Pod 所在网络也能解析和访问。

一套不猜测模型结果的验收要点

安全模型对具体样本的判断必须以真实返回为准,文章不预言某句话一定得到哪个标签。可以使用经过审核的测试集,或者在测试环境使用可控的 Qwen3Guard 响应桩,按”模型返回值 → 插件动作”验证网关逻辑:

请求形态Qwen3Guard 条件插件配置应观察到的动作
非流式请求Safety: ControversialriskLevelBar: Unsafe请求继续访问原模型
非流式请求Safety: ControversialriskLevelBar: Controversial原模型不被调用,返回普通拒答
非流式请求Safety: Unsafe任一合法阈值原模型不被调用,返回普通拒答
非流式响应Safety: UnsafecheckResponse: true原回复被替换为普通拒答
SSE 响应累计文本检查返回 UnsafecheckResponse: true丢弃未释放数据,追加拒答 SSE 与 [DONE]
普通或 SSE安全服务超时或非 200当前 fail-open记录警告并放行

还有三类边界值得单独覆盖:Safety 为未知字符串时应解析失败并 fail-open,而不是把未知标签猜成风险结果;流式累计超过 maxBodyBytes 后应释放缓冲并切换直通;上游返回非 200 时应跳过输出审核直接放行。完整用例列表见插件 README。

验收时应同时观察客户端响应、原模型是否收到请求、Qwen3Guard 调用次数和 Wasm 警告日志。只看到客户端返回 200 不能证明请求一定通过了原模型——因为默认拒答状态码本身也是 200

参数怎么调:先理解影响,再用压测决定

代码给出了默认值,但没有宣称默认值适合所有业务。下面列出每个关键参数变化带来的直接工程影响,最终取值应由真实回复长度、并发量、安全服务时延和风险容忍度共同决定:

参数当前默认值调小或关闭的影响调大的影响
timeoutMs2000更快进入 fail-open,安全服务可用时间窗口更短能容忍更慢推理,但请求或流式释放等待更久
streamBufferChars1000更频繁审核,累计文本重复推理次数增加审核次数下降,但单次检查前缓冲文本更多
maxBodyBytes10485760更早触发流式直通,超长请求或回复更难被完整审核允许更长内容,同时增加单请求潜在缓冲量
checkRequesttrue关闭后不拦截用户输入,但为输出审核仍可能读取请求上下文布尔字段,无”调大”含义
checkResponsetrue关闭后不缓冲或检查原模型回复布尔字段,无”调大”含义
riskLevelBarUnsafe改为 Controversial 会采用更严格策略仅支持 UnsafeControversial 两档
denyCode200可改成业务约定的其他正整数状态码,仅作用于请求和非流式拦截SSE 已经发送状态码后不会受该字段影响
denyMessage默认中文拒答可替换为应用自己的安全提示只改变返回文案,不改变风险判断

推荐的调参顺序是:先确保内容路径与网络链路正确,再确认阈值策略,随后用真实长度分布测试 streamBufferCharsmaxBodyBytes,最后在可用性目标下调整 timeoutMs。如果一开始同时修改所有参数,出现放行或延迟异常时很难判断是哪一层造成的。

7. 上线前检查:从网络到策略逐层确认

建议按”安全服务 → 网关外呼 → 插件策略 → 业务协议”四层逐级检查,避免一开始就用最终业务请求定位所有问题。

flowchart LR
L1["第一层<br/>Qwen3Guard 服务自身"]
L2["第二层<br/>Envoy 到安全服务"]
L3["第三层<br/>插件提取与策略"]
L4["第四层<br/>客户端协议表现"]
L1 -->|"API 返回结构化 Safety"| L2
L2 -->|"DNS / TCP / HTTP 可达"| L3
L3 -->|"路径、阈值、开关正确"| L4
  • 安全服务自身:模型名与实际部署一致,/v1/chat/completions 返回 HTTP 200choices[0].message.content 中存在可解析的 Safety:,且 Prompt 与 Response 两种 messages 结构都能处理。
  • Envoy 到安全服务serviceSourceserviceNameservicePort 与实际服务一致,命名空间或 DNS domain 正确,并且从网关所在网络而不是开发机验证可达;目标服务若配置 IP 白名单,需加入网关实际出口 IP。收到 401403 至少说明链路已打通,连接超时则优先检查网络和白名单。
  • 插件提取与策略:三个 GJSON Path 分别能提取输入、非流式回复和 SSE 增量文本;riskLevelBar 与业务严格程度一致;checkRequestcheckResponse 没有被误关;timeoutMsstreamBufferCharsmaxBodyBytes 已按真实流量评估。
  • 客户端协议表现:普通拒答能被现有 Chat Completions 客户端解析,SSE 拒答包含结束 chunk 和 [DONE],客户端可通过拒答内容或 model: from-security-guard 识别安全响应;不要依赖 HTTP 非 200 判断拦截,因为默认 denyCode 就是 200

常见故障速查

现象与当前实现一致的可能原因优先检查
所有请求都迅速到达原模型外呼 dispatch 失败后 fail-open,或未提取到请求文本Wasm 警告日志、GJSON Path、Envoy cluster
插件外呼返回 503目标 cluster 不存在或后端不可达cluster 名称、ServiceEntry、Service、endpoint
Qwen3Guard 返回 401apiKey 缺失、错误或重复填写 Bearer 只配置原始密钥,检查服务端鉴权
SSE 一直不向客户端释放尚未达到字符窗口,或安全外呼仍在进行streamBufferChars、Qwen3Guard 时延、插件日志
Controversial 没有拦截阈值仍是默认 UnsaferiskLevelBar 设为 Controversial 后复测
返回 HTTP 200 却是拒答默认 denyCode 就是 200检查响应 model 与 assistant 内容
独立 Envoy 无法加载插件数据面不支持插件要求的 Proxy-Wasm ABI使用与当前 Higress 版本匹配的数据面镜像

更多故障场景(非流式响应未审核、SSE 超长后不再审核、测试路由收不到请求体回调等)见插件 README 的常见问题章节。

安全配置中可能包含 apiKey。不要把真实密钥提交到仓库;当前 README 还提示底层 HTTP wrapper 在特定日志级别下可能打印外呼 headers,生产环境需要配置日志脱敏或将相关组件日志级别控制在 warn,避免 Authorization 信息进入日志。

8. fail-open:保护可用性,也要监控风险窗口

安全服务可能超时、不可达或返回异常格式。当前插件选择 fail-open:记录警告并放行当前请求或响应,避免 Qwen3Guard 故障直接阻断全部 AI 业务。

flowchart TD
Call["调用 Qwen3Guard"] --> OK{"HTTP 200 且结果可解析?"}
OK -->|"是"| Risk{"Safety 达到阈值?"}
Risk -->|"是"| Block["拦截"]
Risk -->|"否"| Pass["放行"]
OK -->|"否"| Warn["记录 fail-open 警告"]
Warn --> Pass
Limit["流式累计超过 maxBodyBytes"] --> Bypass["释放缓冲并切换直通"]
Bypass --> Pass

以下情况进入 fail-open:

场景当前动作
网络连接失败或超过 timeoutMs记录日志并放行
Qwen3Guard 返回非 200记录日志并放行
JSON 非预期、choices 为空或缺少 Safety记录日志并放行
GJSON Path 未提取到内容跳过对应审核
流式累计超过 maxBodyBytes释放缓冲,后续切换直通

默认 maxBodyBytes 为 10 MiB。fail-open 降低了安全服务对业务可用性的影响,但失败窗口内不会产生拦截,因此生产环境必须监控 Qwen3Guard 可用率、调用时延、非 200 响应和插件警告日志。需要强制 fail-close 的合规场景,当前版本不能被描述为已经满足。

结语:让安全从模型能力变成网关执行力

Qwen3Guard 提供了公开、可部署的安全判断能力——三级风险、提示词与回复审核、结构化结果、多语言覆盖,以及 Gen 与 Stream 两条技术路线。Higress Qwen3Guard Wasm 插件完成的则是另一半工作:从真实 OpenAI-compatible 流量中提取内容,调用 Qwen3Guard-Gen,把结果映射为网关动作,并处理普通响应和 SSE 流式响应中的协议细节。

最终形成的不是某个应用专属的审核函数,而是一条可复用的安全链路:

用户输入
→ 网关前置审核
→ 原大模型
→ 网关输出审核
→ 安全回复或兼容协议的拒答

它不要求应用改协议,不把安全逻辑复制到每个业务,也不把 Qwen3Guard-Gen 的能力包装成 Qwen3Guard-Stream。它把模型判断、业务策略和流量执行连接起来,让 AI 安全真正进入网关主链路。

参考资料

  1. Qwen3Guard插件Github仓库
  2. Qwen3Guard 官方 GitHub 仓库
  3. Qwen3Guard Technical Report
  4. Qwen3Guard-Gen-4B 官方模型卡
  5. Qwen 官方博客:Qwen3Guard: Real-time Safety for Your Token Stream
  6. Higress 官方文档:使用 Go 语言开发 Wasm 插件