RAG 向量持久化:用 ChromaDB 替换内存存储,支持 Metadata 溯源
系列导读本系列共 6 篇带你从零到一构建完整的 RAG LangGraph MCP 项目。第 1 篇最小 RAG 实现纯 numpy无任何 AI 框架第 2 篇接入 Ollama 本地大模型实现真实语义检索第 3 篇本文接入 ChromaDB 持久化向量数据库第 4 篇用 LangChain 重构 多轮对话第 5 篇LangGraph 多步推理工作流第 6 篇MCP 工具调用协议集成一、第 2 篇的问题向量不持久化第 2 篇的向量数据库存在内存里classVectorStore:def__init__(self):self.texts[]# 内存重启消失self.vectors[]# 内存重启消失问题每次程序重启都要重新 embed 所有文档。8 个文档几秒1000 个文档几分钟10 万个文档几小时这在生产环境是无法接受的。解决方案向量持久化。二、ChromaDB 是什么ChromaDB 是专门为 AI 应用设计的向量数据库向量存储到磁盘重启后直接加载支持 metadata来源、作者、时间等内置 HNSW 索引百万级向量毫秒检索支持 id 去重安全多次插入pipinstallchromadb三、核心变化ChromaVectorStore用 ChromaDB 替换第 2 篇的手写VectorStore接口保持兼容importchromadbfromchromadb.utilsimportembedding_functionsclassChromaVectorStore:def__init__(self,db_path:str,collection_name:str):# 连接到本地 ChromaDB自动创建目录self.clientchromadb.PersistentClient(pathdb_path)# 自定义 embedding 函数调用 Ollama# ChromaDB 会在 add/query 时自动调用这个函数self.embed_fnembedding_functions.OllamaEmbeddingFunction(urlhttp://localhost:11434/api/embeddings,model_namenomic-embed-text,)# get_or_create已存在则复用不存在则新建# 关键同一个 collection_name 不会重复创建self.collectionself.client.get_or_create_collection(namecollection_name,embedding_functionself.embed_fn,metadata{hnsw:space:cosine},# 使用余弦距离)持久化写入defadd_documents(self,documents:list):# 检查哪些 id 已存在id 去重防止重复 embedexistingset(self.collection.get()[ids])new_docs[dfordindocumentsifd[id]notinexisting]ifnotnew_docs:print(f已有{len(existing)}条数据跳过建库)returnself.collection.add(ids[d[id]fordinnew_docs],documents[d[text]fordinnew_docs],metadatas[{source:d[source]}fordinnew_docs],# embeddings 不传ChromaDB 自动调用 embed_fn 生成)print(✅ 建库完成数据已持久化到磁盘)id 去重的意义程序可以安全地多次运行不会重复 embed 已存在的文档。第一次运行建库之后直接复用。带 Metadata 的检索defsearch(self,query:str,top_k:int3)-list:resultsself.collection.query(query_texts[query],n_resultstop_k,include[documents,distances,metadatas],# 包含 metadata)docsresults[documents][0]distancesresults[distances][0]metasresults[metadatas][0]# ChromaDB 返回余弦距离越小越相似转为相似度return[(doc,1-dist,meta[source])fordoc,dist,metainzip(docs,distances,metas)]四、Metadata知道答案来自哪里这是 ChromaDB 相比手写 VectorStore 的重要升级——溯源能力。DOCUMENTS[{id:hr_1,text:年假政策员工入职满一年后享有15天年假...,source:HR手册第3章},{id:hr_2,text:病假政策病假需提供医院证明...,source:HR手册第3章},{id:hr_3,text:请假流程登录OA系统...,source:HR手册第4章},{id:fin_1,text:报销流程填写费用报销单...,source:财务制度第2章},{id:fin_2,text:差旅标准经济舱国内出差...,source:财务制度第3章},]检索时自动返回来源 检索结果 [1] 相似度0.892 [HR手册第4章] 请假流程登录OA系统填写请假申请单... [2] 相似度0.743 [HR手册第3章] 年假政策员工入职满一年后享有15天年假...生成时把来源附在 Prompt 里context_text\n.join(f-{text}来源{src}fortext,_,srcincontexts)这样大模型可以在回答中引用来源用户知道信息出处可信度大幅提升。五、完整代码importollamaimportchromadbfromchromadb.utilsimportembedding_functions EMBED_MODELnomic-embed-textCHAT_MODELqwen2.5:7bDB_PATH./chroma_dbCOLLECTIONcompany_docsDOCUMENTS[{id:hr_1,text:年假政策员工入职满一年后享有15天年假满三年后20天。,source:HR手册第3章},{id:hr_2,text:病假政策病假需提供医院证明当天请假需电话通知直属上级。,source:HR手册第3章},{id:hr_3,text:请假流程登录OA系统填写请假申请单提前3个工作日提交。,source:HR手册第4章},{id:hr_4,text:加班规定工作日加班按1.5倍计算周末加班按2倍计算。,source:HR手册第5章},{id:fin_1,text:报销流程填写费用报销单附发票原件提交财务部审核。,source:财务制度第2章},{id:fin_2,text:差旅标准经济舱国内出差商务舱国际出差超6小时。,source:财务制度第3章},]classChromaVectorStore:def__init__(self,db_path,collection_name):self.clientchromadb.PersistentClient(pathdb_path)self.embed_fnembedding_functions.OllamaEmbeddingFunction(urlhttp://localhost:11434/api/embeddings,model_nameEMBED_MODEL,)self.collectionself.client.get_or_create_collection(namecollection_name,embedding_functionself.embed_fn,metadata{hnsw:space:cosine},)defadd_documents(self,documents):existingset(self.collection.get()[ids])new_docs[dfordindocumentsifd[id]notinexisting]ifnotnew_docs:print(f已有{len(existing)}条跳过建库)returnself.collection.add(ids[d[id]fordinnew_docs],documents[d[text]fordinnew_docs],metadatas[{source:d[source]}fordinnew_docs],)print(✅ 建库完成)defsearch(self,query,top_k3):resultsself.collection.query(query_texts[query],n_resultstop_k,include[documents,distances,metadatas],)return[(doc,1-dist,meta[source])fordoc,dist,metainzip(results[documents][0],results[distances][0],results[metadatas][0],)]defgenerate(query,contexts):context_text\n.join(f-{text}来源{src}fortext,_,srcincontexts)promptf你是企业知识库助手根据资料回答问题。 【参考资料】{context_text}【问题】{query}【回答】fullforchunkinollama.generate(modelCHAT_MODEL,promptprompt,streamTrue):print(chunk[response],end,flushTrue)fullchunk[response]print()returnfullclassChromaRAG:def__init__(self):self.storeChromaVectorStore(DB_PATH,COLLECTION)defbuild_index(self,documents):self.store.add_documents(documents)defquery(self,question,top_k2):resultsself.store.search(question,top_ktop_k)print(f\n 问题{question})fori,(text,score,source)inenumerate(results,1):print(f [{i}]{score:.3f}[{source}]{text})returngenerate(question,results)if__name____main__:ragChromaRAG()rag.build_index(DOCUMENTS)# 第二次运行自动跳过rag.query(我想请假怎么申请)rag.query(出差能坐商务舱吗)六、第二次运行的变化第一次运行 新增 6 条文档... ✅ 建库完成数据已持久化到磁盘第二次运行已有 6 条跳过建库直接从磁盘加载这就是持久化的价值embed 只做一次之后复用。七、ChromaDB 的 HNSW 索引ChromaDB 底层使用HNSWHierarchical Navigable Small World索引是目前最流行的近似最近邻搜索算法第 1~2 篇手写的matrix query_vec是暴力搜索O(N) 复杂度HNSW 是图结构索引查询复杂度接近 O(log N)百万级向量查询在毫秒内完成# 配置 HNSW 参数metadata{hnsw:space:cosine}# 余弦距离语义检索推荐# 还可以配置# hnsw:M: 16 # 每个节点的连接数越大越准但占内存# hnsw:ef_construction: 100 # 建索引时的搜索宽度八、三步对比总结特性第 1 篇第 2 篇第 3 篇Embedding随机向量Ollama 语义向量Ollama 语义向量存储方式内存重启消失内存重启消失磁盘持久化检索算法暴力搜索 O(N)暴力搜索 O(N)HNSW O(log N)Metadata无无支持来源追踪重复 embed每次都做每次都做只做一次总结本文核心改动是用ChromaDB替换手写的内存向量存储持久化向量存磁盘程序重启后无需重新 embed去重通过 id 防止重复插入多次运行安全Metadata追踪每条文档的来源提升可信度HNSW 索引大规模向量毫秒级检索下一篇引入 LangChain用标准组件替换手写代码同时增加多轮对话记忆能力。