首页
/ graphify extraction-spec 详解:语义提取子代理的 Prompt 契约与确定性节点 ID 规范

graphify extraction-spec 详解:语义提取子代理的 Prompt 契约与确定性节点 ID 规范

2026-09-06 13:47:28作者:管翌锬

本文围绕 extraction-spec.md 展开。该文档是 graphify(把任意代码库连同文档、SQL、配置转成可查询知识图谱的 /graphify 技能)在构建流水线 Step 3 Part B 中下发给每个“语义提取子代理”的完整 prompt 模板:它规定了子代理必须遵守的边级置信度准则、file_type 枚举、语义相似边与超边(hyperedge)规则、节点 ID 的确定性生成格式,以及最终输出的 JSON Schema 与 source_file 逐字(verbatim)路径规则。读完本文,你能理解 graphify 如何用一个“纯文本契约 + 漂移守卫测试”保证 LLM 语义提取结果与 AST 确定性提取在同一个图上无缝合并,并能复现其节点 ID、置信度评分与增量更新不产生重复节点的关键机制。

1. 文档定位:何时加载、由谁消费

extraction-spec.md 开头的加载条件写得很明确:

Load this in Step 3 Part B when the corpus has at least one doc, paper, or image chunk. A pure-code corpus skips Part B and never reads this file.

也就是说,它只在语料中至少存在一个文档、论文或图片分块(chunk)时才被加载;纯代码语料直接跳过 Part B,永远不会读这个文件。这与 skill-claw.md Step 3 的流程一致:Part A(AST 确定性提取)与 Part B(语义子代理)并行调度,而纯代码语料走 Fast path——先写一个空的 .graphify_semantic.json 让 Part C 合并阶段有输入,然后直接进入 Part C。

每个语义子代理都会逐字收到这份 prompt,其中 5 个占位符由调度方替换:

占位符 含义
FILE_LIST 本分块负责处理的文件路径列表(必须是 verbatim 绝对路径,见第 8 节)
CHUNK_NUM / TOTAL_CHUNKS 当前分块编号 / 总分块数
DEEP_MODE 是否以 --mode deep 运行(决定推断边的激进度)
CHUNK_PATH 结果 JSON 的落盘绝对路径(完整版 spec 要求,例如 ${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json

从源码结构看,这份 claw 目录下的拷贝并非手工维护的独立文件。仓库里所有宿主(claude、codex、opencode、kilo、copilot、claw、droid、trae、kiro、pi、windows 等)的 references/extraction-spec.md 都由 skillgen 从单一源头 extraction-spec.md(完整版)与 extraction-spec-compact.md(紧凑版,即本文引用的 claw 版本所对应的源)渲染生成,tools/skillgen/expected/ 下还留有各宿主的黄金输出用于 skillgen --check 校验。这种“一份源、多宿主分发”的机制是后文所有跨宿主行为一致性的前提。

2. 子代理的总则:只输出 JSON,且遵守三级置信度

prompt 的第一段规则就锁死了输出形态:

You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.
Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble.

随后是三条核心的边级置信度(confidence tier)规则:

  • EXTRACTED:源文件中明确存在的关系(import、call、citation);
  • INFERRED:合理推断(共享结构、隐含依赖);
  • AMBIGUOUS:不确定——标记出来,不要省略(flag it, do not omit)。

这三档不是装饰:它们贯穿到下游。例如 export.py 中为缺失 confidence_score 的边提供了回退默认值 _CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.55, "AMBIGUOUS": 0.2}——其中 INFERRED 回退用的是准则集 {0.55, 0.65, 0.75, 0.85, 0.95} 的最弱值 0.55 而非 0.5(源码注释明确指出 0.5 是被 spec 明文禁止的“抛硬币”默认值,参见 #2813),AMBIGUOUS 回退到区间 0.1–0.3 的中点 0.2。也就是说,spec 里的每一句话都能在合并/导出代码里找到对应的实现或护栏。

3. 分文件类型的提取规则

3.1 代码文件:只补 AST 找不到的语义边

Code files: semantic edges AST cannot find. Do not re-extract imports. When adding calls edges: source is the caller, target is the callee, never reversed; keep calls within one language.

三条约束各有原因:

  1. 不要重提 import——import 边由确定性 AST 提取器(Part A)负责,语义子代理重复输出会造成重复边;
  2. calls 方向不可反——source 恒为调用方,target 恒为被调方。完整版 spec 还进一步强调 calls 边必须留在单一语言内部:“a Python function cannot calls a JS/TS/Go/Rust/Java symbol … cross-language call edges are phantom artifacts, never emit them”,因为跨语言“调用”在没有运行时桥接时只是幻象边(phantom edge);
  3. 语义边聚焦 AST 表达不了的东西(共享数据、架构模式等)。

3.2 文档/论文文件:六种 file_type 与 rationale 属性

对 doc/paper 文件,spec 要求提取命名概念、实体与引用,并对 file_type 做了硬枚举约束:

file_type MUST be one of exactly these six values: code, document, paper, image, rationale, concept. Any other value is invalid and will be rejected.

同时规定“决策理由”(WHY decisions were made)必须作为 rationale 属性挂在相关节点上,而不是单独建一个 rationale 节点;概念性节点(思想、原则、机制)用 file_type:"rationale",命名概念用 file_type:"concept"。这个“属性而非节点”的设计避免图谱被碎片化理由节点污染,仓库中 rationale 相关测试 也围绕该约定验证行为。

3.3 图片文件:用视觉理解“它是什么”,而非 OCR

Image files: use vision — understand what the image IS, not just OCR

完整版 spec 给出了更细的分类指导:UI 截图提取布局模式与设计决策;图表提取指标与趋势;推文/帖子提取论断与作者;架构图提取组件与连接关系;手写/白板内容把不确定的读法标为 AMBIGUOUS。这条规则对应 graphify 对图片语料的一等公民处理(graphify/skills/claw/references/ 同目录体系下的视觉处理路径)。

3.4 DEEP_MODE 与语义相似边

  • DEEP_MODE--mode deep 时):激进地输出 INFERRED 边——间接依赖、共享假设、潜在耦合;拿不准的标 AMBIGUOUS 而不是丢弃。skill-claw.md 明确要求在 Step 3 开始前记住 --mode deep 是否出现,并把 DEEP_MODE=true 传给 Part B 的每个子代理,“do not lose it”;
  • 语义相似边:当两个概念解决同一问题或表达同一思想、但没有任何结构性链接(无 import、无 call、无 citation)时,加一条 semantically_similar_to 边,标 INFERRED,confidence_score 取 0.6–0.95,且仅限“非显而易见的跨文件关联”(Non-obvious cross-file links only)。完整版 spec 还举了三个例子:两个互不调用的用户输入校验函数;代码里的类与论文里描述的同一算法;以不同方式处理同一失败模式的两个错误类型。

3.5 超边(Hyperedges)

Hyperedges: if 3+ nodes share a concept, flow, or pattern not captured by pairwise edges, add a hyperedge to a top-level hyperedges array. Use sparingly. Max 3 per chunk.

超边捕捉“3 个及以上节点共同参与但成对边表达不了”的群体关系,relation 取值 participate_in | implement | form,每分块最多 3 条。值得注意的历史背景(见 CHANGELOG):早期 graphify extract --backend <…> 的原生 LLM prompt(llm.py 中的 _EXTRACTION_SYSTEM)只在输出 Schema 里展示了空 "hyperedges":[]、从未解释什么是超边,导致所有原生后端静默产出 0 条超边,而 skill 路径(本 spec 完整文档化超边)却能产出——两条提取路径的 prompt 发生了漂移。修复后,原生 prompt 携带了相同的“3 个及以上节点共同参与”指令与填充示例,使同一语料下两条路径的超边行为一致。这是“spec 即契约、多路径必须对齐”这一工程原则的又一例证。

3.6 YAML frontmatter 传播

If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author, contributor onto every node from that file.

四个溯源字段会被复制到该文件产出的每个节点上,保证节点级可溯源(provenance)。

4. 离散置信度评分准则(confidence rubric)

spec 中最“反直觉”也最关键的一段,是对 confidence_score 的强制要求:

confidence_score is REQUIRED on every edge — never omit it, never use 0.5 as a default. EXTRACTED = 1.0 always.

置信档位 允许的取值 语义
EXTRACTED 恒为 1.0 源文件中显式存在的关系
INFERRED 只能从五档中选一个0.95 / 0.85 / 0.75 / 0.65 / 0.55 0.95 直接结构证据;0.85 强推断;0.75 合理推断;0.65 弱推断;0.55 推测但可信
AMBIGUOUS 0.10.3 不确定,标记而非省略

五档 INFERRED 分值的含义(以完整版 spec 的标注为准):

  • 0.95 — 直接结构证据(共享数据结构、具名的跨文件引用);
  • 0.85 — 强推断(清晰的功能对齐,无直接符号链接);
  • 0.75 — 合理推断(共享问题域 + 相似形态,需要解释);
  • 0.65 — 弱推断(主题相关,无形态证据);
  • 0.55 — 推测但可信(仅表层共现)。

为什么用离散档位而不是连续区间?完整版 spec 直接给出了生产观察:“Models follow discrete rubrics better than continuous ranges; the bimodal distribution observed in production (>50% at 0.5, >40% at 0.85+) shows the range guidance is being collapsed to a binary”——即连续区间引导会被模型坍缩成二值分布,而离散准则反而被更忠实执行。如果五档都不适用,应标 AMBIGUOUS 而不是硬选 0.4 或更低。

这个准则同样渗入代码实现:AST 侧各发射点都自带评分,注释中反复引用“the rubric in references/extraction-spec.md”(如 extract.py 中“0.85, not 0.8: the rubric in references/extraction-spec.md …”,以及 extractors/engine.pyextractors/resolution.py 处同样按此准则取值 0.85/0.95)。测试 test_inferred_confidence_rubric.py 则把该准则锁定为回归约束。

5. 节点 ID:确定性、全路径、与 AST 提取器一致

这是整份 spec 里工程密度最高的部分:

Node ID format: lowercase, only [a-z0-9_], no dots or slashes. Format {stem}_{entity} where stem is the full repo-relative path with the extension dropped, every segment joined with _ (each lowercased with non-alphanumeric chars replaced by _) and entity is the symbol name similarly normalized. Use every directory level, not just the immediate parent. src/auth/session.py + ValidateTokensrc_auth_session_validatetoken. Top-level files use just the filename stem. This must match the AST extractor's ID. Never append chunk or sequence suffixes — IDs must be deterministic from the label alone.

拆解成可执行的生成规则:

  1. 字符集:仅小写字母、数字、下划线;
  2. stem:仓库相对路径去掉扩展名后,保留每一级目录,段与段之间用 _ 连接;每段小写化、非字母数字字符替换为 _
  3. entity:符号名做同样的归一化;
  4. 根级文件只用文件名 stem(setup.pysetup,符号 my_funcsetup_my_func);
  5. 禁止追加任何 chunk 序号或序列后缀(如 _c1_chunk2)——同一实体无论被哪个分块处理,必须产生同一个 ID;
  6. 必须与 AST 提取器生成的 ID 完全一致,否则同一个符号会被拆成互不相连的“幽灵重复节点”(ghost-duplicate nodes)。

为什么强调“每一级目录”?extractors/base.py_file_stem 的 docstring 解释了动机(对应 issue #1504):若 stem 只取“父目录 + 文件名”,则 docs/v1/api/README.mddocs/v2/api/README.md 都会塌缩成 api_readme,同名文件在不同目录相互碰撞为“last-writer-wins”的单一节点并静默丢图。全路径 stem 使二者分别得到 docs_v1_api_readmedocs_v2_api_readmeCHANGELOG 记录了这次破坏性变更:“Breaking — node IDs now include the full repo-relative path (#1504, #1509)”,并说明 AST 提取器、LLM system prompt、extraction-spec 与两处手工复制的 stem helper 已对齐到同一条规则(修复 #1509 的 AST↔LLM 分歧)。

ID 的最终生成由 ids.py 完成:make_id(*parts) 先把各 part 拼接,再交给 normalize_id——后者对输入迭代执行 NFKC(casefold(s)) 直到不动点(带 6 次硬上限),然后 [^\w]+ → _、折叠连续下划线并去首尾(源码注释说明这是为组合附加符序列如希腊 ypogegrammeni 做的收敛处理,见 #2614)。语义子代理产出的 ID 因此能被确定性地与 AST 侧 ID 对齐或重映射。

5.1 漂移守卫:spec 里的每个例子都是可执行测试

正因为 spec 是“LLM 的 ground truth”,它一旦与代码漂移就会制造幽灵节点。test_extraction_spec_ids.py 是一个专门的守卫测试:

  • 用正则 `path` + `entity` → `id`(箭头为 U+2192)从所有发行中的 spec 文件里解析出每个 ID 示例(扫描 graphify/skills/**tools/skillgen/fragments/** 下所有 extraction-spec.md,排除 build/expected/);
  • 对每个示例调用生产代码本身extract._file_stem + _make_id)重放,断言结果与 spec 写死一致;
  • 若一个都解析不到(文件搬走或示例格式变了)就 loudly fail,防止守卫本身空转;
  • 另有一条 test_cautionary_wrong_forms_are_actually_wrong,把 spec 中警告的反例(只用文件名 session、只用直接父目录 auth)也锁定到代码上,确保警告不会过期。

也就是说,claw 这份 spec 里的例子 src/auth/session.py + ValidateTokensrc_auth_session_validatetoken 不只是文档示例,而是 CI 会逐条重放的断言。

6. 输出 JSON Schema 逐字段说明

spec 要求子代理输出恰好如下结构(无其他文本):

{
  "nodes": [{
    "id": "auth_session_validatetoken",
    "label": "Human Readable Name",
    "file_type": "code|document|paper|image|rationale|concept",
    "source_file": "<FILE_LIST path verbatim>",
    "source_location": null,
    "source_url": null,
    "captured_at": null,
    "author": null,
    "contributor": null
  }],
  "edges": [{
    "source": "node_id",
    "target": "node_id",
    "relation": "calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for",
    "confidence": "EXTRACTED|INFERRED|AMBIGUOUS",
    "confidence_score": 1.0,
    "source_file": "<FILE_LIST path verbatim>",
    "source_location": null,
    "weight": 1.0
  }],
  "hyperedges": [{
    "id": "snake_case_id",
    "label": "Human Readable Label",
    "nodes": ["node_id1", "node_id2", "node_id3"],
    "relation": "participate_in|implement|form",
    "confidence": "EXTRACTED|INFERRED",
    "confidence_score": 0.75,
    "source_file": "<FILE_LIST path verbatim>"
  }],
  "input_tokens": 0,
  "output_tokens": 0
}

要点:

  • nodesid 按第 5 节规则;file_type 是第 3.2 节的六值硬枚举;溯源四元组(source_url/captured_at/author/contributor)默认 null,仅在 frontmatter 传播规则触发时填充;
  • edgesrelation 八值枚举中,callscites 等显式关系应标 EXTRACTED(1.0),conceptually_related_toshares_data_withsemantically_similar_torationale_for 通常是 INFERRED 并套用第 4 节五档准则;weight 默认 1.0;
  • hyperedges:仅 EXTRACTED/INFERRED 两档(示例中 0.75 落在 INFERRED 准则内),nodes 为成员节点 ID 列表,最多 3 条/分块;
  • input_tokens / output_tokens:供上层统计成本(skill 流水线会汇总到 cost.json 一类产物)。

下游对这份 JSON 有解析护栏:llm.py 中的解析器会强制 nodes/edges/hyperedges 为 dict 列表,并把超边成员引用强制转为可哈希标量 ID(#2486,防止模型把成员写成对象导致后续去重崩溃)。

7. source_file 逐字规则:全量构建与增量更新共用同一基准

spec 的最后一条规则看似琐碎,实则是增量正确性的基石:

source_file RULE: set source_file to the FILE_LIST path for that file VERBATIM (absolute, no shortening to basename, no re-relativizing, no separator change). Keeps full build and --update on one base so build_merge's replace matches instead of duplicating.

即:节点、边、超边的 source_file 必须与 FILE_LIST 中该文件的路径逐字符一致——不得缩成 basename、不得重新相对化、不得改分隔符(路径的正则化与对构建根的相对化由下游引擎统一做)。其直接收益是“全量构建”与 --update 增量重建落在同一节点键基准上:当某文件被重新提取时,build_merge替换(replace-on-re-extract)旧节点而不是追加出一个重复节点。CHANGELOG 中 #1366 的修复正是围绕这条规则固化下来的:“the extraction-spec source_file is pinned to the verbatim path, so the full build and incremental updates never drift on node-key base”;更早的 #1344/#1361 还处理了 --update 时误删已变更文件新节点的回归(更新 runbook 现在只修剪真正被删除的文件,变更文件交给 build_merge 的替换机制对账)。

8. 工程闭环:这条 prompt 还参与了缓存命名空间

从源码结构看,这份 spec 不只是运行时下发的文本,它还是语义缓存的命名空间指纹来源之一。cache.pycli.py 中,语义缓存条目按“提取 prompt 的指纹”归入 cache/semantic/p{fingerprint}/(镜像 AST 缓存的 v{version}/ 布局):skill-claw.md 要求调度方把 spec 的绝对路径(SPEC_PATH)在 Step B0 与 B3 之间原样传递——graphify 升级若改变了 prompt,旧 prompt 产出的缓存条目就会被重新提取而不是被回放;prompt 未变则条目保留(#1939)。这解释了为什么 spec 的逐字加载(而非意译)是硬要求:prompt 文本即缓存键的一部分。

9. 小结:一份 spec,四道防线

extraction-spec.md 放回 graphify 的整体架构里看,它同时承担四重职责,每一重都有对应实现或测试兜底:

  1. LLM 输出契约——JSON Schema + 六值 file_type + 离散置信度准则,由 export.py 的回退默认值与解析护栏兜底;
  2. AST↔LLM 对齐契约——节点 ID 全路径规则必须与 _file_stem/make_id 一致,由 test_extraction_spec_ids.py 逐条重放 spec 示例防止漂移;
  3. 增量一致性契约——source_file verbatim 规则保证全量与 --update 共用节点键基准,build_merge 替换而非复制(#1366);
  4. 缓存一致性契约——spec 文本指纹参与语义缓存命名空间(cache/semantic/p{fingerprint}/,#1939),prompt 变更自动使旧缓存失效。

如果你想在自己的语料上验证这些行为,可以直接查看 worked/ 目录下 httpx、mixed-corpus 等已构建好的样例(含 graph.jsonGRAPH_REPORT.mdreview.md),对照其中节点 ID、confidence_score 分布与超边结构,检验本文描述的规则在真实产物上的体现。

登录后查看全文
热门项目推荐
相关项目推荐