本文主题:先建立后端全局认知,再走通一次 AI 对话从请求入口到 SSE 返回的主链路
适合读者:刚接手 Spring Boot 多模块项目,希望系统理解 AI 客服后端的 Java 开发者
代码基线:当前学习分支源码快照
🐟 这里是yurenpai
27届开发者,主要学习 Java 后端与 AI 应用开发。
这里记录真实项目中的代码调用链、Agent/RAG 工程化、问题排查和开发复盘。
个人理念:
阅读大型项目时,先建立地图,再进入街道,比一开始扎进某个类更高效。

写在前面
先说结论:这个后端不是由多个互不相关的服务拼起来的,而是一个以 Maven 多模块组织、由 Spring Boot 统一装配的模块化单体。普通业务遵循 Controller、Service、Mapper 的经典链路;AI 对话则在此基础上增加了上下文解析、场景路由、模型适配、RAG、SSE 和消息持久化等能力。
本文解决四个问题:
- 根工程、公共模块、业务模块、启动模块和扩展模块分别负责什么;
- 普通 CRUD 与 AI 对话链路为什么复杂度不同;
- 一次聊天请求如何从
ChatRequest进入ChatServiceFacade; - 固定回复和大模型回复最终如何通过 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-chat 和 tst-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。因此,新前端应尽量稳定传入 uuid 或 sessionId,避免同一 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 返回的主链路",主要分析了:
- 后端各一级模块的职责与依赖方向;
- 普通 CRUD 和 AI 聊天链路的结构差异;
- 从 ChatRequest、Controller、Facade 到 SSE 的关键执行顺序;
- 固定回复、模型调用和消息落库之间的关系;
整个过程可以概括为:
text
前端请求 → ChatController → ChatServiceFacade → 上下文与场景路由 → 固定回复 / 模型调用 → SSE → 消息持久化
真正读懂 AI 客服后端,不是记住所有类名,而是先分清"谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存"。
当前进度
OK 已经完成
已完成根 POM、启动模块、公共模块和聊天主链路的静态源码核对;
已整理普通 CRUD 与 AI 对话两条阅读路线;
TODO 后续继续下一篇继续拆解 Maven 依赖、Spring Bean 装配和跨模块接口桥梁;
后续再逐行分析 ChatRequest、ChatController 与 ChatServiceFacade 的入口代码;
小鱼点睛真正读懂 AI 客服后端,不是记住所有类名,而是先分清"谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存"。
下一篇
下一篇将继续分析"Java 多模块项目如何协作:Maven 依赖、Spring 注入与运行时装配",把本文建立的结构认知继续落到具体代码和对象流上。
这篇文章是我在真实项目学习过程中的阶段性记录。不同项目的命名和目录可能不同,但判断职责边界、依赖方向和数据生命周期的方法可以复用。如果内容中还有遗漏,欢迎一起交流。