Java 程序员的 AI 进化论 | Spring Boot 搭企业智能客服,从设计到上线

Java 程序员的 AI 进化论 | Spring Boot 搭企业智能客服,从设计到上线

上个月运营那边找我,说人工客服快顶不住了。8 个人的组,每天收 600 多条工单,七成是「订单怎么查」「发票能不能开」「退货流程是什么」这类翻来覆去的问题。领导拍板要搭一套智能客服,先顶住常见问题,复杂的再转人工。

我花了三周从零搭了一版,跑了一周,日均自动解决率能到 58%,剩下转人工。这篇把我怎么设计、怎么实现、踩了哪些坑都写出来,给同样要做这块的兄弟省点时间。

一、整体设计与架构选型

动手前我先扒了三天现有工单数据,把高频问题归了类。不归不知道,一归类发现 70% 的问题其实集中在 8 个意图上。

问题类型 占比 是否适合 AI 接 处理方式
订单状态查询 22% 调订单接口直接答
发票问题 14% 知识库检索
退换货流程 13% 知识库检索
优惠券使用 9% 知识库检索
物流咨询 8% 调物流接口
账号异常 6% 部分 知识库+转人工
投诉与纠纷 18% 直接转人工
其他长尾 10% 转人工

定下思路:意图识别分流 + 知识库检索兜底 + 命中不了就转人工。不追求 AI 全包,只把那 70% 重复劳动吃掉。

架构上几个核心模块我心里有数了:对话引擎管多轮上下文,知识库管检索,转接策略管何时给人,质量监控管事后复盘。技术栈方面我做了个对比,给你们参考:

方案 优点 缺点 我的结论
纯大模型直答 实现快 幻觉严重、无业务数据 否,不敢上线
大模型+RAG 能控范围、可溯源 检索质量依赖切分 采用
传统意图机器人 可控、便宜 维护成本高、不灵活 与RAG结合做分流
接第三方客服SaaS 省事 数据出境、定制弱 否,公司不让

我选的是意图分类(轻量规则+模型兜底)+ RAG 检索 + 大模型生成的组合。底层 Spring Boot 3.2 + Java 17,对话上下文存 Redis,知识库用 PgVector 做向量检索。

整体流程我画了张图,一眼能看明白从用户提问到回答或转人工的链路:

二、对话引擎:多轮上下文怎么管

这块是整个系统的心脏。用户问「订单 12345 啥时候到」,紧接着问「那能改地址吗」,第二句里的「那」指的是上一句的订单。AI 要能接住这种上下文。

我的做法是每个会话维护一个 Session,对话历史存 Redis,给大模型的时候只取最近 4 轮(8 条消息),再多就截断。理由很简单------上下文塞太长,一是贵,二是大模型会被无关历史带偏。

java 复制代码
package com.example.chatbot.engine;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Component
public class ChatSessionManager {

    private static final int MAX_ROUNDS = 4; // 只保留最近4轮,控制成本和噪音
    private final StringRedisTemplate redis;
    private final ObjectMapper mapper = new ObjectMapper();

    public ChatSessionManager(StringRedisTemplate redis) {
        this.redis = redis;
    }

    // 追加一条消息并裁剪历史,超长自动丢弃最早的消息
    public void appendMessage(String sessionId, String role, String content) {
        String key = "chat:session:" + sessionId;
        try {
            ChatMessage msg = new ChatMessage(role, content);
            redis.opsForList().rightPush(key, mapper.writeValueAsString(msg));
            redis.opsForList().trim(key, -MAX_ROUNDS * 2, -1); // 只留最近4轮
            redis.expire(key, Duration.ofMinutes(30)); // 30分钟无活动自动过期
        } catch (Exception e) {
            throw new RuntimeException("保存会话历史失败", e);
        }
    }

    public List<ChatMessage> getHistory(String sessionId) {
        String key = "chat:session:" + sessionId;
        List<String> raw = redis.opsForList().range(key, 0, -1);
        List<ChatMessage> history = new ArrayList<>();
        if (raw == null) return history;
        for (String s : raw) {
            try {
                history.add(mapper.readValue(s, ChatMessage.class));
            } catch (Exception ignored) { }
        }
        return history;
    }

    // 标记上一轮是否未命中知识库,用于"连续两轮未命中转人工"判断
    public void markMissed(String sessionId, boolean missed) {
        redis.opsForValue().set("chat:missed:" + sessionId,
                String.valueOf(missed), Duration.ofMinutes(30));
    }

    public boolean wasLastMissed(String sessionId) {
        return "true".equals(redis.opsForValue().get("chat:missed:" + sessionId));
    }

    public record ChatMessage(String role, String content) { }
}

这里我踩了第一个坑。第一版我没设截断,把整个会话历史全喂给大模型,有个用户聊了 40 多轮,单次请求 token 直接飙到 1.2 万,接口延迟从 2 秒涨到 9 秒,还偶发超时。更要命的是大模型被早期无关对话干扰,开始答非所问。

加了 trim 只留最近 4 轮后,单次 token 稳定在 1500 以内,延迟回到 2 秒出头。多轮对话不是上下文越多越好,够用就行,多了是负担。

三、知识库检索与回答生成

意图分完流,能查接口的直接查接口(比如订单状态调订单服务),查不了的走 RAG。RAG 这套我之前写过一篇进阶优化的,这里只讲在客服场景里最关键的一环------检索不到时怎么办

很多 RAG 教程默认「总能检索到」,实际线上知识库命中率没那么高。我的做法是设一个相似度阈值,低于阈值的不硬答,直接转人工。宁可少答,不能瞎编。

java 复制代码
package com.example.chatbot.rag;

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class KnowledgeQAService {

    private static final double SIM_THRESHOLD = 0.72; // 低于这个分不答,转人工
    private final VectorSearchService vectorSearch;
    private final LlmClient llm;

    public KnowledgeQAService(VectorSearchService vectorSearch, LlmClient llm) {
        this.vectorSearch = vectorSearch;
        this.llm = llm;
    }

    public QAResult answer(String question, List<ChatMessage> history) {
        List<DocChunk> hits = vectorSearch.search(question, 5);
        if (hits.isEmpty() || hits.get(0).score() < SIM_THRESHOLD) {
            // 检索质量不够,不要硬编,交给人工
            return QAResult.needHuman("知识库未命中,相似度不足阈值");
        }
        String context = buildContext(hits);
        String prompt = """
            你是企业客服助手,只能依据下面提供的资料回答用户问题。
            资料里没有的内容,直接回复"这个问题我需要转给人工客服"。
            资料:%s
            用户问题:%s
            """.formatted(context, question);
        String answer = llm.chat(prompt, history);
        return QAResult.ok(answer, hits.get(0).score());
    }

    private String buildContext(List<DocChunk> hits) {
        StringBuilder sb = new StringBuilder();
        for (DocChunk d : hits) {
            sb.append("- ").append(d.title()).append(":").append(d.text()).append("\n");
        }
        return sb.toString();
    }

    public record DocChunk(String title, String text, double score) { }
    public record QAResult(boolean ok, boolean needHuman, String content, double score) {
        static QAResult ok(String content, double score) {
            return new QAResult(true, false, content, score);
        }
        static QAResult needHuman(String reason) {
            return new QAResult(false, true, reason, 0);
        }
    }
}

第二个坑就在这。第一版我没设阈值,想着让大模型自己判断有没有资料。结果用户问「你们支持比特币支付吗」,知识库里压根没这条,大模型硬是编了一句「支持,请咨询客服开通」,运营差点炸锅。

大模型在没有依据时会"合理地编",这是它最危险的特点。 加了阈值 + Prompt 里明确「没有就转人工」之后,这种幻觉基本没了。阈值我调过几次,0.72 是我用 200 条标注问题测出来的平衡点:

相似度阈值 命中率 幻觉率 转人工率 我的评价
0.60 82% 11% 7% 幻觉太多,不敢用
0.72 68% 2% 23% 平衡点,采用
0.80 51% 0.5% 41% 太保守,转人工太多

四、人工转接与质量监控

转人工不是失败,是设计的一部分。关键是什么时候转、转给谁。

我定的转接规则有三条:检索连续两轮低于阈值、用户明确要求人工、对话里出现负面情绪词。第三条我用了简单关键词匹配,不做复杂情感分析,够用。

java 复制代码
package com.example.chatbot.router;

import org.springframework.stereotype.Service;
import com.example.chatbot.engine.ChatSessionManager;

@Service
public class HumanHandoffPolicy {

    // 负面情绪关键词,命中即转人工(简单粗暴但有效)
    private static final String[] NEGATIVE_WORDS = {
        "投诉", "差评", "骗子", "退款不退", "你们怎么回事", "气死", "态度差"
    };

    private final ChatSessionManager sessionManager;

    public HumanHandoffPolicy(ChatSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public boolean shouldHandoff(String sessionId, String userText, boolean thisRoundMissed) {
        // 规则1:连续两轮未命中知识库
        if (thisRoundMissed && sessionManager.wasLastMissed(sessionId)) {
            return true;
        }
        // 规则2:用户主动要人工
        if (userText.contains("人工") || userText.contains("真人")) {
            return true;
        }
        // 规则3:负面情绪
        for (String w : NEGATIVE_WORDS) {
            if (userText.contains(w)) return true;
        }
        return false;
    }
}

质量监控这块我做得轻,但必须有。每天抽样 5% 的对话人工复核,统计 AI 回答的准确率和满意度。没监控的 AI 客服上线就是裸奔,出了问题你都不知道。

上线一周后的数据我拉了张表:

指标 上线前(纯人工) 上线一周后 变化
日均工单量 612 598 -2%
人工处理量 612 251 -59%
AI 自动解决率 0% 58% +58%
平均响应时长 4分20秒 3秒 -99%
首次解决率 71% 76% +5%
用户满意度 82分 79分 -3分

注意看末尾那行,满意度掉了 3 分。我去翻了抽样记录,发现是 AI 回答虽然快,但有些用户觉得「跟机器人聊不如跟人聊」,体验上打折。这是预料中的,后续我在 AI 回复末尾加了「没解决可回复转人工」的提示,满意度回升到 81 分。

五、踩坑补充与几点提醒

除了前面说的上下文截断和幻觉两个坑,再补几个:

坑三:大模型把内部工单号当订单号查。 用户说「我的单号是 2026081300012」,这是工单号不是订单号,AI 直接拿去查订单接口报空。后来我在意图识别里加了号段规则------订单号 12 位纯数字、工单号 13 位带日期前缀,分开了才不乱。

坑四:高峰期接口限流。 大模型 API 有并发限制,促销日咨询量翻三倍,排队超时用户等不及直接挂了。我加了本地排队 + 超时降级,超过 8 秒没返回就先回一句「正在为您查询,稍等」并转人工,体验好很多。

还有一点想强调:智能客服不是用来替代人工的,是用来过滤噪音的。 把 70% 的重复问题交给 AI,让人工腾出手处理真正需要判断的 30%,这才是正确姿势。指望 AI 全包,到头来一定翻车。

六、总结

这套系统我跑了一周多,最直观的感受是------AI 客服的价值不在于多聪明,而在于把重复劳动自动化,让人工做更有价值的事。搭的时候盯住三个点:检索有没有命中、没命中别硬编、转人工要果断。

给你一份落地检查清单,照着走少踩坑:

检查项 建议
对话上下文 只留最近 4 轮,超过截断,别全量喂模型
检索阈值 用标注数据测出平衡点,0.7 附近起步
幻觉防控 Prompt 明确"无依据转人工",别让模型自己编
转人工规则 连续未命中+主动要求+负面情绪,三选一即转
号码识别 区分订单号/工单号/物流单号,别混查
高峰降级 设超时阈值,超时先安抚再转人工
质量监控 每天抽样 5% 复核,没有监控等于裸奔
满意度兜底 AI 回复末尾留转人工入口,别堵死

有任何问题评论区聊,能答的我都答。

相关推荐
旺仔学长 哈哈2 小时前
别只做车辆 CRUD:Spring Boot 共享汽车管理系统从预约到还车的完整实现---源码53766
java·spring boot·在线预约·共享汽车
weixin_BYSJ19872 小时前
springboot技能与工具共享小程序---附源码29657
java·javascript·spring boot·python·小程序·django·php
全栈弄潮儿2 小时前
4 个新手就能直接套用的 AI 编程提示词模板
chatgpt·openai·ai编程
茶本无香2 小时前
通用报表自动化框架:Java调用Shell传参执行PostgreSQL SQL模板
java·sql·postgresql·shell
weixin_BYSJ19872 小时前
flask民族服饰饰品商城小程序---附源码37399
java·javascript·spring boot·python·小程序·django·php
爱笑的源码基地2 小时前
智慧工地源码, 智慧工地环境监测系统架构与绿色施工闭环设计
java·源码·智慧工地·绿色工地
YDS8292 小时前
大营销平台 —— 第二阶段应用接口实现
java·spring boot·ddd
一水2 小时前
AI 时代审查思维:审查第一篇
java·jvm·数据库·spring
RainCityLucky3 小时前
Java Swing 自定义组件库分享(十六)
java·笔记·后端