Knowledgebase-RagChat 模块设计与实现
这篇笔记记录 interview-guide 项目中 RagChat 模块的设计与接口实现。该模块归属于知识库能力,重点是把“多知识库会话管理、消息持久化、RAG 流式回答、历史上下文”串成一个可持续对话的问答系统。
模块能力概览
- 多知识库会话:创建会话时可绑定多个知识库,后续问答只在当前会话关联的知识范围内检索。
- 会话管理:支持会话列表、详情、重命名、置顶、删除和关联知识库更新。
- 消息持久化:用户问题与 AI 回答分开落库,支持按
messageOrder恢复完整对话历史。 - 流式回答:基于 SSE 返回 AI 生成内容,前端可以边生成边展示。
- RAG 复用:复用
Knowledgebase模块的queryService.answerQuestionStream(...),统一向量检索、提示词构建和降级逻辑。 - 上下文记忆:当前支持最近消息作为短期上下文,后续可扩展会话摘要、语义召回和长期记忆。
核心状态流转
关键接口设计
POST /api/rag-chat/sessions 创建 RAG 聊天会话
返回:
Result<SessionDTO>
调用链:
sessionService.createSession(request);
knowledgeBaseRepository.findAllById(request.knowledgeBaseIds());
sessionRepository.save(session);
ragChatMapper.toSessionDTO(session);
处理流程:
- Controller 接收
CreateSessionRequest,通过@Valid校验knowledgeBaseIds非空。 - Service 根据
knowledgeBaseIds查询知识库列表。 - 校验查询到的知识库数量是否与请求数量一致,不一致则抛出:
BusinessException(ErrorCode.NOT_FOUND, "部分知识库不存在")
- 创建
RagChatSessionEntity。 - 设置会话标题:
- 请求传入非空
title时使用请求标题。 - 未传标题时调用
generateTitle(knowledgeBases)自动生成。
- 请求传入非空
- 通过
session.setKnowledgeBases(new HashSet<>(knowledgeBases))绑定知识库。 - 保存会话并转换为
SessionDTO返回。
GET /api/rag-chat/sessions 获取会话列表
返回:
Result<List<SessionListItemDTO>>
调用链:
sessionService.listSessions();
sessionRepository.findAllOrderByPinnedAndUpdatedAtDesc();
处理要点:
- 会话列表按置顶状态和更新时间倒序展示。
- 返回轻量级
SessionListItemDTO,用于左侧会话列表或历史记录入口。
GET /api/rag-chat/sessions/{sessionId} 获取会话详情
返回:
Result<SessionDetailDTO>
调用链:
sessionService.getSessionDetail(sessionId);
sessionRepository.findByIdWithKnowledgeBases(sessionId);
messageRepository.findBySessionIdOrderByMessageOrderAsc(sessionId);
ragChatMapper.toSessionDetailDTO(session, messages, kbDTOs);
处理流程:
- 按
sessionId查询会话及其关联知识库。 - 会话不存在时抛出:
BusinessException(ErrorCode.NOT_FOUND, "会话不存在")
- 查询该会话下全部消息,并按
messageOrder ASC排序。 - 将关联的
KnowledgeBaseEntity转为KnowledgeBaseListItemDTO。 - 组装
SessionDetailDTO,返回会话信息、知识库列表和消息历史。
PUT /api/rag-chat/sessions/{sessionId}/title 更新会话标题
返回:
Result<Void>
调用链:
sessionService.updateSessionTitle(sessionId, request.title());
sessionRepository.findById(sessionId);
sessionRepository.save(session);
处理要点:
UpdateTitleRequest使用@Valid校验,title不能为空。- 会话不存在时抛出
BusinessException(ErrorCode.NOT_FOUND, "会话不存在")。 - 更新
session.title后保存,实体的@PreUpdate自动刷新updatedAt。
PUT /api/rag-chat/sessions/{sessionId}/pin 切换会话置顶状态
返回:
Result<Void>
调用链:
sessionService.togglePin(sessionId);
sessionRepository.findById(sessionId);
sessionRepository.save(session);
处理逻辑:
Boolean currentPinned = session.getIsPinned() != null ? session.getIsPinned() : false;
session.setIsPinned(!currentPinned);
说明:
isPinned = null时按false处理,再切换为true。- 保存后依赖
@PreUpdate刷新更新时间。
PUT /api/rag-chat/sessions/{sessionId}/knowledge-bases 更新会话关联知识库
返回:
Result<Void>
调用链:
sessionService.updateSessionKnowledgeBases(sessionId, request.knowledgeBaseIds());
sessionRepository.findById(sessionId);
knowledgeBaseRepository.findAllById(knowledgeBaseIds);
session.setKnowledgeBases(new HashSet<>(knowledgeBases));
sessionRepository.save(session);
处理要点:
- 用请求中的
knowledgeBaseIds重新查询知识库实体。 - 使用新的
HashSet覆盖当前会话原有关联知识库。 - 适合用户在同一个聊天窗口中切换或追加知识范围。
DELETE /api/rag-chat/sessions/{sessionId} 删除会话
返回:
Result<Void>
调用链:
sessionService.deleteSession(sessionId);
sessionRepository.existsById(sessionId);
sessionRepository.deleteById(sessionId);
处理要点:
- 删除逻辑放在事务方法中执行。
- 先检查会话是否存在,再删除会话记录。
- 关联消息是否级联删除取决于实体映射和仓库实现。
POST /api/rag-chat/sessions/{sessionId}/messages/stream 发送问题并流式返回
返回:
Flux<ServerSentEvent<String>>
调用链:
sessionService.prepareStreamMessage(sessionId, request.question());
sessionService.getStreamAnswer(sessionId, request.question());
queryService.answerQuestionStream(kbIds, question, history);
sessionService.completeStreamMessage(messageId, fullContent.toString());
处理流程:
- Controller 接收
SendMessageRequest,通过@Valid校验问题内容。 - 调用
prepareStreamMessage(...)做发送前准备:- 查询会话及关联知识库。
- 保存一条
USER消息,状态为已完成。 - 创建一条
ASSISTANT占位消息,内容为空,状态为未完成。 - 更新会话
messageCount并保存。
- Controller 记录 assistant 占位消息的
messageId。 - 创建
StringBuilder fullContent收集完整 AI 回复。 - 调用
getStreamAnswer(...)获取流式回答:- 再次查询会话及关联知识库。
- 读取当前会话绑定的
knowledgeBaseIds。 - 如果开启历史上下文,则加载最近已完成消息作为多轮上下文。
- 调用
queryService.answerQuestionStream(kbIds, question, history)。
- 每收到一个
chunk,先追加到fullContent,再包装为 SSE 事件:
ServerSentEvent.<String>builder()
.data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
.build();
- 流式输出完成后,在
doOnComplete中调用:
sessionService.completeStreamMessage(messageId, fullContent.toString());
将完整 AI 回复写回 assistant 占位消息,并标记为已完成。
8. 流式生成失败时,在 doOnError 中保存部分内容;如果没有任何内容,则保存错误提示。
RAG 流式问答链路
RagChat 本身不重复实现向量检索,而是复用知识库查询服务:
queryService.answerQuestionStream(kbIds, question, history);
核心步骤:
- 根据当前会话绑定的知识库 ID 限定检索范围。
- 可选携带历史上下文,形成多轮问答输入。
- 构建
QueryContext,进行问题归一化、query rewrite、动态topK/minScore设置。 - 调
vectorService.similaritySearch(...)检索相关文档片段。 - 将命中文档拼接为
context。 - 构建 system prompt 和 user prompt,并附加防提示词注入约束。
- 调用
chatClient.prompt().stream().content()输出 token 流。 - 通过
normalizeStreamOutput(...)进行输出规范化。 - 空输入、无命中或异常时返回统一降级文本流。
当前问题与优化方向
当前上下文策略仍以短期记忆为主,主要问题是:
- 上下文容易变长:历史消息原文直接进入 prompt,AI 回答越长,后续 token 成本越高。
- 长期记忆容易丢失:只取最近若干条消息,早期关键信息超过窗口后模型不可见。
- 历史检索不够智能:当前按时间取最近消息,不按当前问题召回相关历史。
- AI 回答也占上下文名额:
maxMessages = 10限制的是消息条数,不是问答轮数,长回答会浪费上下文空间。 - 缺少长期摘要:目前没有会话级摘要、用户画像或偏好记忆。
后续可按分层记忆改造:
- 短期记忆:Redis 缓存最近几轮原始对话,用于快速恢复上下文。
- 长期记忆:PostgreSQL 存储全部消息原文、摘要、状态,保证可靠追溯。
- 可召回记忆:将历史消息摘要或会话摘要写入
pgvector,按当前问题做语义召回。 - 会话摘要:每次完成一轮问答后异步触发总结智能体,更新会话摘要。
- 外部知识:继续使用知识库 RAG 检索结果作为事实来源。
小结
Knowledgebase-RagChat 模块把知识库从“一次性问答接口”扩展成了“可管理、可恢复、可持续对话”的聊天系统。它的关键价值在于:会话负责组织用户上下文和知识库范围,RAG 查询服务负责检索与生成,两者边界清晰,后续扩展长期记忆、会话摘要和语义历史召回时可以逐步演进。