🤖
AI审核中

从“能评论”到“会治理”:我如何实现 AI 审核、AI 回复与表情包系统

Java 16分钟 115浏览 1评论

评论区看起来只是一个输入框、一张评论表和几条回复,但只要加入 AI 审核、自动回复和图片表情,它就会迅速变成一个小型内容治理系统。

在我的博客里,一条评论从提交到公开展示,可能经历文本清洗、本地敏感词过滤、大模型审核、多模态图片识别、状态迁移、AI 回复、邮件通知和前端实时更新。任何一个环节超时、重试或与后台人工操作并发,都可能产生重复回复、状态倒退、违规内容提前曝光等问题。

这篇文章不只介绍如何调用大模型 API,而是复盘我如何把 AI 审核、AI 回复与表情包功能串成一条可靠、可恢复、可人工接管的评论流水线。

一、先把 AI 放到正确的位置

我的项目基于 Spring Boot、MyBatis Plus、MySQL、Thymeleaf 和原生 JavaScript。评论系统原本已经支持楼中楼回复、邮件通知和后台审核,因此接入 AI 时,我没有让模型直接控制数据库,而是把它放在业务服务层中:模型负责给出判断和生成文本,应用负责状态迁移、内容安全和最终落库。

整体流程如下:

flowchart TD
    A[用户提交评论] --> B[内容清洗与安全入库]
    B --> C[状态设为 PENDING]
    C --> D[事务提交后触发异步任务]
    D --> E[本地敏感词预筛]
    E -->|命中| F[REJECTED]
    E -->|未命中| G{是否含可信表情包图片}
    G -->|否| H[文本模型审核]
    G -->|是| I[多模态模型审核]
    H --> J{审核结果}
    I --> J
    J -->|违规| F
    J -->|模型异常| K[AUDIT_FAILED / 人工复核]
    J -->|通过| L[APPROVED]
    L --> M[生成 AI 回复]
    M -->|成功| N[事务内保存回复并置为 AI_REPLIED]
    M -->|失败| O[AI_REPLY_FAILED]
    N --> P[前端展示评论与回复]
    O --> Q[评论仍可见,停止等待回复]

这里最重要的一条原则是:AI 输出只是候选结果,业务状态才是真相。

二、评论先落库,AI 在事务提交后处理

用户提交评论时,我不会同步等待大模型返回。同步调用虽然代码简单,但会把模型延迟直接传递给用户,还可能因为事务回滚造成“AI 已处理、评论却不存在”的悬空任务。

因此,评论保存阶段只做三件事:

  1. 将评论设为 PENDING,默认不公开;
  2. 清洗内容并写入数据库;
  3. 注册事务提交回调,在 afterCommit 中触发异步审核。

核心思路可以概括为:

comments.setIsVisible(CommentAuditStatus.PENDING.getCode());
comments.setIp(CommentAuditStatusCodec.encode(
        comments.getIp(), CommentAuditStatus.PENDING, null));

String escaped = CommentContentHtml.sanitizeSubmission(comments.getContent());
comments.setContent(convertOnlyEmojis(escaped));
commentsMapper.insert(comments);

TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                aiCommentTaskService.asyncProcessComment(comments.getId());
            }
        });

异步任务使用独立线程池执行,失败后会进行有限次数重试。此外,系统每 5 分钟扫描遗留的待审核评论,每 30 分钟重试审核失败或回复失败的评论。即时触发负责体验,定时扫描负责兜底,两者组合后,即使应用在任务触发瞬间重启,评论也不会永久卡住。

三、AI 审核不是一次调用,而是分层过滤

审核链路分为三层。

1. 本地规则做低成本预筛

内容和昵称先经过本地敏感词正则匹配。规则明确、命中即拒绝的内容没必要消耗模型调用,也能减少外部服务故障对基础风控的影响。

但本地规则不适合承担全部审核工作。谐音、上下文攻击、隐晦广告和图片含义都很难靠关键词准确识别,因此未命中的评论还要进入模型审核。

2. 文本与图片分流

普通文本和系统内置小表情走文本模型;只有来自可信表情包域名、且 URL 满足固定路径与文件名格式的图片,才进入多模态模型。

这一区分解决了两个问题。

第一,系统内置表情本身已经有确定语义。服务启动时会预加载表情元数据,审核前把 [:kiss:] 之类的标记替换为文字描述,例如“亲吻、表达喜欢”。模型不需要读取图片,也不会把资源地址误判成外链广告。

第二,斗图表情的语义来自图片本身,只看 URL 无法判断内容。因此系统最多提取有限数量的可信图片,将文字提示和图片 URL 组合成多模态消息。图片模型不可用时,系统不会冒险放行,而是返回 AUDIT_FAILED,交由人工复核。

3. 强约束结构化输出

审核提示词明确列出色情暴力、政治敏感、人身攻击、广告引流、恶意刷屏和昵称违规等规则,同时把系统表情地址列入白名单。模型必须返回固定 JSON:

{
  "approved": false,
  "confidence": 0.96,
  "reason": "广告营销"
}

应用层负责解析和校验,而不是相信一段自然语言。模型返回空内容、格式错误或调用异常时,统一进入失败分支。

模型调用本身也设计了路由降级。文本请求会依次尝试“主凭据 + 首选模型”“主凭据 + 备用模型”“备用凭据 + 首选模型”“备用凭据 + 备用模型”;每条路由内部还有有限重试。这样可以缓解单模型限额或单密钥故障,但不会无限重试拖垮线程池。

四、用状态机解决并发和重试问题

如果只有“可见/不可见”两个状态,就无法区分正在审核、审核拒绝、模型故障、正在回复和回复失败。我的实现将评论拆成七个详细状态:

状态 含义 是否公开
PENDING 等待审核
APPROVED 审核通过
REJECTED 内容违规
AUDIT_FAILED 审核服务异常,待人工处理
AI_REPLYING 已通过,正在生成回复
AI_REPLIED AI 回复已保存
AI_REPLY_FAILED 评论通过,但回复失败

项目历史表结构只有 is_visible,没有独立的详细状态字段。为了兼容旧库,当前实现通过 CommentAuditStatusCodec 把详细状态和原因编码到原有字段中,同时继续使用 is_visible 控制公开性。这是一个实用的平滑迁移方案,但从长期维护看,新增 audit_statusaudit_reasonversion 独立字段会更清晰。

仅有状态枚举还不够,状态更新必须是有条件的。例如 AI 只能把 PENDING 更新为 APPROVED,不能覆盖管理员刚刚做出的人工拒绝。实现中会先读取当前状态,再把旧的可见值和状态编码放进 WHERE 条件:

UPDATE article_comments
SET is_visible = ?, ip = ?, update_time = ?
WHERE id = ?
  AND is_visible = ?
  AND ip = ?;

这相当于一次轻量级 CAS(Compare And Set)。如果更新行数为 0,说明状态已经被其他线程或管理员修改,迟到的 AI 结果立即作废。

状态机也让重试更精确:

  • AUDIT_FAILED 可以重新执行审核;
  • AI_REPLY_FAILED 只重试回复,不重复审核;
  • 人工终态不再进入 AI 流程;
  • 保存 AI 回复与原评论切换到 AI_REPLIED 位于同一事务中,避免“状态已完成但回复没插入”。

这比简单的幂等键更贴合评论业务,因为它同时处理了重试、人工接管和步骤级恢复。

五、AI 回复要理解文章、评论和表情语气

审核通过后,非博主本人提交的评论才会进入自动回复。回复提示词不只包含当前评论,还会带上文章标题和截断后的正文,让模型知道读者在讨论什么。

系统支持友好、专业、幽默三种回复风格,并对回复做了明确限制:针对评论具体内容、控制字数、不暴露 AI 身份、不使用任意 Unicode 表情,只允许从系统已有表情名中选择少量表情,格式固定为 [:表情名:]

这里没有让模型自由生成图片标签,而是让它生成受控的领域标记。应用随后验证表情名并转换为标准 HTML。这样既能保持博客的视觉风格,也避免模型生成不存在的资源或不安全标签。

对于斗图评论,回复模型同样会接收可信图片,从视觉内容中理解对方是在赞同、调侃还是表达疑问。最终回复会经过清理和质量评分,再以普通子评论的形式落库,并带有“以上回复由 AI 自动生成”的透明提示。

通知策略也跟随业务结果:顶级留言先通知博主,回复他人时只通知直接父评论作者;AI 回复成功后,评论提交者收到审核通过与回复内容合并的通知;回复失败则退化为单纯的审核通过通知,避免因为生成环节异常让用户误以为评论也失败了。

六、表情包功能的重点其实是安全边界

表情包选择器的数据来自公开站点,但浏览器并不直接抓取对方页面。后端提供 /api/stickers 适配层,负责搜索、分页、解析和校验,图片文件仍由来源站点直接提供。

后端只接受满足以下条件的图片地址:

  • 必须是 HTTPS;
  • Host 必须精确等于白名单域名,不能是形如 trusted.example.evil.com 的伪装域名;
  • 路径必须匹配 /data/doutula/{32位十六进制ID}.{扩展名}
  • 不允许用户信息、异常端口、查询参数和片段;
  • 扩展名只允许 JPG、JPEG、PNG、GIF、WebP。

抓取器还设置了连接与读取超时、1 MB 响应上限、禁止自动重定向、最多 50 个结果、关键词和页码上限。这些限制不仅是性能优化,也是在降低 SSRF、内存耗尽和上游脏数据风险。

为了减少对第三方站点的依赖,最新列表缓存 10 分钟,搜索结果缓存 20 分钟,LRU 最多保留 240 项。如果上游临时失败,6 小时内的旧缓存仍可返回,并通过 stale 字段提示前端“当前展示缓存结果”。

前端选择器支持搜索、翻页、加载骨架、失败重试和移动端布局。用户点击表情包后,编辑器中插入一个不可编辑、不可拖拽的图片节点;提交时再序列化为规范标记或标准图片标签。

七、为什么 HTML 清洗必须前后端各做一层

contenteditable 编辑器中的内容不能直接作为可信 HTML 入库。用户可以绕过前端请求接口,所以真正的安全边界必须在服务端。

提交时,系统会递归解码 HTML 实体,防止攻击者通过多次编码绕过检查;普通标签统一转义,仅临时提取并保留符合白名单格式的斗图图片。系统内置表情随后由独立逻辑转换为固定尺寸的 <img>

展示历史数据时还会再次规范化,只重建允许的图片、链接和少量格式标签,并为外部新窗口链接补充 noopener noreferrer。邮件内容也复用同一套规范化逻辑,避免网页安全了、邮件模板却重新引入注入风险。

前端的 URL 正则和 DOM 限制主要用于改善体验,后端的白名单重建才负责安全。两层规则应保持一致,但不能把前端校验当作信任依据。

八、前端实时反馈:轮询并不落后,关键是有终态

评论提交后,页面先显示本地占位项,然后每 3 秒查询一次评论详情,最多轮询 3 分钟。接口根据状态返回最小必要信息:

  • PENDING:显示“AI 审核中”;
  • AI_REPLYING:评论已经可见,显示“AI 生成回复中”;
  • AI_REPLIED:插入 AI 回复并停止轮询;
  • REJECTED / AUDIT_FAILED:展示原因并停止轮询;
  • AI_REPLY_FAILED:保留已通过评论,不再等待回复。

对于个人博客的评论量,短时轮询比引入 WebSocket 或消息中间件更轻量。真正重要的是每个状态都有清晰的继续条件和终止条件,否则前端很容易永远等待。

接口还会沿父评论链检查公开性:即使某条子回复本身已通过,只要祖先评论不可见,它也不能通过详情接口泄露。这个细节对楼中楼评论非常重要。

九、测试重点不是“模型答得对不对”

大模型输出具有不确定性,单元测试不应依赖真实模型。我的测试通过模拟 HTTP 服务和数据库状态,重点验证系统约束:

  • 人工已审核的评论会跳过 AI;
  • AI 迟到时不能覆盖管理员状态;
  • 回复失败重试不会重复审核;
  • 文本模型与凭据按预定顺序降级;
  • 可信表情包会作为多模态输入发送;
  • 图片模型无最终答案时转人工审核;
  • 恶意域名、非法协议和异常分页会被拒绝;
  • 上游故障时能返回旧缓存;
  • 评论 HTML、表情标记和 AI 声明经过规范化后仍然安全。

换句话说,测试的是确定性的编排层,而不是不确定性的模型智力。

十、复盘:三个功能最终汇成一个系统

最初看,AI 审核、AI 回复和表情包像是三个独立需求。真正落地后,它们彼此强相关:表情包改变了审核输入,审核决定回复是否执行,回复又要理解表情语气;异步执行带来状态一致性问题,外部图片则扩大了安全边界。

这套实现给我最大的启发有五点:

  1. AI 应负责判断与生成,应用必须掌握流程和最终写入权;
  2. 内容先安全入库,再异步处理,默认不可见;
  3. 文本、小表情和真实图片应走不同的理解路径;
  4. 用显式状态机和条件更新抵抗重试、并发与迟到结果;
  5. 任何外部内容都要经过白名单、限额、缓存和失败降级。

当这些基础设施搭好后,未来无论增加垃圾账号评分、人工复核队列、审核记录表,还是把轮询升级为 SSE,都不需要推翻现有流程。因为系统的核心已经不再是某个具体模型,而是一条边界清晰、能够恢复、允许人工接管的内容治理流水线。

1 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  1 条评论
伴我   湖南省衡阳市