AST 与语义双通道:把知识库写成可复核的图
只信向量或只信 grep 都会瘸腿:AST 给出可复核的语法事实,语义通道外显默会与理由;graphify 把两条写进同一张图。
你以为缺的是更大的上下文窗口。
真相是:你缺的是两条同时生效的合同——一条把「语法上发生了什么」钉死,另一条把「人如何理解它、为何如此」外显。缺一条,系统就会在不可审计的相关性与读不完的文件堆之间摇摆;再缺第三层——把两类产物写进可 diff 的工件与可 grep 的日志——你就只能在聊天窗里「感觉变聪明」,却无法在合并请求里证明哪条边对应哪段证据。
下面用开源项目 graphify(MIT,PyPI 包名 graphifyy)做工程解剖:它不替你宣称哲学立场,但它的数据结构设计,恰好把几种古老的认识论直觉,编译成了可运行的流水线。你要做的不是背工具名,而是借它看清:哲学若不能落进 schema,就只是谈资;schema 若没有哲学自觉,就只是另一堆 JSON。
第二套对照来自本仓库的 wiki-systematic/CLAUDE.md(LLM Wiki Agent):raw/ 只读、wiki/ 由 agent 全权写、index / overview / log 三件套负责导航·综合·审计、[[wikilink]] 充当显式边、lint 把矛盾与孤儿页升格为一等公民。它与 graphify 不是同一实现,但共享同一句工程谚语:别让「推断」与「引用」共用同一个 UI 默认值。
问题与痛点
你有过这种文件夹吗?代码、README、论文 PDF、白板照片、随手 markdown,全丢进一个叫 raw 或 notes 的目录。每次问 AI「这段和那段什么关系」,它要么全文吞,token 烧穿;要么向量检索,回来一堆「像」但连不上调用链。
Karpathy 式 /raw 工作流在 README 里被点名:材料越真实,越混合,单一通道就越尴尬;混合语料里同时存在 可判定语法事实(import/call/类作用域)与 不可形式化的设计史(脚注、白板、PR 讨论),单一通道要么牺牲可复核性,要么牺牲覆盖度。
graphify 给出的不是「再换一个更大的模型」,而是一个朴素翻转:用两轮抽取,把两类知识分开生产,再在图里合并。 第一轮,代码走 tree-sitter AST,不调用 LLM,文件内容不必离机上传(见官方 README 隐私段)。第二轮,文档、论文、图走 子代理 + 多模态模型,专门补 AST 永远抓不到的「概念、关系、设计理由」。合并后仍是一条 NetworkX 有向多重图 的消费面:查询、聚类、导出 HTML/JSON 都只吃同一张 G。
这就是本文要说的双通道:AST 通道 = 语法事实的硬腿;语义通道 = 意义与理由的外显腿。两条腿都落地到同一套节点与边上,才叫知识库,而不是「又一个 embeddings 仓库」。若你把同一思想翻译进 wiki-systematic:raw/ 是只读证据带;wiki/sources/*.md 是语义侧对单源的结构化摘要;entities/ 与 concepts/ 是可复用的指称与抽象;overview.md 是跨源 living map;graph/ 则是 wikilink 的派生物——于是「硬腿 / 软腿」在文件系统里也有文件名。
三条老问题,被写进 schema
1. 你允许系统把「推断」伪装成「事实」吗?
认识论里有一个朴素区分:被文本直接支撑的判断,与超出文本的合理推广。混为一谈时,用户得到的是流畅的幻觉;分置时,你至少知道该在哪里复核。
graphify 把每条边标成 EXTRACTED、INFERRED 或 AMBIGUOUS,并在 ARCHITECTURE.md 里用表格写明语义:例如 EXTRACTED 对应源码里显式出现的 import、调用;INFERRED 对应调用图二遍、上下文共现等推断;AMBIGUOUS 留给人类在 GRAPH_REPORT.md 里收尾。
这不是在装学术。它是在回答 builder 每天都在问的一句:「这句话,到底是读出来的,还是猜出来的?」 可错论在这里变成工程:承认猜,并给猜一个类型与置信结构,比假装全知诚实得多。
2. 世界是由「对象表」组成的,还是由「关系」组成的?
本体论争论可以很长;在工程里,一句就够:没有类型的边,就没有可执行的约束。 向量空间里的「近」不是关系类型,除非你再建一层解释。
graphify 的节点带有 file_type:code、document、paper、image、rationale。边有 relation 词汇(在 skill 层约定如 calls、cites、rationale_for、semantically_similar_to 等)。类型化的关系让下游能做:按关系过滤、按置信展示、按社区(见下)讲故事。
这与「关系优先」的知识表示传统一致:先问谁与谁以何种方式相连,再问 embedding 是否省 token。
3. 知识是为了「下一步探究」
杜威式的实用主义常被误读成「有用就行」。更精确的说法是:知识的价值在解除困惑、支撑下一步行动。 一张永远不对接查询、报告与可视化工作流的图,只是数字废墟。
graphify 的产出物直接服务「接着问」:graph.json 持久化;GRAPH_REPORT.md 给 god nodes 与意外连接;graph.html 给人手交互。README 里还有 token 对比叙事(混合语料上相对反复读原文的倍数),本质是把探究成本从「重复读全集」挪到「读压缩结构」——哲学口号落地成 benchmark 问题。
4. 默会知识与外显知识:双通道不是修辞
波兰尼传统里有一个刺人的区分:大量技能停留在「会做」而难以言传的默会层;工程系统若只保存外显命题,就会在日常协作里反复付「重新发明上下文」的税。代码仓库是最典型的混合体:一半知识已经写在语法树里(谁 import 谁、谁在什么行调用了谁),另一半仍在 README 的转折句、PR 讨论、白板箭头与论文脚注里。
单走 AST,你得到的是骨骼,但读不出「当时为什么拒绝引入那个依赖」。单走语义摘要,你得到流畅的叙事,却可能在「到底有没有这条调用边」上被模型温柔地撒谎。双通道不是「再加一个 embedding 维」,而是承认:默会层与语法层本就不在同一介质里,因此需要两条抽取管线,再用图合并成可一起查询的外显结构。哲学在这里是分工理由,不是 PPT 装饰。
图示:(认识论 × 本体论)
左到右读:同一堆语料里同时躺着「可被语法证明的结构」与「黏在叙述和图像上的意义」;两路外显进同一张图后,用边的置信类型把读出来的和推出来的分开,再把说不清的丢给人类复核——这是把可错论与关系本体论画成一张箭头图。
侧写:wiki-systematic 把同一合同写进路径名
graphify 回答的是「代码 vs 非代码」如何进同一张图;wiki-systematic/CLAUDE.md 回答的是「证据 vs 编纂 vs 派生视图」如何在 Markdown 树里分工。二者拼起来,是一张更烦人但更诚实的检查表:任何知识库若不能指出「哪一层允许覆盖」「哪一层只追加」「哪一层必须由人签字」,就会把运维事故伪装成模型温度问题。
ingest 顺序在 CLAUDE.md 里是硬流程:Read(raw) → 读 index.md + overview.md 取上下文 → 写 sources/<slug>.md → 更新 index → 修订 overview → 补 entity/concept → 标矛盾 → 追加 log.md。注意两个不变量:raw/ 永不被 agent 写回;log.md 只追加(grep ^## \[ 可 tail)。这与 graphify 的「AST pass 不碰模型权重 / 导出物进 graphify-out/」是同一类纪律:把副作用赶到少数几个目录,让 diff 可读。
YAML frontmatter 把页分成 source | entity | concept | synthesis;sources: [] 提供页级血缘,source_file: raw/... 提供文件级锚点——双层 provenance,等价于在边上同时保存 source_file 与 confidence 的文档版。[[wikilink]] 则是「人手与 agent 都承认的显式边」:解析它得到 graph/graph.json,与 graphify Pass1「从语法/文本直接读出的边」同伦;若你日后给 wikilink 加「推断补边」,就应像 graphify 一样另打标签,而不是悄悄写进正文冒充引用。
图示 A:从 raw/ 到 overview/ 的分层栈(D2)
下面把 library/insights/analysis/码析/llmwiki/llm-wiki-knowledge-ontology.md 中的 ASCII 分层栈 重画为 D2:左列是不可变证据,中列是类型化页面,右列是综合与派生物;log 与 index 横跨审计与导航。读图时盯住三条边标签:只读、摘要/引用、派生。
图示 B:页类型与关系(ER 风格,D2)
第二幅图对应 ontology 文档里的 ER 简图:RawDocument 不进 wiki 命名空间;SourcePage 是中介;Entity/Concept 通过 wikilink 互达;Index 编目一切;Synthesis 回答某次 UserQuestion 并回指证据页;GraphArtifact 只吃链接(与可选推断)。若你在自家 wiki 引入「主张对象化」,把 Claim 提成一等类型,这张图会多一个盒子——本体变更应先改图例,再改 ingest prompt。
一张表:graphify 边类型 ↔ wiki 原语(高密度对照)
| graphify / validate.py | wiki-systematic 中可操作的镜像 | 失败时人类该看什么 |
|---|---|---|
EXTRACTED | [[Page]] 出现在正文;source_file 可追溯 | 断链 grep、缺页 lint |
INFERRED + confidence_score | 将来若允许「模型补边」,应写入 graph/ 或单独 YAML,禁止混进正文冒充 wikilink | 置信阈值、每页补边上限 |
AMBIGUOUS | Contradictions 小节;lint 报告里的冲突对 | overview 里必须显式「未决」句,而不是悄悄覆盖 |
relation 词汇 | wikilink 旁的自然语言说明(Connections 列表) | 把关系名收束成受控词表前,先记录自由文本 |
file_type | frontmatter type + tags | 类型漂移时先改 index 分区,再批量改 frontmatter |
这张表的目的不是「强行同构两个项目」,而是逼迫你回答:当 AST 与语义、或 source 与 overview 冲突时,哪一层拥有写锁? graphify 把答案写在 build.py 注释(抽取列表顺序);wiki-systematic 把答案写在 ingest 步骤顺序与「raw 不可变」——写锁位置就是你们的认识论立场。
单腿走路时,账单写在哪里
若你只有结构通道,常见症状是:新人看得懂模块边界,却总在「历史包袱」上踩雷;评审能数清圈复杂度,却说不清一次架构转折的论证链;wiki 侧则对应:有满屏 entities/ 却缺 sources/ 的逐段引用,overview 写成散文。若你只有语义通道,常见症状是:问答像专家,一落到「给我精确到符号的引用」就含糊;多文件重构时,检索回来的段落彼此像,却不构成可编译的证明;wiki 侧则对应:syntheses/ 很厚、[[wikilink]] 很稀,lint 一开全是孤儿页。
双通道要买的不是两倍算力,而是把两类错误分流:语法类错误交给解析器与测试;意义类不确定交给带置信标签的边,让人类知道该打开哪份 diff、哪段设计笔记去签字。你在《知识表示一页说明》里读过的「混合栈」在此落地成并行生产者、单一消费者——消费者是那张图,而不是某一次 chat 的上下文。把同一句话翻译成 wiki 纪律:生产者是「读 raw 写 source + 补实体概念」的 ingest;消费者是 overview + graph + 可选 syntheses;永远不要让某次闲聊直接改 raw/ 或绕开 log.md 静默改写 overview——否则你在工程上复刻了「匿名推断」。
工程层:流水线即论证
官方 ARCHITECTURE.md(v3) 把论证拆成一串纯函数式的阶段,彼此只传 dict 与 NetworkX 图,副作用收口到 graphify-out/。流水线骨架如下(与仓库第 7–9 行一致):
detect() → extract() → build_graph() → cluster() → analyze() → report() → export()读法很简单:
- detect:世界入口,决定哪些文件进语料。
- extract:双通道真正分叉又汇合的地方——代码进 AST 抽取器,非代码进 LLM 抽取器,都吐出
{nodes, edges}。 - build_graph:把多份抽取合并成一张图;这里藏着「两条腿如何同架一副骨骼」的设计选择。
- cluster:README 强调 Leiden 按图拓扑聚类、不依赖单独 embedding 流水线——语义相似若已通过
semantically_similar_to等边进入图,社区发现直接吃结构信号。这是一种本体论上的节约:不把「相似」偷偷定义在另一个黑箱向量空间里而不进图。 - analyze / report / export:把图变成人类与 agent 能继续用的行动界面。
把 CLAUDE.md 的 ingest/query/lint/graph 四工作流叠在同一张表上,你会看到「副作用形状」刻意与 graphify 对齐:读多、写少、写点集中;区别只在于 wiki 的中间表示是 Markdown+wikilink,而不是一次性的 JSON 抽取。对 builder 这意味着:你可以在 CI 里同时跑 「图构建是否绿」 与 「lint 是否零断链」,把两类回归绑在同一扇门上。
合并策略:AST 与语义谁覆盖谁,不是玄学
build.py 文件头的注释把「双通道合并」写成了可改参数的工程事实:NetworkX 对同 id 节点重复 add_node 会覆盖属性;默认顺序是 AST 在前、语义在后,于是语义节点在 id 碰撞时覆盖 AST 节点——语义带来更丰富的跨文件标签,AST 保留更准的 source_location;若你要反过来,也可以调整传入 build() 的抽取列表顺序。
摘录自官方 graphify/build.py(v3) 文件头注释(约第 9–16 行):
# 2. Between files (build): NetworkX G.add_node() is idempotent — calling it
# twice with the same ID overwrites the attributes with the second call's
# values. Nodes are added in extraction order (AST first, then semantic),
# so if the same entity is extracted by both passes the semantic node
# silently overwrites the AST node. This is intentional: semantic nodes
# carry richer labels and cross-file context, while AST nodes have precise
# source_location. If you need to change the priority, reorder extractions
# passed to build().说明:同一实体在不同通道中有不同「侧写」;系统必须显式决定哪一侧写在顶层属性上。 没有这行注释,合并就会沦为「碰巧覆盖了」的暗知识。wiki-systematic 里与之同构的是:同一人物在 sources/foo.md 里被描述为顾问,在 sources/bar.md 里被写成联合创始人——若 ingest 不写入 Contradictions 并触发 overview 的「未决」段,你就把冲突外包给了读者的短期记忆。
schema:最小本体,足够硬
validate.py 在组装图之前 enforcing 一套小但硬的类型世界。下列常量与必填字段摘自官方 graphify/validate.py(v3) 第 4–7 行:
VALID_FILE_TYPES = {"code", "document", "paper", "image", "rationale"}
VALID_CONFIDENCES = {"EXTRACTED", "INFERRED", "AMBIGUOUS"}
REQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"}
REQUIRED_EDGE_FIELDS = {"source", "target", "relation", "confidence", "source_file"}注意 rationale 与 rationale_for 一类边在 skill/README 叙事中的角色:它们把解释学维度(「为什么这样写」)接回图里,而不是让理由散落在 chat 日志里蒸发。双通道里,AST 告诉你「调用了谁」;语义通道告诉你「作者认为这为何必要」——两者都是可引用的节点与边,而不是一次性的自然语言总结。wiki 侧的对位是:concepts/*.md 存「理论」,entities/*.md 存「谁在用/谁提出」,而 sources/*.md 的 Connections 列表把二者钉回某一段原文;缺了第三角,概念页就会退化成百科摘抄。
README 里两轮 pass 的一句话证据
若你只读一处官方自述,读 README.md 的 How it works 段(v3) 就够。核心英文原文如下(略去安装与小节标题,保留论证主干):
graphify runs in two passes. First, a deterministic AST pass extracts structure
from code files (classes, functions, imports, call graphs, docstrings, rationale
comments) with no LLM needed. Second, Claude subagents run in parallel over
docs, papers, and images to extract concepts, relationships, and design
rationale. The results are merged into a NetworkX graph, clustered with Leiden
community detection, and exported as interactive HTML, queryable JSON, and a
plain-language audit report.
Clustering is graph-topology-based — no embeddings. Leiden finds communities
by edge density.它把「哲学」(事实 vs 推断、结构 vs 意义)转写成了「命令」(先 AST、再语义、再拓扑聚类、再导出)。实用哲学在站点里想表达的,正是这种转写能力。README 同段还强调:INFERRED 边可带 confidence_score,与「推断必须可标度」的态度一致——不是禁止猜,而是禁止猜得匿名。
超边:当「三个一起」才成命题
有些关系天然不是二元:A 调 B 且在同一事务里依赖 C,单独两两边会丢结构。graphify 在抽取 JSON 里允许 hyperedges,把多参与者关系收进图对象的元数据(build_from_json 将其挂到 G.graph["hyperedges"])。哲学上这是提醒:本体不必强行二元化;工程上这是提醒:先承认 n 元关系存在,再决定要不要拆成 pairwise 近似。 双通道合并后仍可能留下这类「一团概念」——若你的领域充满协议实现、合规条款或跨模块不变量,值得在自家 schema 里留同类逃生舱。wiki 侧可操作的 hack 不是另起数据库,而是:在 syntheses/ 或 overview 里用显式小节列出「三元不变量」,并在 graph 生成器里把该小节解析成伪超边(仍带 source_file),直到你准备好真正的 hyperedge 存储。
技术栈速记(给要深挖的读者)
- tree-sitter:多语言 AST,本地确定性;依赖链见官方
pyproject.toml中一长串tree-sitter-。 - NetworkX:图容器与合并。
- Leiden(经 graspologic 可选依赖):社区发现。
- vis.js:交互 HTML。
- 宿主模型:Claude / Codex 等,由你的助手环境提供 key;graphify 不捆绑模型权重。
工具会变,分工逻辑不会:硬结构本地算,软语义走你已选择的模型与审计策略。
这和你在《本体论为什么对 AI Builder 有用》里写过的「对象—关系—约束—一致性」是同一张桌子的四条腿:双通道解决的是证据从哪条路进场;本体解决的是进场之后哪些组合合法。少谈其中任何一侧,都会在「模型看起来很聪明」与「系统其实很脆」之间留下裂缝。再加第五条腿:审计面(log.md、lint-report、GRAPH_REPORT 同类物)——没有它,「合法」只存在于某次成功的 chat 心情里。
处方:不装 graphify 也能带走的八个动作
- 先写「通道合同」再写 prompt:你的知识系统是否明确区分「解析器能证明的」与「模型推断的」?若否,你只是在用温度系数祈祷。合同里写清:哪类文件走哪条通道、哪条通道失败时是否降级为「仅索引原文」、以及禁止把推断边写进对外 API 的默认响应。
- 给关系类型起名:哪怕先只有五种边,也胜过一万条无类型 cosine 邻居。名字迫使团队争论本体,争论会暴露漂移。命名时多问一句:这条边在法庭上/在事故复盘里能否被指着源码或文档念出来?
- 合并策略写进文档:当同一实体被两路识别,谁覆盖谁的属性?官方默认「语义覆盖标签、AST 锚定行号」是一种合理起点,但必须成为显式配置,而不是口口相传。否则半年后唯一懂覆盖顺序的人离职,图还在,语义已死。wiki 侧则写成:
**overview是否允许在无新 source 的情况下被 query 直接改写?** 若允许,必须在log.md留下query行并提示审阅 diff。 - 给「猜」留预算与边界:为
INFERRED类输出设上限(每文件、每社区、每轮抽取),并要求指向证据跨度(哪一段 docstring、哪一节 PDF)。没有边界的推断会淹没EXTRACTED,整张图变成散文生成器。 - 把消费路径写进 CI:图或知识库若不能自动产出「一页摘要 + 可查询子图入口」,就默认没人读。把
GRAPH_REPORT类比物接进 PR 模板或 release checklist,让双通道的产出成为合并门槛的一部分,而不是黑客松演示。 **index当 API,不当 README**:任何新页必须反向出现在index.md的对应分区;否则你在训练团队「搜索比目录快」的坏习惯——而搜索在 embeddings 里会复现单通道幻觉。把「缺 index 行」当作编译错误。- 矛盾是资产,不是羞耻:ingest 模板里的
Contradictions小节若长期空白,要么你的源太单一,要么 agent 在讨好你。强制在overview里给矛盾留版面,就像 graphify 给AMBIGUOUS留边类型。 - 把
graph与lint绑成对:断链与孤儿页是 wiki 的「拓扑腐臭」;社区检测是「语义压缩」。两者一起跑,才能分别抓住引用 hygiene 与叙事过度压缩两类失败。
去读你自己的 raw 文件夹一眼。
然后回答一个问题:你希望助手下一次查询时,先打开的是无限长的原文,还是一张承认了自己哪里在读、哪里在猜的图? 若选后者,你已经站在「用哲学建知识库」这一边了——剩下的是把双通道写进你的 schema,而不是写进群公告;若你同时维护 wiki-systematic 式树,再问第二个问题:你希望 diff 先落在 raw/、wiki/sources/,还是直接落在 overview? 只有第二个问题的答案也写进团队文档,才算把「默会」变成「可审计」。
若你要对照阅读本站同栏旧文,可从《本体论为什么对 AI Builder 有用》与《知识表示一页说明》两路夹攻:一篇讲对象与关系白名单,一篇讲表示栈分工;本文补的是双源证据如何在同一张图上和解,并用 wiki-systematic/CLAUDE.md 证明:Markdown + 目录约定 + append-only log 也能承担半张 graphify 合同。ontology 图的 D2 源与 ASCII 祖先见 library/insights/analysis/码析/llmwiki/llm-wiki-knowledge-ontology.md。
更多实现细节与安装见 graphify 仓库 与站点 graphify.net。