首页
/ Graphify 语义抽取子代理协议详解:extraction-spec.md 的置信度评分、节点 ID 规则与输出 JSON Schema

Graphify 语义抽取子代理协议详解:extraction-spec.md 的置信度评分、节点 ID 规则与输出 JSON Schema

2026-09-06 17:37:29作者:齐添朝

本篇以 graphify/skills/pi/references/extraction-spec.md 为主体,逐条解读 graphify 语义抽取子代理(semantic subagent)的完整提示词协议:何时加载、何时跳过,边(edge)的三级置信度与离散评分表,节点 ID 的确定性生成规则,输出 JSON 的节点/边/超边(hyperedge)结构,以及 source_file 逐字保留规则背后的增量更新机制。结合 graphify/export.pygraphify/ids.pytests/test_extraction_spec_ids.py 等源码,可以看清这份提示词不是孤立文本,而是与 AST 抽取器、图构建器、语义缓存共同构成的一份"三方可验证契约"。

这份规范在 graphify 流水线中的位置

extraction-spec.md 是 graphify 语义抽取阶段的子代理提示词模板。按 graphify/skill.md 的定义,Step 3(实体与关系抽取)分为两部分:

  • Part A(结构抽取):对代码文件做确定性 AST 解析,不调用任何 LLM、不消耗 token;
  • Part B(语义抽取):仅当语料中至少存在一个文档、论文或图片 chunk 时才启动,为每个 chunk 派发一个语义子代理。

extraction-spec.md 的加载条件因此非常明确(原文第 3 行):

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.

也就是说,对一个纯代码仓库执行 graphify . 时,Part B 整体跳过,这份规范永远不会被读取;只有文档、论文或图片参与抽取时,它才会被加载,并把模板中的 FILE_LISTCHUNK_NUMTOTAL_CHUNKSDEEP_MODE 占位符替换后**逐字(verbatim)**交给每个子代理。skill.md 还要求替换 CHUNK_PATH(chunk 结果写入的绝对路径),子代理必须把结果写到该文件,父代理在 Step B3 以"文件落盘"作为成功信号。

完整的子代理提示词(模板原文)

以下为本文件正文中定义的提示词模板,完整保留其规则与 schema,供子代理直接消费:

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.

Files (chunk CHUNK_NUM of TOTAL_CHUNKS):
FILE_LIST

Rules:
- EXTRACTED: relationship explicit in source (import, call, citation)
- INFERRED: reasonable inference (shared structure, implied dependency)
- AMBIGUOUS: uncertain — flag it, do not omit
- 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.
- Doc/paper files: named concepts, entities, citations. Store rationale (WHY decisions were made) as a `rationale` attribute on the relevant node, not as a separate node. Use `file_type:"rationale"` for concept-like nodes (ideas, principles, mechanisms) and `file_type:"concept"` for named concepts. `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.
- Image files: use vision — understand what the image IS, not just OCR
- DEEP_MODE (if --mode deep): be aggressive with INFERRED edges — indirect deps, shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
- Semantic similarity: if two concepts solve the same problem or represent the same idea without a structural link (no import, call, or citation), add a `semantically_similar_to` edge marked INFERRED with confidence_score 0.6-0.95. Non-obvious cross-file links only.
- 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.
- If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author, contributor onto every node from that file.
- confidence_score is REQUIRED on every edge — never omit it, never use 0.5 as a default. EXTRACTED = 1.0 always. INFERRED: pick exactly ONE of 0.95 (direct structural evidence), 0.85 (strong inference), 0.75 (reasonable inference), 0.65 (weak inference), 0.55 (speculative but plausible) — never 0.5; if none fit, mark the edge AMBIGUOUS. AMBIGUOUS = 0.1-0.3.

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 + ValidateToken → src_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.

Output exactly this JSON (no other text):
{"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}

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.

提示词对子代理的输出形式做了强约束:只输出合法 JSON,无解释、无 markdown 围栏、无前言。这与 Step B3 的验收逻辑对应——父代理只检查 chunk 文件是否存在且包含 nodes/edges 两个键,任何多余文本都会让 JSON 解析失败。

边置信度:三级定性 + 离散定量评分表

规范把每条边分为三个定性等级,并给每个等级绑定严格的定量取值:

等级 含义 confidence_score 取值
EXTRACTED 关系在源码中显式存在(import、call、citation) 恒为 1.0
INFERRED 合理推断(共享结构、隐含依赖) 只能取 0.95 / 0.85 / 0.75 / 0.65 / 0.55 之一,分别对应直接结构证据、强推断、合理推断、弱推断、"投机但可信"
AMBIGUOUS 不确定 0.1-0.3,且不得省略该边,只标记

三条硬性禁令值得注意:

  1. 禁止默认值 0.5——"never use 0.5 as a default"。规范的理由是 0.5 既不属于 INFERRED 离散集,又会把"无证据"伪装成"中等强度"。
  2. 不匹配即降级——若 INFERRED 的五个档位都不贴切,必须把边标记为 AMBIGUOUS,而不是挑一个"差不多"的分值。
  3. AMBIGUOUS 不是垃圾桶——"flag it, do not omit",不确定也要进入图,只是置信度压到 0.1-0.3,让下游(如查询打分)自行降权。

这份评分表并非只存在于提示词里,Python 侧有对应的兜底实现。graphify/export.py 中定义了缺失 confidence_score 时的回退值:

# Fallback scores for an edge that carries a confidence tier but no
# confidence_score. The INFERRED default was 0.5, which references/extraction-spec.md
# rules out in as many words — "never omit it, never use 0.5 as a default" — and
# which is not in the discrete INFERRED set {0.55, 0.65, 0.75, 0.85, 0.95} either.
# ... a missing score is an absence of evidence about strength, so the honest
# fallback is the weakest value the rubric allows, not a midpoint that reads as
# a coin flip (#2813).
_CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.55, "AMBIGUOUS": 0.2}

代码注释直接引用了本规范原文作为设计依据:INFERRED 缺失分值的兜底取离散集的下限 0.55(最弱档)而非中点 0.5,因为"缺失分数是关于强度的证据缺失"。注释还说明"如今每个 AST 发射点都自带分数,这只是最后防线而非常规路径"——即规范约束了 LLM 产出,代码兜底了程序产出,两侧对同一评分表。

节点 ID 规则:LLM 与 AST 抽取器的确定性契约

规范中的 Node ID 一节要求:

  • 小写,仅含 [a-z0-9_],不允许点号或斜杠;
  • 格式为 {stem}_{entity},其中 stem 是完整仓库相对路径去掉扩展名、各段以下划线连接(每段转小写、非字母数字替换为下划线),entity 是符号名做同样归一化;
  • 使用全部目录层级,而非仅直接父目录;
  • 示例:src/auth/session.py + ValidateTokensrc_auth_session_validatetoken;顶层文件只用文件名 stem;
  • 不得追加 chunk 序号或序列后缀——ID 必须仅由 label 确定性推出。

这条规则的关键在于最后一句 "This must match the AST extractor's ID":语义子代理产出的节点 ID 必须与 AST 抽取器对同一符号产出的 ID 完全一致,否则同一个实体会被分裂成互不连通的"幽灵节点"。

仓库中 graphify/ids.py 的模块 docstring 把这一约束形式化为"三方一致"问题:

Three independent producers must agree on node IDs or the graph splits a single entity into disconnected ghost nodes:

  1. The AST extractor (extract._make_id) — deterministic, per-language.
  2. The semantic subagents (LLM) — follow the node-ID spec in the skill prompt.
  3. The graph builder (build._normalize_id) — reconciles edge endpoints when the LLM emits IDs with slightly different punctuation or casing than the AST.

也就是说,LLM 子代理正是这三方生产者中的第二方;第三方(图构建器)会在 LLM 产出的 ID 标点或大小写略有出入时做对账归一化。normalize_id 的归一化配方为:先对 casefold 与 NFKC 规范化迭代到不动点(处理 İi + U+0307 这类大小写折叠产生组合记号的情形),再把连续非词字符替换为单个下划线、折叠重复下划线、去除首尾下划线;make_id 则把各段用 _ 连接后送入 normalize_id

更值得一提的是仓库用测试把这份提示词"钉死"在代码上。tests/test_extraction_spec_ids.py 是一个漂移守卫:它用正则解析所有 extraction-spec.md(含各宿主机副本与 skillgen 片段)中形如 `path` + `entity` → `id` 的示例,然后调用生产代码 _make_id(_file_stem(path), entity) 逐条断言复现结果:

def _ast_symbol_id(path: str, entity: str) -> str:
    """Reproduce the symbol ID the AST extractor emits for a file + symbol, using
    the real production helpers (not a re-implementation)."""
    return _make_id(_file_stem(Path(path)), entity)

测试同时锁定反例:仅文件名的 session 和仅直接父目录的 auth_session 两种"看起来差不多"的 ID 形式都被断言为错误——注释指出,自 stem 改为完整仓库相对路径(#1504)后,这两种旧形式都会与 AST 抽取器不一致。这意味着:如果将来有人手改 extraction-spec.md 里的示例写错,或 ID 生成函数行为变化,CI 会立刻失败,提示"spec 示例和 ID 函数必须一起改"。

输出 JSON Schema 逐字段解读

提示词末尾给出的 JSON 骨架定义了四类顶层字段,子代理"必须精确输出这份 JSON(不得有其他文本)"。

nodes(节点)

  • id:按上述规则生成的确定性节点 ID;
  • label:人类可读名称;
  • file_type必须且只能codedocumentpaperimagerationaleconcept 六值之一,"其他值无效且会被拒绝"。其中 rationale 用于概念式节点(思想、原则、机制),concept 用于有名称的概念;规范还要求文档/论文中的"决策理由(WHY)"应作为相关节点的 rationale 属性存储,而不是单独建节点;
  • source_file:见下文逐字规则;
  • source_locationsource_urlcaptured_atauthorcontributor:默认 null,frontmatter 命中时填充。

edges(边)

  • source/target:节点 ID,方向有硬性语义——对 calls 边,source 是调用方、target 是被调方,"never reversed",且 calls 边必须保持单语言内(跨语言调用由 AST 侧负责);
  • relation:枚举为 calls | implements | references | cites | conceptually_related_to | shares_data_with | semantically_similar_to | rationale_for
  • confidenceconfidence_score:按上文评分表,二者缺一不可;
  • weight:边权重,schema 示例取 1.0

hyperedges(超边):当 3 个及以上节点共享一个无法用两两边表达的概念、流程或模式时使用,relation 枚举为 participate_in | implement | formconfidence 只允许 EXTRACTED | INFERRED(没有 AMBIGUOUS 档),且每个 chunk 最多 3 条——"Use sparingly"。从 graphify/export.pyattach_hyperedges 实现可以看出,超边会挂到图对象的 G.graph["hyperedges"] 元数据中持久化,并且对无 id 的历史条目做了 h.get("id") 防御(注释说明语义抽取器确实可能产出无 id 的超边,构建器原样保留),这与规范中"超边要有 id"的 schema 形成互补约束。

input_tokens / output_tokens:schema 中固定为占位 0。按 graphify/skill.md Step B3 的要求,chunk JSON 中的 token 数永远是占位零值,真实用量由父代理从 Agent 工具返回的 usage 字段读回并写回 chunk JSON,再参与合并(total_in += d.get('input_tokens', 0)),用于合并后打印 Merged N chunks: X in / Y out tokens

source_file 逐字规则与增量更新

规范对 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.

其工程意义在于全量构建与 --update 增量更新必须共享同一个路径基准:合并阶段(build_merge)按 source_file 做"替换而非追加"匹配,如果子代理把绝对路径截短成 basename、或换成相对路径、或改动分隔符,同一文件在两次构建间的产物就会匹配不上,导致节点/边被复制而不是替换,图里出现重复实体。这条规则把 LLM 的自由发挥空间压缩到零——路径必须与 FILE_LIST 中给出的路径逐字符一致。

与之配套的是 YAML frontmatter 规则:若某文件带有 --- ... --- frontmatter,须把其中的 source_urlcaptured_atauthorcontributor 复制到该文件产出的每个节点上,作为溯源元数据。

DEEP_MODE、语义相似边与图片 vision 规则

规范还定义了三个可选/条件行为:

  • DEEP_MODE(对应 CLI 的 --mode deep,skill.md 要求在派发前把 DEEP_MODE=true 传给每个子代理):鼓励对 INFERRED 边更激进——间接依赖、共享假设、潜在耦合都要提取,但"不确定的标 AMBIGUOUS,不要省略"。这使 deep 模式产出更密的推断边,同时以 AMBIGUOUS 档位控制噪声。
  • 语义相似边:两个概念解决同一问题或表达同一思想、但没有任何结构性连接(无 import、无 call、无 citation)时,添加 semantically_similar_to 边,置信度 INFERRED、分值落在 0.6-0.95,且限定"只有非显而易见的跨文件关联才值得建边"。从源码结构看,这类边在下游是被特殊对待的:graphify/analyze.py 在计算跨边界洞察时明确排除 semantically_similar_to(注释称其为"genuine cross-boundary insight"),graphify/report.py 也会为其加 [semantically similar] 标注单独呈现。
  • 图片 vision 规则:对图片文件要求"理解这张图是什么,而不只是 OCR 文字"——即提取图片表达的架构、流程或实体关系,而不是转录图中文字。这与 skill.md Step B1 的分块策略一致:每张图片单独成 chunk("vision needs separate context"),保证视觉上下文不被同 chunk 的文本稀释。

提示词版本与语义缓存的绑定

extraction-spec.md 不只是提示词,它同时是缓存键的一部分graphify/skill.md Step B0 检查语义缓存时的说明写道:

SPEC_PATH below is the absolute path of the references/extraction-spec.md that ships beside this SKILL.md — the same file Step B2 loads and hands to every subagent. It is the extraction prompt, so cache entries are attributed to it: when a graphify upgrade changes the prompt, entries produced by the old one are re-extracted instead of replayed, and unchanged prompts keep their entries (#1939).

也就是说,缓存条目按"文件内容 + 提示词"归属:升级 graphify 若修改了 extraction-spec.md 的提示词文本,旧缓存自动失效、相关文件重新抽取;提示词未变则缓存继续命中。实现落在 graphify/cache.pycheck_semantic_cache(files, root, prompt_file=SPEC_PATH)save_semantic_cache(..., prompt_file=SPEC_PATH):两处必须传入同一个 SPEC_PATH,且"do not drop the argument"。缓存判定函数内部以 prompt_file 解析提示词指纹(_resolve_prompt_fp),把不同提示词版本的产物隔离到不同缓存目录。

另一个值得了解的旁路是:若环境已设置 GEMINI_API_KEY/GOOGLE_API_KEY,skill.md 指示改用 graphify/llm.pyextract_corpus_parallel(files, backend="gemini") 做语义抽取,而非派发子代理。从源码结构看,llm.py 内嵌了一份与本规范同构的 JSON schema(file_type 六值、relation 枚举、confidence/confidence_score 字段一致,且节点多了 rationale 属性),说明 Gemini 后端与子代理后端被刻意约束在同一输出契约上,Part C 的合并逻辑无需区分产物来源。

小结:一份"可被代码验证的提示词"

回到 graphify/skills/pi/references/extraction-spec.md 本身,它的价值可以归纳为三层:

  1. 协议层:用一段紧凑提示词规定了语义子代理的全部行为边界——纯 JSON 输出、三级置信度 + 离散分值表、六值 file_type、超边限额、frontmatter 溯源、vision 要求、deep 模式开关;
  2. 契约层:节点 ID 规则与 AST 抽取器(graphify.extract._make_id/_file_stem,归一化收敛于 graphify/ids.py)对齐,source_file 逐字规则与 build_merge 的增量替换对齐,输出 schema 与 Gemini 后端(graphify/llm.py)对齐;
  3. 可验证层tests/test_extraction_spec_ids.py 把 spec 文本中的 ID 示例直接当作测试数据解析并断言,graphify/export.py 的兜底分值表引用 spec 原文,缓存模块以 spec 文件路径做版本指纹。

因此,阅读这份规范的正确姿势不是把它当作"给 LLM 的建议",而是把它当作 graphify 语义抽取子系统的接口定义:改它,就要同时检查 ID 生成函数、评分兜底、缓存指纹三处依赖,而仓库的测试已经为这种联动准备好了漂移检测。

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