接口参考:
https://help.aliyun.com/zh/model-studio/decision-model-api?spm=a2c4g.11186623.0.i0#是非判断示例-h4
20 条标注样本实测:违规拦截率 100%、漏放率 0%、误拦率 0%、平均 35~59ms/条。
代码:
content_safety.py/content_safety.mjs,报告:内容安全检测报告.md
一、为什么不用普通大模型做审核
用 qwen 做内容审核的典型做法是:给一段 system prompt,让它输出 JSON 判定。问题是它要"生成"------生成就有 token 成本、有延迟、有格式不稳定的风险,还得写解析兜底。
百炼的决策模型 (decision-model-preview)是另一条路:一次请求带上若干"问题",一次前向返回所有问题的判定和概率分布,不生成任何文本。输出长度固定为 0,所以延迟与内容长度基本无关,实测服务端 40~100ms。
它支持三种问题类型:
| 类型 | 语义 | 返回 |
|---|---|---|
noul |
是非判断 | P(yes),0~1 |
score |
有序量表 | 等级期望值(可落在两级之间,如 1.43)+ 完整概率分布 |
choice |
多选一 | 选项名 + 置信度 + 概率分布 |
二、第一个坑:文档里的域名调不通
文档写的是:
https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/systemone
实际踩坑记录(都是真跑的):
| 请求 | 结果 |
|---|---|
https://dashscope.aliyuncs.com/compatible-mode/v1/systemone |
❌ 404(公共域名根本没这条路由) |
https://cn-beijing.maas.aliyuncs.com/... |
❌ 连接失败(只有 SOA 记录) |
https://default.cn-beijing.maas.aliyuncs.com/... |
❌ 400 Workspace endpoint is invalid. |
https://ws-test.cn-beijing.maas.aliyuncs.com/... |
❌ 403 Workspace endpoint access denied. |
.../chat/completions + model=decision-model-preview |
❌ 403 access_denied(决策模型不走 chat 协议) |
https://trial.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/systemone |
✅ 200 |
结论:用试用域名 trial.cn-beijing.maas.aliyuncs.com 即可,不需要 WorkspaceId 。业务空间专属域名要控制台「业务空间管理」里的空间 ID,新加坡地域换成 trial.ap-southeast-1.maas.aliyuncs.com。
三、最小可用示例
python
import os, requests
resp = requests.post(
"https://trial.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/systemone",
headers={"Authorization": f"Bearer {os.environ['QWEN_LLM_API_KEY']}",
"Content-Type": "application/json"},
json={
"model": "decision-model-preview",
"state": "这个傻X客服,赶紧退钱!再不处理我就去举报你们。",
"questions": {
"abuse": {"type": "noul", "instructions": "这段内容是否在辱骂、人身攻击?"},
"risk": {"type": "score", "instructions": "整体有害程度?",
"criteria": ["无风险", "低风险", "中风险", "高风险"]},
"action": {"type": "choice", "instructions": "该怎么处置?",
"criteria": {"pass": "放行", "review": "转人工", "block": "拦截"}},
},
},
timeout=30,
)
print(resp.json()["answers"])
真实返回:
json
{
"answers": {
"abuse": {"type": "noul", "noul": 0.87},
"risk": {"type": "score", "score": 1.95, "confidence": 0.65,
"probabilities": {"0": 0.15, "1": 0.2, "2": 0.21, "3": 0.44}},
"action": {"type": "choice", "choice": "review", "confidence": 0.49,
"probabilities": {"pass": 0.03, "review": 0.66, "block": 0.31}}
},
"usage": {"input_tokens": 86},
"latency_ms": 51.5
}
三个问题、一次请求、51ms、86 个输入 token,没有任何输出 token。注意 action 只给了 review、置信度 0.49 ------ 这就是后面要讲的核心问题。
(上面这段是可运行文件 blog_demo_min.py,python blog_demo_min.py 直接出结果。)
四、把审核需求变成 10 个问题
内容审核要的不是"像不像违规",而是可解释的分类 + 明确的处置。所以拆成 10 个问题一起问:
- 1 个
choice:处置动作(pass / review / block) - 1 个
score:整体风险等级 0~3 - 8 个
noul:涉政敏感、色情低俗、暴力恐怖、违法犯罪、辱骂攻击、垃圾营销、隐私泄露、自杀自残
实测 10 个问题全量一次请求:888 输入 token,服务端 98ms。问题数不设上限(建议 ≤16),延迟随问题数近线性增长,所以别一个类目发一次请求。
五、最大的坑:三个信号单独用都会翻车
这是踩得最深的地方。以下全是原始返回:
| 样本 | action |
risk |
noul |
只看它会怎样 |
|---|---|---|---|---|
| 售后咨询 | review 置信 0.97 |
0.13 | 8 类全 0 | 正常售后被误转人工 |
| 小说里的战争描写 | pass 置信 1.0 |
0.35 | violence=0.83 |
正常文学被误拦 |
| 出售改装手枪和弹药 | pass 置信 0.95 |
0.12 | illegal=0.54 |
卖枪直接漏放 |
| 泄露他人身份证号 | pass |
0.03 | privacy=0.95 |
隐私泄露漏放 |
原因各不相同:
action是"整体处置倾向",但对投诉、差评、抱怨类文本有强烈的 review 偏置;risk是"整体有害程度",对明确的违法交易反而给分很低;noul判的是**"是否涉及"**,不是"是否违规" ------ 小说写战争,确实"涉及"暴力。
结论:不要相信任何一个单一信号。
六、两道解法
1. 改问题措辞(效果立竿见影)
action 的 instructions 里明说:
严格按标准判定:不要因为文本是投诉、差评、抱怨或情绪化表达就选 review;这些只要不含违规内容就选 pass。review 仅用于确有违规嫌疑但证据不足的情况。
noul 从"是否涉及 "改成"是否在传播、宣扬、教唆或促成":
仅仅是提及、讨论、批评、新闻报道、文学影视描写、反诈与安全提示不算违规,一律判否。
改完后:售后样本 review(0.97) → pass(0.94),文学样本 violence 0.83 → 0。提示词在决策模型上同样重要,甚至更重要------它的每个问题都是一次独立判断,措辞直接决定判定尺度。
2. 投票融合
python
BLOCK_THRESHOLD, REVIEW_THRESHOLD = 0.5, 0.2
SEVERE = ("politics", "porn", "violence", "illegal", "selfharm") # 高危类目
severe_hit = any(cat[c] >= BLOCK_THRESHOLD for c in SEVERE)
max_cat = max(cat.values())
if (risk >= 2.5
or (severe_hit and risk >= 0.5) # 高危类目单信号 + 风险不为 0 → 拦截(补"卖枪")
or (max_cat >= 0.9 and (risk >= 1.5 or action == "block")) # 双信号共振
or pii_block): # 身份证号/银行卡号
return "block"
if action in ("block", "review") or risk >= 1.5 or max_cat >= BLOCK_THRESHOLD or pii:
# 纠偏:模型只因文本"像投诉"就给 review,且类目与风险均无违规信号 → 放行
return "pass" if (max_cat < REVIEW_THRESHOLD and risk < 1) else "review"
return "pass"
两条原则:
block必须多信号共振 ,唯一的例外是"高危类目命中 + 风险不为 0"(涉政/色情/暴恐/违法/自残),专治上面那个risk=0.12的卖枪样本;- 单一信号最多升到
review转人工,绝不静默放行。审核系统里,漏放比误拦贵得多。
七、结构化信息交给正则,别交给模型
泄露身份证号那条,模型给的是 risk=0.03、action=pass,只有 privacy 类目到了 0.95。指望模型挡住隐私泄露不可靠,正则才是对的工具:
python
_PII = [
("idcard", "身份证号", "block", r"(?<!\d)\d{17}[\dXx](?!\d)|(?<!\d)\d{15}(?!\d)"),
("bankcard", "银行卡号", "block", r"(?<!\d)\d{16,19}(?!\d)"),
("phone", "手机号", "review", r"(?<!\d)1[3-9]\d{9}(?!\d)"),
("email", "邮箱", "review", r"[\w.+-]+@[\w-]+\.[A-Za-z]{2,}"),
]
小坑:18 位身份证号同时也满足"16-19 位银行卡",要记录已命中区间去重,否则一条文本报两个类目。
八、别忘了平台自带的兜底
涉政那条样本,请求还没到模型就被百炼网关拒了:
HTTP 400 Input data may contain inappropriate content.
这不是调用失败,是平台已经判定违规。必须把它当成"拦截"结论,而不是降级重试或报错------否则最危险的那类内容反而会被漏放:
python
if "inappropriate content" in msg.lower():
return {"action": "block", "reason": "平台输入审核拒绝", ...}
九、实测结果
20 条标注样本:10 条正常/边界(含反诈提示、时政新闻、小说战争描写、文明差评、维权投诉)+ 10 条覆盖全部 8 类违规。
| 指标 | 决策模型 | qwen-flash 降级 |
|---|---|---|
| 违规拦截率 | 100% | 100% |
| 违规漏放率 | 0% | 0% |
| 正常放行率 | 100% | 100% |
| 误拦率 | 0% | 0% |
| 平均耗时 | 35~59ms/条 | ~280ms/条 |
十、上线的几个建议
- 三道闸:平台输入审核(免费兜底)→ 决策模型判 10 个问题 → 投票融合分流。
- 失败方向要保守 :超时/异常按
review;平台 400inappropriate content按block。 - 降级要自动 :决策模型不可用时无感切到
qwen-flash(字段一致),并把路由类失败(400/403/404)在进程内缓存,避免批量时每条都白跑一次。 - 留痕 :落库
request_id、8 类概率、riskLevel、命中的信号,便于回溯和调阈值。 - 缓存:按文本哈希缓存判定结果,审核场景重复率很高。
=代码=
bash
# -*- coding: utf-8 -*-
"""博客里那段"最小可用示例"的原样验证,确保贴出去就能跑。"""
import json
import os
import requests
resp = requests.post(
"https://trial.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/systemone",
headers={"Authorization": f"Bearer {os.environ['QWEN_LLM_API_KEY']}",
"Content-Type": "application/json"},
json={
"model": "decision-model-preview",
"state": "这个傻X客服,赶紧退钱!再不处理我就去举报你们。",
"questions": {
"abuse": {"type": "noul", "instructions": "这段内容是否在辱骂、人身攻击?"},
"risk": {"type": "score", "instructions": "整体有害程度?",
"criteria": ["无风险", "低风险", "中风险", "高风险"]},
"action": {"type": "choice", "instructions": "该怎么处置?",
"criteria": {"pass": "放行", "review": "转人工", "block": "拦截"}},
},
},
timeout=30,
)
print("HTTP", resp.status_code)
print(json.dumps(resp.json(), ensure_ascii=False, indent=2))