1. 项目到底在解决什么问题
评论审核这件事,做过内容社区的人都知道有多痛苦。我见过不少团队,早期靠运营同学肉眼盯评论,一天几百条还能应付,等用户量上来,一天几万条的时候,人眼根本看不过来。更麻烦的是,现在的垃圾评论早就不是“打广告”“骂人”这么简单了,涉黄、涉赌、引流、辱骂、政治敏感、恶意刷屏,各种花样轮着来,光靠关键词黑名单根本拦不住。
这个项目做的事情,就是用 SpringBoot 搭一个评论审核服务,把 AI 模型的能力接进来,让机器先帮我们判断一条评论是不是有问题。同时,对于正常评论,系统还能根据评论内容自动生成回复,把用户互动率拉起来。说白了,就是给社区评论场景配一个 7x24 小时不间断的“AI 管理员”。
我最初做这个项目的起因很直接。当时手上有个社区类项目,评论量起来之后,用户投诉和垃圾信息一起暴涨,运营团队天天加班。我们试过第三方审核 API,但费用不低,而且每次请求都有网络延迟,评论是异步审核,用户体验受影响。后来想,既然团队本来就有 Java 技术栈,为什么不自己封装一层审核服务,把模型推理和业务逻辑全部收口到 SpringBoot 里,做成一个可复用、可扩展的审核中台。
这个项目适合谁参考?如果你是后端开发,想了解怎么把 AI 能力集成到业务系统里;或者你是架构师,需要在评论、弹幕、内容发布等场景做内容安全方案;又或者你只是对 SpringBoot 整合 AI 模型感兴趣,想看看一个真实项目的落地路径,都可以拿这篇文章当参考。下面我会从整体设计、模型选型、核心实现、踩坑记录这几个维度,把整个项目的来龙去脉讲清楚。
2. 整体设计与技术选型思路
2.1 为什么用 SpringBoot 做审核服务,而不是单独搞一个 Python 服务
在做技术选型之前,我先把需求拆了一遍。评论审核本质上是个管道型任务:接收评论 -> 预处理 -> 模型判断 -> 结果回调 -> 自动回复。这个链路里,SpringBoot 的优势非常明显。
团队的技术栈是 Java,评论业务本身就写在 SpringBoot 服务里,如果审核逻辑单独拆到 Python 服务,那就要多维护一套 RPC 或者 HTTP 调用,还得考虑 Python 服务的部署、监控、日志,运维成本直接翻倍。用 SpringBoot 统一收口,审核服务和评论服务可以共享基础设施,比如注册中心、配置中心、日志系统、监控告警,开发人员也不用切换语言,心智负担小很多。
可能有人会问,AI 模型大部分是 Python 生态,SpringBoot 能跑模型吗?这里要分开看。如果是传统的机器学习模型,比如朴素贝叶斯、LR、随机森林,完全可以用 Java 重新实现推理逻辑,或者用 PMML、ONNX 这类跨语言格式导出模型,Java 侧加载推理。如果是深度学习模型,比如 BERT、GPT 系列,常规做法是模型单独部署在 GPU 推理服务上,SpringBoot 通过 HTTP 或者 gRPC 调用推理服务。我这次采用的是后者,因为评论审核需要理解的语义复杂度不低,用轻量级预训练模型效果更可控。
还有一点很关键,SpringBoot 天然适合做异步任务。评论审核不能阻塞用户发评论的接口,否则用户发一条评论要等几百毫秒甚至几秒,体验会很差。所以我的设计是:用户提交评论后,接口立刻返回“评论已收到”,然后把审核任务丢到消息队列或者线程池异步处理,等模型结果出来之后再更新评论状态。这套异步机制在 SpringBoot 里实现非常顺手,用线程池加消息队列就能搞定。
2.2 整体架构:审核管道 + 回调机制 + 自动回复
这个项目我最终落地了三层架构。第一层是接入层,负责接收评论审核请求和返回审核结果;第二层是审核引擎层,把评论内容做预处理、模型推理、规则过滤、人工复核;第三层是业务动作层,审核通过就发布,审核不通过就拦截,疑似违规就进人工队列,同时触发自动回复。
审核管道可以拆成几个环节:敏感词快速过滤 -> 文本向量化 -> 模型分类 -> 规则引擎二次判断。为什么流程这么设计?因为模型推理是有成本的,而且耗时相对较长。先用敏感词库把明显违规的评论快速拦截,能省掉一部分模型调用;模型分类给一个初判结果;规则引擎再根据业务自定义规则做补充,比如同一用户短时间内发多条相似评论,这属于刷屏行为,模型不一定能判断出来,但规则可以。
自动回复模块相对独立。评论审核通过后,系统会判断这条评论是否值得回复。判断标准包括评论长度、情感倾向、是否包含疑问句、用户历史互动率等。满足条件就把评论内容和用户信息组装成 Prompt,调用生成模型得到回复文案,再由运营人员在后台确认或直接自动发布。
前后端交互流程我用了一个状态机,评论有这些状态:待审核、审核中、已通过、已拦截、待人工复核、已回复。后端用枚举管理这些状态,每次状态变更都记录日志,方便追踪问题。
2.3 审核模型怎么选:分类模型 + 生成模型要分开
很多人一听到 AI 审核,就想着用一个模型全搞定,比如直接上 ChatGPT 让它判断“这条评论是否违规”。但实际项目中,我不会这么干,原因有两点。
第一,成本问题。大模型接口按 token 计费,评论审核这种高并发场景,每天可能要调用几十万次,费用非常夸张。第二,延迟问题。大模型推理耗时较长,即便是流式输出,对审核链路来说也太慢了。
所以我采用的是双模型策略:
| 场景 | 模型类型 | 选型考量 |
|---|---|---|
| 评论内容分类 | 轻量级文本分类模型(如基于预训练模型的微调版本) | 毫秒级响应,可本地部署,判断速度快 |
| 自动回复生成 | 生成式大模型 | 回复需要语义理解和生成能力,对延迟不敏感,可以异步生成 |
文本分类模型负责判断评论属于正常、广告、辱骂、涉黄、政治敏感等类别。这里我用的是一个开源预训练模型,在中文评论数据集上做了微调,分类准确率能做到 90% 以上。生成模型用的是大模型 API,为了控制成本,只在用户评论质量较高、值得回复时才调用。
当然,如果你们团队有算法能力,也可以把分类模型直接换成更大的模型,或者用多模型投票机制。但对我来说,SpringBoot 这边更关心的是模型怎么接入、结果怎么处理,模型本身可以随时替换。所以我封装了一层模型服务接口,分类模型和生成模型都走同一套接入协议,后续换模型不影响业务代码。
3. 核心功能拆解与实现细节
3.1 评论审核管道的完整实现
先看审核管道的代码结构。我用 SpringBoot 的接口 + 策略模式把审核管道拆成多个独立组件,每个组件只做一件事,方便扩展。
public interface CommentAuditHandler { AuditResult handle(CommentAuditContext context); int order(); }每个审核组件都实现这个接口,比如敏感词过滤组件、模型分类组件、规则引擎组件,然后在管道里按 order 排序依次执行。
@Service public class AuditPipeline { private final List<CommentAuditHandler> handlers; public AuditPipeline(List<CommentAuditHandler> handlers) { this.handlers = handlers.stream() .sorted(Comparator.comparing(CommentAuditHandler::order)) .collect(Collectors.toList()); } public AuditResult execute(CommentAuditContext context) { for (CommentAuditHandler handler : handlers) { AuditResult result = handler.handle(context); if (result.shouldTerminate()) { return result; } } return context.getFinalResult(); } }这套设计的好处很明显。新增加一种审核策略,比如图片识别,只需要新增一个 Handler 实现类,不需要改动已有代码。真实项目中这个优势特别重要,因为审核策略会经常调整。
敏感词过滤组件我用的是 Aho-Corasick 算法,这个算法非常适合多关键词匹配场景。假设黑名单里有几万个敏感词,对每条评论做一次遍历匹配,时间复杂度是线性的,性能非常好。加载词库的时候,我先把词库构建成 AC 自动机,然后缓存起来。词库更新时,用版本号控制,定时刷新。
模型分类组件的核心逻辑是调用远程推理服务。这里我是用 HTTP 调用的方式,因为推理服务是独立部署的。为了提高性能,我用连接池管理 HTTP 连接,超时时间设成 500ms,超过就直接降级为规则判断,不能因为模型超时阻塞整个审核流程。
3.2 自动回复模块:不是所有评论都值得回复
自动回复这个功能,听起来简单,好像拿到评论直接调用大模型生成回复就行。但实际操作下来,有很多细节要处理。
首先是触发条件。不能用户随便发一句“哈哈哈”就去调大模型,一方面浪费钱,另一方面回复质量会很差,用户反而觉得是机器人。我加了一套评分机制,从评论长度、情感丰富度、是否包含提问、是否涉及热点话题等维度打分,只有得分超过阈值的评论才进入自动回复流程。
public class ReplyScoreCalculator { public int calculate(Comment comment) { int score = 0; if (comment.getContent().length() >= 10) { score += 2; } if (comment.getContent().contains("?")) { score += 3; } if (comment.getEmotion() != null && comment.getEmotion() != Emotion.NEUTRAL) { score += 2; } return score; } }这个阈值我调了好几轮。设得太低,会产生大量无意义回复;设得太高,很多有价值的评论得不到回应。最后把阈值定在 5 分,并且接入了黑名单机制,某些类型的评论(比如纯表情、纯数字)直接过滤掉。
生成回复的时候,Prompt 设计非常关键。我一开始的 Prompt 是“请回复以下评论:xxx”,生成的结果太通用,完全没有针对性。后来改了 Prompt 结构,把用户昵称、评论内容、评论情感、文章主题都传进去,生成的回复质量提升非常明显。
你是一个社区运营助手,正在回复用户对文章《XXX》的评论。 用户昵称:小明 用户评论:这篇文章对新手太友好了,尤其是第三章,解决了我一直以来的困惑。 请用自然、友好、略带幽默的语气回复这条评论,字数不超过50字。生成结束后,回复文案还要过一遍敏感词过滤,防止模型生成不安全内容。这一步很容易被忽略,但非常必要。另外我还会对生成的回复做去重,用 SimHash 算法计算相似度,如果两条回复太相似,就只保留一条,避免用户感觉像复读机。
3.3 人工复核流程的设计细节
AI 审核不可能做到 100% 准确,所以人工复核环节必须保留。我的设计是:模型判断为违规的评论,如果置信度特别高,直接拦截;如果置信度在临界区间,进入人工复核队列。
这里有一个关键点:人工复核队列的优先级。不能让所有疑似评论都堆在一起,否则运营人员还是看不过来。我为每条评论计算了一个风险评分,按分数从高到低排序,运营人员只需要处理最危险的 20% 评论,剩下的低风险评论直接按模型结果处理。
人工复核界面我没做太复杂,就是一个待审列表,每条评论显示内容、模型分类、风险评分、相似历史案例。运营人员可以一键通过、拦截或者拉黑用户。每次人工操作都会记录日志,这些日志后续可以回流到模型的训练数据,形成闭环优化。
4. 参数配置与 SpringBoot 集成实践
4.1 配置文件怎么组织,不同环境怎么切换
SpringBoot 的多环境配置在这种项目中特别实用。我分了 dev、test、prod 三套环境,每套环境有独立的配置。最核心的配置项包括模型服务地址、线程池参数、消息队列地址、敏感词库版本号。
# application-prod.yaml audit: model: classify-url: http://internal-classify-service:8080/predict classify-timeout-ms: 500 generate-url: https://api.llm.example.com/v1/generate generate-timeout-ms: 3000 pool: core-size: 8 max-size: 32 queue-capacity: 10000 rules: ad-keywords-enabled: true high-risk-confidence: 0.95 manual-review-confidence: 0.8这里要特别说明线程池参数。一开始我用的默认线程池,结果高并发下审核任务排队严重。后来观察了一下,审核任务大部分时间在等模型返回,属于 IO 密集型任务,所以核心线程数可以设置大一点,但队列长度不能无限长,否则内存会爆。
我最终配置是核心线程 8,最大线程 32,队列容量 10000,拒绝策略是 CallerRunsPolicy。这意味着如果队列满了,任务会回到调用线程执行,不会丢失任务,但会拖慢主流程,算是一种背压机制。这样配置的好处是系统不会因为任务堆积而宕机,只是响应变慢,在审核场景下可以接受。
4.2 接口设计:同步接口 + 异步回调双通道
评论审核的接口设计,我走了同步和异步两条路。
对于必须要立刻知道审核结果的场景,比如用户发布会触发敏感提示,我用同步接口,前端等待审核结果返回。这种情况下,审核链路只走敏感词过滤和规则引擎,不走模型分类,因为模型太慢。对于大部分评论,走异步通道:用户发评论接口立刻返回成功,后台异步审核,审核完成之后通过消息队列通知评论服务更新状态。
POST /api/comment Request Body: { "content": "这篇文章太有帮助了", "userId": 123, "articleId": 456 } Response: { "commentId": 789, "status": "AUDITING" }异步审核的回调我用的不是 HTTP 回调,而是内部消息通知。因为评论服务和审核服务在同一个应用里,没有必要跨网络回调,直接发 Spring 事件,监听器处理评论状态更新,简单可靠。
自动回复和审核是彻底解耦的。审核通过后,评论服务会发布一个“评论审核通过”事件,自动回复模块监听这个事件,判断是否需要生成回复。这样做的好处是,如果自动回复服务出问题,不会影响评论发布主流程。
4.3 敏感词库的动态更新机制
敏感词库不能写死在配置里,否则每次更新词库都要重新发布应用。我的做法是用数据库存储敏感词,应用启动时加载到本地内存,同时开启一个定时任务,每隔一分钟查一次数据库,如果有更新就把最新词库同步到内存里的 AC 自动机。
这里有个并发问题要处理。如果词库正在构建新的 AC 自动机,同时有请求在用旧自动机做匹配,直接用替换引用就行,旧请求走完自然结束,新请求走新自动机,不会出现数据不一致。
词库来源我接了好几个渠道:人工维护的黑名单词、第三方内容安全服务返回的违规词、用户举报评论中提取的高频违规词。这些渠道的词统一汇聚到数据库,后台可以手动管理词库,也可以看每个词的拦截量。运营同学反馈这个功能特别好用,因为能直观看到哪些词是当前违规的主流。
5. 实战过程:从 Demo 到可上线服务
5.1 第一步:先把最简单的规则引擎跑通
我刚开始搭这个项目的时候,没有一上来就接模型,而是先实现规则引擎。因为规则引擎是确定性的,逻辑清晰,容易测试。第一步做好之后,整个审核服务的骨架就出来了,后续往里面加模型分类器只是填肉。
规则引擎我用了表达式引擎来配置规则,这样运营同学不需要改代码就能调整规则。比如可以配置这样的规则:如果评论中出现某个营销词汇且评论长度小于 20,判定为广告。规则配置存数据库,后台可以动态增删。
这一步做完之后,最基础的审核能力已经具备。我当时的验收标准是:审核接口能调用、敏感词过滤能生效、状态流转正常、人工复核队列能展示数据。
5.2 第二步:接模型之前,先搭好模型服务的抽象层
接模型的时候,我踩了一个不小的坑。最开始我直接写了一个 ClassifierClient,接口是 model.predict(content),然后业务代码直接 new 这个客户端。后来想换成另一个模型,发现业务代码改了一堆,非常痛苦。于是重构了一下,定义了一个通用的 ModelGateway。
public interface ModelGateway { ModelResult classify(String text); String generate(String prompt); }生产环境用 HTTP 实现远程调用,测试环境用 Mock 实现返回固定结果。这样做的好处是,开发环境不需要连真实的模型服务,本地跑测试也快。每次切换模型,只需要修改配置里的服务地址,业务代码完全不用动。
模型接入之后,我对分类结果做了一个置信度映射。模型的原始输出是一个概率分布,比如正常 0.7、广告 0.2、辱骂 0.1。我把概率转换成置信度等级,并且设置了一个规则:最高概率大于等于 0.95 直接拦截,概率在 0.8 到 0.95 之间进入人工复核,低于 0.8 的正常发布。这个阈值非常关键,直接影响误杀率和漏网率。这个值不是拍脑袋定的,而是通过对历史评论做回放测试得到的。
5.3 第三步:自动回复模块的 Prompt 迭代
自动回复模块最花时间的部分在 Prompt 调试。第一版简单 Prompt 生成的回复非常生硬,比如用户评论“写得真好”,回复是“感谢您的支持”。这种回复属于废话文学,用户看了没有任何感觉。
后来我做了几个改动:把文章上下文传进去,让模型知道用户在讨论什么;把用户评论中的核心观点提取出来,在回复里做针对性回应;限制回复字数,避免长篇大论。经过几轮迭代,生成的回复质量明显好转,运营同学反馈说“看起来像真人运营在回复”。
不过我还是保留了人工审核自动回复的开关。默认情况下,自动回复先进入草稿状态,运营人员在后台一键确认发布。跑了一段时间,确认生成的回复没有安全问题,才逐步放开全自动。这里我建议所有做自动回复功能的人,初期一定要加人工确认环节,一旦模型出现问题,后悔都来不及。
5.4 第四步:性能压测与调优
上线前我对审核服务做了压测。压测数据是模拟用户评论的高峰流量,目标是最少支撑每秒 100 条评论审核。
第一次压测结果不理想,QPS 只有 40 左右,瓶颈在模型调用。因为每一条评论都要调用远程分类服务,HTTP 连接建立和销毁开销很大。后来我做了两个优化:HTTP 连接池复用连接,减少握手开销;增加本地缓存,相同内容的评论直接走缓存,不再调用模型。对于实际场景来说,用户刷屏时大量评论内容相同或相似,这个缓存命中率相当高,性能提升立竿见影。
第二个瓶颈在数据库。审核记录每次都要写入数据库,高峰期写入压力很大。我改成先写内存队列,批量刷入数据库,写入性能提升了好几倍。代价是数据库会有短暂延迟,但对审核场景来说完全没问题。
最终压测结果,单机 QPS 稳定在 150 左右,远超预期。
6. 常见问题与排查技巧实录
6.1 模型误杀率太高,正常评论被拦截
这种情况一般不是模型本身的问题,而是阈值设置不合理。我第一次上线的时候,置信度阈值设得太低,导致很多正常评论被模型判成疑似违规,用户体验非常差。
排查方法:把被拦截的评论导出来,逐条看模型输出的概率分布,找出被误杀评论的概率值区间,然后调整阈值。还有一个技巧是,加入一个“白名单机制”,对于高信用用户,降低拦截阈值;对于新注册用户,提高拦截阈值。这套策略上线后,误杀率下降非常明显。
6.2 自动回复内容太生硬,用户一眼看出是机器人
这个问题我前面提过,核心原因是 Prompt 不够好。排查时不要只看生成结果,要看 Prompt 里传递的信息是否充足。如果只给模型一句评论内容,模型只能给出泛泛的回复。把文章标题、文章主旨、评论上下文、用户特征信息都传进去,模型的表现会有质的提升。
另外,要注意生成参数的设置。温度参数太高,回复会偏离主题;太低,回复又像复读机。我最后把温度设成了 0.7,既有一点创造性,又不会太离谱。
6.3 审核链路超时,评论接口响应变慢
出现这个问题,先要区分是同步审核还是异步审核。同步审核链路里,敏感词过滤是内存操作,应该很快,如果超时,问题大概率在规则引擎配置上。我遇到过一条正则表达式写得太复杂导致匹配耗时几秒钟的情况,直接把正则拆成多个简单规则就好。
异步审核超时,要看线程池有没有被打满。排查方法很简单,看线程池的活跃线程数和队列大小,如果队列一直增长,说明消费者能力不足。这时候优先考虑扩容消费者节点,而不是盲目调大线程池。等模型服务响应变快了,再把线程池调回来。
6.4 数据一致性问题:评论状态和审核结果不一致
这个问题的原因往往在消息重复消费。消息队列在某些异常情况下会重复投递消息,如果审核回调逻辑没有做幂等处理,就可能出现同一条评论被审核两次,状态被覆盖。
解决方法是加一个消费幂等判断。处理每条消息前,先查评论当前状态,如果是终态(已拦截、已发布),直接跳过;否则才执行状态更新。状态更新要走乐观锁,用版本号控制并发,否则两个人同时处理同一条评论,后写覆盖先写,数据就乱了。
7. 我在实际落地中踩过的坑和体会
做这个项目前前后后花了一个多月,最大的体会是:AI 审核不是把模型接进来就完事,工程化才是大头。
模型推理只是整个链路的一环,上游有内容预处理、敏感词过滤、触发条件判断,下游有状态流转、人工复核、数据回流。每一环都需要细致的工程设计和反复打磨。上线之后,我不是盯着模型准确率,而是盯着误杀率和漏网率。这两个指标才是业务真正关心的,模型准确率高不代表安全。现状是有些违规内容模型能识别,但某些变体或者绕过多字、谐音的词,模型还是会有漏网。所以系统里保留人工复核通道非常有必要。
调试 Prompt 是一件很有意思的事。同样的模型,Prompt 写得好不好,生成结果天差地别。我建议大家在做自动回复功能时,一定要把 Prompt 当成产品需求来对待,写完之后要找不同的人测试反馈,多轮迭代。
另外,日志和监控一定要从第一天就做好。审核服务出问题的时候,都需要借助日志来判断是模型出了问题还是链路出了问题。我给每个审核任务分配了一个 requestId,整个审核链路的所有日志都带上这个 ID,排查问题时用 ID 一查就能看到完整调用链。这个习惯帮我省了很多时间。
最后说一个扩展思路。这个审核服务的架构不只是能做评论审核,弹幕审核、文章内容审核、用户昵称审核、图片 OCR 审核,都是同一套管道模型。未来如果业务方需要审核短语音内容,只需要新增一个音频转文本的 Handler,就能复用后面所有的审核组件。想把这个项目做成中台体系的团队,可以顺着这个方向继续扩展。
这个项目的完整代码我整理在代码仓库里了,包含审核管道、模型网关、自动回复模块、人工复核接口、数据库建表 SQL,有需要的可以参考。有问题可以直接评论区讨论,我看到了都会回。