1773109225
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
## 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: <system_config.API_KEY>`
|
||||
- 代码:`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<List<MessageListVO>>`
|
||||
- `MessageListVO`:`core/src/main/java/com/cmcc/ai/core/vo/MessageListVO.java`
|
||||
- 内含 `contents: List<MessageContentVO>`
|
||||
|
||||
这用于“进入会话后加载历史对话”的场景。
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
Reference in New Issue
Block a user