判定流水线
一份文件从「躺在某个目录里」到「出现在投影的正确位置、进了索引、留了账」,中间要过五段流水线。这一篇说明每一段在干什么、你能插手哪一段、以及出错时从哪一段查起。manifest / journal / plan / rules.lock 四个格式契约件已由库格式公开规范 FORMAT v0.1(仓库 docs/FORMAT.md)逐字段冻结,第三方可依赖。
01五步流水线
| 步 | 名称 | 输入 → 输出 | 有没有副作用 |
|---|---|---|---|
| ① | 采集信号 | 文件元信息 → signals 结构 | 无,只读 |
| ② | 规则判分 | 信号 + 规则 → S1–S4 分数 | 无,纯计算 |
| ③ | 判定梯 | 分数 → 库/目标/置信/理由 | 无,纯计算 |
| ④ | 三态分流 | 置信 → 直落 / 建议 / 待归 | 无,纯计算 |
| ⑤ | 投影写盘 | 判定 → .kb/ 全部产物 | 有,只在 apply 时发生 |
②③④ 之间零 IO。这是刻意的:内核对同一份输入必须给出同一个答案,所以它不能读时钟、不能读网络、不能读环境变量。
02判分的四类信号
| 信号 | 内容 | 权重 |
|---|---|---|
| S1 显式 | .xhproject 这类标记文件、.kbfold.yaml 里的显式声明 | 0.95 |
| S2 名称 | 文件名与路径里的关键词(词表来自模板) | 0.80 |
| S3 结构 | 同目录下兄弟文件的类型分布是否像某个场景 | 0.6 |
| S4 类型 | 扩展名本身的分布倾向 | 0.3 |
权重不是概率,是「谁更该被信」的顺序。四类信号不会相加,而是按下面的梯子逐级下降。
03L0–L6 七级判定梯
从最高开始逐级下降,命中即停,不再往下看:
| 级 | 依据 | 置信 | 说明 |
|---|---|---|---|
| L0 | 显式标记 | 0.95 | 见到 .xhproject 就是它,不解释 |
| L1 | 名称全等 | 0.92 | 知识库名与目录名完全一致 |
| L2 | 模糊唯一 | 0.85 | 模糊匹配只有一个候选 |
| L3 | 词表命中 | 0.80 | 关键词命中 |
| L4 | 历史投票 | 0.75 | 同目录其他文件的既有归属 |
| L5 | 本地打分 | 0.40–0.70 | S3/S4 加权,区间宽是因为它本来就不可靠 |
| L6 | 兜底 | — | 进 _inbox/ |
残差 = 0.95 − 实际置信,会写进报告。残差大的文件就是「最该人工看一眼」的那批,比按置信排序更有用。
04三态分流
| 置信区间 | 处理 | 界面上长什么样 |
|---|---|---|
| ≥ 0.90 | 直落,不需要确认 | 正常行 |
| 0.70 – 0.89 | 给出建议 | 建议行 + 灰字理由 |
| < 0.70 或平手 | 进 _inbox/ 附候选 | 待归行,没有「猜一个」按钮 |
第三条是这套工具的性格:宁可空着,不替你猜。
05增量:第二次跑只碰动过的文件
六步:扫描 → 指纹 → 比对 manifest → 只对变化集判定 → 只动差异的投影与索引 → 原子提交 manifest。
指纹先比 size + mtime,只有这俩变了才去算 SHA-256——这是为了在十万文件的目录上把 IO 压下来。
变化被分成四态:
| 态 | 判据 | 做什么 |
|---|---|---|
| 新增 | manifest 无记录 | 判定 + 投影 + 建块 |
| 变更 | 同路径,hash 变了 | 重解析,重建该文件的块 |
| 移动 | 同 hash,新路径 | 只改链接,不重解析(省掉约 90% 的活) |
| 消失 | 有记录,盘上没有 | 打 tombstone,绝不删索引,可撤回 |
「移动」是这里最值钱的一态。用户拖一次目录,普通工具会当成「删了 200 个文件又新增 200 个文件」,KBFold 只更新 200 条链接。
另有两个防抖机制:
settled终态集:已经人工定案的文件记settled: true,后续批次不再重复判定。- 组合键幂等:
batch = hash(规则版本 + 变化集),同一规则、同一批变化重跑,不会产生第二批动作。
06manifest.json 长什么样
{
"version": 1,
"rules_locked": "sha256:8f3c…",
"files": {
"星海项目/合同/2024-采购合同-v2.pdf": {
"size": 412903,
"mtime": 1730188800,
"hash": "sha256:1a2b…",
"kb": "XH-2024",
"target": "02-合同",
"confidence": 0.95,
"reason": "L0:显式标记 .xhproject",
"settled": false
}
}
}大目录上这个文件会很大,所以是分片存储的(按顶层目录切片),十万文件也不至于每次整读。
reason 字段是人话,直接显示在报告与 kbfold explain 里。这个格式是完全公开的:第三方工具可以读写 .kb/,我们不会加私有字段。一个能被别人接管的台账,才可能变成事实标准。
07apply 的六步链路
| 步 | 干什么 | 失败时 |
|---|---|---|
| ① 扫描 | 生成 manifest(只读) | 整批不开始 |
| ② 规划 | 生成 plan.json(IR) | 整批不开始 |
| ③ 执行 | 按 batch_id 整批事务 | 整批停,不留半成品 |
| ④ 记账 | 追加 journal.jsonl | 写不进 journal,该步不许执行 |
| ⑤ 留底 | 原件快照进 trash/<batch>/ | 磁盘不足,拒绝执行 |
| ⑥ 撤销 | kbfold undo <batch> | hash 不符,停下报冲突 |
④ 是整个设计的枢纽:先记账再动手,账写不下去就不动手。 所以不存在「改了但没记」的状态。
journal.jsonl 一行一条,追加不改写:
{"batch":"20261030-1412-a3f9","op":"rename","from":"合同/a.pdf","to":"02-合同/2024-星海-采购合同-v2.pdf","hash_before":"sha256:1a2b…","hash_after":"sha256:1a2b…","ts":1730189520}
{"batch":"20261030-1412-a3f9","op":"undo","from":"02-合同/2024-星海-采购合同-v2.pdf","to":"合同/a.pdf","hash_before":"sha256:1a2b…","hash_after":"sha256:1a2b…","ts":1730191200}注意第二行:undo 本身也是一条 append 记录,不是把第一行删掉。所以任何时刻把 journal 从头读一遍,都能重放到「现在到底是什么状态」。
08原子性
- 同卷(同一个盘):改名与移动走
MoveFileEx,本身是原子操作。 - 跨卷(C: → D:):系统只提供 copy,所以只能 copy → 校验 hash → 删源,三步不可能原子。
因此跨卷批次一律按「整批成功或整批停」执行,绝不逐个文件试运气。
09归档:三档,全都不动
| 档 | 时间 | 做什么 |
|---|---|---|
| 活跃 | < 30 天 | 全量投影 + 索引,不提示。约 80% 的文件 |
| 近线 | 30 – 180 天 | 仍全量索引,只在 audit 里列「可归档清单」并标明体积。约 15% |
| 归档 | > 180 天 | 什么都不做。只有你显式 kbfold archive --to <目标> 才生成移动计划 |
第三档最容易做错:很多整理工具会自动把老文件搬到「归档区」,然后你再也找不到它。KBFold 永远不自动搬,方向必须由人给。
10检索:三路融合
kbfold search 同时走三条路,只读名次不读分数,用 RRF(k=60)融合:
| 路 | 实现 | 缺席时 |
|---|---|---|
| 语义 | bge-m3 问句向量,取 top 8 | Ollama 没起 ⇒ 静默跳过,不报错 |
| 内容 | FTS5 取 12 个候选,2 字词退化为 LIKE | sqlite 不可用 ⇒ 本路跳过 |
| 关键词 | 五个字段 LIKE + 中文二元组,上限 16 个词元 | 从不缺席 |
命中子块会回灌父块(子块 512 字符,父块上下文 1600 字符、上限 4000),这样返回的是能读的整段,而不是断句。
四条容量闸:单库 10 万块 / 单库矩阵 512MB / 全进程 1GB / 单查询 500ms,都在 80% 处预警。超闸不是崩溃,是把对应的那一路降级并在结果里注明。
所有结果都带 [n],可回溯到文件与行号——这一点写进了 AI 工具的调用说明里,见「架构与设计」中的引用纪律五条。