11 KiB
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
{
"content": "用户输入的文本",
"sessionId": 123,
"parentMessageId": 456,
"contentType": "text",
"role": "user"
}
实际必填/有效字段(按当前后端实现)
- 必填:
content - 可选:
sessionIdsessionId == null:后端会创建一个新会话sessionId != null:后端按该会话进行多轮
2.2 前端接收方式:SSE
core/src/main/java/com/cmcc/ai/core/vo/MessageVO.java:20-72
{
"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
逻辑:
-
首次对话(
param.sessionId == null)parentMessageId被强制设为0L(常量DEFAULT_PARENT_MSG_ID)- 创建
session_info记录(summary 取首条消息截断) - 代码:
SessionServiceImpl.java:449-453
-
非首次对话(
param.sessionId != null)- 查询该 session 下
message_info的最大 id 作为parentMessageId - 代码:
SessionServiceImpl.java:454-456 - Mapper:
MessageInfoMapper.xml:143-145(select max(id))
- 查询该 session 下
这意味着当前实现的父子关系是“链式”的:每次提问默认挂在上一条消息后面,而不是维护一棵分支对话树。
3.2 历史消息拼接(多轮上下文)
历史消息查询:
queryHistoryMsg(sessionId):SessionServiceImpl.java:596-607
构造规则:
-
第一条固定是 system prompt
generateFirstMsg():SessionServiceImpl.java:614-617- system prompt 来源:系统配置
OLLAMA_CHART_SYSTEM_MSG - 若配置不存在则使用默认常量
SYSTEM_MSG
-
从 DB 查出该 session 的所有历史
message_content(按mc.id asc)- SQL:
MessageContentMapper.xml:151-157 - 每条记录映射为
ChatMessage{role, content}
- SQL:
-
把“本次用户提问”也 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_infomessage_infomessage_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 promptAPI_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
结构:
{
"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.contentmessageVO.reasoningContent = delta.reasoningContentmessageVO.msgStatus:- chunk 中包含
finish_reason / stop_reason或 line 为[DONE]时,置为finished - 否则为
sending
- chunk 中包含
文件里还存在
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.contentgenerateDeepSeekChatMessage():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. 多轮对话是如何实现的?(关键点总结)
本项目的多轮对话实现方式可以概括为:
- 每次用户发消息时:
- 先把用户消息写入 DB(
message_info + message_content)
- 先把用户消息写入 DB(
- 组装“system + 历史 messages + 本次 user message”形成完整 messages 数组
- 历史消息来自 DB
message_content,按mc.id asc
- 历史消息来自 DB
- 把完整 messages 一次性发给上游
/v1/chat/completions(stream=true) - 上游返回流式 chunk:
- 后端逐 chunk 转成
MessageVO,通过 SSE 推给前端
- 后端逐 chunk 转成
- 流结束后:
- 把 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
它会:
- 从
system_config读取:EVALUATE_TASK_URLCALLBACK_URL
- 读取“本次问/答”的 message_content 内容
- POST 到评估系统(请求包含 query/response/callbackUrl)
- 评估系统回调:
POST /evaluation/callback- Controller:
web/src/main/java/com/cmcc/ai/web/rest/EvaluationCallController.java:36-46
- Controller:
这条链路不影响前端展示主流程,但会写入 evaluation_result 表并支撑后续统计/导出。
9. 给前端的最小对接清单(建议)
9.1 发起对话
POST /session/chat- body:
{ "content": "...", "sessionId": 123 }
9.2 处理 SSE
- 每个 event 的
data是 JSON 字符串(MessageVO) - 用
content做增量拼接 - 用
msgStatus判断完成 - 首次对话从第一条 event 里取
sessionId,用于后续多轮
9.3 加载历史
POST /session/message/list- body:
{ "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