RAG 系统踩坑:管理后台删除文档后,客服系统崩了
RAG 系统踩坑管理后台删除文档后客服系统崩了前言在构建基于 RAG 的智能客服系统时我遇到了一个隐蔽的 bug通过管理后台删除文档后客服系统查询相同问题时直接崩溃报错AttributeError: NoneType object has no attribute get。重启客服系统后恢复正常。这个问题困扰了我一段时间最终发现是独立进程间的向量数据库缓存不一致导致的。这篇文章记录了问题的发现、分析和解决过程。一、系统架构我的智能客服系统有两个独立进程进程 1管理后台web/admin.py └── 上传/删除文档 → 修改 ChromaDB 进程 2客服系统web/app.py └── 用户提问 → 查询 ChromaDB → 生成回答两个进程共享同一个 ChromaDB 磁盘目录chroma_db/但各自在内存中维护了一份副本。┌─────────────┐ ┌─────────────┐ │ 管理后台 │ │ 客服系统 │ │ 内存副本 A │ │ 内存副本 B │ └──────┬──────┘ └──────┬──────┘ │ │ └───────┬───────────┘ │ ┌──────┴──────┐ │ chroma_db/ │ ← 磁盘上的数据 │ (SQLite) │ └─────────────┘二、问题现象2.1 正常流程用户提问传感器不亮了怎么办 ↓ 检索 ChromaDB → 找到 3 篇相关文档 ↓ LLM 生成回答 → 请按以下步骤排查...2.2 异常流程管理后台删除文档 ↓ 用户提问传感器不亮了怎么办 ↓ 检索 ChromaDB → 找到 3 篇幽灵文档已被删除但内存中还在 ↓ metadata 为 None → AttributeError → 崩溃2.3 错误日志AttributeError: NoneType object has no attribute get File app/agents/base.py, line 114, in _two_step_retrieve source r[metadata].get(source, ) ^^^^^^^^^^^^^^^^^三、根因分析3.1 ChromaDB 的工作方式ChromaDB 是嵌入式数据库类似 SQLite特性说明存储数据保存在磁盘chroma_db/ 目录加载启动时加载到内存缓存进程内维护内存副本3.2 问题根源时间线 T1: 客服系统启动 → 加载 ChromaDB 到内存副本 B T2: 管理后台删除文档 → 修改磁盘数据 T3: 客服系统查询 → 用内存副本 B还是旧数据 T4: 内存中有已删除文档的记录但实际数据已不存在 T5: metadata 为 None → 崩溃3.3 为什么不直接读磁盘ChromaDB 为了性能启动时加载数据到内存后续查询走内存缓存。这是合理的设计但在多进程场景下会导致数据不一致。四、解决方案4.1 方案对比方案说明优点缺点A. 错误处理metadata 为 None 时跳过简单防崩溃不解决数据不一致B. 每次查询重新加载每次查询前重新创建 ChromaDB 客户端数据实时最新有性能开销C. API 通知管理后台调用客服 API 触发重新加载精确控制实现复杂4.2 最终方案A B 组合方案 B每次查询重新加载defget_vectorstore(): 每次调用都重新创建客户端确保读取磁盘最新数据 clientchromadb.PersistentClient(pathCHROMA_PERSIST_DIR)returnclient.get_or_create_collection(nameCHROMA_COLLECTION_NAME)方案 A错误处理兜底forrinresults:# 安全检查metadata 可能为 Nonemetadatar.get(metadata)ifmetadataisNone:print(f[Agent] 警告跳过 metadata 为空的结果)continuesourcemetadata.get(source,)4.3 性能分析ChromaDB 重新加载的性能开销操作耗时ChromaDB 重新加载10-50msEmbedding API 调用200-500msLLM 回答生成1-3 秒总响应时间2-4 秒结论ChromaDB 重新加载只占总时间的 1-2%影响可忽略。五、向量数据删除的坑5.1 问题删除文档时需要同时清理三样东西PDF 文件data/raw/Markdown 文件data/processed/向量数据ChromaDB前两个用文件路径删除很简单。但第三个有问题。5.2 source 字段格式不匹配ChromaDB 中存储的 source 格式02-实验环境的搭建\02-实验环境的搭建.md但删除时构建的格式可能不同# 代码构建的格式sourcef{pdf_name_no_ext}\\{pdf_name_no_ext}.md# 如果 PDF 在子目录中实际格式是# 子目录\文件名\文件名.md格式不匹配 → 删除失败 → 向量数据残留。5.3 解决方案模糊匹配defdelete_vector_data(record):# 先尝试精确匹配collection.delete(where{source:source})# 如果失败模糊匹配all_datacollection.get(include[metadatas])ids_to_delete[]fori,metainenumerate(all_data[metadatas]):ifpdf_name_no_extinmeta.get(source,):ids_to_delete.append(all_data[ids][i])ifids_to_delete:collection.delete(idsids_to_delete)六、总结核心要点要点说明问题根源独立进程各自缓存 ChromaDB 内存副本解决方案每次查询重新加载 错误处理兜底性能影响ChromaDB 重新加载 10-50ms可忽略删除坑source 字段格式不匹配用模糊匹配解决适用场景RAG 系统有多个独立进程管理后台和问答系统分离部署需要实时更新知识库经验教训嵌入式数据库的缓存机制SQLite、ChromaDB 等嵌入式数据库会在进程内缓存数据多进程场景需要注意同步错误处理很重要即使数据理论上应该一致也要加兜底逻辑性能取舍10-50ms 的重新加载开销换来数据一致性完全值得文末结语这个 bug 让我 debug 了一段时间最终发现是 ChromaDB 的缓存机制在多进程场景下的陷阱。在 RAG 系统开发中数据一致性问题容易被忽视但一旦遇到就很隐蔽。如果你的 RAG 系统也有多个独立进程建议在查询前重新加载向量数据库10-50ms 的开销换来数据一致性非常值得。