从零读懂 AI 智能客服(一):模块职责与 SSE 聊天链路(后端架构)

本文主题:先建立后端全局认知,再走通一次 AI 对话从请求入口到 SSE 返回的主链路

适合读者:刚接手 Spring Boot 多模块项目,希望系统理解 AI 客服后端的 Java 开发者

代码基线:当前学习分支源码快照


🐟 这里是yurenpai

27届开发者,主要学习 Java 后端与 AI 应用开发。

这里记录真实项目中的代码调用链、Agent/RAG 工程化、问题排查和开发复盘。

个人理念:

阅读大型项目时,先建立地图,再进入街道,比一开始扎进某个类更高效。


写在前面

先说结论:这个后端不是由多个互不相关的服务拼起来的,而是一个以 Maven 多模块组织、由 Spring Boot 统一装配的模块化单体。普通业务遵循 Controller、Service、Mapper 的经典链路;AI 对话则在此基础上增加了上下文解析、场景路由、模型适配、RAG、SSE 和消息持久化等能力。

本文解决四个问题:

  1. 根工程、公共模块、业务模块、启动模块和扩展模块分别负责什么;
  2. 普通 CRUD 与 AI 对话链路为什么复杂度不同;
  3. 一次聊天请求如何从 ChatRequest 进入 ChatServiceFacade
  4. 固定回复和大模型回复最终如何通过 SSE 返回并保存。

说明:内容基于当前学习分支的源码快照进行静态分析,不同分支或后续版本的目录与实现可能发生变化。

代码说明:除明确标注为完整源码外,文中的代码片段均根据当前源码提炼为简化示意,只保留与本节有关的字段、注解或调用关系;文中的"已核对"表示完成静态源码核对,不等同于运行测试通过。


一、先建立后端整体架构认知

后端根目录:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\
├─ pom.xml
├─ tst-pharma-admin\
├─ tst-pharma-common\
├─ tst-pharma-modules\
├─ tst-pharma-extend\
├─ docs\
└─ logs\

这不是四个互不相关的后端项目,而是一个:

Maven 多模块的 Spring Boot 单体应用

可以先记住这张关系图:

text 复制代码
                         根 pom.xml
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
          ▼                  ▼                  ▼
 tst-pharma-common    tst-pharma-modules  tst-pharma-extend
  公共基础能力            业务模块             独立扩展服务
          │                  │
          └──────────┬───────┘
                     ▼
             tst-pharma-admin
              组装并启动主应用

依赖方向大致是:

text 复制代码
common
  ↑
modules
  ↑
admin

也就是:

text 复制代码
业务模块依赖公共模块
启动模块依赖业务模块

不能反过来让:

text 复制代码
common 依赖 chat
system 依赖 admin

否则很容易出现循环依赖。


二、根 pom.xml 是干什么的

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\pom.xml

它的打包方式是:

xml 复制代码
<packaging>pom</packaging>

说明它自己不直接生成可运行的业务 JAR,主要负责三件事。

1. 聚合模块

xml 复制代码
<modules>
    <module>tst-pharma-admin</module>
    <module>tst-pharma-common</module>
    <module>tst-pharma-extend</module>
    <module>tst-pharma-modules</module>
</modules>

在根目录执行 Maven 构建时,会按照模块依赖关系一起编译。

2. 统一版本

当前主要技术版本包括:

text 复制代码
Java                 17
Spring Boot          3.5.8
MyBatis-Plus         3.5.14
Sa-Token             1.44.0
Redisson             3.51.0
LangChain4j          1.13.0
Weaviate Client      1.19.6

子模块不需要分别写一遍版本号。

3. 统一构建规则

例如:

text 复制代码
Maven 编译插件
测试插件
Spring Boot 依赖管理
不同环境的 Maven Profile

因此根 pom.xml 可以理解为:

整个后端工程的"总目录和统一规则"。
小鱼点睛

根 POM 负责统一规则和聚合构建,tst-pharma-admin 才是把业务模块装入同一个 Spring Boot 应用的启动者。聚合构建运行时装配不是一回事。


三、tst-pharma-admin:启动和装配层

目录:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\
├─ pom.xml
└─ src\main\
   ├─ java\com\tst\pharma\
   │  ├─ TstPharmaApplication.java
   │  ├─ TstPharmaServletInitializer.java
   │  ├─ config\
   │  │  └─ MapperConflictResolver.java
   │  └─ controller\
   │     ├─ AuthController.java
   │     ├─ CaptchaController.java
   │     └─ IndexController.java
   └─ resources\
      ├─ application.yml
      ├─ application-dev.yml
      ├─ application-prod.yml
      ├─ logback-plus.xml
      └─ banner.txt

它为什么业务代码很少

因为它不是主要业务模块,而是:

把其他模块装到一起,然后启动 Spring Boot。

它的 pom.xml 依赖了:

text 复制代码
tst-pharma-system
tst-pharma-generator
tst-pharma-chat
tst-pharma-workflow
tst-pharma-aiflow

所以最后运行的是:

text 复制代码
tst-pharma-admin

但是实际加载进去的业务包括:

text 复制代码
系统管理
AI 聊天
知识库
业务工作流
AI 流程编排
代码生成

启动类

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\java\com\tst\pharma\TstPharmaApplication.java

核心注解:

java 复制代码
@SpringBootApplication
public class TstPharmaApplication {
}

启动类位于:

text 复制代码
com.tst.pharma

而其他模块中的类也基本位于:

text 复制代码
com.tst.pharma.xxx

因此 Spring Boot 启动后会扫描依赖中的:

java 复制代码
@Controller
@RestController
@Service
@Component
@Configuration

这就是为什么 ChatController 明明不在 admin 目录中,启动 admin 后仍然能访问。

当前启动类的特殊行为

当前 main() 启动之前执行了:

text 复制代码
killPortProcess(6039);

含义是:

text 复制代码
启动程序
  ↓
检查 6039 端口
  ↓
如果被占用
  ↓
在 Windows 上通过 taskkill 强制结束占用进程
  ↓
再启动 Spring Boot

这不是 Spring Boot 标准行为,而是项目自定义行为。

所以以后发现某个使用 6039 的 Java 进程被结束,需要先想到这里。


四、tst-pharma-common:公共基础能力层

目录:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-common\

它不是一个单独业务,而是一组可复用的公共组件。

必须优先掌握

text 复制代码
tst-pharma-common-core
├─ 公共返回对象 R
├─ 异常
├─ 工具类
└─ 基础常量

tst-pharma-common-web
├─ BaseController
├─ Web 配置
├─ 全局异常处理
└─ Web 过滤器

tst-pharma-common-mybatis
├─ BaseMapperPlus
├─ PageQuery
├─ TableDataInfo
├─ MyBatis-Plus 配置
└─ 数据库公共实体

tst-pharma-common-satoken
├─ 登录用户获取
├─ 权限认证
└─ LoginHelper

tst-pharma-common-redis
├─ RedisUtils
├─ Redisson
└─ 缓存公共能力

tst-pharma-common-sse
├─ SseEmitterManager
├─ SseMessageUtils
├─ SSE 事件对象
└─ SSE 连接管理

tst-pharma-common-chat
├─ ChatRequest
├─ ChatModelVo
├─ IChatService
├─ IChatModelService
└─ 工作流与聊天共用接口

用到的时候再深入

text 复制代码
common-json
common-log
common-tenant
common-doc
common-encrypt
common-security
common-idempotent
common-sensitive

当前阶段不用逐个阅读

text 复制代码
common-excel
common-mail
common-sms
common-oss
common-social
common-websocket
common-job
common-translation

你要注意:

common 不是"杂物目录",而是可以被多个业务模块复用、且不应该依赖具体业务的基础组件。

例如 SSE 连接不只聊天模块能用,所以放在:

text 复制代码
tst-pharma-common-sse

而不是直接塞进:

text 复制代码
tst-pharma-chat

五、tst-pharma-modules:主要业务代码

目录:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\
├─ tst-pharma-system\
├─ tst-pharma-chat\
├─ tst-pharma-aiflow\
├─ tst-pharma-workflow\
└─ tst-pharma-generator\

1. tst-pharma-system

主要负责:

text 复制代码
用户
角色
菜单
部门
岗位
字典
系统参数
租户
登录日志
操作日志

它属于后台系统基础业务。

2. tst-pharma-chat

这是我们现在最需要掌握的模块,负责:

text 复制代码
聊天会话
聊天消息
模型配置
模型供应商
模型调用
系统提示词
客服场景分类
医疗风险路由
上下文实体识别
固定回复
SSE 流式响应
知识库
文档切片
Embedding
向量检索
RAG
Rerank
MCP
智能体
可观测性

因此它不是一个简单的聊天 CRUD 模块。

3. tst-pharma-aiflow

负责:

text 复制代码
AI 节点
模型节点
知识库节点
条件节点
节点输入输出
AI 流程编排

它解决的是:

多个 AI 节点怎样按照流程执行。

4. tst-pharma-workflow

负责普通业务流程,例如:

text 复制代码
审批
流程节点
流程实例
任务流转
业务办理

注意区分:

text 复制代码
aiflow      AI 能力和节点的编排
workflow    传统业务审批和流程流转

5. tst-pharma-generator

负责:

text 复制代码
读取数据库表结构
生成 Entity
生成 BO、VO
生成 Mapper
生成 Service
生成 Controller
生成前端 CRUD 页面

它主要是开发辅助工具。


六、tst-pharma-extend:独立扩展服务

目录:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-extend\
├─ tst-pharma-monitor-admin\
└─ tst-pharma-snailjob-server\

分别用于:

text 复制代码
monitor-admin
└─ Spring Boot Admin 服务监控

snailjob-server
└─ 定时任务和分布式任务调度

它们与 admin 不完全相同。

admin 是主业务应用入口,而这两个更接近独立辅助服务。你当前学习聊天代码时,只需要知道它们的用途,不需要深入。


七、一个普通业务模块的标准分层

用系统参数配置作为例子。

源码结构:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\
└─ tst-pharma-modules\tst-pharma-system\src\main\java\com\tst\pharma\system\
   ├─ controller\system\
   │  └─ SysConfigController.java
   ├─ service\
   │  ├─ ISysConfigService.java
   │  └─ impl\
   │     └─ SysConfigServiceImpl.java
   ├─ mapper\
   │  └─ SysConfigMapper.java
   └─ domain\
      ├─ SysConfig.java
      ├─ bo\
      │  └─ SysConfigBo.java
      └─ vo\
         └─ SysConfigVo.java

标准调用关系:

text 复制代码
HTTP 请求
   ↓
Controller
   ↓
Service 接口
   ↓
ServiceImpl
   ↓
Mapper
   ↓
MyBatis-Plus
   ↓
MySQL

返回过程:

text 复制代码
MySQL 查询结果
   ↓
Entity
   ↓
对象转换
   ↓
VO
   ↓
Controller
   ↓
JSON
   ↓
前端

每一种类的职责

Controller
text 复制代码
SysConfigController.java

负责:

text 复制代码
接收 HTTP 请求
校验参数
检查权限
调用 Service
包装返回结果

例如:

java 复制代码
@GetMapping("/list")
public TableDataInfo<SysConfigVo> list(
        SysConfigBo config,
        PageQuery pageQuery) {
    return configService.selectPageConfigList(config, pageQuery);
}

Controller 不应该直接写 SQL,也不应该堆很多复杂业务判断。

Service 接口
text 复制代码
ISysConfigService.java

负责定义:

text 复制代码
系统参数模块能提供哪些业务能力

例如:

text 复制代码
查询配置
新增配置
修改配置
删除配置
刷新缓存
ServiceImpl
text 复制代码
SysConfigServiceImpl.java

负责真正实现业务逻辑:

text 复制代码
检查配置键是否重复
读写数据库
更新缓存
校验内置配置能否删除
Mapper
text 复制代码
SysConfigMapper.java

负责数据库访问:

java 复制代码
public interface SysConfigMapper
        extends BaseMapperPlus<SysConfig, SysConfigVo> {
}

因为继承了 BaseMapperPlus,很多基础 CRUD 不用重复编写。

Entity
text 复制代码
SysConfig.java

对应数据库表:

java 复制代码
@TableName("sys_config")
public class SysConfig extends TenantEntity {
}

主要用于:

text 复制代码
数据库字段映射
MyBatis-Plus 查询
数据库新增修改
BO
text 复制代码
SysConfigBo.java

BO 是 Business Object,主要接收业务输入。

例如:

java 复制代码
@NotBlank(message = "参数名称不能为空")
private String configName;

它可以带:

text 复制代码
参数校验
查询条件
业务输入字段
VO
text 复制代码
SysConfigVo.java

VO 是 View Object,用于返回前端。

它只暴露前端需要的数据,避免把数据库 Entity 直接返回。


八、为什么 tst-pharma-chat 比普通 CRUD 复杂

聊天模块顶级结构:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\
└─ tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\
   ├─ agent\
   ├─ config\
   ├─ constant\
   ├─ controller\
   ├─ domain\
   ├─ enums\
   ├─ factory\
   ├─ mapper\
   ├─ mcp\
   ├─ observability\
   └─ service\

普通 CRUD 只需要

text 复制代码
Controller
Service
Mapper
Entity、BO、VO

AI 聊天还要解决

text 复制代码
使用哪个模型?
模型是哪家供应商?
请求是否属于医疗高风险?
是否属于客服范围?
是否直接固定回复?
是否需要查询知识库?
是否需要把历史消息交给模型?
怎样流式返回?
什么时候保存用户消息?
什么时候保存助手消息?
模型出错后怎样关闭 SSE?
同一用户开多个窗口怎样隔离?

所以额外出现了:

text 复制代码
factory
├─ 根据供应商选择模型适配实现

provider / chat impl
├─ 构建不同供应商的 ChatModel

prompt
├─ 系统提示词

scene
├─ 客服场景识别和路由

scene\context
├─ 最近消息和上下文实体解析

retrieval
├─ 检索编排

vector
├─ 向量库操作

embed
├─ 文本向量化

rerank
├─ 检索结果重排序

observability
├─ 模型调用和检索过程观测

mcp
├─ 外部工具能力

小鱼点睛

AI 聊天没有抛弃 Controller、Service 和 Mapper,而是在传统持久化链路之上增加了上下文、路由、模型、RAG 与 SSE 编排。复杂度来自新增的决策和传输职责,而不是目录名称本身。


九、common-chattst-pharma-chat 的区别

这两个名字很容易混淆。

tst-pharma-common-chat

text 复制代码
公共聊天协议和接口

例如:

text 复制代码
ChatRequest
ChatModelVo
IChatService
IChatModelService
RoleType

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-common\tst-pharma-common-chat\src\main\java\com\tst\pharma\common\chat\domain\dto\request\ChatRequest.java

tst-pharma-chat

text 复制代码
聊天业务的真正实现

例如:

text 复制代码
ChatController
ChatServiceFacade
ChatMessageServiceImpl
ChatSceneRouter
知识库检索
模型供应商实现

这样设计的主要好处是:

text 复制代码
workflow、aiflow 等其他模块
可以只依赖 common-chat 中的接口
而不必直接依赖完整的 chat 实现模块

这是在降低跨模块耦合。


十、开始走完整聊天链路

先看完整地图:

text 复制代码
前端发送 POST /chat/send
        │
        ▼
Sa-Token 登录认证、Web 过滤器
        │
        ▼
ChatRequest 反序列化和参数校验
        │
        ▼
ChatController.sseChat()
        │
        ▼
ChatServiceFacade.sseChat()
        │
        ├─ 校验图片地址
        ├─ 获取服务端登录用户
        ├─ 获取登录 Token
        ├─ 生成当前窗口 SSE 标识
        ├─ 构建轻量上下文
        ├─ 执行客服场景路由
        ├─ 创建 SSE 连接
        └─ 保存用户消息
                │
                ▼
          根据路由结果分流
                │
      ┌─────────┼─────────────┐
      │         │             │
      ▼         ▼             ▼
   固定回复   工作流/思考模式   普通模型聊天
      │                         │
      │                         ├─ 查询模型配置
      │                         ├─ 加入系统提示词
      │                         ├─ 加载历史消息
      │                         ├─ 可选知识库 RAG
      │                         ├─ 可选图片
      │                         ├─ 选择模型供应商
      │                         └─ 调用流式大模型
      │                                  │
      ▼                                  ▼
发送固定 SSE                    onPartialResponse
保存助手消息                          │
发送 done                             ▼
关闭连接                       持续发送 SSE content
                                         │
                                         ▼
                                onCompleteResponse
                                         │
                                保存完整助手消息
                                发送 done
                                关闭 SSE

十一、第一站:ChatRequest

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-common\tst-pharma-common-chat\src\main\java\com\tst\pharma\common\chat\domain\dto\request\ChatRequest.java

主要分成两类字段。

前端传入的字段

text 复制代码
model
├─ 使用的模型名称

content
├─ 用户输入内容

imageUrls
├─ 用户上传的图片,最多 4 张

sessionId
├─ 会话 ID

knowledgeId
├─ 知识库 ID

appId
├─ 应用 ID

uuid
├─ 当前聊天窗口 ID

enableThinking
├─ 是否开启深度思考

enableWorkFlow
├─ 是否使用工作流

workFlowRunner
├─ 工作流参数

isResume、reSumeRunner
└─ 工作流人机交互恢复参数

其中:

java 复制代码
@NotEmpty
private String model;

@NotEmpty
private String content;

会经过 @Valid 校验。

后端运行时补充的字段

text 复制代码
chatModelVo
├─ 后端从数据库查到的模型完整配置

emitter
├─ 当前 SSE 连接

userId
├─ 服务端登录用户 ID

tokenValue
├─ 服务端 Token

sseConnectionId
├─ 当前窗口的 SSE 唯一标识

contextMessages
└─ 最终交给模型的完整上下文

尤其要记住:

text 复制代码
userId 和 tokenValue 不能信任前端传入值
必须由后端登录状态获取

当前代码确实是通过:

text 复制代码
LoginHelper.getUserId();
StpUtil.getTokenValue();

获得。


十二、第二站:ChatController

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\controller\chat\ChatController.java

核心代码非常简单:

java 复制代码
@PostMapping("/send")
@ResponseBody
public SseEmitter sseChat(
        @RequestBody @Valid ChatRequest chatRequest) {
    return chatService.sseChat(chatRequest);
}

它只做三件事:

text 复制代码
1. 接收 POST /chat/send
2. 把 JSON 转成 ChatRequest 并校验
3. 调用 ChatServiceFacade

这里没有写场景判断、模型调用、知识库查询,这是正确的。

因为 Controller 的职责只是:

HTTP 边界,不应该成为业务逻辑中心。


十三、第三站:ChatServiceFacade.sseChat()

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\service\chat\impl\ChatServiceFacade.java

这是聊天主链路的编排中心。

它不是普通的 CRUD Service,而是一个:

Facade 门面服务:把认证、上下文、路由、SSE、知识库、模型调用和消息保存串起来。

核心执行顺序如下。

第一步:校验图片

text 复制代码
validateImageUrls(chatRequest.getImageUrls());

检查:

text 复制代码
图片数量
URL 长度
URL 协议
不安全地址

第二步:获取登录用户

java 复制代码
Long userId = getCurrentUserId();
String tokenValue = getCurrentTokenValue();

不是从请求体直接取用户身份。

第三步:生成 SSE 连接标识

java 复制代码
String sseConnectionId =
        buildSseConnectionId(tokenValue, chatRequest);

优先使用:

text 复制代码
Token + uuid

没有 uuid 时使用:

text 复制代码
Token + sessionId

如果两者都没有,当前代码会兼容旧调用并退回到仅使用 Token。因此,新前端应尽量稳定传入 uuidsessionId,避免同一 Token 下的连接相互替换。

这样同一个登录用户可以同时打开多个聊天窗口,互不关闭对方的 SSE 连接。

关系是:

text 复制代码
userId
└─ sseConnectionId A
└─ sseConnectionId B
└─ sseConnectionId C

第四步:构建轻量路由上下文

text 复制代码
ChatRoutingContext routingContext =
        chatContextResolver.resolve(chatRequest, userId);

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\service\chat\scene\context\ChatContextResolver.java

它只读取:

text 复制代码
当前输入
当前图片
最近最多 20 条文本消息
上一条助手回复
订单号、运单号、处方号等上下文实体
药品名称

它明确不会做:

text 复制代码
不调用大模型
不调用 RAG
不调用业务接口
不发送 SSE

产生:

text 复制代码
ChatRoutingContext
├─ currentContent
├─ hasCurrentImages
├─ recentMessages
├─ entities
└─ lastAssistantMessage

第五步:客服场景路由

text 复制代码
ChatRouteResult routeResult =
        chatSceneRouter.route(routingContext);

路由器:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\service\chat\scene\ChatSceneRouter.java

当前真实顺序是:

text 复制代码
1. 医疗风险识别
2. 订单、物流、处方、售后、人工等业务分类
3. 客服服务范围判断
4. 范围外连续追问判断
5. 上下文指代和信息不足判断
6. 决定固定回复还是继续请求模型

路由结果:

text 复制代码
ChatRouteResult
├─ sceneType
├─ riskLevel
├─ action
├─ matchedRule
└─ replyTemplate

常见 action

text 复制代码
DIRECT_REPLY
├─ 医疗风险固定回复

SCOPE_GUIDE
├─ 范围外引导

CLARIFY
├─ 信息不足,要求用户补充

BUSINESS_PLACEHOLDER
├─ 业务接口暂未真正接通时的占位回复

HUMAN_GUIDE
├─ 引导人工客服

CONTINUE_CHAT
└─ 继续进入系统提示词、RAG 和大模型

第六步:建立 SSE

java 复制代码
SseEmitter emitter =
        sseEmitterManager.connect(userId, sseConnectionId);

连接管理器位于:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-common\tst-pharma-common-sse\src\main\java\com\tst\pharma\common\sse\core\SseEmitterManager.java

它维护的结构相当于:

java 复制代码
Map<用户ID, Map<SSE连接标识, SseEmitter>>

所以一个用户可以拥有多个窗口连接。

第七步:保存用户消息

text 复制代码
chatMessageService.saveChatMessage(
    userId,
    chatRequest.getSessionId(),
    chatRequest.getContent(),
    RoleType.USER.getName(),
    chatRequest.getModel()
);

无论后面进入:

text 复制代码
固定回复
工作流
普通大模型

用户原始消息都先统一保存一次。

小鱼点睛

聊天入口的关键顺序是:先从服务端取得可信身份,再建立用户隔离的 SSE 标识和路由上下文,最后保存原始消息并分流。前端传来的 userId 或 Token 不能作为认证依据。


十四、两条最重要的后续分支

分支 A:固定回复

例如:

text 复制代码
医疗紧急情况
严重不良反应
停药换药
范围外问题
上下文不明确
人工客服
业务接口占位

调用:

text 复制代码
FixedReplyService

链路:

text 复制代码
replyTemplate
   ↓
FixedReplyTemplateProvider
   ↓
获得固定安全文案
   ↓
SSE 发送 content
   ↓
保存助手消息
   ↓
SSE 发送 done
   ↓
关闭连接

这条链路:

text 复制代码
不查询模型配置
不调用知识库
不调用大模型

分支 B:继续模型聊天

只有路由动作是:

text 复制代码
CONTINUE_CHAT

才进入模型链路:

text 复制代码
ChatModelVo chatModelVo =
        requireChatModel(chatRequest);

然后构建最终上下文:

text 复制代码
SystemMessage
   ↓
历史对话
   ↓
经过 RAG 和图片增强的当前 UserMessage

具体顺序:

text 复制代码
1. 加入全局系统提示词
2. 如果有 knowledgeId,执行知识库 RAG
3. 如果有图片,添加图片内容
4. 从数据库加载会话历史
5. 移除刚保存的重复用户消息
6. 把增强后的当前用户消息放到最后

之后:

text 复制代码
AbstractChatService chatService =
        chatServiceFactory.getOriginalService(providerCode);

工厂根据模型供应商选择实现:

text 复制代码
OpenAI 兼容供应商
Ollama
通义
智谱
其他自定义供应商

再构建:

text 复制代码
StreamingChatModel streamingChatModel =
        chatService.buildStreamingChatModel(
            chatModelVo,
            chatRequest
        );

最终调用:

text 复制代码
streamingChatModel.chat(contextMessages, handler);

十五、模型返回后的 SSE 链路

模型每返回一个片段,就触发:

java 复制代码
onPartialResponse(String partialResponse)

处理:

text 复制代码
片段追加到 StringBuilder
   ↓
SSE 发送 content 事件
   ↓
前端逐字显示

模型返回完成后触发:

text 复制代码
onCompleteResponse(ChatResponse completeResponse)

处理:

text 复制代码
取出完整助手回复
   ↓
保存助手消息到数据库
   ↓
发送 done 事件
   ↓
关闭 SSE 连接

出现异常时:

text 复制代码
记录后端异常
   ↓
向前端发送固定安全错误文案
   ↓
关闭 SSE 连接

前端不会直接看到模型供应商的原始异常、密钥或内部接口信息。


十六、阅读时先记住这三张图

后端模块图

text 复制代码
common  → 公共积木
modules → 业务实现
admin   → 组装并启动
extend  → 独立辅助服务

普通 CRUD 图

text 复制代码
Controller
→ Service
→ ServiceImpl
→ Mapper
→ MySQL

AI 聊天图

text 复制代码
Controller
→ ChatServiceFacade
→ 上下文解析
→ 场景路由
→ 固定回复 / 工作流 / 大模型
→ SSE
→ 消息保存

十七、接下来阅读源码的正确顺序

阅读下一阶段源码时,不建议直接从 872 行的 ChatServiceFacade 第一行读到最后一行。

建议按下面顺序:

text 复制代码
第 1 组:请求入口
├─ ChatRequest.java
├─ ChatController.java
└─ ChatServiceFacade.sseChat()

第 2 组:前置路由
├─ ChatRoutingContext.java
├─ ChatContextResolver.java
├─ ChatSceneRouter.java
├─ ChatRouteResult.java
├─ ChatRouteAction.java
└─ ChatSceneType.java

第 3 组:固定回复
├─ FixedReplyService.java
├─ FixedReplyTemplateProvider.java
├─ FixedReplySseSender.java
└─ DefaultFixedReplySseSender.java

第 4 组:模型调用
├─ AiCustomerSystemPromptProvider.java
├─ ChatServiceFactory.java
├─ AbstractChatService.java
└─ 各供应商 ChatService 实现

第 5 组:知识库
├─ KnowledgeRetrievalService.java
├─ CustomVectorRetriever.java
├─ VectorStoreService.java
└─ KnowledgeInfoService

第 6 组:消息和 SSE
├─ ChatMessageServiceImpl.java
├─ PersistentChatMemoryStore.java
├─ SseEmitterManager.java
└─ SseMessageUtils.java

下一步最适合从第一组的三个文件逐行走,重点搞明白:

text 复制代码
前端到底传了什么
后端补充了什么
路由发生在什么时候
用户消息为什么在调用模型前保存
SSE 为什么要使用 userId + uuid/sessionId 隔离

这三点理解后,再进入 ChatSceneRouter,整个项目的聊天主线就不会乱了。


总结

本文围绕"先建立后端全局认知,再走通一次 AI 对话从请求入口到 SSE 返回的主链路",主要分析了:

  1. 后端各一级模块的职责与依赖方向;
  2. 普通 CRUD 和 AI 聊天链路的结构差异;
  3. 从 ChatRequest、Controller、Facade 到 SSE 的关键执行顺序;
  4. 固定回复、模型调用和消息落库之间的关系;

整个过程可以概括为:

text 复制代码
前端请求 → ChatController → ChatServiceFacade → 上下文与场景路由 → 固定回复 / 模型调用 → SSE → 消息持久化

真正读懂 AI 客服后端,不是记住所有类名,而是先分清"谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存"。

当前进度

OK 已经完成

  • 已完成根 POM、启动模块、公共模块和聊天主链路的静态源码核对;

  • 已整理普通 CRUD 与 AI 对话两条阅读路线;
    TODO 后续继续

  • 下一篇继续拆解 Maven 依赖、Spring Bean 装配和跨模块接口桥梁;

  • 后续再逐行分析 ChatRequest、ChatController 与 ChatServiceFacade 的入口代码;
    小鱼点睛

真正读懂 AI 客服后端,不是记住所有类名,而是先分清"谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存"。

下一篇

下一篇将继续分析"Java 多模块项目如何协作:Maven 依赖、Spring 注入与运行时装配",把本文建立的结构认知继续落到具体代码和对象流上。


这篇文章是我在真实项目学习过程中的阶段性记录。不同项目的命名和目录可能不同,但判断职责边界、依赖方向和数据生命周期的方法可以复用。如果内容中还有遗漏,欢迎一起交流。

相关推荐
陈皮波比茶1 小时前
Swagger
java
别动我齐刘海1 小时前
机器人运动控制学习4——状态估计 State Estimation
c++·人工智能·学习·目标检测·机器学习·机器人·自动驾驶
Behaviour1 小时前
Unity AI 生态横评对比:官方 AI Beta / Unity CLI / 团结 Codely / 社区 MCP 四大阵营选型
人工智能·unity·ai·游戏引擎·aigc·ai编程
盈飞无限1 小时前
AI智能SPC软件,制造业质量数字化转型核心
人工智能
狂云歌1 小时前
2025年读书回顾,AI+游戏+历史
人工智能·学习·游戏
码视野1 小时前
基于 Vue3 + Element Plus 的【企业级 AI 智能体工作流与私有知识库 (RAG) 协同平台】设计与实现(附完整源码与PRD)
人工智能·vue3
玹外之音1 小时前
Spring AI + Elasticsearch 向量存储实战:从零构建智能文档检索系统
人工智能·spring·elasticsearch
CHEEVEN_QY1 小时前
B2B制造企业的AI搜索信息基建:llms.txt、Schema与FAQ落地实践
人工智能·faq·b2b制造
小闫BI设源码1 小时前
Elasticsearch面试必看:如何让客户端精准选择节点高效执行请求?
java·elasticsearch·面试宝典·深入解析