日期: 2026-09-03
状态: 已批准(Lexi 确认方案 C 全量对标)
版本: v1.0.0
kb-cli 是 Go 实现的知识库索引与检索 CLI(SQLite + FTS5 + 知识图谱),服务 LPG 知识库(~/rag-lpg-obsidian,约 768 个 .md 文件)。
CodeGraph(colbymchenry/codegraph)是代码知识图谱工具,其核心机制经源码分析确认:
unresolved_refs 表记录解析失败的引用(failed + name_tail),下次同步有新符号出现时自动重试provenance 字段(静态解析 vs 启发式合成),消费方一眼区分kb-cli 现状痛点(源码分析确认):
| 痛点 | 位置 |
|---|---|
| 排序纯文本加权,无图结构信号 | internal/search/engine.go |
| aliases 已解析但搜索完全未用 | internal/vault/parser.go / internal/index/fts.go |
| 索引新鲜度靠 git commit hash 比对,未 commit 的编辑不可见 | internal/index/cache.go |
| 索引变更即全量重建,无增量 | cmd/index.go / cmd/rebuild.go |
| wikilink 模糊匹配(去编号前缀/标题部分匹配)可能产生假边,无可信度标注 | internal/graph/builder.go |
| 悬空 wikilink 直接丢弃,无留痕无自动补全 | internal/graph/builder.go |
| 搜索结果全量 dump 内容,无预算控制 | internal/search/engine.go WithContent |
explore 命令供 Hermes agent 一次调用拿到完整上下文(原文 + 关联 + 预算控制)新增 internal/graph/rwr.go:
map[nodeID]float64(游走质量),归一化到 [0,1]最终排序分(internal/search/engine.go 修改):
finalScore = norm(textScore) * textWeight + rwrMass * (1 - textWeight)
norm(textScore):本批次内 min-max 归一化到 [0,1](textScore 沿用现有位置加权:path/title/tag/content + 通用词/实体词降权;批次只有 1 条结果时 norm=1)rwrMass:已归一化 [0,1]search.text_weight,默认 0.5),两信号量纲对齐后直接加权nodes 表加列 aliases TEXT(JSON 数组),parser 已解析 frontmatter aliases,直接落库aliases 4.0, title 3.0, content 1.0(aliases 是 FAQ 症状词主战场,最高权重)FTSSearch 对 aliases 列的命中在 textScore 中按 title 档计权nodes 表加列 status TEXT(parser 从 frontmatter 提取,缺省视为"已解决")待审阅 时 finalScore × 0.6--json 输出含 status 字段)edges 表加列 provenance TEXT:
| 值 | 含义 |
|---|---|
exact |
wikilink 文本 == 目标标题,或 == 文件名(去 .md 和编号前缀)精确匹配 |
fuzzy |
标题包含关系等模糊匹配(现有 matchesWikilink 的非精确分支) |
tag / entity |
tag/entity 虚拟边(非 wikilink,不适用可信度概念,但统一存值) |
消费规则:RWR 邻接中 fuzzy 边权重 ×0.5;graph query / graph related 输出标注 provenance。
新增表:
CREATE TABLE IF NOT EXISTS unresolved_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_node INTEGER NOT NULL,
link_text TEXT NOT NULL,
name_tail TEXT NOT NULL, -- link_text 去掉 [[ 内的锚点/标题修饰后的尾部
status TEXT NOT NULL DEFAULT 'pending', -- pending | failed
created_at TEXT DEFAULT (datetime('now')),
last_attempt TEXT
);
CREATE INDEX idx_unresolved_tail ON unresolved_links(name_tail) WHERE status='failed';
retryUnresolved:unresolved_links 的 name_tail 精确命中--force)时清空表并重新解析internal/index/cache.go 重写为 reconcile.go:
{path: (size, mtime, sha256)}(sha256 仅对 size/mtime 变化的文件计算,避免全库哈希开销)retryUnresolved(§2.2)meta.git_commit 仍记录(供 status 展示),但**不再作为重建判据**index build 默认走增量对账路径;index build --force 保留全量重建(ClearData + 重建 + 清空 unresolved_links)。
NeedsRebuild 逻辑废弃:search 命令的 pre-flight 改为"快速对账"——只比对 size/mtime(不读内容不哈希),有差异才触发完整对账。768 文件 stat 开销 <50ms。
CREATE VIRTUAL TABLE nodes_fts USING fts5(
title, content, tags, aliases,
content='nodes', content_rowid='id',
tokenize='unicode61'
);
-- 触发器:nodes 的 INSERT/DELETE/UPDATE 同步维护 nodes_fts
PopulateFTS 全量重建路径,增量维护--force 全量重建时执行 INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild') 一次性重灌新增 schema_versions 表(同 codegraph)。Open 时按版本顺序应用迁移:
迁移幂等(列已存在则跳过),老库自动升级,无需手动重建。
cmd/explore.go(新):
kb-cli explore <问题> [--budget 16000] [--top 5] [--json]
流程:
--budget(默认 16000 字节,硬上限 32000)分配预算:按 finalScore 降序依次分配,每文档至少 800 字节,超预算的文档截断到整段边界#~####)切段,只输出命中查询词的段落(整段不截半句);文档总长 < 分配预算时整篇输出以上为文档原文直出(含行号),agent 无需再读文件;--json 输出结构化预算诊断:KB_EXPLORE_DEBUG=1 时 stderr 输出每文档分配/实际/占比表(对标 codegraph 的 explore 诊断,默认关闭且不影响输出字节)。
config.yaml 新增:
search:
text_weight: 0.5 # 文本分权重(图分权重 = 1 - text_weight)
explore:
default_budget: 16000 # 默认字节预算
hard_budget: 32000 # 硬上限
top_n: 5
vault 扫描(stat对账) → 变更文件解析(parser) → 节点/边增量写入(SQLite+FTS触发器)
↘ 悬空链接入 unresolved_links
查询: 关键词 → FTS5(bm25) → 种子 → RWR(图质量) → 双信号排序 → [search 列表 | explore 原文+关联]
单元测试(Go test):
- rwr_test.go:收敛性、种子外孤立节点质量≈0、tag 半权重生效
- reconcile_test.go:新增/修改/删除/未变四场景,边重建正确性
- unresolved_test.go:悬空入表 → 新节点入库 → 自动解析建边删行
- migration_test.go:v1 老库 → v2 迁移幂等,FTS 重灌后查询正确
- explore_test.go:预算分配、段落截取不截半句、硬上限
端到端验证(~/rag-lpg-obsidian 768 文件实库):
1. 旧版索引 vs 新版索引,同一组售后查询(含症状词,如"红绿闪""充气失败")排序对比:目标文档 rank 提升
2. 增量同步耗时 vs 全量重建耗时
3. 未 commit 编辑:修改一个文档后直接 kb-cli search,新内容可见
4. 悬空补全:新建一个文档标题匹配既有悬空链接,确认双链自动建边
5. explore 输出在预算内且原文与文件一致(diff 校验)
| 风险 | 缓解 |
|---|---|
| RWR 权重调不当导致排序退化 | text_weight 可配置;端到端用固定查询集回归对比,退化即回调 |
| 增量对账边界 case(git mv、文件重命名) | 重命名 = 删+增,正确性不受影响;mtime 未变但内容变的极端 case 由 sha256 二次确认兜底 |
| 老库迁移破坏现有索引 | 迁移前自动备份 .db 文件(cp 到 .db.bak);迁移失败可回滚 |
| explore 段落截取对无标题结构的文档失效 | 无标题文档整篇输出(受预算约束) |