> 增量登记复用了"全量重建"的入口,把之前所有人工判断静默回滚了
一个分层语料库有张状态表,记每本书在哪一层(L0 全部登记 / L1 进向量库 / BAD 隔离 / SUPERSEDED 被更好的版本顶掉)。登记新书时图省事调了 bookdb.init()。
结论先行¶
一个分层语料库有张状态表,记每本书在哪一层(L0 全部登记 / L1 进向量库 /
BAD 隔离 / SUPERSEDED 被更好的版本顶掉)。登记新书时图省事调了
bookdb.init()。
它的语义是 "corpus.jsonl 是真相,层级不一致就按它更新"。
而 corpus.jsonl 是最早那一步分诊的原始产物 —— 里面没有后来
体检标的 BAD,也没有版本择优标的 SUPERSEDED。一跑下去:
BAD 29 → 0 SUPERSEDED 10 → 2 L1 222 → 257
29 本广告空壳和 8 本被替代的旧版本全被放回 L1,而且没有任何报错。
还原靠的是隔离区记录没被动:里面留着 "体检:仅 1 页,含推广话术"和"已被更好版本替代:…"的原话, 按它精确回填;BAD 那批再跑一遍推导脚本重新判(顺带把另一批也扫了, 现在比原来更全)。
增量登记就该只做增量,别复用"全量重建"的入口。
现在登记脚本自带 insert(),只插新文档,绝不碰已有文档的层级。
为什么¶
"以某个文件为真相"(declarative / reconcile 语义)是个很好的设计 —— Terraform、Kubernetes、Ansible 都是这个模型。它的前提是 那个文件真的是真相的全部。
而这里的真相是两个来源的叠加:
corpus.jsonl—— 自动分诊的结果,可重算。- 后续的人工/半自动判断(BAD、SUPERSEDED)—— 不在那个文件里,且不可重算。
init() 只知道第 1 个,所以它的"修正不一致"在逻辑上完全正确:
它把状态恢复到了它所理解的真相。bug 不在 init() 里,
在"把一个只知道一半真相的入口当成通用入口"这个调用决定里。
为什么没有报错:因为对 init() 来说这不是错误状态,是待收敛状态。
reconcile 型操作天生不会对"我要改很多东西"报警 ——
改很多东西正是它的工作。所以它需要的不是错误处理,是改动量的闸门。
还原之所以可能,是因为那些判断的理由被存在了另一个字段里,
而那个字段恰好不在 init() 的管辖范围内。这是运气,但也是一个可以
刻意设计的性质:把结论和理由存在不同的生命周期里,
结论可以被重算、被覆盖,理由不行。
同 进 git 的判据只有一句:明天这个仓库能不能把它重算出来:
真正不可再生的那一小块必须被单独保护。
怎么用¶
不可重算的状态不要和可重算的状态放在同一张表的同一列里。
如果必须放,就给它一个 reconcile 操作看不见的字段
(这个项目是 stages.error 里的原话)。
判据:这一列的值,自动流程能不能重新算出来?不能,就不能让它被收敛。
全量入口和增量入口分开,命名上要让误用变难。
init() / reconcile() / sync() 这类名字应该在函数开头就
拒绝"只想加一条"的调用方。给增量操作一个专用函数
(insert() 只插新、不碰旧),比在文档里写"注意不要用 init 来加书"有效。
reconcile 型操作要有改动量闸门和 dry-run。
$ bookdb init --dry-run
would change 37 docs: BAD 29→0, SUPERSEDED 8→0, L1 +37
refusing: --max-changes is 5, pass --force to override
阈值挡的是量级错误。加一本新书时改动量是 1; 它要改 37 本已有的,这个比值本身就是警报 (同 清洗步骤必须有一道"我是不是清过头了"的自检——它差点删掉一整本书)。
人工判断要留原话,不只留标签。 "BAD" 这个标签不可还原, "体检:仅 1 页,含推广话术"可还原 —— 后者既能人工复核, 也能被脚本重新推导。标签是结论,原话是证据, 事故还原的时候只有证据有用。
我自己在另一个项目上踩的同一类坑:一个撤稿脚本的 --restore
没有选择参数,于是它恢复了全部,而我只想恢复一条。
修法一样 —— 给它加 --only SLUG,并且让不带参数的调用
在文档里写清楚"不带它就是全部"。
凡是"批量操作 + 一个你只想作用于其中一条的意图",
缺少选择参数就是一次事故。
出处¶
~/Tools/booksearch/docs/RAG_PLAN.md §16.4「事故:bookdb.init()
把之前所有人工判断回滚了」(BAD 29 → 0、SUPERSEDED 10 → 2、
L1 222 → 257、靠 stages.error 的原话回填、p9b_register.py
现在自带 insert())。