跳到正文
博客园· Minion~~·· 2 小时前AI 评分55

智语开源企业级 AI 智能客服平台:微服务 + RAG + 全链路可观测

从 0 到 1 搭建一套企业级 AI 智能客服平台:微服务 + RAG + 全链路可观测 - Minion~~

AI 导读

作者开源了基于 Spring Cloud、LangChain4j 与 Milvus 的企业级 AI 智能客服平台「智语」,采用 MIT 协议,代码托管在 GitHub 和 Gitee。

正文

开源仓库:GitHub · Gitee(MIT 协议)


一、为什么要自己造一套

做过客服系统的同学大概都有同感:市面上的 SaaS 客服要么黑盒、要么按坐席收费、要么 AI 能力只有"接个大模型 API"这一层。真落到企业里,问题往往卡在这几处:

  1. AI 回答不可控:模型张口就来,答错了没人知道是"检索没召回"还是"模型幻觉";
  2. 知识库只管上传,不管效果:文档传进去,检索召回率多少、哪些问题没命中,全是黑盒;
  3. 接入一个新行业就要改代码:电商要查物流,金融要查账,改完发版;
  4. 出问题定位不了:一条对话跨了 5 个服务,日志散在各处,只能靠 grep 拼时间线。

所以我用 Spring Cloud + LangChain4j + Milvus 做了一套可私有化部署的智能客服平台(项目名「智语」),核心目标只有一句话:

AI 的每一步都要可配置、可回查、可降级。

下面按"看得见的部分(界面)→ 看不见的部分(架构与代码)→ 怎么跑起来"来展开。


二、核心特性一览

先把能力摊开,后面再看每块具体怎么落地。系统分五条主线:

🤖 AI 智能对话

  • 基于大语言模型的智能问答,支持上下文会话管理;
  • 意图识别与智能路由,自动判断用户需求;
  • Function Call 工具调用,支持扩展 AI 能力;
  • 多轮对话记忆管理,基于 Redis 持久化存储。

📚 知识库检索(RAG)

  • 传统 FAQ 管理:FAQ 的增删改查、分类管理;
  • RAG 检索增强生成:
    • 支持多格式文档(PDF / TXT / DOCX / MD);
    • 智能文本切片,chunkSize 与 overlap 可配置;
    • 向量化存储,Chroma 用于本地开发、Milvus 用于生产;
    • 两阶段检索(向量召回 + Rerank 重排)提升精度;
    • 多用户会话隔离与防幻觉机制,自定 Prompt 模板约束。

🎫 工单系统

智能工单创建与流转、状态跟踪、优先级管理及统计报表。

💬 实时通信

基于 WebSocket 的即时消息推送、在线客服聊天及连接状态管理。

👥 客户管理

完整的客户信息管理、标签与分组、客户画像及坐席账号管理。

这五条主线之上,还挂了知识图谱、多模态知识、行业包可插拔、全链路可观测这些增强能力——界面部分往下看,代码部分看第三章。


三、RAG 工作流程

RAG 是这个系统的核心链路,完整走一遍是 8 步:

① 用户提问
      ↓
② 文本向量化(Embedding,nomic-embed-text)
      ↓
③ 向量相似度检索(Chroma 开发 / Milvus 生产)
      ↓
④ Rerank 重排优化(BGE-Reranker 二次排序)
      ↓
⑤ 取回相关的文档片段
      ↓
⑥ 构建 Prompt(把检索到的上下文注入提示词)
      ↓
⑦ LLM 生成答案(qwen:7b 等)
      ↓
⑧ 返回用户

三个容易被忽略但决定效果的点:

  1. 为什么必须两步检索。纯向量召回看的是语义相近,Top-K 里经常混进"意思接近但答案不对"的片段。加一层 Rerank 交叉编码重排,把 query 和片段拼起来打分,精度提升明显,代价只是一次模型推理的延迟。
  2. 切片参数比模型更重要。chunkSize 太大,召回片段噪声多;太小,答案被切成两半。overlap 就是为了不让关键句正好落在切口上。这两个参数在文档知识库里可配,不同行业的最优值不一样。
  3. 防幻觉靠约束,不靠祈祷。Prompt 模板里明确写"仅依据下列资料回答,资料未覆盖时回答未找到",再配合上一节的三态返回,模型才有"承认不知道"的出口。

四、先看图:系统长什么样

4.1 数据概览

数据概览

指标卡 + 对话趋势 + 客户趋势 + 工单状态分布 + 坐席排行。

页面滚动动图:

数据概览滚动

4.2 C 端智能对话

C 端智能对话

用户侧没有侧边栏,顶栏是吉祥物+问候语,主区域是对话流。三个功能在这页闭环:

  • 转人工:分「已指派坐席」与「排队中」两种态渲染,没坐席时不会借一位在线坐席来"假装接通";
  • 引用来源:消息下方列出命中的知识条目(#id 问题);
  • 富媒体附件:点选、拖拽、粘贴三种上传入口,图片可直接放大预览。

4.3 坐席工作台(三栏)

坐席工作台

左边"我的会话"、中间对话、右边客户快照(客户编号、联系方式、最近订单、最近工单)。坐席不用来回切页面查客户历史,这是客服系统里最容易被低估的效率点。

4.4 知识库:问答 + 文档 + 图谱 + 多模态

问答管理(含向量化状态、未命中转问、批量向量化):

知识库问答管理

语义检索实测动图(输入问题 → 走 RAG → 返回答复与引用条目):

知识库语义检索

注意动图里返回的引用是 #1 如何退款?——回答必须能溯源到具体知识条目,这是让运营敢用 AI 的前提。

文档知识库:

文档知识库

知识图谱(三元组构建 + 节点/关系管理):

知识图谱

多模态知识(图片 / 音频 / 视频统一入库与标注):

多模态知识

4.5 模型管理:能力与供应商解耦

模型管理

按能力(对话 / 意图识别 / 向量 / 重排 / 视觉 / 多模态)分类注册模型,每个能力可以挂不同的供应商:Ollama 本地、DashScope、DeepSeek、任意 OpenAI 兼容端点。业务代码只认"能力",不认"厂商"。

意图配置与填槽配置:

意图配置

填槽配置

4.6 工单:列表与详情

工单管理

工单详情弹窗动图(列表只放关键列,完整字段走详情接口):

工单详情

4.7 开放接入:行业包可插拔

行业包与工具

这是整套系统里设计得最"克制"的一块:内核不认识任何行业。电商包、金融包只是连接器与工具模板,开关一开就有"查物流/退款/查账"能力,关掉就退回通用客服形态。同一套代码可以同时给两家不同行业的客户用。

配套还有回调通知、卡片模板、出站安全、提示词包、接口导入、Widget 白标:

回调通知

卡片模板

4.8 运维:日志、链路、健康

运维总览:

运维总览

一条对话跨了网关、坐席服务、知识库、开放平台,所以必须有全链路视角。点"调用树"能看到服务 → 类 → 方法三层展开:

调用树弹窗

调用树还能一键复制成纯文本,贴到工单里给同事看。日志查询按 requestId / traceId 反查:

日志查询

4.9 系统管理

用户、角色、菜单、租户、技能组:

用户管理

角色管理

菜单管理


五、看不见的部分:架构与关键代码

5.1 技术栈

层次 技术选型
后端 Java 17、Spring Boot 3.2.0、Spring Cloud Gateway、MyBatis Plus 3.5.5
数据 MySQL 8.0+、Redis 6.0+、Milvus(生产)/ Chroma(开发)
AI/ML LangChain4j 0.32.0、Ollama、Embedding 向量化、BGE-Reranker 重排
模型 通义千问、ChatGLM、qwen:7b、nomic-embed-text(本地可通过 Ollama 拉起)
通信 WebSocket
前端 Vue 3.x、Vite 4.x、Element Plus、Axios、Vue Router 4.x

5.2 微服务拓扑

模块 职责 端口
ai-cs-gateway 网关:路由、负载均衡、鉴权、限流、请求 ID 8080
ai-cs-websocket 实时消息推送(WebSocket) 8081
ai-cs-agent AI 对话编排:意图、填槽、Function Call、流式输出 8082
ai-cs-knowledge 知识库:FAQ、文档切片、向量检索、RAG、图谱、多模态 8083
ai-cs-base-service 基础域:客户、坐席、租户、权限、配置 8084
ai-cs-workorder 工单与工作流 8085
ai-cs-open 开放平台:连接器、工具、Webhook、卡片 8086
ai-cs-ops 运维控制台后端 8087
ai-cs-job 定时任务 8088

数据层分工:MySQL 存主数据,Redis 承担缓存与会话记忆,Chroma / Milvus 承担向量存储。

一次对话的典型链路(即运维页"调用树"里能展开的那条,截图内为演示数据):

gateway          校验登录态 → 转发
ai-cs-agent      加载会话上下文 → 叠加行业提示词 → 识别意图 → 填槽
ai-cs-knowledge  RAG 检索命中问答第 2 条
ai-cs-open       调用查询物流工具(首包超时 → 重试 → 成功)
ai-cs-agent      调用对话模型生成 → 流式写出片段 → 落库
ai-cs-open       触发出站回调:会话进展

5.3 RAG 的三态返回,而不是"有/无"

这是我改得最值钱的一处设计。早期 semanticSearch 只返回字符串,上层无法区分"知识库里真没有"和"模型服务挂了",于是模型故障时用户看到的是"抱歉没有找到相关信息"——运营以为要补知识,实际是服务挂了。

现在固定三态:

public RagSearchResultDTO semanticSearch(String question, String tenantCode,
                                         String sessionId, String industryPrompt) {
    if (!StringUtils.hasText(question)) {
        return RagSearchResultDTO.miss(MISS_REPLY);
    }
    String tenant = KnowledgeFaqService.normalizeTenant(tenantCode);
    try {
        List<MilvusHit> hits = retrieve(tenant, question);
        List<KnowledgeFaq> faqs = loadPublishedFaqs(tenant, hits);
        if (faqs.isEmpty()) {
            // 未命中:记录问题供运营补知识,topScore 帮助判断「差一点命中」
            missService.record(tenant, question, sessionId, topScore(hits));
            return RagSearchResultDTO.miss(MISS_REPLY);
        }
        List<String> contextDocs = faqs.stream().map(this::toContextDoc).toList();
        String reply = generate(question, contextDocs, industryPrompt);
        return RagSearchResultDTO.hit(reply, buildCitations(hits, faqs));
    } catch (Exception e) {
        // 模型/向量任何一环不可用都归 UNAVAILABLE,让上层走「服务不可用」而不是「没查到」
        log.warn("RAG 语义检索不可用 tenant={} question({})", tenant, maskQuestion(question), e);
        return RagSearchResultDTO.unavailable("知识库检索暂不可用");
    }
}

三个细节值得单独说:

  • 向量命中后一定回表:loadPublishedFaqs 按 faqId 查库,只保留"启用 + 已发布"的条目。这样即使删除失败留下了脏向量,也不会答出已下线的知识。
  • 未命中要落库:把问题、租户、topScore 记进 cs_knowledge_miss,运营在后台能看到"哪些问题没人答",还能一键"转问"补成新问答。
  • 异常不等于未命中:UNAVAILABLE 走另一套话术,并且日志里问题会脱敏(maskQuestion)。

5.4 意图识别的三级容错

意图是"配置优先、代码兜底"的典型场景:租户在后台自己加意图,不用发版。

public IntentDTO getIntent(String userMsg, String tenantCode) {
    if (!StringUtils.hasText(userMsg)) return fallbackConsult();
    try {
        // 1. 拉租户启用的意图清单,失败返回空列表(不抛异常)
        List<IntentConfigDTO> intents = loadEnabledIntents(tenantCode);
        String intentList = buildIntentList(intents);

        // 2. 清单非空 → 定点替换默认清单;为空 → 用默认五类模板
        String prompt = StringUtils.hasText(intentList)
                ? buildPromptWithIntents(intentList, userMsg)
                : PromptConst.fill(PromptConst.INTENT_PROMPT, userMsg);

        String raw = modelRouter.chatForType(ModelTypeEnum.INTENT.getCode(), messages, null);
        return normalize(raw, intents);
    } catch (Exception e) {
        // 模型调用或解析异常,统一降级为「咨询」并打上 llmDegraded=true
        return fallbackConsult();
    }
}

降级后为什么不直接退化成"闲聊":llmDegraded=true 是给上层看的信号,AiAgentService 见到它会返回"系统繁忙"。如果把"模型故障"伪装成"正常闲聊",监控和调用方就都失去感知了。

5.5 权限:两套东西,别混为一谈

前端权限分两层,一开始我混在一起,结果"菜单藏了但手敲地址能进":

  • 档位(platform / admin / super):管入口隔离与粗粒度准入;
  • 岗位角色码(AGENT / SUPERVISOR / KNOWLEDGE_OPS / ADMIN / SUPER_ADMIN):管"侧栏看到哪些菜单、能进哪些路径"。

一个账号可挂多个角色码,权限取并集。路由守卫里两道判断:

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  if (to.meta.requiresAuth && !token) return next('/login')

  if (token && to.meta.requiresAuth) {
    const { ok, redirect } = checkAccess(to.path)   // 档位:普通用户只能进聊天/帮助中心
    if (!ok) {
      if (isSuperOnlyPath(to.path)) ElMessage.warning('该功能仅超级管理员可用')
      return next(redirect)
    }
    // 岗位角色:菜单里看不到的路径同样不放行,与侧栏共用 config/menus.js 那份矩阵
    if (!canVisitPath(to.path, getRoleCodes())) {
      ElMessage.warning('当前岗位没有该功能的权限')
      return next(homeAfterLogin('admin'))
    }
  }
  next()
})

关键点:菜单过滤与路由拦截必须共用同一份角色矩阵(config/menus.js),否则迟早出现"菜单里没有、但地址栏能进"的漏网之鱼。

还有一个体感细节:不同岗位登录后落地页不同。坐席落工作台、知识运营落知识库、主管与管理员落数据概览。否则低权限账号一登录就被自己的首页权限挡一次,用户会以为"登录失败"。


六、怎么跑起来

环境:JDK 17+、Maven 3.6+、MySQL 8.0+、Redis 6.0+、Node.js、Ollama,向量库 Chroma(开发)或 Milvus(生产)。

想走完整 AI 链路就得把 Ollama 和向量库拉起来;只浏览界面则不需要。

# 1. 拉代码(国内推荐 Gitee,海外可用 GitHub)
git clone https://gitee.com/huangrenhui/ai-cs-parent.git
# 或:git clone https://github.com/huangrenhui/ai-cs-parent.git
cd ai-cs-parent

# 2. 初始化数据库
mysql -uroot -p < database/init.sql

# 3. RAG 前置服务:拉模型 + 起向量库
ollama pull qwen:7b
ollama pull nomic-embed-text
docker run -d -p 8000:8000 -v ./chroma:/chroma/chroma chromadb/chroma

# 4. 编译并启动微服务
mvn clean install -DskipTests
mvn -pl ai-cs-gateway spring-boot:run      # 其余服务同理,或按需逐个启动

# 5. 前端(独立工程,不在 Maven modules 里)
cd ai-cs-frontend
npm install
npm run dev     # http://localhost:5173

七、部署与安全

7.1 部署架构

环境 形态
开发 本地直接跑,各服务连本机 MySQL / Redis / Chroma
生产 Nginx 负载均衡 → Gateway 集群 → 微服务集群 + 各自数据集群

7.2 安全设计

  • 网关统一鉴权,业务服务不直接暴露;
  • 密码 BCrypt 加密存储,不落明文;
  • 全站 HTTPS;
  • 防 SQL 注入(MyBatis Plus 参数化 + 入参校验);
  • XSS / CSRF 防护。

7.3 性能与可观测

  • 性能:Redis 缓存热点数据、数据库索引、HikariCP 连接池、耗时操作异步化、列表全量分页、向量检索参数调优;
  • 监控:Spring Boot Actuator 暴露健康与指标,ELK 做日志分析,链路追踪(Sleuth / Zipkin),关键指标配置告警。

八、适用场景与项目亮点

8.1 用在哪儿

  • 企业客服中心:替代传统 IVR + 人工的排队模式,AI 先接住 80% 的重复问题;
  • 技术支持系统:把产品手册、FAQ、历史工单喂进知识库,做技术自助问答;
  • 内部知识管理:企业内部文档检索,找制度、找流程不用再问同事;
  • 在线教育平台:课程咨询、报名答疑、教务常见问题自动应答。

8.2 亮点

  1. 微服务架构:按域拆分,服务独立扩展,单点故障不扩散;
  2. RAG 真正落地:两阶段检索 + 可配切片 + 回表校验,不是"接个向量库就算 RAG";
  3. 文档版本管理:版本创建、查询、回退、基于 MD5 与文件大小的对比,以及内容去重;
  4. 多用户会话隔离:租户与用户维度双重隔离,会话记忆不串;
  5. 高可扩展:模型按能力注册,行业包与工具可插拔,加能力不改内核。

九、这套东西的边界

  • 知识库检索目前以 FAQ 为主,文档切片链路有完整实现但尚未与 FAQ 双路召回合并(代码里留了明确的扩展点注释);
  • 知识图谱支持三元组构建与查询,生产级图数据库(Neo4j / Nebula)是可选项,默认轻量实现;
  • Flowable 流程引擎默认未启用,工单流转当前走状态机,流程编排页属于待接通的演示配置;
  • AI 工具目录内置部分是前端 catalog,自定义 HTTP 工具的落库接口仍是内存桩,接生产需要补持久化;
  • ai-cs-ops 是控制台后端,指标采集走 Micrometer + Prometheus,告警通道对接需要按企业实际情况补;
  • 多租户是"数据隔离 + 配置隔离",不是物理隔离,合规要求高的场景要单独评估。

把"没做的"写清楚,比把"做了的"吹得多更可信。


十、写在最后

这套系统做下来,最大的体会是:AI 客服的难点不在模型,而在"模型之外"——知识有没有召回、回答能不能溯源、出了故障怎么降级、跨服务怎么定位。

如果你也在做类似的东西,有三条经验可以直接拿走:

  1. 检索必须回表校验,别让脏向量答出下线知识;
  2. 异常与未命中必须分开,否则监控和运营都会被误导;
  3. 演示环境要能独立跑起来,一个 Mock 开关省下的沟通成本远超实现成本。

仓库(MIT 协议,欢迎 Star / Issue):

文档入口:docs/文档说明.md(架构、接口、数据库、部署、测试都在里面)。


来源:博客园 · cnblogs.com