## 1. 对话相关模块与入口 ### 1.1 HTTP 接口入口(后端) 聊天(SSE 流式):`POST /session/chat` 查询会话列表:`POST /session/page/list` 查询某会话的历史消息列表:`POST /session/message/list` 消息反馈(点赞/点踩/评论):`POST /session/message_feedback` ### 1.2 鉴权与用户上下文 对 `/session/**` 路径做拦截: Filter:`web/src/main/java/com/cmcc/ai/web/filter/UserAuthFilter.java:31-55` --- ## 2. 前端怎么传参(对话接口) ### 2.1 发送消息:`POST /session/chat` ```json { "content": "用户输入的文本", "sessionId": 123, "parentMessageId": 456, "contentType": "text", "role": "user" } ``` #### 实际必填/有效字段(按当前后端实现) - **必填**:`content` - **可选**:`sessionId` - `sessionId == null`:后端会创建一个新会话 - `sessionId != null`:后端按该会话进行多轮 ### 2.2 前端接收方式:SSE `core/src/main/java/com/cmcc/ai/core/vo/MessageVO.java:20-72` ```json { "sessionId": 1, "messageId": 10002, "parentMessageId": 10001, "role": "assistant", "contentType": "text", "msgStatus": "sending", "content": "本次增量 token", "reasoningContent": "(可选)模型推理内容" } ``` 当流结束时,后端会发送一条 `msgStatus=finished` 的消息(通常 `content` 为空或为最后一段),并结束 SSE 连接。 --- ## 3. 后端对话编排流程(从 Controller 到落库) 核心实现类: - `core/src/main/java/com/cmcc/ai/core/service/impl/SessionServiceImpl.java` ### 3.1 会话创建 / parentMessageId 选择 入口:`generateChatResponses(TextMessageParam param, Long userId)` - `SessionServiceImpl.java:446-464` 逻辑: 1. **首次对话**(`param.sessionId == null`) - `parentMessageId` 被强制设为 `0L`(常量 `DEFAULT_PARENT_MSG_ID`) - 创建 `session_info` 记录(summary 取首条消息截断) - 代码:`SessionServiceImpl.java:449-453` 2. **非首次对话**(`param.sessionId != null`) - 查询该 session 下 `message_info` 的最大 id 作为 `parentMessageId` - 代码:`SessionServiceImpl.java:454-456` - Mapper:`MessageInfoMapper.xml:143-145`(`select max(id)`) > 这意味着当前实现的父子关系是“链式”的:每次提问默认挂在上一条消息后面,而不是维护一棵分支对话树。 ### 3.2 历史消息拼接(多轮上下文) 历史消息查询: - `queryHistoryMsg(sessionId)`:`SessionServiceImpl.java:596-607` 构造规则: 1. 第一条固定是 **system prompt** - `generateFirstMsg()`:`SessionServiceImpl.java:614-617` - system prompt 来源:系统配置 `OLLAMA_CHART_SYSTEM_MSG` - 若配置不存在则使用默认常量 `SYSTEM_MSG` 2. 从 DB 查出该 session 的所有历史 `message_content`(按 `mc.id asc`) - SQL:`MessageContentMapper.xml:151-157` - 每条记录映射为 `ChatMessage{role, content}` 3. 把“本次用户提问”也 append 到 historyMsg(作为最后一条 user 消息) - `aiChatDialog()`:`SessionServiceImpl.java:632-634` 最终 historyMsg 会被完整送给大模型(多轮上下文就是这么实现的)。 ### 3.3 用户消息落库 在真正请求大模型之前,用户消息会先落库: - 插入 `message_info`(用户侧):`insertMessage(…)` - `SessionServiceImpl.aiChatDialog():628-630` - 插入 `message_content`(用户文本):`insertMessageContent(…)` - `SessionServiceImpl.aiChatDialog():630-631` 对应表结构见 `web/src/main/resources/init.sql`: - `session_info` - `message_info` - `message_content` --- ## 4. 调用的哪个 AI 接口?请求长什么样? ### 4.1 AI 接口地址与模型配置来自哪里 后端不在配置文件里写死模型与地址,而是从 DB 的 `system_config` 表读取: - 读取 service:`SystemConfigServiceImpl.java:31-50` - key 枚举:`common/src/main/java/com/cmcc/ai/enums/SysConfigKeyEnum.java:14-31` 对话相关的 key: - `OLLAMA_MODEL`:模型名 - `OLLAMA_CHART_URL`:多轮对话接口地址 - `OLLAMA_CHART_SYSTEM_MSG`:system prompt - `API_KEY`:调用上游 AI 服务时的 Authorization(代码里默认按 Bearer 使用,具体值建议视为敏感信息) 在示例初始化 SQL 中(`init.sql:70-75`),`OLLAMA_CHART_URL` 指向一个 **OpenAI 兼容风格** 的路径:`/v1/chat/completions`。 > 虽然命名叫 `OLLAMA_*`,但实际对接的上游接口形态更接近 OpenAI/DeepSeek 的 Chat Completions 流式接口。 ### 4.2 后端发给上游 AI 的请求体 组装请求的代码: - `aiChatDialog()`:`SessionServiceImpl.java:648-653` 请求体类型:`OllamaMutiplePrompt` - `common/src/main/java/com/cmcc/ai/model/OllamaMutiplePrompt.java:20-36` 结构: ```json { "model": "<来自 system_config.OLLAMA_MODEL>", "messages": [ { "role": "system", "content": "..." }, { "role": "user", "content": "..." }, { "role": "assistant", "content": "..." }, { "role": "user", "content": "本次问题" } ], "stream": true } ``` HTTP 请求(后端 → 上游 AI): - 方法:POST - URL:`system_config.OLLAMA_CHART_URL` - Header:`Authorization: ` - 代码:`SessionServiceImpl.handleMessageWithWebClient():156-175` - Accept:`text/event-stream` --- ## 5. AI 接口返回的什么?后端如何解析? 上游 AI 返回被按“流式”逐行消费: - `bodyToFlux(String.class)`:`SessionServiceImpl.handleMessageWithWebClient():170-178` 后端假定每一行 `line`: - 要么是 `"[DONE]"` - 要么是一个 JSON chunk,可反序列化为 `ChatCompletionChunk` 解析模型(按 DeepSeek/OpenAI streaming chunk 风格): - `common/src/main/java/com/cmcc/ai/model/ChatCompletionChunk.java` - 主要取值:`choices[0].delta.content` - 推理字段:`choices[0].delta.reasoning_content` 对应解析与映射: - `generateDeepSeekChatMessage()`:`SessionServiceImpl.java:272-311` 映射结果是内部统一的 `MessageVO`: - `messageVO.content = delta.content` - `messageVO.reasoningContent = delta.reasoningContent` - `messageVO.msgStatus`: - chunk 中包含 `finish_reason / stop_reason` 或 line 为 `[DONE]` 时,置为 `finished` - 否则为 `sending` > 文件里还存在 `generateChatMessage()` 用于解析 `OllamaStreamResponse`(`SessionServiceImpl.java:321-345`),但当前 `handleMessageWithWebClient()` 实际调用的是 `generateDeepSeekChatMessage()`(`SessionServiceImpl.java:184`)。 --- ## 6. 后端给前端返回的什么? ### 6.1 SSE 推送的内容 后端对每个上游 chunk 都会向前端推一个 SSE event: - `ServerSentEvent.builder(JSON.toJSONString(message)).build()` - 代码:`SessionServiceImpl.handleMessageWithWebClient():178-189` 也就是: - SSE 的 `data` = `MessageVO` 的 JSON 字符串 前端收到后: - 通过 `messageId` 把同一条 assistant 消息的 token 拼起来 - 通过 `msgStatus` 判断是否完成 - 通过 `sessionId` 确定归属会话 ### 6.2 对话完成后的落库与状态更新 后端在发起上游请求时,会先创建一条“assistant message_info(sending)”: - `assistantMessage = insertMessage(…, MsgStatusEnum.SENDING)` - 代码:`SessionServiceImpl.handleMessageWithWebClient():162-168` 流式接收过程中: - `contentBuilder` 累积每个 chunk 的 `delta.content` - `generateDeepSeekChatMessage():303-306` 流结束后(`doFinally`): - 正常完成: - `message_content` 插入完整文本(role=assistant) - `message_info.msg_status` 更新为 `finished` - 并触发异步评价(见下一节) - 代码:`SessionServiceImpl.handleMessageWithWebClient():191-195` + `handlerResponseMessage():208-218` - 异常 / 取消: - 仍会尽量保存已累积内容 - 并将 `message_info.msg_status` 更新为 `error/cancelled` - 代码:`handlerResponseMessage():219-235` ### 6.3 非流式查询:历史消息列表接口返回 - `POST /session/message/list` - 返回:`ApiResponse>` - `MessageListVO`:`core/src/main/java/com/cmcc/ai/core/vo/MessageListVO.java` - 内含 `contents: List` 这用于“进入会话后加载历史对话”的场景。 --- ## 7. 多轮对话是如何实现的?(关键点总结) 本项目的多轮对话实现方式可以概括为: 1. 每次用户发消息时: - 先把用户消息写入 DB(`message_info + message_content`) 2. 组装“system + 历史 messages + 本次 user message”形成完整 messages 数组 - 历史消息来自 DB `message_content`,按 `mc.id asc` 3. 把完整 messages 一次性发给上游 `/v1/chat/completions`(stream=true) 4. 上游返回流式 chunk: - 后端逐 chunk 转成 `MessageVO`,通过 SSE 推给前端 5. 流结束后: - 把 assistant 的完整文本一次性落库 即:**上下文存储在 DB,每次请求时“全量带上历史”**。 --- ## 8. 评价(可选链路,和对话强相关) 对话完成后会触发一次“问答质量评估”的异步调用: - 触发点:`handlerResponseMessage()` 的 `ON_COMPLETE` 分支 - `SessionServiceImpl.java:209-217` 调用实现:`EvaluationServiceImpl.evaluationResult()` - `core/src/main/java/com/cmcc/ai/core/service/impl/EvaluationServiceImpl.java:115-160` 它会: 1. 从 `system_config` 读取: - `EVALUATE_TASK_URL` - `CALLBACK_URL` 2. 读取“本次问/答”的 message_content 内容 3. POST 到评估系统(请求包含 query/response/callbackUrl) 4. 评估系统回调:`POST /evaluation/callback` - Controller:`web/src/main/java/com/cmcc/ai/web/rest/EvaluationCallController.java:36-46` 这条链路不影响前端展示主流程,但会写入 `evaluation_result` 表并支撑后续统计/导出。 --- ## 9. 给前端的最小对接清单(建议) ### 9.1 发起对话 - `POST /session/chat` - body: ```json { "content": "...", "sessionId": 123 } ``` ### 9.2 处理 SSE - 每个 event 的 `data` 是 JSON 字符串(`MessageVO`) - 用 `content` 做增量拼接 - 用 `msgStatus` 判断完成 - **首次对话**从第一条 event 里取 `sessionId`,用于后续多轮 ### 9.3 加载历史 - `POST /session/message/list` - body: ```json { "sessionId": 123 } ``` (后端只读取 `sessionId` 字段:`SessionController.java:78-83`) --- ## 10. 关键代码定位索引 - 对话接口:`web/…/SessionController.java:98-105` - 对话编排:`core/…/SessionServiceImpl.java:446-654` - 上游流式请求与解析:`core/…/SessionServiceImpl.java:149-196` + `272-311` - 历史消息查询 SQL:`core/…/MessageContentMapper.xml:151-157` - 系统配置 key:`common/…/SysConfigKeyEnum.java:14-31` - 初始化表结构与默认配置:`web/src/main/resources/init.sql`