Ai面试项目:knowledgebase-RagChat模块

interview-guide项目 knowledgebase-RagChat模块的设计与接口实现

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);

处理流程:

  1. Controller 接收 CreateSessionRequest,通过 @Valid 校验 knowledgeBaseIds 非空。
  2. Service 根据 knowledgeBaseIds 查询知识库列表。
  3. 校验查询到的知识库数量是否与请求数量一致,不一致则抛出:
BusinessException(ErrorCode.NOT_FOUND, "部分知识库不存在")
  1. 创建 RagChatSessionEntity
  2. 设置会话标题:
    • 请求传入非空 title 时使用请求标题。
    • 未传标题时调用 generateTitle(knowledgeBases) 自动生成。
  3. 通过 session.setKnowledgeBases(new HashSet<>(knowledgeBases)) 绑定知识库。
  4. 保存会话并转换为 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);

处理流程:

  1. sessionId 查询会话及其关联知识库。
  2. 会话不存在时抛出:
BusinessException(ErrorCode.NOT_FOUND, "会话不存在")
  1. 查询该会话下全部消息,并按 messageOrder ASC 排序。
  2. 将关联的 KnowledgeBaseEntity 转为 KnowledgeBaseListItemDTO
  3. 组装 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());

处理流程:

  1. Controller 接收 SendMessageRequest,通过 @Valid 校验问题内容。
  2. 调用 prepareStreamMessage(...) 做发送前准备:
    • 查询会话及关联知识库。
    • 保存一条 USER 消息,状态为已完成。
    • 创建一条 ASSISTANT 占位消息,内容为空,状态为未完成。
    • 更新会话 messageCount 并保存。
  3. Controller 记录 assistant 占位消息的 messageId
  4. 创建 StringBuilder fullContent 收集完整 AI 回复。
  5. 调用 getStreamAnswer(...) 获取流式回答:
    • 再次查询会话及关联知识库。
    • 读取当前会话绑定的 knowledgeBaseIds
    • 如果开启历史上下文,则加载最近已完成消息作为多轮上下文。
    • 调用 queryService.answerQuestionStream(kbIds, question, history)
  6. 每收到一个 chunk,先追加到 fullContent,再包装为 SSE 事件:
ServerSentEvent.<String>builder()
    .data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
    .build();
  1. 流式输出完成后,在 doOnComplete 中调用:
sessionService.completeStreamMessage(messageId, fullContent.toString());

将完整 AI 回复写回 assistant 占位消息,并标记为已完成。 8. 流式生成失败时,在 doOnError 中保存部分内容;如果没有任何内容,则保存错误提示。

RAG 流式问答链路

RagChat 本身不重复实现向量检索,而是复用知识库查询服务:

queryService.answerQuestionStream(kbIds, question, history);

核心步骤:

  1. 根据当前会话绑定的知识库 ID 限定检索范围。
  2. 可选携带历史上下文,形成多轮问答输入。
  3. 构建 QueryContext,进行问题归一化、query rewrite、动态 topK/minScore 设置。
  4. vectorService.similaritySearch(...) 检索相关文档片段。
  5. 将命中文档拼接为 context
  6. 构建 system prompt 和 user prompt,并附加防提示词注入约束。
  7. 调用 chatClient.prompt().stream().content() 输出 token 流。
  8. 通过 normalizeStreamOutput(...) 进行输出规范化。
  9. 空输入、无命中或异常时返回统一降级文本流。

当前问题与优化方向

当前上下文策略仍以短期记忆为主,主要问题是:

  • 上下文容易变长:历史消息原文直接进入 prompt,AI 回答越长,后续 token 成本越高。
  • 长期记忆容易丢失:只取最近若干条消息,早期关键信息超过窗口后模型不可见。
  • 历史检索不够智能:当前按时间取最近消息,不按当前问题召回相关历史。
  • AI 回答也占上下文名额:maxMessages = 10 限制的是消息条数,不是问答轮数,长回答会浪费上下文空间。
  • 缺少长期摘要:目前没有会话级摘要、用户画像或偏好记忆。

后续可按分层记忆改造:

  • 短期记忆:Redis 缓存最近几轮原始对话,用于快速恢复上下文。
  • 长期记忆:PostgreSQL 存储全部消息原文、摘要、状态,保证可靠追溯。
  • 可召回记忆:将历史消息摘要或会话摘要写入 pgvector,按当前问题做语义召回。
  • 会话摘要:每次完成一轮问答后异步触发总结智能体,更新会话摘要。
  • 外部知识:继续使用知识库 RAG 检索结果作为事实来源。

小结

Knowledgebase-RagChat 模块把知识库从“一次性问答接口”扩展成了“可管理、可恢复、可持续对话”的聊天系统。它的关键价值在于:会话负责组织用户上下文和知识库范围,RAG 查询服务负责检索与生成,两者边界清晰,后续扩展长期记忆、会话摘要和语义历史召回时可以逐步演进。