AI生图服务在正常提示词下输出不应有的裸露内容,通常不是模型权重被注入恶意数据,而是NSFW过滤链路没有形成完整闭环。文本侧可能漏掉了同义词和拆词表达,推理侧没有启用安全等级参数,输出侧也没有做像素级图像复核。要彻底降低这类问题的出现概率,需要把过滤动作分布在请求进入、采样过程和结果返回三个节点上。

一、先定位问题:为什么负面提示词不够用
很多使用者习惯在负面提示词里加入 nude、naked、nsfw 等词汇,但扩散模型对这些词的理解并不稳定。提示词可以被同音字、拼音、符号替换甚至多语言绕过,例如用 n*de 或俄语中的对应表达。单纯在负面提示词堆词无法覆盖所有变体,必须加入文本归一化步骤,统一大小写、去除零宽字符、还原形近字,再做敏感词判定。
输入侧过滤只是第一关。即使文本完全干净,模型仍可能因为训练数据中的边界样本而生成裸露内容。此时需要专门的图像分类器对生成结果打分,例如 NudeNet 或 OpenNSFW,分数超过阈值就执行模糊、替换占位图或直接阻断。下面这段代码展示了输入侧文本过滤的基本思路。
import unicodedata
def normalize_prompt(text):
# 去除零宽字符并统一小写
text = text.translate({0x200B: None, 0x200C: None})
text = unicodedata.normalize("NFKC", text).lower()
return text
def is_sensitive(text):
blocked_terms = ["nude", "naked", "nsfw", "без одежды", "n*de"]
normalized = normalize_prompt(text)
for term in blocked_terms:
if term in normalized:
return True
return False
输出侧同样需要独立于模型参数的检测。把图像分类器放在生成流程的末尾,可以拦截模型推理阶段漏掉的裸露内容。对图像检测来说,阈值设置比单纯开关更重要,过高的阈值会漏判,过低又容易把正常的人体艺术、泳装照片误杀。
from nudenet import NudeClassifier
classifier = NudeClassifier()
def post_check(image_path):
result = classifier.classify(image_path)
unsafe_score = result[image_path].get("unsafe", 0.0)
if unsafe_score > 0.65:
return "block"
elif unsafe_score > 0.35:
return "blur"
return "pass"
安全等级设置只是整个过滤体系中的一环,不能替代文本审核和图像复核。如果只在模型参数里限制,遇到同音词、符号替换或多语言提示词时仍然容易被绕过。稳定的方案是把文本归一化、模型安全参数和图像后处理组合成三层过滤。
二、Stable Diffusion WebUI 与 ComfyUI 的过滤配置
在 Stable Diffusion WebUI 中,安全性配置分散在设置页和扩展里。可以进入 Settings 下与 Safety 或 NSFW 相关的选项,勾选过滤开关并设置检测阈值。如果使用了 sd-webui-nsfw-filter 扩展,还能在脚本区域调整检测模型和动作。配置完成后,建议在负面提示词模板里统一追加 nude, naked, explicit,但不要以为这样就万事大吉,它只能作为辅助手段。
ComfyUI 用户可以在工作流中加入专门的过滤节点。加载模型时优先选择带 NSFW 过滤的 Checkpoint Loader,或者在工作流末端添加 Image NSFW Filter 节点,选择 NudeNet 模型作为后端,将输出连接到模糊或替换节点。这样即使前面的采样流程产生了不当内容,也会在最后一步被拦截。
模型训练数据本身无法保证绝对干净,推理时又存在随机性,所以后置过滤必须始终存在。为了方便统一管理,可以把过滤策略写到配置文件里,便于在部署环境快速调整。下面是一个简化配置示例。
{
"nsfw_policy": {
"text_filter": {
"enabled": true,
"languages": ["zh", "en", "ru"],
"normalize": true
},
"image_filter": {
"backend": "nudenet",
"threshold": 0.65,
"action": "blur",
"blur_radius": 25
},
"fallback": "block"
}
}
三、安全等级设置:四个档位如何选择
安全等级本质上对应不同的检测阈值和处理动作。关闭级别的过滤通常只拦截明确违法内容,低等级允许大部分成人向创作,中等适合大众产品,高等级则面向未成年人或教育场景。等级越高,图像误杀率越大,正常的人体绘画、泳装或医学插图都可能被拦下。
| 等级 | 图像阈值 | 输出动作 | 适用场景 |
|---|---|---|---|
| 关闭 | 0.0 | 不拦截 | 私有化部署、艺术研究 |
| 低 | 0.85 | 模糊 | 成人向创作平台 |
| 中 | 0.65 | 替换占位图或阻断 | 大众头像生成、营销工具 |
| 高 | 0.50 | 阻断 | 未成年人产品、教育类 |
实际选择可以用渐进式策略:上线初期先使用中等级,收集误报和漏报数据后调整。也可以对不同用户组设置不同等级,例如游客强制高等级,认证艺术家降低为低等级。这样能在内容安全和用户体验之间取得平衡。
def get_policy(level):
table = {
"off": {"threshold": 0.00, "action": "none"},
"low": {"threshold": 0.85, "action": "blur"},
"medium":{"threshold": 0.65, "action": "block"},
"high": {"threshold": 0.50, "action": "block"}
}
return table.get(level, table["medium"])
四、对抗绕过与持续验证
文本侧的绕过手法一直在变化,除了同音字和拆字,还会利用 Unicode 零宽字符、多语言混用甚至 emoji 替代。图像侧同样存在对抗样本,模型可能生成半透明衣物、遮挡构图,导致单一分类器给出低分。因此需要在归一化基础上,对输入做多语言映射,对输出引入多个检测模型投票。
输出侧不建议只依赖一个分类器。推荐使用 NudeNet 与 OpenNSFW 做双模型投票,或者再加入 CLIP 分类做二次复核。对于分数处于中间区间的样本,应转入人工审核队列,而不是直接放行或一刀切拦截。下面是一个双模型投票的简单实现。
def ensemble_check(image_path):
score_a = nudenet_score(image_path)
score_b = opennsfw_score(image_path)
avg = (score_a + score_b) / 2.0
if score_a > 0.75 and score_b > 0.55:
return "block"
if avg > 0.60:
return "review"
return "pass"
配置上线后还需要持续验证。准备一个固定测试集,包含正常人体艺术、泳装、医学插图、灰色擦边内容以及明确裸露图像,定期计算误杀率和漏判率。尤其要关注中文语境下的同音词和拼音绕过,因为这些样本往往不会被英文敏感词库覆盖。只有把文本审核、安全等级和图像复核串成闭环,并保持测试集更新,才能真正解决 AI 生图出现不应有裸露内容的问题。