起点:一个装饰性图谱

我的知识库门户里一直有个”图谱”页面:力导向布局、节点按来源着色、能拖拽缩放。
看起来很像回事。

直到我看了一眼数据:

nodes=1104  links=3

1104 个节点,3 条边。 边全部来自正文里的 [[双链]],而我几乎不写双链。
这个页面唯一的用途是截图好看。

为什么不直接用 LLM 抽

2026 年这个方向最热的是 GraphRAG(微软)和 LightRAG(港大),
它们的做法都是:用 LLM 从文本里抽实体和关系。

我想过,但算了三笔账:

  1. 额度:免费 10,000 neurons/天。用 LLM 抽 1187 篇文档,一次就可能吃掉大半,
    而且抽取质量不稳定,重跑一次结果就变;
  2. 可复现:规范判定要求”同输入同输出”。LLM 抽出来的图,今天和明天不一样,
    基于它做的判定就没法解释;
  3. 可维护:词表写错了,我能定位到具体词条;模型抽错了,我只能”再抽一次看看”。

所以我选了确定性路线:结构化事实优先,LLM 留到以后按需补充。

三层抽取

层 来源 产出 覆盖率
L0 结构事实 路径、front-matter 文档→目录/来源/设备/标签 100%
L1 显式链接 [[双链]]、Markdown 相对链接 文档→文档 约 1250 条
L2 词典实体 内置词表 + 正则 文档→实体 约 540 条

L0 是覆盖率的关键:每篇文档都”属于”某个目录、来自某个工具、在某台设备上。
这些关系不需要”理解”,只需要解析。先把确定的东西全拿到,再谈语义。

最终:

nodes=1649  edges=15955
belongs_to 9875 tagged 1907 mdlink 1248
from_source 1187 on_device 1187 mentions 539

三个必须处理的坑

坑 1:枢纽节点会毁掉可视化

device 和 source 这两个节点几乎连到所有文档。从任意文档出发走两跳:

文档 → 设备 → 另外 295 篇文档

图上一秒变毛线球。解法是展示但不穿透:枢纽类节点画出来,但不再从它继续扩展;
再加单层 60 个节点的上限。

坑 2:伪双链

正文里的 Markdown 示例会被误当成真链接:

比如可以写 [[{ "tag": "md", "text": "..." }]]

原来的 31 条”断链”里,有 24 条是这种噪声。加了一条过滤规则
(长度、非法字符、协议前缀)后剩 7 条真实断链。

坑 3:中文分词

FTS5 用 unicode61,会把一整串中文当成一个 token。
所以「索引管线怎么分批嵌入」这种整句查询,MATCH 命中是 0,LIKE 兜底也难。
短期靠”查两个关键词”,中期要接 jieba 预分词。

规范判定:让规则可写、可复现

规则放在内容仓的 policy/rules/*.yaml,和笔记一起版本化:

- id: fm.required
name: front-matter 必填字段
kind: front_matter
severity: error
params:
required: ["title", "source"]

关键设计是 kind 是封闭枚举,YAML 只传参数、不写 SQL。
内容仓是可写的(笔记软件、网页、客户端都能改),如果规则里能塞 SQL,
等于给内容仓开了一个注入面。

结果

1187 篇 × 13 条规则 → 2068 条判定记录
违规 881 条:warn 125 + info 756,error 0
按规则:doc.orphan 756 / title.dup 107 / naming.no_space 11 / 断链 7

error = 0 说明敏感信息扫描(私钥、API Key)在内容仓里零命中——这是我最想看到的结果。

至于那 756 条”孤立文档”:它们全是机器导入的会话记录,本来就没有入链。
规则的意义不是消灭数字,而是让数字可解释。

还没做的

  • 传递闭包目前只到 10 轮,复杂层级还没压测;
  • 图谱的社区聚类(Leiden)没做——要做也打算用纯 TS,不调 LLM;
  • 增量判定:现在每次全量重算,规模再大就得改。

下一步如果做,我会先做增量,而不是做更花哨的图算法。
因为真实使用中,”改一篇笔记后多久能看到判定更新”比”图好看不好看”重要得多。