RAG优化2:Chroma 持久化存储 + 多轮对话记忆 RAG
前置回顾:优化后的 RAG 链路:用户问题 → Hybrid 混合检索 → Reranker 重排筛选 → 组装 Prompt → LLM 生成答案 本章解决两个产品化痛点:
- Chroma 持久化:之前代码每次运行程序都要重新加载文档、切分、向量化、入库;持久化后向量数据库保存在本地磁盘,程序重启不用重复构建知识库。
- 多轮对话记忆:支持连续问答,模型能理解上下文历史,后续提问可以指代上文内容(例如 “那它的调优方式是什么?”)。
1 ChromaDB 持久化存储配置
Chroma 默认内存模式,程序退出向量数据全部丢失。
开启持久化,向量、文档、元数据会保存到本地文件夹,使用persist_directory参数指定存储目录。
import chromadb from chromadb.utils import embedding_functions # ---------------------- 持久化Embedding函数(本地Embedding模型) ---------------------- # 使用本地Embedding模型,不调用线上API local_ef = embedding_functions.SentenceTransformerEmbeddingFunction( model_name="./models/bge-small-zh-v1.5" # 本地Embedding模型路径 ) # ---------------------- 创建持久化Chroma客户端 ---------------------- # persist_directory:向量库保存到本地文件夹 chroma_db client = chromadb.PersistentClient(path="./chroma_db") # 获取/创建集合;如果集合已经存在,直接加载磁盘上的向量库,不会重复入库 collection = client.get_or_create_collection( name="knowledge_base", embedding_function=local_ef )
✅ 两种场景使用
- 首次构建知识库(只执行一次)
文档加载、切分,添加到 collection,执行 add方法,即collection.add(),数据自动落盘。后续重启程序,不再运行这段入库代码。
from langchain.text_splitter import RecursiveCharacterTextSplitter # 1.加载文档、切分文本 raw_text = """这里放你的知识库文档内容""" text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50) chunks = text_splitter.split_text(raw_text) # 2.批量写入向量库,持久化保存 ids = [f"doc_{i}" for i in range(len(chunks))] collection.add( documents=chunks, ids=ids ) print("知识库写入完成,向量数据已持久化到 ./chroma_db")
- 程序重启,直接加载已有知识库(日常问答使用)
程序再次启动,只需要执行上面
PersistentClient+get_or_create_collection,不需要重新 add 文档,直接读取磁盘向量,节省大量时间。
💡 持久化注意事项
- 不要手动修改
./chroma_db文件夹内文件,会损坏向量库;- 新增文档:继续调用
collection.add()追加写入;- 删除文档:
collection.delete(ids=["doc_0"]);- 备份知识库:直接复制整个
./chroma_db文件夹。
2 多轮对话记忆 RAG 设计思路
普通单轮 RAG:每次提问独立,模型不知道历史对话。 多轮 RAG 核心流程:
- 用户输入本轮问题;
- 对话记忆模块读取历史问答;
- 对本轮原始问题做问题改写(Query Rewrite):把简写、指代类问题,结合历史对话,扩写成完整独立问题,再送入向量检索;
举例: 历史:用户:什么是动态召回流程? 助手:xxxx 用户:那它的优化手段有哪些? 改写后的检索 query:动态召回流程的优化手段有哪些?
- 使用改写后的 query 执行 Hybrid 混合检索 + Reranker 重排;
- 组装 Prompt:【系统提示词】+【历史对话】+【检索到的参考资料】+【用户本轮问题】;
- LLM 生成回答;
- 将本轮「用户问题 + 模型回答」存入对话记忆,供下一轮使用。
2.1 对话记忆存储(简单内存版,适合 demo)
# 对话历史,用列表存储,每一条是字典
chat_history = []
def add_chat_history(user_query, llm_answer):
"""新增一轮问答到对话记忆"""
chat_history.append({"user": user_query, "assistant": llm_answer})
# 控制最大历史轮数,防止prompt过长,超出上下文窗口
max_history = 4
if len(chat_history) > max_history:
chat_history.pop(0) # 丢弃最早一轮对话
2.2 Query 改写:多轮问答关键步骤
如果直接拿简写问题去向量检索,检索效果很差。使用本地大模型,基于对话历史,重构检索 query。
def rewrite_query(original_user_query, chat_history): """ 结合对话历史,改写用户问题,生成适合向量检索的独立完整query """ if not chat_history: return original_user_query rewrite_prompt = f""" 下面是历史对话记录: {chat_history} 用户当前提问:{original_user_query} 任务:基于历史对话,把当前用户问题改写为一个独立完整的问题,用于知识库检索。 只输出改写后的问题,不要多余解释。 """ # 调用本地LLM,得到改写后的query output = llm.create_completion(prompt=rewrite_prompt, max_tokens=128, temperature=0) new_query = output["choices"][0]["text"].strip() return new_query
2.3 多轮 Prompt 模板组装
Prompt 里分为三块:系统约束 + 历史对话 + 检索参考资料 + 当前用户问题
def build_multi_round_prompt(chat_history, context_content, current_user_query): history_text = "" for item in chat_history: history_text += f"用户:{item['user']}\n助手:{item['assistant']}\n" prompt_template = """ 你是知识库问答助手,请严格根据【参考资料】回答用户问题。 如果参考资料没有相关信息,直接回答知识库无相关内容,禁止编造信息。 【历史对话】 {history_text} 【参考资料】 {context_content} 【当前用户问题】 {current_user_query} 请简洁回答: """ final_prompt = prompt_template.format( history_text=history_text, context_content=context_content, current_user_query=current_user_query ) return final_prompt
3 完整多轮问答主流程代码
# ========== 多轮问答主函数 ========== def multi_round_chat(user_question): # Step1:改写query(带历史对话) rewrite_q = rewrite_query(user_question, chat_history) print(f"原始问题:{user_question},改写后检索query:{rewrite_q}") # Step2:混合检索+Reranker重排(复用第七章代码) # 向量检索 + BM25检索 →合并候选文档 →Reranker打分筛选Top3 vector_results = collection.query(query_texts=[rewrite_q], n_results=5) vector_retrieved_docs = vector_results['documents'][0] # BM25检索逻辑省略,复用上节代码
query_tokens = jieba_cut(rewrite_q)
bm25_results = bm25_search(query_tokens)
# Reranker重排得到精选上下文context_content
context_content = reranker_rank(retrieved_docs, rewrite_q)
# Step3:组装多轮Prompt prompt = build_multi_round_prompt(chat_history, context_content, user_question) # Step4:本地大模型生成答案 output = llm.create_completion(prompt=prompt, max_tokens=512, temperature=0.1) answer = output["choices"][0]["text"].strip() # Step5:保存本轮问答到对话历史 add_chat_history(user_question, answer) return answer # -------- 测试对话 -------- if __name__ == "__main__": while True: user_input = input("请输入问题:") if user_input == "exit": break res = multi_round_chat(user_input) print(f"\n助手回答:{res}\n")
4 记忆方案扩展选型
- 内存记忆(上面示例):程序关闭对话记录丢失;适合 Demo、本地测试。
- 文件持久记忆:把
chat_history写入 json 文件,重启程序加载历史;适合单用户本地使用。 - 向量记忆(长对话):对话轮数非常多时,把历史对话向量化检索,只召回相关历史片段,避免 Prompt 过长(长上下文 RAG)。
5 常见踩坑点
- Prompt 超长:历史对话 + 检索上下文叠加,容易超过本地 LLM 上下文窗口。
- 对策:限制最大历史轮数;对历史做摘要压缩;
- 改写 Query 引入错误:改写模型理解错误,生成错误检索词;
temperature设置 0 降低随机性; - Chroma 持久化版本兼容:Chroma 升级版本后,旧的持久化数据库可能无法加载,注意锁定版本。

浙公网安备 33010602011771号