同一份文件多个版本,向量库怎么管
一、一个真实教训
给湖北理工计算机学院做了个官网问答助手。教务处把学院规章制度 PDF 全切片存进了向量库。
学生问"实验课旷课几次取消考试资格",Agent 回了三段矛盾的内容:2023 版说 3 次、2024 版说 2 次、2025 版说 2 次但附加了"需提前报备"。三段全召回,LLM 稀里糊涂拼了个"可能 2 到 3 次"。
根因:同一份文件的三次修订版都躺在向量库里,检索时全召回,没有版本意识。
二、先想清楚:是不是同一份文件
解决的第一步是——教务处上传一份新 PDF 时,系统先判断它是"哪份文件的第几个版本"。
新PDF上传 → 提取标题+正文前500字
│
├─ 标题完全相同 → 修订版
│ 《考试管理办法》=《考试管理办法》✓
│
├─ 标题高度相似 → 算指纹
│ 《考试管理办法》vs《考试管理办法(修订)》→ 去括号后相同 ✓
│
└─ 都不满足 → 全新文件
指纹怎么算:把标题 + 正文前 500 字做归一化(去括号和年份差异),算 MD5。跟前一篇 embedding 里给 chunk 做 ID 的思路一样——内容决定哈希,同一份文件的不同版本即使标题有细微差异也能靠内容匹配上。
function 计算指纹(标题, 前500字):
归一化文本 = 标题 + 前500字
归一化文本 = 去掉年份括号(归一化文本) // "2024"→"YYYY"
归一化文本 = 去掉多余空白(归一化文本)
return MD5(归一化文本)
指纹相同时直接判定为同一份文件(标题一模一样的情况)。指纹不同但标题相似度 > 80% 时也判定为修订版——政策文件严谨,宁可不合并也别误合并。
三、判定为修订版后,怎么更新
最粗暴的做法:删掉旧版全部 chunk,把新版重新切片重新 embedding。
这可以,但浪费。一份 20 页的文件可能只改了两段。而且旧版不能删——审计需要("那天给学生说的是按照哪个版本的规定?")。
更好的做法:逐 chunk 对比,只更新变化的部分。
function 增量更新(旧版chunks, 新版chunks):
for idx, 新chunk in enumerate(新版chunks):
旧chunk = 旧版chunks[idx] // 同一位置
if 内容实质相同(旧chunk, 新chunk):
// 复用旧向量,省一次 embedding
新chunk.向量ID = 旧chunk.向量ID
else:
// 这段内容变了,重新 embedding
新chunk.向量ID = embedding(新chunk.内容)
标记旧chunk.向量ID 为"旧版本,仅审计用"
实质相同的判断:去掉空白和标点后文本一致。
四、检索时默认只看最新版
存入多版本后,普通查询只查最新版。除非明明用户问"以前是怎么规定的"。
function 查询策略(用户问题):
if 问题不含 "以前""旧版""变更""原来""历史":
// 普通模式:只查最新版
过滤条件 = "version = max(version) AND status = active"
else:
// 历史模式:查所有版本
过滤条件 = "1=1"
return 向量检索(用户问题, 过滤条件)
效果:
问:"考试作弊怎么处理"
→ 普通模式 → 只查2025版 chunk → 答案唯一 ✓
问:"考试作弊以前是怎么规定的"
→ 历史模式 → 2024/2025版全返回 → Agent 对比回答变化 ✓
五、完整流程串一遍
2024-04 教务处上传《考试管理办法》v1
→ 指纹匹配: 无 → 新文件,创建 doc_001
→ 60个chunk全部embedding
2025-03 教务处上传《考试管理办法》v2
→ 指纹匹配: 标题相似度 > 80% → 同一文件,版本号=2
→ 60个chunk逐个对比: 52个不变(复用), 8个变了(重新embedding,新MD5 id自动覆盖旧)
2025-06 学生问"作弊怎么处理"
→ 普通模式 → version过滤=max → 只查v2的8个新chunk
→ 返回2025版规定 ✓
2025-06 教师问"作弊以前怎么规定的"
→ 历史模式 → 不限制version → v1/v2全返回
→ Agent 对比: "2024版严重警告,2025版改为记过"
六、核心数据结构(概念级)
不需要复杂的 SQL schema,理解思路就够了:
表: 文件
字段: id, 标题, 部门, 状态(active/superseded)
表: 版本
字段: id, 文件id, 版本号, 文件名, 内容指纹(MD5), 上传时间
表: chunk
字段: id(MD5), 版本id, 序号, 内容, 向量ID, 状态(active/archived)
chunk 的 id 直接用内容的 MD5——跟 embedding 那篇的做法完全一致。内容不变 id 不变,增量更新时自动覆盖,不需要查旧 id 再删。
每次查 chunk 时,JOIN 版本表取最新的 版本号。
七、总结
没有一行具体的库调用——因为用什么库不重要,思路才重要。五条规则:
- 去重用指纹 + 规则:标题精确匹配 + 指纹相似度 > 80% = 修订版。不调 LLM,零延迟
- Chunk 级增量更新:对比后只改变化的 chunk,不变的不重新 embedding
- 检索默认看最新版:普通查询只返回 max(版本号) 的 chunk
- 旧版归档不删:审计追溯用
- 历史查询走特殊路由:检测到"以前""原来"关键词才放开版本限制
代码量不大——两张表 + 指纹计算函数 + 增量对比逻辑 + 查询策略路由,200 行搞定。