从0到1搭建企业级RAG系统(五):多轮对话、查询改写与智能前端——迈向生产级交互
前言在第四篇文章中我们完成了一个具备混合检索、Reranker精排、双层缓存和流式输出的RAG后端系统。它已经能够高效、精准地回答用户问题但从产品体验的角度看仍有两个明显的短板单轮问答缺乏记忆每次提问都是孤立的无法处理“它有什么优点”这类依赖上下文的追问。交互界面简陋只有终端curl命令和简单的Gradio测试页没有会话管理无法像ChatGPT那样保留历史对话。本文作为本系列的第五篇将完整记录我们如何解决上述问题把LiteRAG从一个“能回答问题的API”升级为一个“能聊天、有记忆、界面友好”的智能助手。核心内容包括多轮对话与记忆管理基于Redis的会话存储与上下文拼接查询改写让系统理解“它/这个/那个”等指代本地LLM部署Ollama qwen3.5:2b摆脱云端API的不稳定Gradio界面重构从单会话到多会话管理侧边栏动态列表、自动标题生成系统量化评估与深度反思手工评估、自动化评估尝试以及一次持续十小时的问题排查历程全文将延续“设计理念 → 实现细节 → 踩坑复盘 → 量化收益”的风格为你的项目演进和面试准备提供最真实的素材。一、起点回顾第四篇结束时我们拥有什么能力维度实现方案状态知识库14篇AI文章718条向量✅混合检索向量 BM25 RRF融合✅Reranker精排BGE-Reranker-base动态显存管理✅LLM生成云端API阿里云百炼qwen3.5-flash✅流式输出SSE格式✅L1精确缓存Redis 问题归一化✅L2语义缓存Milvus 向量相似度匹配✅性能压测Benchmark脚本 P50/P95/P99数据✅缺失的用户体验能力无法进行多轮对话追问“它有什么优点”会失败前端界面简陋无会话管理刷新即丢失云端API稳定性差压测频繁超时接下来的所有工作就是为了补齐这三块拼图。二、查询改写让系统“听懂”指代2.1 问题场景用户先问“什么是RAG”再问“它有什么优点”。第二个问题中的“它”指代“RAG”。如果直接送入检索器BM25和向量检索都会把“它”当作一个独立的词来处理无法召回RAG优点相关的内容。解决方案在检索之前增加一个查询改写步骤将依赖上下文的模糊问题改写成独立、完整的查询。2.2 实现方案创建app/core/query_rewriter.py封装两种改写模式单轮改写将口语化、模糊的问题标准化如“RAG那玩意儿是啥啊” → “RAG的定义是什么”。多轮改写结合对话历史消解指代如“它有什么优点” → “RAG的优点是什么”。核心Prompt设计text你是一个专业的查询改写助手。请结合对话历史将用户的当前问题改写为一个完整的、不依赖历史上下文的独立问题。 ## 改写规则 1. 消除指代不明将它、这个、那个等代词替换为历史中提到的具体实体。 2. 补全省略信息如果问题不完整请根据历史进行补充。 3. 输出格式只输出改写后的独立问题不要添加任何解释或引号。2.3 降级策略与踩坑云端API偶尔超时或返回空不能让它阻塞主流程。我设计了优雅降级改写失败或返回空时自动回退到原始查询。多轮场景下如果改写失败直接将对话历史拼接到Prompt中让LLM自行理解兜底方案。踩坑改写结果为空本地qwen3.5:2b模型在处理改写Prompt时偶尔会返回空字符串。如果不处理后续的检索将收到空查询导致完全偏离。解决方案pythonrewritten response.choices[0].message.content.strip() if not rewritten: logger.warning(查询改写结果为空回退到原始查询) return query2.4 效果验证原始查询改写后查询结果“RAG那玩意儿是啥啊”“RAG的定义是什么”L2缓存命中延迟~50ms“它有什么优点”“RAG的优点是什么”L2缓存命中延迟~70ms“咋优化召回率”“如何优化RAG系统的检索召回率”全流程延迟~15s量化收益查询改写使同义问题的缓存命中率显著提升Benchmark测试中P50延迟从全流程的20秒降至毫秒级。三、多轮对话与会话管理3.1 架构设计多轮对话的核心是记住同一个会话的历史。我使用Redis作为会话存储数据结构设计如下Keysession:{session_id}ValueJSON数组每轮追加{role: user, content: ...}和{role: assistant, content: ...}TTL1小时自动清理僵尸会话3.2 流程集成修改app/api/v1/endpoints/chat.py接收请求时根据session_id从Redis加载历史。将历史送入查询改写模块生成独立查询。执行检索和生成。将本轮问答追加到历史中存回Redis。关键代码片段pythonsession_id request.session_id or str(uuid.uuid4()) history get_session_history(session_id) if history: optimized_query query_rewriter.rewrite_query_with_history(original_query, history) else: optimized_query query_rewriter.rewrite_query(original_query) # ... 检索、生成 ... history.append({role: user, content: original_query}) history.append({role: assistant, content: answer}) save_session_history(session_id, history)3.3 降级兜底当查询改写失败时直接将历史格式化为文本拼接到Prompt中。触发条件optimized_query original_query且history非空。pythonfinal_prompt optimized_query if history and optimized_query original_query: history_context format_history_as_context(history) final_prompt f对话历史\n{history_context}\n\n当前问题{original_query} logger.info(多轮对话降级使用历史拼接)这保证了即使改写服务不可用多轮对话仍能正常工作。四、本地LLM部署摆脱云端API的不稳定4.1 痛点阿里云百炼的免费API在并发场景下极不稳定压测时频繁超时60秒严重阻碍了功能验证和性能测试。4.2 方案选型决定引入本地Ollama部署理由零成本完全免费无Token限制稳定性本地推理不受网络波动影响隐私安全数据不出本地速度可控2B模型推理延迟仅1-3秒模型选择qwen3.5:2b4-bit量化完美适配RTX 3060 6GB显存。4.3 网络打通Ollama安装在Windows宿主机WSL2中的代码需要访问。最终方案Windows设置环境变量OLLAMA_HOST0.0.0.0允许外部连接。WSL2通过Windows主机名访问http://WYM.mshome.net:11434。修改.env配置LLM_BASE_URLhttp://WYM.mshome.net:11434/v1。备选方案如果主机名无法解析可改用Windows的局域网IP通过ipconfig获取并确保Windows防火墙允许11434端口入站。4.4 性能对比指标云端API (qwen3.5-flash)本地Ollama (qwen3.5:2b)平均延迟10-30秒1-3秒并发稳定性频繁超时/限流稳定无失败回答质量较高中等2B模型能力有限成本免费额度有限零成本结论本地2B模型在开发测试阶段是理想选择生产环境可平滑替换为更强模型如4B/7B或云端付费API。五、Gradio界面重构从单会话到多会话管理5.1 初始状态与目标第四篇结束时我们有一个简单的Gradio界面只支持单轮对话无历史记录刷新即丢。目标实现类似ChatGPT的侧边栏会话列表支持新建/切换/删除会话自动生成标题。5.2 核心数据结构pythonsession_list gr.State(value[]) # 所有会话 current_session_id gr.State(valueNone) # 当前激活的会话ID # 每个会话对象结构 { id: uuid, title: 自动生成的标题, history: [{role: user, content: ...}, ...] }5.3 动态渲染侧边栏使用gr.render装饰器根据session_list动态生成会话按钮pythongr.render(inputs[session_list]) def render_sessions(sessions): if not sessions: gr.Markdown(*暂无历史对话*) return for sess in sessions: with gr.Row(): btn gr.Button(sess[title], elem_classessession-btn) del_btn gr.Button(️, elem_classesdelete-session-btn) btn.click(fnswitch_session, inputs[gr.State(sess[id])], ...) del_btn.click(fndelete_session, inputs[gr.State(sess[id])], ...)5.4 自动标题生成当用户发送第一条消息时调用大模型生成一个简短的标题如“RAG的定义”更新到会话对象的title字段。5.5 踩坑状态更新不触发UI刷新问题场景发送第一条消息后respond函数中修改了current_session[title]但侧边栏标题未更新。原因Gradio的gr.State通过对象引用是否变化来判断是否需要通知依赖组件。直接修改字典内部字段不会改变对象引用。解决方案修改标题后创建列表的浅拷贝触发状态更新pythonif len(current_session[history]) 2 and current_session[title] 新对话: current_session[title] _generate_chat_title(message) session_list list(session_list) # 关键创建新引用六、系统量化评估与深度反思6.1 初始评估与问题暴露为了科学衡量系统质量我构建了一个包含5个核心问题的测试集并采用人工评估的方式对每个回答进行了忠实度、相关性和检索精准度的评测。初始评估结果如下编号问题FaithfulnessAnswer Relevancy检索片段1检索片段2检索片段31什么是RAG✅✅✅✅✅2RAG有哪些优点✅❌❌❌❌3混合检索是什么✅✅✅✅✅4Reranker在RAG中的作用是什么✅✅✅❌❌5什么是LLM Agent✅✅✅✅✅第2题“RAG有哪些优点”的失败尤为刺眼——系统始终返回RAG的定义而知识库中明明有论述优点的文章。6.2 十小时的排查一场“怀疑一切”的工程实践这个问题开启了我项目中最漫长、也最有价值的一次调试。排查路径如下阶段怀疑对象尝试的解决方案结果1分块策略不当测试递归分块256/512/1024、语义分块❌ 全部失败2Embedding模型太弱从BGE-small384维升级到BGE-M31024维❌ 仍然失败3知识库缺少优点文档手动创建rag_advantages.md并灌入❌ 仍然失败4BM25索引未更新删除bm25_index.pkl重建索引✅ 检索片段出现优点文档5L2语义缓存污染清空semantic_cacheCollection✅ 云端API返回完整回答6本地Ollama生成空回答临时切回云端API验证✅ 检索与生成全链路打通最终定位的真凶不是Embedding模型能力不足也不是分块策略有问题而是两个缓存层的失效——BM25索引未更新导致新文档未被覆盖L2语义缓存污染导致错误回答被反复命中。6.3 修复后的最终评估结果清空缓存、重建BM25索引、并强制Embedding模型本地离线加载后重新评估全部5个样本编号问题FaithfulnessAnswer Relevancy检索片段1检索片段2检索片段31什么是RAG✅✅✅✅✅2RAG有哪些优点✅✅✅✅✅3混合检索是什么✅✅✅✅✅4Reranker在RAG中的作用是什么✅✅✅✅✅5什么是LLM Agent✅✅✅✅✅最终指标Faithfulness忠实度1.00Answer Relevancy答案相关性1.00Context Precision精准率31.0015/15个片段全部相关6.4 核心经验RAG系统最隐蔽的瓶颈这次调试经历让我深刻认识到“RAG系统中最容易被忽视的瓶颈不是Embedding模型的能力也不是分块策略的优劣而是缓存一致性和索引时效性。”在面试中这段经历比任何教科书式的回答都更有说服力。它证明了我具备科学对照实验的设计能力逐层排除的调试方法论对RAG系统全链路检索、缓存、索引、生成的深刻理解6.5 与RAGAS自动化评估的对比在项目演进过程中我也尝试集成了RAGAS自动化评估框架。虽然由于本地2B模型能力和版本兼容性问题未能稳定产出量化分数但评估管道已成功打通。手工评估与自动化评估互为补充前者灵活直观后者可规模化共同构成了系统质量保障的双保险。七、持久化的探索与最终决策7.1 尝试的方案gr.BrowserState官方推荐的持久化组件但在Gradio 6.x中API不稳定。手动localStorage JS通过.then()执行JS保存但返回值干扰状态更新。gr.update(js...)尝试通过gr.update执行副作用仍未能完美解决。7.2 最终决策战略性放弃刷新持久化经过十余次迭代权衡投入产出比后决定保留当前全部核心功能多轮对话、多会话管理、自动标题生成、美观UI。暂时搁置刷新持久化刷新页面数据丢失但功能演示完全不受影响。作为已知限制写入文档在面试或博客中坦诚说明并给出生产环境的解决方案。面试话术参考“当前前端版本为快速验证核心交互采用了内存级会话管理。在实际生产环境中我会通过gr.BrowserState或后端Redis将会话数据持久化确保刷新后体验无缝。这并不影响本项目对RAG系统核心能力的完整展示。”7.3 给读者的扩展建议如果读者希望继续挑战持久化一个可行的方向是将session_list和current_session_id存储到后端的Redis中与多轮对话的会话存储复用页面加载时通过API拉取。这样既能实现刷新保留又避开了Gradio前端状态管理的复杂性。八、最终系统能力矩阵第五篇结束时能力维度实现方案状态知识库规模41篇AI文章2161条向量✅混合检索向量 BM25 RRF✅Reranker精排BGE-Reranker-base动态显存管理✅LLM生成云端API 本地Ollama双模式✅流式输出SSE格式✅L1/L2双层缓存Redis Milvus毫秒级响应✅查询改写单轮/多轮改写 优雅降级✅多轮对话与记忆Redis会话存储 上下文拼接✅新增多会话管理侧边栏动态列表 新建/切换/删除✅新增自动标题生成大模型生成会话标题✅新增本地LLM部署Ollama qwen3.5:2b✅新增系统量化评估手工评估 RAGAS管道✅新增Gradio可视化界面浅色主题 响应式布局✅升级刷新持久化已知限制生产环境可扩展⏳规划中九、写在最后从第四篇到第五篇我们完成了LiteRAG从“能回答”到“会聊天”的蜕变。这个过程并不顺利——云端API的超时、Gradio持久化的反复失败、状态更新的各种坑以及那次持续十小时的“怀疑一切”式排查——但正是这些真实的工程挑战让项目从一个“玩具Demo”成长为具备生产级思考的“准产品”。技术亮点回顾查询改写 多轮记忆让系统理解上下文缓存命中率大幅提升。本地LLM部署摆脱云端不稳定推理延迟降至1-3秒。多会话管理接近ChatGPT的交互体验自动标题生成锦上添花。深度调试经历十小时排查定位到BM25索引和L2缓存污染问题成为面试中最有说服力的案例。务实的工程权衡在持久化上及时止损优先保证核心功能稳定。无论你是正在准备AI实习面试还是想系统学习RAG系统从后端到前端的完整演进相信这个系列都能为你提供最真实、最硬核的参考。本文是【从0到1搭建企业级RAG系统】系列的第五篇也是前端交互升级的收官之作。