Files
higress/plugins/wasm-go/extensions/ai-security-guard/README.md
JianweiWang c21a38e783 feat(ai-security-guard): structured x_higress deny response, error-path metrics, and AI logging (#3894)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: rinfx <yucheng.lxr@alibaba-inc.com>
2026-05-29 10:45:10 +08:00

368 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: AI内容安全
keywords: [higress, AI, security]
description: 阿里云内容安全检测
---
## 功能说明
通过对接阿里云内容安全检测大模型的输入输出保障AI应用内容合法合规。
## 运行属性
插件执行阶段:`默认阶段`
插件执行优先级:`300`
## 配置说明
| Name | Type | Requirement | Default | Description |
| ------------ | ------------ | ------------ | ------------ | ------------ |
| `serviceName` | string | requried | - | 服务名 |
| `servicePort` | string | requried | - | 服务端口 |
| `serviceHost` | string | requried | - | 阿里云内容安全endpoint的域名 |
| `accessKey` | string | requried | - | 阿里云AK |
| `secretKey` | string | requried | - | 阿里云SK |
| `action` | string | requried | - | 阿里云ai安全业务接口 |
| `securityToken` | string | optional | - | 阿里云安全令牌(用于临时凭证) |
| `checkRequest` | bool | optional | false | 检查提问内容是否合规 |
| `checkResponse` | bool | optional | false | 检查大模型的回答内容是否合规,生效时会使流式响应变为非流式 |
| `requestCheckService` | string | optional | llm_query_moderation | 指定阿里云内容安全用于检测输入内容的服务 |
| `responseCheckService` | string | optional | llm_response_moderation | 指定阿里云内容安全用于检测输出内容的服务 |
| `requestContentJsonPath` | string | optional | `messages.@reverse.0.content` | 指定要检测内容在请求body中的jsonpath |
| `responseContentJsonPath` | string | optional | `choices.0.message.content` | 指定要检测内容在响应body中的jsonpath |
| `responseStreamContentJsonPath` | string | optional | `choices.0.delta.content` | 指定要检测内容在流式响应body中的jsonpath |
| `responseContentFallbackJsonPaths` | array | optional | [`choices.0.message.content`, `content.#(type=="text")#.text`] | 当 `responseContentJsonPath` 提取为空时,按顺序尝试这些兜底路径;与主路径相同的项会自动跳过;显式配置为空数组 `[]` 可禁用兜底 |
| `responseStreamContentFallbackJsonPaths` | array | optional | [`choices.0.delta.content`, `delta.text`] | 当 `responseStreamContentJsonPath` 提取为空时,按顺序尝试这些流式兜底路径;与主路径相同的项会自动跳过;显式配置为空数组 `[]` 可禁用兜底 |
| `denyCode` | int | optional | 200 | 指定内容非法时的响应状态码 |
| `denyMessage` | string | optional | openai格式的流式/非流式响应 | 指定内容非法时的响应内容 |
| `protocol` | string | optional | openai | 协议格式非openai协议填`original` |
| `openAIDenyResponseFormat` | string | optional | legacy | OpenAI 包装拒答的响应形态,取值为 `legacy``structured`。默认 `legacy` 保持历史兼容;配置为 `structured` 时在 `choices[0].x_higress_guardrail` 输出结构化拦截详情 |
| `contentModerationLevelBar` | string | optional | max | 内容合规检测拦截风险等级,取值为 `max`, `high`, `medium` or `low` |
| `promptAttackLevelBar` | string | optional | max | 提示词攻击检测拦截风险等级,取值为 `max`, `high`, `medium` or `low` |
| `sensitiveDataLevelBar` | string | optional | S4 | 敏感内容检测拦截风险等级,取值为 `S4`, `S3`, `S2` or `S1` |
| `customLabelLevelBar` | string | optional | max | 自定义检测拦截风险等级,取值为 max, high, medium, low |
| `riskAction` | string | optional | block | 风险处置动作,取值为 `block``mask``block` 表示按风险等级阈值拦截请求,`mask` 表示当 API 返回脱敏建议时使用脱敏内容替换敏感字段。注意:脱敏功能仅适用于 MultiModalGuard 模式 |
| `timeout` | int | optional | 2000 | 调用内容安全服务时的超时时间 |
| `bufferLimit` | int | optional | 1000 | 调用内容安全服务时每段文本的长度限制 |
| `consumerRequestCheckService` | map | optional | - | 为不同消费者指定特定的请求检测服务 |
| `consumerResponseCheckService` | map | optional | - | 为不同消费者指定特定的响应检测服务 |
| `consumerRiskLevel` | map | optional | - | 为不同消费者指定各维度的拦截风险等级 |
### 拒绝响应结构
内容被拦截时,插件(`MultiModalGuard` action会构造以下结构化 JSON 对象。`protocol: original`、MCP 与图像生成路径直接或间接返回该对象OpenAI 文本生成包装路径默认保持历史兼容形态,只有配置 `openAIDenyResponseFormat: structured` 时才会把该对象嵌入到 OpenAI 响应中。
```json
{
"code": 200,
"denyMessage": "很抱歉,我无法回答您的问题",
"blockedDetails": [
{
"type": "contentModeration",
"level": "high"
}
]
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | int | 在 `text_generation`(OpenAI 包装) 与 `image_generation` 路径下为网关返回的 HTTP 状态码,取自 `denyCode`(默认 `200`);在 `protocol=original``mcp` 路径下为安全服务返回的业务码(`Response.Code`,成功检测时为 `200` |
| `denyMessage` | string | 人类可读拦截文案。OpenAI 包装路径下始终存在,取 `denyMessage`(默认 `很抱歉,我无法回答您的问题`)`protocol=original` / `image_generation` / `mcp` 路径下取 `denyMessage`,未配置时省略该字段(`omitempty` |
| `blockedDetails` | array | 命中拦截的维度明细;若安全服务未返回 `Detail`,则根据顶层 `RiskLevel`/`AttackLevel` 自动合成。命中维度为空时返回 `[]` |
| `blockedDetails[].type` | string | 风险类型:`contentModeration` / `promptAttack` / `sensitiveData` / `maliciousUrl` / `modelHallucination` / `customLabel` |
| `blockedDetails[].level` | string | 风险等级:`high` / `medium` / `low`;敏感数据为 `S1``S4` |
> 说明:当前实现的拒答 body 仅包含上述字段。不输出安全服务的 `RequestId`、单条 `Suggestion` 与原始业务码(`guardCode`);安全服务的 `RequestId` 通过 AI 日志 `safecheck_request_ids` 字段暴露(见下文 AI Log 章节)。
各协议承载位置:
- **`text_generation`OpenAI默认 `legacy`**:不输出 `x_higress_guardrail` 或历史 `x_higress` 字段;`choices[0].message.content` / 首帧 `delta.content` 保持历史内容形态RiskBlock 为 JSON 字符串mask fallback 为拒答文案),`finish_reason``"stop"`,流式响应仍以 `data: [DONE]` 结束
- **`text_generation`OpenAI`structured` 非流式)**`choices[0].message.content` 承载可读拦截文案(即 `denyMessage`,未配置时默认为 `很抱歉,我无法回答您的问题`);上述结构体作为嵌入对象放入 `choices[0].x_higress_guardrail`(不是 JSON 字符串)
- **`text_generation`OpenAI`structured` 流式 SSE**:首帧 `delta.content` 承载可读拦截文案;上述结构体仅在最后一个 chunk 中作为嵌入对象放入 `choices[0].x_higress_guardrail`,随后以 `data: [DONE]` 结束流
- **`text_generation``protocol=original`**:上述结构体直接作为 JSON 响应 body 返回(不包 OpenAI 外壳,不新增 `x_higress_guardrail`)
- **`image_generation`**:上述结构体直接作为 JSON 响应 body 返回HTTP 403
- **`mcp`JSON-RPC**:上述结构体序列化为 JSON 字符串后放入 `error.message`
- **`mcp`SSE**:同上,通过 SSE 事件返回
`openAIDenyResponseFormat` 只影响 OpenAI 包装拒答的 body 形态拦截判断、fail-open 行为、metric 与 AI Log 字段不随该配置变化。该字段只能配置在插件全局,不能放入 `consumerRiskLevel`
补充说明一下内容合规检测、提示词攻击检测、敏感内容检测三种风险的四个等级:
- 对于内容合规检测、提示词攻击检测:
- `max`: 检测请求/响应内容,但是不会产生拦截行为
- `high`: 内容安全检测/提示词攻击检测 结果中风险等级为 `high` 时产生拦截
- `medium`: 内容安全检测/提示词攻击检测 结果中风险等级 >= `medium` 时产生拦截
- `low`: 内容安全检测/提示词攻击检测 结果中风险等级 >= `low` 时产生拦截
- 对于敏感内容检测:
- `S4`: 检测请求/响应内容,但是不会产生拦截行为
- `S3`: 敏感内容检测结果中风险等级为 `S3` 时产生拦截
- `S2`: 敏感内容检测结果中风险等级 >= `S2` 时产生拦截
- `S1`: 敏感内容检测结果中风险等级 >= `S1` 时产生拦截
- 对于自定义检测customLabel
- `max`: 检测请求/响应内容,但是不会产生拦截行为
- `high`: 自定义检测结果中风险等级为 `high` 时产生拦截
- 注意:阿里云 API 对 customLabel 维度仅返回 `high``none` 两个等级,不同于其他维度的四级划分。配置为 `high` 即可在检测命中时拦截,配置为 `max` 则不拦截。`medium``low` 为配置兼容性保留,但 API 不会返回这些等级。
- 对于风险处置动作riskAction
- `block`: 按各维度的风险等级阈值判断是否拦截
- `mask`: 当 API 返回 `Suggestion=mask` 时使用脱敏内容替换敏感字段,当 `Suggestion=block` 时仍会拦截
- 注意:脱敏功能仅适用于 MultiModalGuard 模式action 配置为 MultiModalGuard其他模式不支持脱敏
## 配置示例
### 前提条件
由于插件中需要调用阿里云内容安全服务所以需要先创建一个DNS类型的服务例如
![](https://img.alicdn.com/imgextra/i4/O1CN013AbDcn1slCY19inU2_!!6000000005806-0-tps-1754-1320.jpg)
阿里云内容安全配置示例:
```yaml
requestCheckService: llm_query_moderation
responseCheckService: llm_response_moderation
```
阿里云AI安全护栏配置示例
```yaml
requestCheckService: query_security_check
responseCheckService: response_security_check
```
### 检测输入内容是否合规
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkRequest: true
```
### 检测输入与输出是否合规
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: green-cip.cn-shanghai.aliyuncs.com
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkRequest: true
checkResponse: true
```
### 配置 OpenAI 结构化拒答
默认 `openAIDenyResponseFormat: legacy` 保持历史响应形态。若需要在 OpenAI 响应中输出结构化拦截详情,可配置:
```yaml
openAIDenyResponseFormat: structured
```
### 使用临时安全凭证
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
securityToken: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
checkRequest: true
```
### 为不同消费者指定不同的检测服务
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkRequest: true
consumerSpecificRequestCheckService:
consumerA: llm_query_moderation_strict
consumerB: llm_query_moderation_relaxed
consumerSpecificResponseCheckService:
consumerA: llm_response_moderation_strict
consumerB: llm_response_moderation_relaxed
```
### 指定自定义内容安全检测服务
用户可能需要根据不同的场景配置不同的检测规则,该问题可通过为不同域名/路由/服务配置不同的内容安全检测服务实现。如下图所示,我们创建了一个名为 llm_query_moderation_01 的检测服务,其中的检测规则在 llm_query_moderation 之上做了一些改动:
![](https://img.alicdn.com/imgextra/i4/O1CN01bAtcvn1N9sB16iiZR_!!6000000001528-0-tps-2728-822.jpg)
接下来在目标域名/路由/服务级别进行以下配置,指定使用我们自定义的 llm_query_moderation_01 中的规则进行检测:
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkRequest: true
requestCheckService: llm_query_moderation_01
```
### 配置非openai协议例如百炼App
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkRequest: true
checkResponse: true
requestContentJsonPath: "input.prompt"
responseContentJsonPath: "output.text"
denyCode: 200
denyMessage: "很抱歉,我无法回答您的问题"
protocol: original
```
### 配置响应内容兜底提取路径
当主路径提取不到内容时,可按优先级顺序配置兜底路径,兼容多种返回协议:
```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkResponse: true
responseContentJsonPath: "choices.0.message.content"
responseStreamContentJsonPath: "choices.0.delta.content"
responseContentFallbackJsonPaths:
- "output.text"
- 'content.#(type=="text")#.text'
responseStreamContentFallbackJsonPaths:
- "payload.delta"
- "delta.text"
```
如需严格模式(主路径未命中即跳过,不走兜底),可显式关闭兜底:
```yaml
responseContentFallbackJsonPaths: []
responseStreamContentFallbackJsonPaths: []
```
## 可观测
### Metric
ai-security-guard 插件提供了以下监控指标:
- `ai_sec_request_deny`: 请求内容安全检测失败请求数
- `ai_sec_response_deny`: 模型回答安全检测失败请求数
#### 图像响应阶段 metric/ai_log 字段重命名(过渡期)
历史上图像生成插件(`lvwang/multi_modal_guard/image/openai.go``lvwang/multi_modal_guard/image/qwen.go`)在**响应阶段**命中风险时错误地写入了请求阶段字段。本次版本修正了语义,并在 1~2 个发版周期内保留**双写过渡**
| 行为 | 旧值(错误,将在后续版本移除) | 新值(推荐) |
| --- | --- | --- |
| 计数器(deny) | `ai_sec_request_deny` | `ai_sec_response_deny` |
| ai_log 耗时(pass + deny) | `safecheck_request_rt` | `safecheck_response_rt` |
| ai_log 状态(deny) | `safecheck_status="reqeust deny"`(典型拼写错误,**本次直接废弃,不再写入** | `safecheck_status="response deny"` |
过渡期内图像响应阶段会同时写入新旧两组 `*_deny` 计数器和 `safecheck_*_rt` 字段;`safecheck_status` 只写新值。看板与告警请尽快切换到 `response_*` 字段名;当前依赖 `reqeust deny`(拼写错误版本)状态串的图像响应告警需要立即改为 `response deny`
### Trace
如果开启了链路追踪ai-security-guard 插件会在请求 span 中添加以下 attributes:
- `ai_sec_risklabel`: 表示请求命中的风险类型
- `ai_sec_deny_phase`: 表示请求被检测到风险的阶段取值为request或者response
### AI Log
ai-security-guard 插件会将每次提交给内容安全服务的检测结果写入 AI 访问日志,用于将网关日志和阿里云内容安全请求关联起来:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `safecheck_requests` | array | 检测提交事件数组,每个元素为 `{"requestId"?: string, "phase": string, "modality": string, "result": string}` |
| `safecheck_request_ids` | array | 当前网关请求内所有有效内容安全 `RequestId`,按提交完成顺序保留,不去重、不截断 |
| `safecheck_request_id` | string | 最新一个有效内容安全 `RequestId`,用于兼容只读取单值的日志消费方 |
| `safecheck_status` | string | 历史兼容字段,反映本次网关请求最后一次状态变更的语义(详见下方枚举) |
| `safecheck_request_rt` / `safecheck_response_rt` | int | 请求/响应阶段安全检测的耗时(毫秒) |
| `safecheck_riskLabel` / `safecheck_riskWords` | string | 命中风险时的风险标签与风险词(取自安全服务返回的第一个命中结果) |
`safecheck_requests[].phase` 取值为 `request``response``modality` 取值为 `text``image``mcp``result` 表示**该次提交事件本身的处理结果**(而非网关最终对外动作),取值与含义如下:
| `result` 取值 | 含义 |
| --- | --- |
| `pass` | 该次提交检测通过 |
| `deny` | 该次提交命中风险,网关已对外返回拒答 |
| `mask` | 该次提交命中风险且 `Action=Mask`,安全服务返回了脱敏文本并用于改写请求体 |
| `error` | 该次提交本身处理失败HTTP 非 200、业务 Code 非 200、反序列化失败、构造拒答响应失败、调用内容安全服务失败等。错误发生在**响应阶段流式回调**且原因是构造拒答响应失败时,网关会 fail-open直接放行上游缓冲内容此时 `safecheck_status=build_fallback_pass`,对应事件 `result=error` 表示这次安全提交未完成 |
只有安全服务响应中的 `RequestId` 是 JSON 字符串且 `strings.TrimSpace(RequestId) != ""` 时,才会写入 `requestId``safecheck_request_ids``safecheck_request_id`;缺失、空字符串、空白字符串或非字符串值不会写入空占位。
每一次提交尝试都会生成一个 `safecheck_requests` 事件,包括 HTTP 非 200、业务失败码以及调用内容安全服务失败等错误场景错误结果会记录为 `result=error`。需要精确审计多次提交、流式分段或图片多次检测时,应优先使用 `safecheck_requests`
`safecheck_status` 枚举(历史字段,按"最后一次状态变更"覆盖,存在多次提交时仅保留最后一次的语义)
| `safecheck_status` 取值 | 含义 |
| --- | --- |
| `request pass` | 请求阶段所有提交均通过 |
| `request mask` | 请求阶段命中 mask请求体已被脱敏文本改写 |
| `reqeust deny` | 请求阶段命中风险,网关返回拒答(注:拼写为 `reqeust`,沿用历史,保持向后兼容) |
| `request error` | 请求阶段安全提交本身失败HTTP/反序列化/调用安全服务等),网关 fail-open 放行 |
| `response pass` | 响应阶段所有提交均通过 |
| `response deny` | 响应阶段命中风险,网关返回拒答 |
| `response error` | 响应阶段安全提交本身失败,网关 fail-open 放行 |
| `build_fallback_pass` | 响应阶段流式回调里构造拒答响应失败,网关 fail-open 直接放行上游缓冲内容 |
## 请求示例
```bash
curl http://localhost/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "这是一段非法内容"
}
]
}'
```
当配置 `openAIDenyResponseFormat: structured` 时,请求内容会被发送到阿里云内容安全服务进行检测。如果请求内容检测结果为非法,网关将返回形如以下的回答:
```json
{
"id": "chatcmpl-AAy3hK1dE4ODaegbGOMoC9VY4Sizv",
"object": "chat.completion",
"created": 1727078400,
"model": "from-security-guard",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "作为一名人工智能助手,我不能提供涉及色情、暴力、政治等敏感话题的内容。如果您有其他相关问题,欢迎您提问。"
},
"logprobs": null,
"finish_reason": "stop",
"x_higress_guardrail": {
"code": 200,
"denyMessage": "作为一名人工智能助手,我不能提供涉及色情、暴力、政治等敏感话题的内容。如果您有其他相关问题,欢迎您提问。",
"blockedDetails": [
{
"type": "contentModeration",
"level": "high"
}
]
}
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
```