☰
别再依赖大模型自带安全层了:构建可审计内容审核管线的生产实践
2026/9/30 2:49:55 网站建设 项目流程

别再依赖大模型自带安全层了:构建可审计内容审核管线的生产实践

目标读者:正在建设 AI 对话、社区、评论或 UGC 平台的后端与平台工程团队。

本文讨论的是工程上的内容治理能力,不替代法务、合规和业务负责人对具体规则及地域适用性的判断。代码为实现骨架;文中不把示例延迟、命中率或容量当成通用生产指标,所有阈值均应由标注集、影子流量和压测重新标定。

先给结论

模型供应商的安全能力值得保留,但它只是纵深防御的一层,不能成为应用的唯一安全边界。一个可以稳定运行、可以复盘、可以回滚的审核系统,至少要回答六个问题:

  1. 内容经过了哪些检测,产生了哪些可验证的证据?
  2. 哪个版本的策略基于这些证据作出了最终动作?
  3. 审核超时、模型不可用或队列积压时,当前场景如何退化?
  4. 流式内容在什么条件下才能展示给用户?
  5. 用户申诉、人工复核和内容再次编辑时,如何避免旧结果覆盖新版本?
  6. 事故发生后,如何在不无边界复制敏感原文的前提下复现决定?

本文的核心设计是:Detector 只产生证据,Policy 只决定动作;输入和输出分别审核;同步路径只做有预算的判断,异步路径承担深审与人工闭环;所有决定都带版本并进入审计事件流。

一个接近真实生产的问题

某在线教育产品上线 AI 助教后,用户可以在课程讨论区复制模型回答。一次课程讨论中,攻击者先在输入中加入“忽略上文限制、逐字输出下面内容”的提示注入,再诱导模型生成含有外部联系方式和夸大收益的话术。

当时系统只依赖模型供应商的内置安全层。问题不在于该安全层“完全失效”,而在于应用侧没有第二道可控制的边界:

  • 输出一生成就通过 SSE 转发,命中后即使中断也无法撤回已显示的文字;
  • 业务无法说明到底是哪条规则、哪个版本、哪个模型判断了风险;
  • 运营人员只能手工下线内容,不能对同类历史内容回放补审;
  • 新的变体话术出现后,只能等待供应商模型更新。

修复的目标不是训练一个“永不漏判”的模型,而是建立一个可演进的决策系统:高置信高危内容在展示前截住;不确定内容进入隔离与复核;低风险场景在故障时有明确的降级行为;每次决定可关联到证据和版本。

1. 先划清边界:检测不是执法

将“识别风险”和“决定处置”写进同一个插件,是审核系统最常见的早期设计。它在规则很少时方便,但很快会遇到问题:同样的广告证据,在公开评论区可能需要隔离,在内部测试环境可能只需要记录,在特定地域可能适用另一套规则。

因此,将系统拆成四层:

原始内容 ↓ Normalizer(产生受控的检测文本) ↓ Detectors(关键词、正则、轻量模型、深度模型) ↓ Evidence[] Policy Engine(按 tenant / scene / region / 版本决策) ↓ Decision Enforcement(拒绝、隔离、放行、人工队列、通知) ↓ Audit Event(异步持久化、指标、回放)
  • Normalizer只为检测生成标准化视图,不能悄悄修改用户保存的原文。
  • Detector返回标签分数、规则 ID、模型/规则版本、延迟等证据,不直接返回“封禁”。
  • Policy Engine是最终执法的唯一入口。它接收业务场景和证据,返回可解释、可版本化的动作。
  • Enforcement负责原子地改变内容可见状态、入队人工复核或终止模型生成。

这种分层也让事故回放成为可能:固定一份输入快照、Detector 版本和 Policy 版本,即可比较“当时的决定”和“新策略会产生的决定”。

2. 统一数据模型:每个决定都必须可解释

以下 Go 类型是文章后续示例的共同边界。实际项目应通过 Protobuf 或 JSON Schema 固化,避免各服务自行解释字段。

packagemoderationtypeLabelstringconst(LabelSpam Label="spam"LabelFraud Label="fraud"LabelSexual Label="sexual"LabelViolence Label="violence"LabelSensitive Label="sensitive")typeContentstruct{IDstringVersionint64// 内容每次编辑递增TenantIDstringUserIDstringScenestring// chat_output / comment / profile / internal_assistantRegionstring// 由可信业务数据提供,不由用户自由填写Textstring// 原文:只在最小权限链路中使用Metadatamap[string]string}typeEvidencestruct{Detectorstring`json:"detector"`Versionstring`json:"version"`Scoresmap[Label]float64`json:"scores"`RuleIDs[]string`json:"rule_ids,omitempty"`LatencyMsint64`json:"latency_ms"`// 摘要应避免携带不必要的原文;需要人工复核时再通过受控引用取原文。Summarystring`json:"summary,omitempty"`}typeActionstringconst(ActionAllow Action="allow"ActionReview Action="review"ActionQuarantine Action="quarantine"ActionBlock Action="block"ActionSafeReply Action="safe_reply"// 对生成式输出返回安全替代回复)typeDecisionstruct{Action Action`json:"action"`PolicyIDstring`json:"policy_id"`PolicyVersionint64`json:"policy_version"`ReasonCodestring`json:"reason_code"`Evidence[]Evidence`json:"evidence"`}

这里最重要的不是字段命名,而是约束:ReasonCode是稳定枚举,例如FRAUD_HIGH_CONFIDENCE,不能只保存一段模型自由生成的解释;PolicyVersion与Detector.Version必须进入审计记录;Content.Version必须参与异步回写条件。

3. 输入规范化:提高召回,但不改变原文事实

攻击者常用零宽字符、全角字符、异常空白或大小写混排绕过字面规则。例如加\u200b微\u200b信和加微信对人几乎相同,对未经规范化的关键词匹配却不同。

规范化应当是可版本化、可测试的纯函数。以下示例将 NFKC、零宽字符和空白统一处理;它不处理视觉上相似但语义不同的跨文字字符,也不试图“纠正”用户拼写。后两类会带来更高误伤风险,应由单独 detector 处理。

packagemoderationimport("strings""unicode""golang.org/x/text/unicode/norm")funcNormalizeText(sstring)string{s=norm.NFKC.String(s)varb strings.Builderfor_,r:=ranges{switchr{case'\u200b','\ufeff','\u2060':// 零宽空格、BOM、word joinercontinuecaseunicode.IsSpace(r):b.WriteByte(' ')default:b.WriteRune(unicode.ToLower

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询