本文主题:先建立后端全局认知,再走通一次 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 返回并保存。
说明:内容基于当前学习分支的源码快照进行静态分析,不同分支或后续版本的目录与实现可能发生变化。
代码说明:除明确标注为完整源码外,文中的代码片段均根据当前源码提炼为简化示意,只保留与本节有关的字段、注解或调用关系;文中的“已核对”表示完成静态源码核对,不等同于运行测试通过。
一、先建立后端整体架构认知
后端根目录:
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 单体应用
可以先记住这张关系图:
根 pom.xml │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ tst-pharma-common tst-pharma-modules tst-pharma-extend 公共基础能力 业务模块 独立扩展服务 │ │ └──────────┬───────┘ ▼ tst-pharma-admin 组装并启动主应用依赖方向大致是:
common ↑ modules ↑ admin也就是:
业务模块依赖公共模块 启动模块依赖业务模块不能反过来让:
common 依赖 chat system 依赖 admin否则很容易出现循环依赖。
二、根pom.xml是干什么的
文件:
D:\Tools\java\aiwork\tst-pharma-main-backend\pom.xml它的打包方式是:
<packaging>pom</packaging>说明它自己不直接生成可运行的业务 JAR,主要负责三件事。
1. 聚合模块
<modules><module>tst-pharma-admin</module><module>tst-pharma-common</module><module>tst-pharma-extend</module><module>tst-pharma-modules</module></modules>在根目录执行 Maven 构建时,会按照模块依赖关系一起编译。
2. 统一版本
当前主要技术版本包括:
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. 统一构建规则
例如:
Maven 编译插件 测试插件 Spring Boot 依赖管理 不同环境的 Maven Profile因此根pom.xml可以理解为:
整个后端工程的“总目录和统一规则”。
小鱼点睛
根 POM 负责统一规则和聚合构建,
tst-pharma-admin才是把业务模块装入同一个 Spring Boot 应用的启动者。聚合构建和运行时装配不是一回事。
三、tst-pharma-admin:启动和装配层
目录:
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依赖了:
tst-pharma-system tst-pharma-generator tst-pharma-chat tst-pharma-workflow tst-pharma-aiflow所以最后运行的是:
tst-pharma-admin但是实际加载进去的业务包括:
系统管理 AI 聊天 知识库 业务工作流 AI 流程编排 代码生成启动类
文件:
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\java\com\tst\pharma\TstPharmaApplication.java核心注解:
@SpringBootApplicationpublicclassTstPharmaApplication{}启动类位于:
com.tst.pharma而其他模块中的类也基本位于:
com.tst.pharma.xxx因此 Spring Boot 启动后会扫描依赖中的:
@Controller@RestController@Service@Component@Configuration这就是为什么ChatController明明不在admin目录中,启动admin后仍然能访问。
当前启动类的特殊行为
当前main()启动之前执行了:
killPortProcess(6039);含义是:
启动程序 ↓ 检查 6039 端口 ↓ 如果被占用 ↓ 在 Windows 上通过 taskkill 强制结束占用进程 ↓ 再启动 Spring Boot这不是 Spring Boot 标准行为,而是项目自定义行为。
所以以后发现某个使用 6039 的 Java 进程被结束,需要先想到这里。
四、tst-pharma-common:公共基础能力层
目录:
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-common\它不是一个单独业务,而是一组可复用的公共组件。
必须优先掌握
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 └─ 工作流与聊天共用接口用到的时候再深入
common-json common-log common-tenant common-doc common-encrypt common-security common-idempotent common-sensitive当前阶段不用逐个阅读
common-excel common-mail common-sms common-oss common-social common-websocket common-job common-translation你要注意:
common不是“杂物目录”,而是可以被多个业务模块复用、且不应该依赖具体业务的基础组件。
例如 SSE 连接不只聊天模块能用,所以放在:
tst-pharma-common-sse而不是直接塞进:
tst-pharma-chat五、tst-pharma-modules:主要业务代码
目录:
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
主要负责:
用户 角色 菜单 部门 岗位 字典 系统参数 租户 登录日志 操作日志它属于后台系统基础业务。
2.tst-pharma-chat
这是我们现在最需要掌握的模块,负责:
聊天会话 聊天消息 模型配置 模型供应商 模型调用 系统提示词 客服场景分类 医疗风险路由 上下文实体识别 固定回复 SSE 流式响应 知识库 文档切片 Embedding 向量检索 RAG Rerank MCP 智能体 可观测性因此它不是一个简单的聊天 CRUD 模块。
3.tst-pharma-aiflow
负责:
AI 节点 模型节点 知识库节点 条件节点 节点输入输出 AI 流程编排它解决的是:
多个 AI 节点怎样按照流程执行。
4.tst-pharma-workflow
负责普通业务流程,例如:
审批 流程节点 流程实例 任务流转 业务办理注意区分:
aiflow AI 能力和节点的编排 workflow 传统业务审批和流程流转5.tst-pharma-generator
负责:
读取数据库表结构 生成 Entity 生成 BO、VO 生成 Mapper 生成 Service 生成 Controller 生成前端 CRUD 页面它主要是开发辅助工具。
六、tst-pharma-extend:独立扩展服务
目录:
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-extend\ ├─ tst-pharma-monitor-admin\ └─ tst-pharma-snailjob-server\分别用于:
monitor-admin └─ Spring Boot Admin 服务监控 snailjob-server └─ 定时任务和分布式任务调度它们与admin不完全相同。
admin是主业务应用入口,而这两个更接近独立辅助服务。你当前学习聊天代码时,只需要知道它们的用途,不需要深入。
七、一个普通业务模块的标准分层
用系统参数配置作为例子。
源码结构:
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标准调用关系:
HTTP 请求 ↓ Controller ↓ Service 接口 ↓ ServiceImpl ↓ Mapper ↓ MyBatis-Plus ↓ MySQL返回过程:
MySQL 查询结果 ↓ Entity ↓ 对象转换 ↓ VO ↓ Controller ↓ JSON ↓ 前端每一种类的职责
Controller
SysConfigController.java负责:
接收 HTTP 请求 校验参数 检查权限 调用 Service 包装返回结果例如:
@GetMapping("/list")publicTableDataInfo<SysConfigVo>list(SysConfigBoconfig,PageQuerypageQuery){returnconfigService.selectPageConfigList(config,pageQuery);}Controller 不应该直接写 SQL,也不应该堆很多复杂业务判断。
Service 接口
ISysConfigService.java负责定义:
系统参数模块能提供哪些业务能力例如:
查询配置 新增配置 修改配置 删除配置 刷新缓存ServiceImpl
SysConfigServiceImpl.java负责真正实现业务逻辑:
检查配置键是否重复 读写数据库 更新缓存 校验内置配置能否删除Mapper
SysConfigMapper.java负责数据库访问:
publicinterfaceSysConfigMapperextendsBaseMapperPlus<SysConfig,SysConfigVo>{}因为继承了BaseMapperPlus,很多基础 CRUD 不用重复编写。
Entity
SysConfig.java对应数据库表:
@TableName("sys_config")publicclassSysConfigextendsTenantEntity{}主要用于:
数据库字段映射 MyBatis-Plus 查询 数据库新增修改BO
SysConfigBo.javaBO 是 Business Object,主要接收业务输入。
例如:
@NotBlank(message="参数名称不能为空")privateStringconfigName;它可以带:
参数校验 查询条件 业务输入字段VO
SysConfigVo.javaVO 是 View Object,用于返回前端。
它只暴露前端需要的数据,避免把数据库 Entity 直接返回。
八、为什么tst-pharma-chat比普通 CRUD 复杂
聊天模块顶级结构:
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 只需要
Controller Service Mapper Entity、BO、VOAI 聊天还要解决
使用哪个模型? 模型是哪家供应商? 请求是否属于医疗高风险? 是否属于客服范围? 是否直接固定回复? 是否需要查询知识库? 是否需要把历史消息交给模型? 怎样流式返回? 什么时候保存用户消息? 什么时候保存助手消息? 模型出错后怎样关闭 SSE? 同一用户开多个窗口怎样隔离?所以额外出现了:
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
公共聊天协议和接口例如:
ChatRequest ChatModelVo IChatService IChatModelService RoleType文件:
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.javatst-pharma-chat
聊天业务的真正实现例如:
ChatController ChatServiceFacade ChatMessageServiceImpl ChatSceneRouter 知识库检索 模型供应商实现这样设计的主要好处是:
workflow、aiflow 等其他模块 可以只依赖 common-chat 中的接口 而不必直接依赖完整的 chat 实现模块这是在降低跨模块耦合。
十、开始走完整聊天链路
先看完整地图:
前端发送 POST /chat/send │ ▼ Sa-Token 登录认证、Web 过滤器 │ ▼ ChatRequest 反序列化和参数校验 │ ▼ ChatController.sseChat() │ ▼ ChatServiceFacade.sseChat() │ ├─ 校验图片地址 ├─ 获取服务端登录用户 ├─ 获取登录 Token ├─ 生成当前窗口 SSE 标识 ├─ 构建轻量上下文 ├─ 执行客服场景路由 ├─ 创建 SSE 连接 └─ 保存用户消息 │ ▼ 根据路由结果分流 │ ┌─────────┼─────────────┐ │ │ │ ▼ ▼ ▼ 固定回复 工作流/思考模式 普通模型聊天 │ │ │ ├─ 查询模型配置 │ ├─ 加入系统提示词 │ ├─ 加载历史消息 │ ├─ 可选知识库 RAG │ ├─ 可选图片 │ ├─ 选择模型供应商 │ └─ 调用流式大模型 │ │ ▼ ▼ 发送固定 SSE onPartialResponse 保存助手消息 │ 发送 done ▼ 关闭连接 持续发送 SSE content │ ▼ onCompleteResponse │ 保存完整助手消息 发送 done 关闭 SSE十一、第一站:ChatRequest
文件:
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主要分成两类字段。
前端传入的字段
model ├─ 使用的模型名称 content ├─ 用户输入内容 imageUrls ├─ 用户上传的图片,最多 4 张 sessionId ├─ 会话 ID knowledgeId ├─ 知识库 ID appId ├─ 应用 ID uuid ├─ 当前聊天窗口 ID enableThinking ├─ 是否开启深度思考 enableWorkFlow ├─ 是否使用工作流 workFlowRunner ├─ 工作流参数 isResume、reSumeRunner └─ 工作流人机交互恢复参数其中:
@NotEmptyprivateStringmodel;@NotEmptyprivateStringcontent;会经过@Valid校验。
后端运行时补充的字段
chatModelVo ├─ 后端从数据库查到的模型完整配置 emitter ├─ 当前 SSE 连接 userId ├─ 服务端登录用户 ID tokenValue ├─ 服务端 Token sseConnectionId ├─ 当前窗口的 SSE 唯一标识 contextMessages └─ 最终交给模型的完整上下文尤其要记住:
userId 和 tokenValue 不能信任前端传入值 必须由后端登录状态获取当前代码确实是通过:
LoginHelper.getUserId(); StpUtil.getTokenValue();获得。
十二、第二站:ChatController
文件:
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-modules\tst-pharma-chat\src\main\java\com\tst\pharma\controller\chat\ChatController.java核心代码非常简单:
@PostMapping("/send")@ResponseBodypublicSseEmittersseChat(@RequestBody@ValidChatRequestchatRequest){returnchatService.sseChat(chatRequest);}它只做三件事:
1. 接收 POST /chat/send 2. 把 JSON 转成 ChatRequest 并校验 3. 调用 ChatServiceFacade这里没有写场景判断、模型调用、知识库查询,这是正确的。
因为 Controller 的职责只是:
HTTP 边界,不应该成为业务逻辑中心。
十三、第三站:ChatServiceFacade.sseChat()
文件:
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、知识库、模型调用和消息保存串起来。
核心执行顺序如下。
第一步:校验图片
validateImageUrls(chatRequest.getImageUrls());检查:
图片数量 URL 长度 URL 协议 不安全地址第二步:获取登录用户
LonguserId=getCurrentUserId();StringtokenValue=getCurrentTokenValue();不是从请求体直接取用户身份。
第三步:生成 SSE 连接标识
StringsseConnectionId=buildSseConnectionId(tokenValue,chatRequest);优先使用:
Token + uuid没有uuid时使用:
Token + sessionId如果两者都没有,当前代码会兼容旧调用并退回到仅使用Token。因此,新前端应尽量稳定传入uuid或sessionId,避免同一 Token 下的连接相互替换。
这样同一个登录用户可以同时打开多个聊天窗口,互不关闭对方的 SSE 连接。
关系是:
userId └─ sseConnectionId A └─ sseConnectionId B └─ sseConnectionId C第四步:构建轻量路由上下文
ChatRoutingContext routingContext = chatContextResolver.resolve(chatRequest, userId);文件:
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它只读取:
当前输入 当前图片 最近最多 20 条文本消息 上一条助手回复 订单号、运单号、处方号等上下文实体 药品名称它明确不会做:
不调用大模型 不调用 RAG 不调用业务接口 不发送 SSE产生:
ChatRoutingContext ├─ currentContent ├─ hasCurrentImages ├─ recentMessages ├─ entities └─ lastAssistantMessage第五步:客服场景路由
ChatRouteResult routeResult = chatSceneRouter.route(routingContext);路由器:
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当前真实顺序是:
1. 医疗风险识别 2. 订单、物流、处方、售后、人工等业务分类 3. 客服服务范围判断 4. 范围外连续追问判断 5. 上下文指代和信息不足判断 6. 决定固定回复还是继续请求模型路由结果:
ChatRouteResult ├─ sceneType ├─ riskLevel ├─ action ├─ matchedRule └─ replyTemplate常见action:
DIRECT_REPLY ├─ 医疗风险固定回复 SCOPE_GUIDE ├─ 范围外引导 CLARIFY ├─ 信息不足,要求用户补充 BUSINESS_PLACEHOLDER ├─ 业务接口暂未真正接通时的占位回复 HUMAN_GUIDE ├─ 引导人工客服 CONTINUE_CHAT └─ 继续进入系统提示词、RAG 和大模型第六步:建立 SSE
SseEmitteremitter=sseEmitterManager.connect(userId,sseConnectionId);连接管理器位于:
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它维护的结构相当于:
Map<用户ID,Map<SSE连接标识,SseEmitter>>所以一个用户可以拥有多个窗口连接。
第七步:保存用户消息
chatMessageService.saveChatMessage( userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel() );无论后面进入:
固定回复 工作流 普通大模型用户原始消息都先统一保存一次。
小鱼点睛
聊天入口的关键顺序是:先从服务端取得可信身份,再建立用户隔离的 SSE 标识和路由上下文,最后保存原始消息并分流。前端传来的
userId或 Token 不能作为认证依据。
十四、两条最重要的后续分支
分支 A:固定回复
例如:
医疗紧急情况 严重不良反应 停药换药 范围外问题 上下文不明确 人工客服 业务接口占位调用:
FixedReplyService链路:
replyTemplate ↓ FixedReplyTemplateProvider ↓ 获得固定安全文案 ↓ SSE 发送 content ↓ 保存助手消息 ↓ SSE 发送 done ↓ 关闭连接这条链路:
不查询模型配置 不调用知识库 不调用大模型分支 B:继续模型聊天
只有路由动作是:
CONTINUE_CHAT才进入模型链路:
ChatModelVo chatModelVo = requireChatModel(chatRequest);然后构建最终上下文:
SystemMessage ↓ 历史对话 ↓ 经过 RAG 和图片增强的当前 UserMessage具体顺序:
1. 加入全局系统提示词 2. 如果有 knowledgeId,执行知识库 RAG 3. 如果有图片,添加图片内容 4. 从数据库加载会话历史 5. 移除刚保存的重复用户消息 6. 把增强后的当前用户消息放到最后之后:
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);工厂根据模型供应商选择实现:
OpenAI 兼容供应商 Ollama 通义 智谱 其他自定义供应商再构建:
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel( chatModelVo, chatRequest );最终调用:
streamingChatModel.chat(contextMessages, handler);十五、模型返回后的 SSE 链路
模型每返回一个片段,就触发:
onPartialResponse(StringpartialResponse)处理:
片段追加到 StringBuilder ↓ SSE 发送 content 事件 ↓ 前端逐字显示模型返回完成后触发:
onCompleteResponse(ChatResponse completeResponse)处理:
取出完整助手回复 ↓ 保存助手消息到数据库 ↓ 发送 done 事件 ↓ 关闭 SSE 连接出现异常时:
记录后端异常 ↓ 向前端发送固定安全错误文案 ↓ 关闭 SSE 连接前端不会直接看到模型供应商的原始异常、密钥或内部接口信息。
十六、阅读时先记住这三张图
后端模块图
common → 公共积木 modules → 业务实现 admin → 组装并启动 extend → 独立辅助服务普通 CRUD 图
Controller → Service → ServiceImpl → Mapper → MySQLAI 聊天图
Controller → ChatServiceFacade → 上下文解析 → 场景路由 → 固定回复 / 工作流 / 大模型 → SSE → 消息保存十七、接下来阅读源码的正确顺序
阅读下一阶段源码时,不建议直接从 872 行的ChatServiceFacade第一行读到最后一行。
建议按下面顺序:
第 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下一步最适合从第一组的三个文件逐行走,重点搞明白:
前端到底传了什么 后端补充了什么 路由发生在什么时候 用户消息为什么在调用模型前保存 SSE 为什么要使用 userId + uuid/sessionId 隔离这三点理解后,再进入ChatSceneRouter,整个项目的聊天主线就不会乱了。
总结
本文围绕“先建立后端全局认知,再走通一次 AI 对话从请求入口到 SSE 返回的主链路”,主要分析了:
- 后端各一级模块的职责与依赖方向;
- 普通 CRUD 和 AI 聊天链路的结构差异;
- 从 ChatRequest、Controller、Facade 到 SSE 的关键执行顺序;
- 固定回复、模型调用和消息落库之间的关系;
整个过程可以概括为:
前端请求 → ChatController → ChatServiceFacade → 上下文与场景路由 → 固定回复 / 模型调用 → SSE → 消息持久化真正读懂 AI 客服后端,不是记住所有类名,而是先分清“谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存”。
当前进度
[OK] 已经完成
- 已完成根 POM、启动模块、公共模块和聊天主链路的静态源码核对;
- 已整理普通 CRUD 与 AI 对话两条阅读路线;
[TODO] 后续继续
- 下一篇继续拆解 Maven 依赖、Spring Bean 装配和跨模块接口桥梁;
- 后续再逐行分析 ChatRequest、ChatController 与 ChatServiceFacade 的入口代码;
小鱼点睛
真正读懂 AI 客服后端,不是记住所有类名,而是先分清“谁接请求、谁做决策、谁执行回复、谁负责传输、谁负责保存”。
下一篇
下一篇将继续分析“Java 多模块项目如何协作:Maven 依赖、Spring 注入与运行时装配”,把本文建立的结构认知继续落到具体代码和对象流上。
这篇文章是我在真实项目学习过程中的阶段性记录。不同项目的命名和目录可能不同,但判断职责边界、依赖方向和数据生命周期的方法可以复用。如果内容中还有遗漏,欢迎一起交流。