删除(10个): - API与后端对接规范.md(已被后端对接规范.md 替代) - image_processing_flow.md(已合入 RAG数据流程.md) - 状态码功能更新说明.md(已合入后端对接规范.md) - 题目模板.md(字段名与实际 API 不一致,以对接指南为准) - 出题批卷系统设计.md(旧版出题设计,已被对接指南+变更说明替代) - 测试指南.md(出题 API 格式过时,模型名过时) - 企业文档更新管理方案.md(设计方案,已实施完成) - 版本管理实施完成报告.md(里程碑报告,已完成归档) - 生产路径优化计划.md(行号已偏移,阶段状态过时) 更新(7个): - 出题批题后端对接指南.md:添加 generate-smart 端点、question_content→content - 开发与系统模块说明.md:更新 reranker 和 LLM 模型名称 - 多源信息融合指南.md:标注生产路径 vs 备用路径 - 架构与部署方案.md:更新数据归属对照表(4个 SQLite DB) - 认证与权限配置指南.md:出题 API 更新为 /exam/generate - 向量库边界风险分析.md:标注 P1 旧切片残留和 P2 文件更新已修复 - 风险边界问题修复注意事项.md:标注所有场景已修复
8.9 KiB
向量库边界风险分析
基于对 knowledge/、core/engine.py、api/chat_routes.py、api/document_routes.py 全链路代码的审查,覆盖 chunk_id 生成、多库检索合并、RRF 融合、引用构建、文档预览、DocStore 存储、上传/删除等环节。
P0:多库 RRF 融合按 chunk_id 去重 — 同名文件结果被吞
位置:core/engine.py 第 1841 行 reciprocal_rank_fusion()
if doc_id not in doc_scores:
doc_scores[doc_id] = {'score': 0.0, 'doc': doc, 'meta': meta}
doc_scores[doc_id]['score'] += rrf_score
场景:public_kb 和 dept_1_kb 都上传了 规章制度.pdf,两个库中的 chunk_id 完全相同(规章制度.pdf_0、规章制度.pdf_1 ...)。多库并行检索后进入 RRF 融合,doc_scores 以 chunk_id 为 key,来自不同 collection 的同名 chunk 被视为同一文档——分数累加,但只保留先入的那条 doc 和 meta,另一个 collection 的切片被静默丢弃。
影响:
- 用户查询可能只看到
public_kb的版本,dept_1_kb的同名文件完全不出现在结果中 - 引用溯源的
_collection字段指向错误的库
修复方向:将 RRF 的 key 从 doc_id 改为 f"{coll_name}/{doc_id}"(复合键),或者在 chunk_id 前拼接 collection 名称。需要同步修改下游 MMR、Rerank、上下文扩展等环节的 ID 处理。
P0:DocStore 文件碰撞 — 跨库同名切片的表格/图片数据互相覆盖
位置:knowledge/processing.py 第 345、374 行
doc_path = docstore_dir / f"{doc_id}.json" # doc_id = chunk_id = "filename_N"
场景:public_kb 的 规章制度.pdf 第 3 个切片(chunk_id = 规章制度.pdf_2)包含一个表格,写入 .data/docstore/规章制度.pdf_2.json。之后 dept_1_kb 也上传了同名文件,其第 3 个切片也写入 .data/docstore/规章制度.pdf_2.json,直接覆盖了前者。
影响:表格原始 Markdown 或图片引用路径被覆盖,引用跳转时展示的是错误 collection 的表格内容。
修复方向:DocStore 路径改为 {collection}_{doc_id}.json,或在 JSON 内增加 collection 字段并检测冲突。
P1:同名文件重复上传 — 旧切片残留 + 搜索结果重复
✅ 已修复(2026-06-04):上传接口现在自动替换同名文件,旧切片自动标记为
superseded。详见 风险边界问题修复注意事项.md。
位置:api/document_routes.py 第 246-250 行
if os.path.exists(filepath):
timestamp = datetime.now().strftime('_%Y%m%d_%H%M%S')
filename = f"{name}{timestamp}{ext_part}"
场景:用户向 public_kb 上传 制度.pdf(v1),后来更新为 v2 再次上传同名文件。系统自动重命名为 制度_20260604_120000.pdf,v1 的旧切片(制度.pdf_0 ... 制度.pdf_N)仍然存在于向量库中。搜索结果同时返回 v1 和 v2 的内容,用户无法区分。
影响:
- 搜索结果中出现重复内容(不同文件名但内容相近)
- MMR 去重可能保留 v1 的切片而排除 v2 的
- 文档列表
list_documents()显示两个文件,用户难以判断哪个是最新版
修复方向:上传时检测是否存在同名文件,若有则先调用 delete_document() 清除旧切片,再写入新版本(或增加版本管理字段自动关联)。
P1:_collection 字段回退逻辑 — 多库时始终指向第一个 collection
位置:api/chat_routes.py 第 1529-1536 行
if not meta.get('_collection'):
if collections and len(collections) == 1:
meta['_collection'] = collections[0]
elif collections:
meta['_collection'] = collections[0] # 多知识库时回退到第一个
else:
meta['_collection'] = 'public_kb'
场景:在单库检索路径下(非 _search_multi_kb),ChromaDB 原生返回的 metadata 不含 _collection 字段。当 collections 列表包含多个库名时,所有结果的 _collection 都被设为 collections[0]——即使某些结果实际来自其他库。
影响:引用跳转时 preview API 用错误的 collection 查询,返回 404 或其他文档的切片。
现状缓解:生产环境走 _search_multi_kb 路径时,每个 collection 的 metadata 都在查询时被正确注入了 _collection(engine.py 第 1406 行),所以这个回退逻辑实际上很少被触发。但作为防御性代码,它的行为是错误的。
修复方向:回退逻辑应使用 meta.get('collection')(入库时写入的字段),而不是硬编码 collections[0]。
P1:search_multiple() 去重仅按 ID — 低层 API 的同名文件问题
位置:knowledge/search.py 第 289-292 行 _merge_multiple_results()
seen = set()
for item in all_items:
if item['id'] not in seen:
seen.add(item['id'])
unique_items.append(item)
场景:与 P0 RRF 问题类似,但在 SearchMixin.search_multiple() 这个独立的检索 API 中。虽然生产 RAG 路径不走这个方法(走 engine 的 _search_multi_kb),但任何直接调用 kb_manager.search_multiple() 的代码都会遇到同样的去重问题。
修复方向:去重键改为 (collection, id) 复合键。
P2:引用构建的字段名不一致 — collection vs _collection
位置:knowledge/manager.py 第 281 行写入 "collection": kb_name;api/chat_routes.py 第 590 行读取 meta.get('_collection', '')
场景:入库时 metadata 存的是 collection(无下划线),但 citation 构建时读的是 _collection(有下划线)。两个字段名不一致,如果 metadata 中没有被后续流程注入 _collection,citation 的 collection 字段会返回空字符串。
影响:前端引用跳转 URL 缺少 collection 部分,preview API 路径不完整。
现状缓解:生产 RAG 路径在检索阶段会注入 _collection(engine.py 第 1406 行),所以正常流程下不会触发。但其他非标准路径(如直接查询 ChromaDB)会暴露此问题。
修复方向:_build_citation 同时读取两个字段名:meta.get('_collection') or meta.get('collection', '')。
P2:文件无原地更新机制
✅ 已修复(2026-06-04):上传接口新增自动替换机制(
replaced=true),同名文件自动替换旧版本。
场景:用户上传 制度.pdf v1 后发现内容有误,修改后想替换。当前系统没有 "更新文件" 接口,只能删除后重新上传。如果用户不知道要先删除,就会触发 P1 的重复问题。
修复方向:upload 接口增加 "如果同名文件已存在则替换" 选项(先 delete_document 再 add_file_to_kb),或提供独立的 "更新文档" API。
P2:元数据文件与磁盘状态不同步
位置:knowledge/collection.py 第 362 行 list_collections() 依赖 self._metadata JSON 文件
场景:metadata JSON 文件存储在 knowledge/vector_store/kb_metadata.json。如果该文件损坏或被手动修改,list_collections() 返回的列表与磁盘上实际的 ChromaDB 目录不一致。虽然有自动补充逻辑(扫描磁盘目录自动注册),但仅限于回退场景。
影响:向量库可能在列表中不可见,或列表中出现已不存在的向量库。
P3:文件名含下划线
位置:api/chat_routes.py 第 578 行
chunk_index = int(str(chunk_id_raw).rsplit('_', 1)[-1])
场景:文件名为 2026_年度_制度.pdf 时,chunk_id 为 2026_年度_制度.pdf_0。
现状:使用 rsplit('_', 1) 从右边分割,最后一个 _ 后的数字能正确解析。此场景当前不会出错。但如果文件名为 test_.pdf(尾部下划线),chunk_id 为 test_.pdf_0,rsplit 仍正常工作。无需修复,记录备查。
风险总结
| 等级 | 风险 | 核心原因 | 触发条件 | 状态 |
|---|---|---|---|---|
| P0 | RRF 融合吞结果 | 去重 key 缺少 collection | 多库有同名文件 | 未修复 |
| P0 | DocStore 覆盖 | 存储路径缺少 collection | 多库有同名含表格/图片文件 | 未修复 |
| P1 | 旧切片残留 | 重复上传只改名不替换 | 同名文件二次上传 | ✅ 已修复 |
| P1 | _collection 回退错误 | 硬编码 collections[0] | 单库路径 + 多 collection | 未修复 |
| P1 | search_multiple 去重 | 去重 key 缺少 collection | 直接调用低层 API | 未修复 |
| P2 | citation 字段名不一致 | collection vs _collection |
非标准查询路径 | 未修复 |
| P2 | 无文件更新机制 | 设计缺失 | 用户需要替换文档 | ✅ 已修复 |
| P2 | 元数据不同步 | JSON 文件可能损坏 | 手动操作或异常退出 | 未修复 |
| P3 | 文件名含下划线 | 无问题(rsplit 兼容) | — | 无需修复 |