Spring AI 流式对话系统:基于DFA算法的分级敏感词过滤与全链路合规设计
前言
在AI对话类应用的落地过程中,内容安全是不可绕过的核心环节。单一的“命中即拦截”策略会严重损伤用户体验,而完全依赖第三方内容安全接口又会带来高昂的调用成本与响应延迟。
本文基于 DFA 确定性有限自动机算法,实现了一套分级敏感词初筛机制,并与 Spring AI + WebSocket 流式对话流程深度结合,构建了“输入初筛分级处置 + 输出二次合规校验”的全链路安全方案,在保障合规的同时兼顾了对话的流畅性与系统性能。
一、整体架构设计思路
整套合规体系围绕 AI 对话的完整生命周期设计,分为三个核心阶段:
- 输入侧本地初筛
- 严重违规内容:直接拦截并返回提示,终止流程,同时记录审计日志
- 一般违规内容:自动替换为
*号脱敏,继续进入对话流程 - 高风险疑似内容:调用第三方内容安全服务做深度检测,不通过则拦截
- 模型流式交互:通过 Spring AI 调用大模型,以 WebSocket 分片推送的方式实现流式响应
- 输出侧二次校验:大模型生成完整回复后,再次执行敏感词检测,若命中严重违规则触发消息撤回机制
该架构的核心优势在于:绝大多数合规请求通过本地 DFA 算法毫秒级完成校验,仅少量边缘 case 走第三方深度检测,在合规性、成本、性能三者之间取得平衡。
二、核心实现:分级DFA敏感词过滤器
2.1 风险等级枚举定义
首先定义三级风险等级,作为全链路处置的统一标准:
publicenumRiskLevel{/** * 严重违规:直接拦截 */SEVERE,/** * 一般违规:替换脱敏 */NORMAL,/** * 合规 */PASS}2.2 DFA 分级字典树过滤器
DFA 算法的核心是通过字典树(Trie)结构存储敏感词,将文本扫描的时间复杂度降低到 O(n)。我们在传统实现的基础上,在词尾节点存储风险等级,从而支持分级处置能力。
importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importjavax.annotation.PostConstruct;importjava.util.*;@Slf4j@ComponentpublicclassSensitiveWordFilter{/** * DFA 字典树根节点 */privatefinalMap<Character,Map>root=newHashMap<>();/** * 敏感词替换符 */privatestaticfinalcharREPLACE_CHAR='*';/** * 模拟敏感词库(生产环境建议从数据库/配置中心动态加载) * key = 敏感词,value = 对应风险等级 */privatestaticfinalMap<String,RiskLevel>SENSITIVE_WORD_MAP=Map.of("严重违禁词1",RiskLevel.SEVERE,"严重违禁词2",RiskLevel.SEVERE,"普通违规词1",RiskLevel.NORMAL,"普通违规词2",RiskLevel.NORMAL);/** * 服务启动时构建带等级标记的 DFA 字典树 */@PostConstructpublicvoidinit(){buildTire(SENSITIVE_WORD_MAP);log.info("敏感词过滤器初始化完成,共加载{}个敏感词",SENSITIVE_WORD_MAP.size());}/** * 构建带风险等级的字典树 */privatevoidbuildTire(Map<String,RiskLevel>wordLevelMap){root.clear();for(Map.Entry<String,RiskLevel>entry:wordLevelMap.entrySet()){Stringword=entry.getKey();RiskLevellevel=entry.getValue();if(word==null||word.isEmpty()){continue;}Mapcurrent=root;for(inti=0;i<word.length();i++){charc=word.charAt(i);Mapnext=(Map)current.get(c);if(next==null){next=newHashMap<>();current.put(c,next);}current=next;// 在词的结束位置存入风险等级,替代传统的 isEnd 标记if(i==word.length()-1){current.put("level",level.name());}}}}/** * 检测整段文本的最高风险等级 * 采用最长匹配原则,命中严重违规时直接返回,提前终止扫描 */publicRiskLevelgetMaxRiskLevel(Stringtext){if(text==null||text.isEmpty()){returnRiskLevel.PASS;}RiskLevelmaxLevel=RiskLevel.PASS;for(inti=0;i<text.length();i++){Mapcurrent=root;intj=i;RiskLevelmatchedLevel=null;while(j<text.length()){charc=text.charAt(j);current=(Map)current.get(c);if(current==null){break;}// 匹配到敏感词结尾,记录当前等级if(current.containsKey("level")){matchedLevel=RiskLevel.valueOf((String)current.get("level"));}j++;}// 更新整段文本的最高风险等级if(matchedLevel!=null&&matchedLevel.ordinal()>maxLevel.ordinal()){maxLevel=matchedLevel;// 性能优化:命中严重违规直接返回,无需继续扫描if(maxLevel==RiskLevel.SEVERE){returnRiskLevel.SEVERE;}}}returnmaxLevel;}/** * 快捷判断:是否包含严重违规内容 */publicbooleanisSevereViolation(Stringtext){returnRiskLevel.SEVERE.equals(getMaxRiskLevel(text));}/** * 按最长匹配原则,将所有敏感词替换为 * 号 */publicStringreplaceSensitiveWord(Stringtext){if(text==null||text.isEmpty()){returntext;}char[]chars=text.toCharArray();for(inti=0;i<chars.length;i++){Mapcurrent=root;intj=i;intmatchEndIndex=-1;while(j<chars.length){charc=chars[j];current=(Map)current.get(c);if(current==null){break;}if(current.containsKey("level")){matchEndIndex=j;// 记录最长匹配的结束位置}j++;}// 命中敏感词,将区间内所有字符替换if(matchEndIndex!=-1){for(intk=i;k<=matchEndIndex;k++){chars[k]=REPLACE_CHAR;}i=matchEndIndex;// 跳过已匹配区间,避免重复扫描}}returnnewString(chars);}/** * 动态刷新敏感词库,无需重启服务 */publicvoidrefreshWordMap(Map<String,RiskLevel>newWordMap){buildTire(newWordMap);log.info("敏感词库已刷新,共{}个词",newWordMap.size());}}💡 设计要点:采用最长匹配原则避免短词优先导致的漏判;对严重违规场景做提前返回优化,大幅提升高风险场景下的检测效率;支持动态刷新词库,适配运营侧快速迭代的合规需求。
2.3 统一校验结果封装
基于过滤器能力,封装统一的输入校验入口,返回标准化的处置结果:
publicFilterResultcheckInput(Stringtext){RiskLevellevel=getMaxRiskLevel(text);if(level==RiskLevel.SEVERE){returnFilterResult.block("您的内容包含违规信息,无法发送");}if(level==RiskLevel.NORMAL){StringsafeText=replaceSensitiveWord(text);returnFilterResult.passWithReplace(safeText);}returnFilterResult.pass();}三、Spring AI 流式会话中的全链路集成
敏感词校验不是独立的工具模块,需要深度嵌入对话的完整生命周期。以下是基于 WebSocket + Spring AI 的流式会话服务实现,覆盖输入校验、上下文管理、流式输出、输出校验、上下文持久化全流程。
importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.web.socket.WebSocketSession;importreactor.core.publisher.Flux;importjava.util.List;@Slf4j@Component@RequiredArgsConstructorpublicclassAiSessionService{privatefinalStringRedisTemplateredisTemplate;privatefinalChatModelchatModel;privatefinalSensitiveWordServicesensitiveWordService;privatestaticfinalStringCONTEXT_KEY="ai:context:";privatestaticfinalintMAX_HISTORY_ROUND=10;/** * WebSocket 流式对话主入口 */publicvoidstreamChat(LonguserId,WebSocketSessionsession,Stringcontent){// 1. 输入侧敏感词分级校验SensitiveWordService.FilterResultresult=sensitiveWordService.checkInput(content);if(result.isBlocked()){WsMessageUtil.send(session,"ai_error",result.getMsg());return;}StringsafeContent=result.getSafeContent();// 2. 从 Redis 加载历史上下文,维护多轮对话能力List<Message>history=loadContext(userId);history.add(newUserMessage(safeContent));truncateHistory(history);// 超出最大轮次则截断旧消息// 3. 调用大模型,流式分片返回Promptprompt=newPrompt(history);Flux<ChatResponse>flux=chatModel.stream(prompt);StringBuilderfullAnswer=newStringBuilder();flux.doOnNext(resp->{Stringchunk=resp.getResult().getOutput().getContent();if(chunk!=null){fullAnswer.append(chunk);// 逐字推送给前端,实现打字机效果WsMessageUtil.send(session,"ai_chunk",chunk);}}).doOnComplete(()->{// 4. 输出侧二次合规校验SensitiveWordService.FilterResultoutputResult=sensitiveWordService.checkOutput(fullAnswer.toString());if(outputResult.isBlocked()){WsMessageUtil.send(session,"ai_withdraw",outputResult.getMsg());return;}// 5. 校验通过,将本轮对话存入 Redis 上下文history.add(newAssistantMessage(outputResult.getSafeContent()));saveContext(userId,history);WsMessageUtil.send(session,"ai_end","");}).doOnError(e->{log.error("AI调用异常 userId:{}",userId,e);WsMessageUtil.send(session,"ai_error","生成失败,请稍后重试");}).subscribe();}/** * 从 Redis 加载历史对话上下文 */privateList<Message>loadContext(LonguserId){// 实现逻辑:从 Redis Hash/String 中读取并反序列化为 Message 列表}/** * 保存对话上下文到 Redis */privatevoidsaveContext(LonguserId,List<Message>history){// 实现逻辑:序列化后写入 Redis,设置过期时间}/** * 截断超长历史,控制 Token 消耗 */privatevoidtruncateHistory(List<Message>history){if(history.size()>MAX_HISTORY_ROUND*2){history.subList(0,history.size()-MAX_HISTORY_ROUND*2).clear();}}}💡 关键设计:输出侧校验放在流式生成完成后执行,既保证了前端的打字机响应速度,又确保最终留存的上下文内容合规;若校验命中严重违规,通过
ai_withdraw指令通知前端撤回已展示的内容,避免违规信息扩散。
四、方案亮点与优化方向
4.1 核心优势
- 分级处置更灵活:摒弃“一刀切”的拦截模式,对轻微违规内容采用脱敏处理,在合规前提下最大程度保留用户体验
- 高性能低延迟:DFA 算法本地毫秒级完成检测,不依赖外部接口,完全不会拖慢对话响应速度
- 全链路覆盖:输入、输出双端校验,既管控用户侧违规输入,也规避大模型生成风险内容
- 可扩展性强:词库支持动态刷新,可无缝接入第三方内容安全接口作为二级检测能力
4.2 后续优化方向
- 增加白名单机制,避免通用词汇被误拦截
- 支持中英文混合匹配、大小写不敏感检测
- 引入相似度算法,规避变形敏感词(谐音、拆字、加符号)
- 增加审计日志与命中统计,支撑运营侧的词库迭代
总结
基于 DFA 算法的分级敏感词过滤方案,是 AI 对话系统内容安全体系中性价比极高的第一道防线。它既能独立完成大部分场景的合规校验,也可以作为第三方安全服务的前置初筛层,有效降低调用成本。
结合 Spring AI 的流式能力与 Redis 上下文管理,最终可以构建出既安全合规、又具备流畅用户体验的智能对话系统。
标签
Java, Spring AI, DFA算法, 敏感词过滤, WebSocket流式对话