Files
notes/000-inbox/ibs-ai 项目 AI 对话流程.md
T
2026-03-01 01:43:46 +08:00

361 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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_infosending)”:
- `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`