30 秒判断:这不一定是文件丢了,更可能是“当前知识库”被一批新任务重新定义了。任务成功、文档登记、索引生成和活动快照发布是四种不同状态;只检查数据库里有没有文件,无法证明检索侧现在能看到几篇。
适合谁读:已经跑通过“上传文件 → 向量检索”,正在做批量导入、增量更新、版本管理或知识库运维的人;也适合正在设计同步、索引和发布链路的工程师。
系列位置:本文属于《学习问答助手Agent实践》的实现复盘部分。系列前 9 篇先完成了需求定义、架构设计与评测方法;进入实现后,第一篇《AI 写代码越快,为什么我反而越没有掌控感?》讨论控制模型,本篇接着复盘设计原则落到真实任务链后暴露的故障。前期入库设计篇讲的是原则上应该怎样构建。
结构速览:§0 故障现场;§1 为什么文件没丢但知识库变小了;§2 四个状态不能混;§3 错误的发布模型;§4 哪些“快速修复”不该做;§5 正确的增量快照;§6 真正应该补的测试;§7 产品层面的启示;§8 协作复盘;§9 一页速查。
证据标记:✅ 实测=有真实界面复现;🧪 回归=已有自动化测试固定行为;🤔 推断=由当前机制推导出的结论。
0. 故障现场
一个很普通的用户操作序列:
- 先选择 3 个 HTML 文件;
- 后台任务完成,任务状态显示成功;
- 再选择另外 4 个 HTML 文件;
- 第二个任务也显示成功;
- 打开“当前知识库”,只看到 4 篇。
用户自然会问:
我先后入库了 3 篇和 4 篇,知识库到底应该有几篇?
按照“单用户、单知识库、增量更新”的产品语义,答案应该是:
3 + 4 = 7
实际却是:
第二批入库后,活动知识库里只剩第二批的 4 篇
更迷惑的是,前 3 篇并没有从本地文档登记库消失,旧快照也还在。它们只是没有进入“下一次检索实际读取的那份活动快照”。
✅ 实测:这个现象在真实桌面界面中复现,不是单纯的前端计数错误。
1. 为什么“文件没丢”,知识库却变小了
Agentic RAG 的入库不是一次文件复制,而是一条会跨多个状态层的流水线:
原始 HTML
↓
文档身份与版本
↓
章节树与 blocks
↓
dense / keyword 索引
↓
collection 快照
↓
活动指针
↓
检索真正能看到的内容
这条链路至少包含四种不同事实:
| 层 | 回答的问题 | 有数据是否等于用户能搜到 |
|---|---|---|
| 原始 artifact/文档登记 | 系统保存过什么 | 否 |
| 候选索引 | 这批 blocks 是否可检索 | 否 |
| collection 快照 | 一组固定版本的数据和索引是否完整 | 否 |
| 活动快照指针 | 当前检索正在使用哪份快照 | 是 |
故障发生时:
- 文档登记库里有旧文章;
- 旧快照仍然保存着前 3 篇;
- 新批次的 4 篇也完成了索引;
- 但活动指针切到了一张“只包含本批 4 篇”的新快照。
所以,从“数据是否还在磁盘上”看,没有丢;从“用户下一次搜索能看到什么”看,确实丢了 3 篇。
🤔 推断:只要系统存在“活动快照”这一层,就不能用数据库里的文档总数回答“当前知识库有几篇”。
2. 四个状态不能混
很多 RAG 原型用一句话描述入库:
文件已经处理完成。
但“处理完成”至少有四种含义:
2.1 任务成功
表示某个后台任务完成了它负责的动作。
问题是,任务可能只负责当前批次。任务成功并不自动说明全局知识库状态正确。
2.2 文档已登记
表示系统已经分配稳定身份和版本。
这也不等于文章已经建立索引,更不等于它进入了当前可检索集合。
2.3 索引已生成
表示这批 blocks 已经生成向量和关键词候选索引。
候选索引仍然是“构建材料”,不是已经发布的活动知识库。
2.4 当前知识库已更新
只有活动快照指针切换成功,下一次检索才会读取新的文档集合。
因此,真正需要验证的不是:
任务有没有成功?
而是:
任务成功之后,活动快照中的文档集合是否符合用户预期?
这两句话看起来接近,测试价值完全不同。
3. 错误的发布模型:把“本批结果”当成“整个知识库”
最初的实现逻辑可以简化成:
新快照 = 本批成功的 docs + 本批成功的 rows
这个模型在第一次导入时完全正常:
第一批:0 + 3 = 3
第二次导入也不会报错:
第二批:0 + 4 = 4
它会非常安静地把“追加”变成“替换”。
更准确地说,代码没有主动删除第一批;它只是创建了一份只装第二批的新快照,然后让活动指针切过去。在快照模型里,这等价于告诉系统:
从这一刻开始,整个知识库只有这 4 篇。
为什么单批测试看不出来
如果每个测试都只做一次导入,那么:
- 第一批发布 3 篇,正确;
- 第二批发布 4 篇,单看本次也正确;
- 索引、manifest、checksum 都对;
- 活动指针也只指向一份内部完整的快照。
每个局部都“正确”,连续操作却违反产品语义。
这是很典型的状态迁移错误:局部操作的断言通过了,操作序列的系统不变量没有被测试。
从人机协作角度看,这里真正缺的也不只是一条测试。AI 可以把“本批结果生成快照”实现得局部正确,但原来没有被明确交付给它、也没有被负责人检查的,是更高一层的产品不变量:
同一个知识库连续导入两批不同文档,旧文档必须继续存在。
这正说明,切片开始时只写“支持批量入库”还不够。控制契约必须同时写清用户可观察的状态结果,例如“3 篇后再导入 4 篇,当前知识库必须是 7 篇”。
4. 哪些“快速修复”不该做
看到问题后,很容易想到几种快速方案。
方案 A:从文档登记库读取所有文档重新发布
这个方案看起来可以让文档数变成 7,但它绕过了当前活动快照。
如果用户先回滚到旧快照,再从登记库读取“所有最新文档”,系统会把回滚结果又悄悄覆盖掉。
错误不只在数量,而是在合并基线。
方案 B:每批都全量重新解析、重新 embedding
这可以保证结果统一,但旧文章的内容没有变化,却要重复抓取、切块、调用 embedding。成本高,速度慢,还会制造不必要的新版本和外部请求。
方案 C:索引行直接 append
向量表和关键词表追加新行看似简单,但同一篇文档更新时,旧版本 rows 也会残留。
最终可能同时搜到同一文档的两个版本。
方案 D:发现数量不对就切回上一个快照
回滚只能撤销一次发布,不能解决“下一次增量仍然会覆盖”的机制问题。
判断原则:修复必须基于“旧活动快照 + 本批变更”生成新状态,而不是从另一个事实源重新拼答案。
5. 正确的增量快照
最终采用的状态迁移是:
新活动集合 =
旧活动快照中未被本批替代的文档
+ 本批成功文档
对应到索引:
新索引 rows =
旧快照中保留文档的 rows
+ 本批成功文档的 rows
这套合并规则需要同时满足五条不变量。
不变量 1:不同来源继续追加
本批没有出现的旧 doc_id 原样保留。
旧:[A, B, C]
新批:[D, E, F, G]
结果:[A, B, C, D, E, F, G]
不变量 2:同一文档按版本替换
本批出现相同 doc_id 时,用它覆盖旧版本,而不是增加一篇:
旧:[A(v1), B, C]
新批:[A(v2)]
结果:[A(v2), B, C]
不变量 3:artifact 和索引必须一起替换
不能只换文档清单,不换 rows;也不能只换向量,不换关键词索引。
旧文档的 artifacts 和 rows 原样进入新快照,新版本则整组进入。
不变量 4:本批失败不能删除旧文档
旧:[A, B]
新批:[C 成功, D 失败]
结果:
documents = [A, B, C]
failed_documents = [D]
失败只表达“本批哪一个输入没有完成”,不能据此清空以前已经成功的内容。
不变量 5:合并基线是旧活动快照
不是文档登记库,也不是“磁盘上最新版本”,而是用户此刻正在使用的 activity snapshot。
否则回滚后再导入,会把用户明确选择回滚掉的内容重新带回来。
✅ 回归:当前实现用 3 篇后追加 4 篇的自动化测试固定了 7 篇结果,并同时校验 manifest 与索引 rows。
6. 真正应该补的测试
修复之后,最有价值的测试不是再测一遍单批发布,而是测试操作序列。
6.1 连续批次
任务 A:导入 3 篇
任务 B:导入另外 4 篇
断言:活动 manifest 7 篇
断言:索引 rows 覆盖同一个 7 篇集合
只检查 manifest 不够,因为可能出现“清单有 7 篇,索引仍只有 4 篇”。
6.2 版本替换
第一次:A(v1)
第二次:A(v2)
断言:文档数仍为 1
断言:活动版本为 v2
断言:索引只保留 v2 rows
6.3 partial 保留
旧库:[A, B]
新批:[C 成功, D 失败]
断言:活动库为 [A, B, C]
断言:failed_documents 只包含 D
6.4 回滚后的增量
v1:[A]
v2:[A, B]
回滚到 v1
再导入 C
断言:结果为 [A, C],不是 [A, B, C]
这条测试专门防止实现偷偷从“最新登记库”取数据。
6.5 数量、清单和索引三方对账
最终至少要同时满足:
manifest.documents
== collection rows 里的 distinct doc_id
== 用户界面显示的文章清单
🧪 回归:这些路径现在由连续批次、版本替换、partial 保留和回滚测试共同覆盖。
7. 产品层面的启示
这个故障不只是后端发布算法问题,它也暴露了产品状态不可见。
用户最初无法回答:
- 我到底入库了几篇?
- 都是哪几篇?
- 上一次操作成功了吗?
- 为什么任务显示成功,知识库数量却不对?
如果界面只显示一个“已完成”,用户只能猜测。
因此修完发布逻辑后,产品还需要同时提供两个事实源:
| 用户问题 | 事实源 |
|---|---|
| 现在能搜到什么 | 当前活动快照的文档清单 |
| 刚才做过什么 | 持久化的入库任务记录 |
它们不能互相代替:
- 任务成功不等于知识库一定变化;
- 知识库有 7 篇也不代表最近一次任务成功;
- 文档登记库有旧版本,不代表当前检索可见。
对用户最重要的不是 snapshot,而是“现在有什么”和“刚才发生了什么”。这也是负责人通过真实操作发现系统模型偏差的控制接口。
8. 协作复盘
这件事改变了我对分工的判断:
- AI 很快实现并验证了局部快照机制,但“任务成功是否等于知识库正确”仍需要人来追问;
- 人真正发现问题,不是靠逐行读代码,而是用连续操作恢复系统级心智模型;
- 序列测试后来由 AI 实现,但应该更早由控制契约明确出来;
- 当前知识库清单和操作历史不是装饰页面,而是让负责人和用户共同观察系统状态的控制接口。
因此在这个问题上,更合适的分工不是“AI 写、人审”,而是:
人定义系统不变量
AI 实现并写出验证
机器执行回归
人用真实操作确认产品语义
9. 一页速查
故障:先导入 3 篇,再导入 4 篇,活动知识库只剩 4 篇。
根因:新快照只合并了当前任务的 docs/rows,没有合并旧活动快照。
为什么没早发现:单批测试全部通过,没有测试连续操作后的系统不变量。
正确公式:
新活动集合 =
旧活动快照中未被本批替代的文档
+ 本批成功文档
必须同时检查:
- manifest 文档集合;
- dense/keyword 索引 rows;
- 当前界面文章清单;
- previous snapshot 与回滚;
- 同
doc_id的版本替换。
最重要的一句话:
任务成功只是事件;活动快照才是用户此刻真正拥有的知识库状态。