考察点
这道题表面问的是工程流程,实际考的是你有没有真正运维过一个会持续变化的知识库。面试官想听到的是:变更怎么被发现、影响范围怎么收敛到最小、更新过程中线上检索会不会读到脏数据。追问通常会往「删除的文档怎么处理」「embedding 模型升级了怎么办」「更新时正在进行的会话怎么办」这几个方向走。
参考答案
先明确「更新」到底有哪几种
实际系统里知识库变更至少分四类,处理方式完全不同:
- 新增文档:纯追加,最简单,走正常入库流水线即可。
- 文档内容修改:需要识别哪些 chunk 变了,只重建变化的部分。
- 文档删除:最容易被忽略,但最要命——不删干净会检索到已下线的内容,合规场景(比如用户要求删除自己的数据)会出事故。
- embedding 模型升级:这个没法增量,向量空间整个变了,必须全量重建,通常配合双索引灰度切换。
变更检测:用内容哈希做 chunk 级 diff
全量重建慢的根源是把没变的 chunk 也重新 embedding 了一遍。核心思路是给每个 chunk 算内容哈希(比如对 chunk 文本做 SHA-256),元数据里存 doc_id、chunk_index、content_hash、updated_at。
新一轮更新时,文档重新切 chunk 后逐块比对哈希:
- 哈希没变:跳过,不动向量库;
- 哈希变了或新增:重新 embedding 并 upsert;
- 旧 chunk 在新版本里不存在:从向量库删除。
这样一篇 10 页的文档改了一句话,实际只有一两个 chunk 需要重算,embedding 成本和时间下降一两个数量级。文档级再加一层文件哈希(mtime + size + 内容 hash),整篇没变的文档连解析都跳过。
向量库的操作语义
主流向量库都支持按 ID upsert 和 delete,工程上有几个细节:
- chunk ID 用确定性规则生成(
doc_id + chunk_index或内容哈希),保证幂等,重跑任务不会产生重复向量; - 删除要走「软删除 + 异步清理」:先在元数据库标记
deleted,检索侧过滤,后台任务再真正从向量库移除,避免删除和检索并发时出现幻读; - 注意有些向量库的 delete 是逻辑删除,底层 segment 里数据还在,长期高频删除会拖慢检索,需要定期 compact 或重建 segment。
一致性与失败恢复
更新流水线必须幂等且可重入。实际做法是建一张 ingestion 任务表,记录每个文档的处理状态(解析中 / 已切分 / 已向量化 / 完成),任何一步失败从断点重跑。解析、切分、embedding、写入四步里,embedding 是最贵也最容易失败的一步(API 限流),要有重试和限流退避。
一致性上,推荐「先写后生效」:新 chunk 写入向量库成功后,再更新元数据里的当前版本指针。检索永远读「当前版本」,这样更新中途失败,线上读到的还是旧版本,不会出现一半新一半旧。
变更触发的三种模式
- 定时轮询:每小时/每天扫一遍数据源比对哈希,适合内部文档库,实现最简单;
- 事件驱动:对接数据源 Webhook(飞书/Notion/Git 的变更事件),秒级生效,适合时效性要求高的场景;
- 手动触发:运营在管理后台点「重新索引」,适合低频精品内容。
生产上通常是事件驱动为主、定时全量对账兜底——事件可能丢,每天凌晨跑一次哈希对账能把漂移修回来。
embedding 模型升级的特殊处理
这是最贵的场景。老模型向量和新模型向量不在同一个语义空间,混着用检索质量直接崩。做法是新建一个 collection 全量重建,后台跑完后原子切换检索指向,旧 collection 保留一段时间作为回滚备份。重建期间可以双跑对比检索质量,确认新模型没有回退再切。
可能的追问
- 文档删除了,但用户之前的会话里已经引用了那段内容怎么办?——向量库必须同步删除,已生成的历史回答一般不回溯修改,但要在会话上下文层做权限/有效性校验,避免后续轮次继续引用失效内容。
- 更新期间正在检索的用户会读到什么?——用版本指针隔离,检索固定读「已生效版本」,新数据写完再切指针,用户要么全旧要么全新,不会读到中间态。
- 百万级文档全量对账太慢怎么办?——文档级先比文件元信息(mtime/size)粗筛,只对疑似变更的做内容 hash;对账任务分片并行,按 doc_id 范围拆给多个 worker。
- chunk 哈希比对有什么坑?——chunk 内容要先把空白符归一化再算 hash,否则解析器版本变化导致的多一个空格会让全库 chunk 失效;另外 chunk 里如果嵌了时间戳等易变元数据,要把它们排除在 hash 计算之外。