AI 大模型生产级内容风控实战|敏感词过滤 + 内容审核 + 违规拦截 + 日志审计

文章标签

#AI 大模型内容安全 #SSE 流式风控 #敏感词过滤 #AI 问答风控 #SpringBoot3 #内容审核

阅读收益

  1. 搭建AI 问答上下行双向风控体系(用户提问 + AI 回答)
  2. 实现毫秒级敏感词拦截、文本脱敏、对话阻断能力
  3. 解决大模型幻觉、输出违规内容等线上风险
  4. 落地完整风控日志审计,支持违规溯源,满足合规要求
  5. 整套方案适配 SSE 流式架构,代码可直接投入商用

一、前言:为什么 AI 产品必须落地内容风控?

很多开发者完成 SSE 流式对话、上下文记忆、Token 计费功能后直接上线 AI 应用,极易踩合规大坑:上线后遭遇整改、产品下架。

AI 应用核心安全风险:

  1. 用户输入包含涉政、辱骂、色情、诈骗等违规内容;
  2. 大模型存在幻觉问题,随机输出不良、误导性信息;
  3. 缺少对话留存、违规记录,无法溯源,达不到商用合规标准。

常规第三方内容审核接口存在明显短板:接口调用延迟高,无法适配 SSE 打字机流式场景。

本文搭建一套分层防护方案:本地敏感词前置过滤 + AI 输出内容校验 + 违规脱敏 + 审计日志,完美兼容流式问答架构。

二、整体风控架构设计

四层生产防护模型

  1. 提问前置拦截:用户输入先校验,敏感内容直接阻断,避免无效调用大模型,节省 Token 成本
  2. 模型输出校验:AI 流式输出完成后,对完整回复进行安全检测
  3. 违规文本脱敏:识别敏感内容自动替换星号,兼顾用户体验与安全
  4. 全量日志审计:正常对话、违规对话统一持久化,满足合规溯源需求

完整业务流程

用户发起提问 → 本地敏感词校验 → 违规直接拦截并记录日志 → 合规请求转发大模型 → 获取流式结果 → AI 回复内容安全校验 → 脱敏处理 → 前端推送结果 → 保存对话审计日志

三、项目基础依赖

<!-- Lombok简化实体类 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!-- 工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.24</version>
</dependency>
<!-- JSON解析 -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>
<!-- Http客户端,调用大模型接口 -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

四、高性能敏感词工具类(Trie 字典树)

采用字典树算法实现敏感词匹配,时间复杂度 O (n),支持上万条敏感词毫秒检索,支持内容脱敏。

import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;

/**
 * AI内容安全:敏感词风控工具类
 * Trie字典树实现,支持敏感词检测、文本脱敏
 */
@Component
public class SensitiveWordUtil {

    private static final Map<Character, Map<?, ?>> ROOT = new HashMap<>();
    private static final String END_FLAG = "isEnd";

    // 敏感词库,生产环境建议迁移至数据库,支持动态配置
    private static final List<String> SENSITIVE_LIST = Arrays.asList(
            "赌博", "色情", "嫖娼", "诈骗", "刷单", "偷渡", "暴力", "毒品"
    );

    /**
     * 项目启动初始化敏感词字典树
     */
    @PostConstruct
    public void initSensitiveWord() {
        for (String word : SENSITIVE_LIST) {
            if (StrUtil.isBlank(word)) {
                continue;
            }
            Map<Character, Map<?, ?>> currentNode = ROOT;
            char[] chars = word.toCharArray();
            for (char c : chars) {
                if (!currentNode.containsKey(c)) {
                    currentNode.put(c, new HashMap<>());
                }
                currentNode = (Map<Character, Map<?, ?>>) currentNode.get(c);
            }
            // 标记词语结束
            currentNode.put(END_FLAG, true);
        }
    }

    /**
     * 检测文本是否包含敏感词
     */
    public boolean hasSensitive(String text) {
        if (StrUtil.isBlank(text)) {
            return false;
        }
        char[] chars = text.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            Map<Character, Map<?, ?>> currentNode = ROOT;
            for (int j = i; j < chars.length; j++) {
                char c = chars[j];
                if (!currentNode.containsKey(c)) {
                    break;
                }
                currentNode = (Map<Character, Map<?, ?>>) currentNode.get(c);
                if (currentNode.containsKey(END_FLAG)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 敏感词脱敏,匹配词汇替换为*
     */
    public String filterSensitive(String text) {
        if (StrUtil.isBlank(text)) {
            return text;
        }
        StringBuilder sb = new StringBuilder(text);
        char[] chars = text.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            Map<Character, Map<?, ?>> currentNode = ROOT;
            int sensitiveWordLength = 0;
            for (int j = i; j < chars.length; j++) {
                char c = chars[j];
                if (!currentNode.containsKey(c)) {
                    break;
                }
                currentNode = (Map<Character, Map<?, ?>>) currentNode.get(c);
                sensitiveWordLength++;
                if (currentNode.containsKey(END_FLAG)) {
                    // 敏感字符替换
                    for (int k = 0; k < sensitiveWordLength; k++) {
                        sb.setCharAt(i + k, '*');
                    }
                }
            }
        }
        return sb.toString();
    }
}

五、风控审计日志实体(合规必备)

所有对话强制留存记录,用于安全审计、违规溯源。

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("ai_security_log")
public class AiSecurityLog {

    @TableId(type = IdType.AUTO)
    private Long id;

    // 会话唯一标识
    private String sessionId;
    // 用户提问内容
    private String userQuestion;
    // AI返回回答内容
    private String aiAnswer;
    // 是否违规:0正常 1违规
    private Integer illegalFlag;
    // 违规说明
    private String illegalReason;
    // 创建时间
    private LocalDateTime createTime;
}

六、改造 SSE 流式对话接口,集成完整风控逻辑

实现能力:提问前置拦截、AI 输出脱敏、违规日志记录、会话上下文更新。

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import okhttp3.*;

@RestController
@RequestMapping("/ai")
public class AiSecurityChatController {

    @Resource
    private SensitiveWordUtil sensitiveWordUtil;
    @Resource
    private AiSessionContext aiSessionContext;
    @Resource
    private AiSecurityLogMapper securityLogMapper;

    private static final String API_KEY = "sk-xxxx";
    private static final String AI_URL = "https://api.deepseek.com/v1/chat/completions";

    /**
     * 集成风控的AI流式对话接口
     */
    @GetMapping(value = "/chat/secure/stream", produces = "text/event-stream;charset=UTF-8")
    public SseEmitter secureChatStream(@RequestParam String sessionId,
                                       @RequestParam String prompt) {
        SseEmitter emitter = SseUtil.createEmitter();

        new Thread(() -> {
            // 1.前置校验:用户提问敏感词拦截
            if (sensitiveWordUtil.hasSensitive(prompt)) {
                SseUtil.send(emitter, "【系统拦截】您的提问包含违规内容,请规范提问!");
                saveSecurityLog(sessionId, prompt, "", 1, "用户提问包含敏感词,对话已拦截");
                SseUtil.complete(emitter);
                return;
            }

            try {
                // 2.读取历史对话上下文
                List<AiMessage> historyList = aiSessionContext.getHistoryMessage(sessionId);
                historyList.add(new AiMessage("user", prompt));

                // 3.组装大模型请求参数,开启流式返回
                JSONObject json = new JSONObject();
                json.put("model", "deepseek-chat");
                json.put("stream", true);
                json.put("temperature", 0.7);
                json.put("messages", historyList);

                OkHttpClient client = new OkHttpClient();
                RequestBody body = RequestBody.create(json.toString(),
                        MediaType.get("application/json; charset=utf-8"));
                Request request = new Request.Builder()
                        .url(AI_URL)
                        .addHeader("Authorization", "Bearer " + API_KEY)
                        .post(body)
                        .build();

                Response response = client.newCall(request).execute();
                BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()));

                StringBuilder aiReplyBuffer = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    if (line.startsWith("data: ") && !line.contains("[DONE]")) {
                        String data = line.substring(6);
                        JSONObject resJson = JSON.parseObject(data);
                        String content = resJson.getJSONArray("choices")
                                .getJSONObject(0)
                                .getJSONObject("delta")
                                .getString("content");
                        if (content != null) {
                            aiReplyBuffer.append(content);
                            SseUtil.send(emitter, content);
                        }
                    }
                }

                String rawAiReply = aiReplyBuffer.toString();
                // 4.AI输出内容脱敏处理
                String safeReply = sensitiveWordUtil.filterSensitive(rawAiReply);

                // 5.保存审计日志
                if (sensitiveWordUtil.hasSensitive(rawAiReply)) {
                    saveSecurityLog(sessionId, prompt, rawAiReply, 1, "AI输出识别敏感词,内容已脱敏");
                } else {
                    saveSecurityLog(sessionId, prompt, rawAiReply, 0, "正常对话");
                }

                // 更新会话上下文
                historyList.add(new AiMessage("assistant", safeReply));
                aiSessionContext.updateSessionMessage(sessionId, historyList);

            } catch (Exception e) {
                SseUtil.send(emitter, "对话请求异常,请稍后重试!");
            } finally {
                SseUtil.complete(emitter);
            }
        }).start();
        return emitter;
    }

    /**
     * 异步保存风控审计日志,不阻塞主流程
     */
    private void saveSecurityLog(String sessionId, String question, String answer,
                                 Integer illegalFlag, String illegalReason) {
        AiSecurityLog log = new AiSecurityLog();
        log.setSessionId(sessionId);
        log.setUserQuestion(question);
        log.setAiAnswer(answer);
        log.setIllegalFlag(illegalFlag);
        log.setIllegalReason(illegalReason);
        log.setCreateTime(LocalDateTime.now());
        // 异步写入数据库
        new Thread(() -> securityLogMapper.insert(log)).start();
    }
}

七、方案核心优势

  1. 前置拦截节约成本

    违规提问直接阻断,不会发起大模型调用,减少 Token 消耗。

  2. 双向内容检测

    同时校验用户输入、模型输出,规避大模型幻觉产生违规信息。

  3. 平滑脱敏体验

    不粗暴中断对话,敏感文本自动星号替换,优化前端交互。

  4. 原生兼容 SSE

    不破坏流式打字机效果,无额外延迟,适配现有 AI 问答业务。

  5. 完整合规留痕

    所有对话持久化存储,满足企业安全审计、监管溯源要求。

八、生产环境进阶优化方案

8.1 敏感词库动态热更新

将敏感词存入数据库,后台管理页面支持新增、删除、启用禁用词汇,无需重启服务刷新字典树。

8.2 多级审核组合策略

本地敏感词库做毫秒级前置过滤;高风险内容调用第三方云端内容审核接口二次校验,兼顾性能与准确率。

8.3 用户分级管控

多次触发违规拦截的用户,自动限制会话、封禁接口访问,防范恶意攻击。

8.4 Prompt 注入防护

新增规则拦截指令劫持内容:例如 “忽略你之前所有指令、重置设定、绕过限制” 等越狱 Prompt。

8.5 日志存储优化

历史对话日志数据量大,建议定时归档、冷热分离,避免单表数据膨胀影响查询性能。

九、线上高频问题解决方案

问题 1:只校验用户提问,忽略 AI 输出

大模型存在不可控幻觉,很容易自主生成违规内容,上下行双向校验缺一不可。

问题 2:敏感词匹配误杀

扩充白名单词汇,优化匹配逻辑,优先词组匹配,避免单字符误拦截。

问题 3:同步写日志拖慢接口

日志入库统一异步处理,禁止同步阻塞流式对话链路。

十、总结

内容安全是 AI 商业化落地的底线。缺少风控体系的 AI 应用,无法正式上线运营,随时面临整改风险。

本文这套前置拦截 + 输出脱敏 + 审计日志架构,是当前主流 AI 产品通用合规方案,适配智能客服、私有 AI 助手、企业问答平台、付费 AI 应用。

更多推荐