首页
/ graphify 语义抽取子代理规范:extraction-spec.md 的提示词契约、置信度评分体系与节点 ID 规则

graphify 语义抽取子代理规范:extraction-spec.md 的提示词契约、置信度评分体系与节点 ID 规则

2026-09-06 16:12:05作者:侯霆垣

本文以 graphify 的 Kiro 技能内置参考文档 graphify/skills/kiro/references/extraction-spec.md 为主体,完整拆解这份“抽取子代理提示词”(extraction subagent prompt)的加载时机、全部抽取规则、置信度离散评分表与节点 ID 生成契约,并结合 graphify/cache.pygraphify/ids.pytests/test_extraction_spec_ids.py 等源码与测试,解释这套规范为什么必须与 AST 抽取器保持逐字节一致,以及它如何支撑 graphify 的增量更新与缓存复用。读完后你将能够独立读懂并维护 graphify 的语义抽取协议:给定一个文档/论文/图片语料分片,知道 LLM 子代理必须输出什么 JSON、按什么规则打置信度分、如何生成与 AST 侧完全对齐的节点 ID。

这份规范文件在 graphify 中的位置

extraction-spec.md 是 graphify 各宿主技能(Claude Code、Codex、Kiro 等)在 /graphify 流程 Step 3 Part B(语义抽取阶段) 才加载的参考文件。文档开头三句话就定下了它的加载契约:

  • 只有当语料中至少存在一个 doc、paper 或 image 分片时才加载。纯代码语料(例如对普通仓库直接跑 /graphify .)会整体跳过 Part B,永远不读取这个文件——代码关系由确定性的 AST 解析(Part A)负责,无需 LLM 参与;
  • 每个语义抽取子代理**逐字(verbatim)**接收文件中围栏代码块内的提示词,宿主只需替换四个占位符:FILE_LIST(本分片文件列表)、CHUNK_NUM/TOTAL_CHUNKS(分片序号/总数)、DEEP_MODE(是否启用 --mode deep);
  • Kiro 这一版是紧凑版(compact),与 Claude 版的完整提示词相比去掉了示例枚举,且要求子代理直接内联返回 JSON,而不是把结果写到磁盘的 CHUNK_PATH

宿主技能 graphify/skill-kiro.md 中给出了配套的运行约束,理解了这些约束才能正确读懂规范:

  1. 快速路径:若 detect 阶段发现零个文档/论文/图片,直接跳过 Part B,但必须先写一个空的语义结果文件.graphify_semantic.json)——Part C 合并阶段无条件读取该文件,缺失会直接 FileNotFoundError
  2. 缓存检查(Step B0):派发子代理前先查哪些文件已有缓存抽取结果,只把 .graphify_uncached.txt 中列出的文件派发给子代理。缓存条目以本规范文件本身作为“抽取提示词”进行归属:当 graphify 升级导致提示词内容变化时,旧提示词产生的条目会重新抽取而不是直接回放(详见下文“缓存与提示词指纹”);
  3. 派发方式(Step B2):所有子代理必须在同一条消息中一次性全部派发,且必须使用 subagent_type="general-purpose"——只读的 Explore 类型无法把分片结果写盘,会导致抽取结果被静默丢弃;
  4. 失败处理:单个分片 JSON 非法时打印警告并跳过,不中止整轮;超过一半分片失败或丢失才停止并提示用户检查子代理类型。

子代理提示词全文(规范主体)

规范文件的核心就是围栏内这段提示词。下面完整给出原文(占位符 FILE_LISTCHUNK_NUMTOTAL_CHUNKS 在实际派发时被替换为真实值),随后逐条解析:

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.

规则逐条解析

三态置信度分类:EXTRACTED / INFERRED / AMBIGUOUS

规范为每条边定义了三档 confidence,这是整个语义抽取的质量骨架:

档位 判定标准 典型场景
EXTRACTED 关系在源文件中显式存在 import、函数调用、论文引用(citation)
INFERRED 合理推断 共享数据结构、隐含依赖
AMBIGUOUS 不确定 必须标记出来,而不是省略

第三条尤其关键:规范明确要求“flag it, do not omit”。不确定的关系不是被丢弃,而是被打上低置信度标记进入图,留给后续查询与人工审查。这与 graphify 的“每条边都有解释”(every edge explained)设计哲学一致。

按文件类型的差异化抽取规则

代码文件:只提取 AST 找不到的语义边(语义调用、共享数据、架构模式),不要重新抽取 import(AST 阶段已经拿到);添加 calls 边时方向严格固定——source 必须是调用方,target 是被调方,绝不能反;且 calls 边必须限制在单一语言内,跨语言的调用边在 graphify 里被视为幻影产物(phantom artifact)。

文档/论文:抽取命名概念、实体与引用。关于“理由”(rationale)有专门的存储约定:决策的 WHY(设计意图、取舍)必须作为节点上的 rationale 属性,而不是单独建节点;file_type 的语义被细分为——rationale 用于概念型节点(思想、原则、机制),concept 用于具名概念。且 file_type 只允许六个枚举值:codedocumentpaperimagerationaleconcept,其他取值“无效且会被拒绝”。

图片文件:必须用视觉能力理解图片是什么,而不是只做 OCR。

DEEP_MODE 与语义相似度边

  • DEEP_MODE:当用户传入 --mode deep 时,DEEP_MODE=true 会被传递给每一个子代理(graphify/skill-kiro.md 在 Step 3 开头就强调必须记录并透传该标志,不能丢失)。开启后子代理要激进地补 INFERRED 边——间接依赖、共享假设、潜在耦合——拿不准的标 AMBIGUOUS 而不是省略。
  • 语义相似度:若两个概念解决同一问题、表达同一思想,但彼此之间不存在任何结构性链接(无 import、无 call、无 citation),添加一条 semantically_similar_to 边,标记为 INFERRED,confidence_score 取 0.6–0.95。规范限定这类边只用于“不明显”的跨文件关联,避免把显而易见的相似堆进图里。

超边(Hyperedges)

3 个及以上节点共同参与一个无法用两两边表达的概念、流程或模式时(例如“所有实现同一协议/接口的类”“整条认证流程里的函数”),向顶层 hyperedges 数组添加一条超边。规范两条纪律:谨慎使用(只在群体关系提供两两边之外的信息时才加)、每个分片最多 3 条

YAML frontmatter 传播

如果源文件带 YAML frontmatter(--- ... ---),其中的 source_urlcaptured_atauthorcontributor 必须复制到该文件产出的每一个节点上。这保证了知识库语料的来源元数据在图谱层面可追溯。

confidence_score:离散评分表(rubric)

这是全规范中最“反直觉”的一条:confidence_score 在每条边上必填,且永远不能用 0.5 作默认值。取值被限制为离散集合:

confidence 允许的 score 语义
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 档位都不合适,把边标成 AMBIGUOUS”——这是规范给出的唯一出口。源码侧有两处佐证这套评分表不只是提示词约定:

  1. 合并阶段的兜底分值与评分表严格对齐graphify/export.py 定义了缺分兜底值 _CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.55, "AMBIGUOUS": 0.2},注释明确写道:历史默认 0.5 正是规范逐字禁止的取值,且它不在离散集合 {0.55, 0.65, 0.75, 0.85, 0.95} 内;缺失分值意味着“对强度没有证据”,诚实的兜底是量表允许的最弱值 0.55,而不是读起来像抛硬币的中点;
  2. AST 抽取器自己也引用这张评分表graphify/extractors/engine.py 在发出“符号可被调用”这一推断边时固定使用 confidence_score: 0.85,注释写明 “0.85 = 'strong inference' on the extraction-spec rubric”。也就是说,确定性解析器发出的 INFERRED 边与 LLM 子代理发出的边使用同一把尺子,合并后的置信度分布才是可比的。

节点 ID 格式:与 AST 抽取器严格一致

规范对节点 ID 的约束是:

  • 只允许小写字母、数字、下划线([a-z0-9_]),不含点与斜杠
  • 格式 {stem}_{entity}stem去掉扩展名的完整仓库相对路径,每一级目录段都保留并用 _ 连接(每段小写化、非字母数字字符替换为 _),entity 是同样归一化后的符号名;
  • 必须用全部目录层级,不能只用直接父目录——这正是 src/auth/session.py + ValidateTokensrc_auth_session_validatetoken 这个例子存在的意义;
  • 顶层文件(无父目录)只用文件名字干;
  • 必须与 AST 抽取器生成的 ID 完全一致,否则会制造孤儿/幽灵重复节点;
  • 永远不要追加 chunk 号或序号后缀——ID 必须由标签本身确定性生成:同一实体无论被哪个分片处理,产出同一 ID。

这套契约在源码中有一个真正的“单一事实源”:graphify/ids.pynormalize_id / make_id。其模块 docstring 直言不讳:AST 抽取器、语义子代理(LLM)、图构建器三个独立生产方必须对同一实体生成相同 ID,否则图会把一个实体分裂成互不相连的幽灵节点。归一化配方为:casefold 与 NFKC 规范化迭代至不动点(两者不可交换,单遍处理对土耳其语 İ、希腊文组合重音等序列不稳定),再将非词字符段替换为单个下划线、压缩重复下划线、去除首尾下划线。

更值得注意的是配套的漂移守卫测试 tests/test_extraction_spec_ids.py。它用正则 `路径` + `符号` → `ID` 直接解析仓库内每一份 extraction-spec.md 里的示例(包括各宿主版本与 tools/skillgen/fragments 的源模板),逐一断言真实的 extract._make_id(_file_stem(path), entity) 能复现文档中写出的期望 ID;同时锁定反例——“只用文件名”与“只用直接父目录”两种旧格式产出的 ID 必须与正确 ID 不同。测试文件注释解释了动机:规范是手工维护的散文,可能与代码悄悄漂移,而这个 bug 类(#811 Unicode 坍缩、#550 同名文件冲突、#1033 AST 与 LLM 文件节点不匹配、#1104)都源于 ID 配方在 extractbuild 中各抄一份、靠镜像 docstring 维持同步。该守卫同时保护两个方向:规范示例被改成错误值会失败,ID 函数变更导致示例失效也会失败

输出 JSON Schema

规范要求输出“恰好是这个 JSON,没有别的文本”,完整字段如下(按原文 schema 展开):

{
  "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
}

几个细节:

  • relation 枚举覆盖八种关系,其中 semantically_similar_torationale_for 是纯语义边,calls/cites 等是结构边;超边的 relation 则收敛为 participate_in|implement|form 三种;
  • 顶层 input_tokens/output_tokens 用于记账语义抽取的 token 开销;
  • 边上的 weightconfidence_score 并存:前者是图算法权重,后者是证据强度,两者语义不同不可混用。

source_file VERBATIM 规则:让全量构建与增量更新对齐

规范最后一条规则要求 source_file 逐字符复制 FILE_LIST 中的路径:绝对路径、不缩成 basename、不重新相对化、不改分隔符。其目的在原文最后一句:让全量构建(full build)与增量更新(--update)保持同一基准,使 build_merge 的替换逻辑能匹配到已有节点并替换,而不是累积重复节点。合并入口在 graphify/build.pybuild_merge 中;从源码结构看,合并时以节点 ID 为主键做替换/去重,而节点又通过 source_file 锚定到具体文件——路径只要有任何形式的“美化”(改分隔符、丢前缀),锚定就会失效,增量更新就会退化为不断追加重复节点。这条规则与“ID 不得带分片后缀”是同一设计意图的两半:一切确定性标识都只能来自内容与位置本身

缓存与提示词指纹:规范文件本身成为缓存键的一部分

理解了“子代理逐字接收这份提示词”之后,graphify/cache.py 中的 prompt_fingerprint 就有了着落。graphify 的语义缓存把抽取提示词本身作为缓存条目的归属维度:

  • 技能路径下,提示词就是 references/extraction-spec.md 文件(cache.py 第 105–106 行注释原文即如此),Python 直连 LLM 路径则归属 _extraction_system 系统提示;
  • 指纹取 SHA-256 前 12 位(_PROMPT_FP_LEN = 12),计算前统一换行符、去行尾空白并去除首尾空白——同一份 spec 文件在 Windows 上以 CRLF 检出、在 Linux 上以 LF 写缓存,不能因此指纹不同,否则每次 Windows 运行都看起来像“提示词变了”,白白触发重抽取与重新计费;
  • 由此得到行为保证:graphify 升级时若本规范文件内容变化,旧提示词产出的缓存条目会被重新抽取而不是回放;提示词不变则缓存继续生效。

另外两处与规范细节呼应的实现值得一并留意:

  • 语义缓存对 frontmatter 的处理同样谨慎:graphify/cache.py 用整行正则(恰好三个短横线加可选行尾空白)识别 frontmatter 分隔符,注释说明 startswith("---") 这类子串匹配会误吞 ---- 分隔线与 --- text 行文,把其上方内容从哈希里静默丢掉(#1259)——这与规范里“frontmatter 传播”规则共享同一个解析前提;
  • 分片结果合并时的超边去重也考虑了“无 ID 超边”的边界:graphify/export.pyattach_hyperedges 对缺失 id 的持久化超边做了 .get("id") 保护,避免历史 graph.json 中无 ID 超边导致每次增量重抽取都抛 KeyError(#2775)。

紧凑版与完整版的关系

Kiro/Codex 目录下的是紧凑版提示词(标题即 “extraction subagent prompt (compact)”),与 Claude 等宿主的完整版相比:规则语义完全一致(三态置信度、六值 file_type、离散评分表、超边上限、frontmatter 传播、节点 ID 规则、source_file VERBATIM),但删去了大段示例枚举,且要求子代理内联返回 JSON(“Output exactly this JSON (no other text)”),而完整版会额外要求子代理用 Write 工具把结果写到指定的绝对路径 CHUNK_PATH(相对路径在子代理中解析基准未定义,会被静默丢失)。两份文本都由 tools/skillgen 的片段模板渲染生成并随各宿主技能分发,节点 ID 示例的漂移守卫测试对 graphify/skillstools/skillgen/fragments 两个根下的所有 extraction-spec.md 统一生效,因此各宿主版本间的契约漂移同样被测试捕获。

实战速查

  1. 何时会读到这份规范:只有 Part B 被触发时——语料里存在文档、论文或图片分片。纯代码仓库跑 /graphify . 时该文件完全不参与流程,且无需任何 API key;
  2. 派发子代理时:替换 FILE_LISTCHUNK_NUMTOTAL_CHUNKSDEEP_MODE 四个占位符后逐字下发;--mode deep 必须透传为 DEEP_MODE=true;Kiro/Codex 宿主使用 general-purpose 型子代理并让其内联返回 JSON;
  3. 校验输出file_type 六值、confidence 三态、INFERRED 五档离散分值(永不 0.5)、节点 ID 全小写下划线格式、source_file 逐字符一致——任何一条不符都会被下游合并或校验拒绝,或制造幽灵重复节点;
  4. 维护该规范:改动提示词内容会改变提示词指纹,从而使语义缓存条目整体失效重抽;改动节点 ID 示例必须与 graphify/extract_file_stem/_make_id 同步,并让 tests/test_extraction_spec_ids.py 的漂移守卫通过,否则示例即 ground truth 的假设被破坏。
登录后查看全文
热门项目推荐
相关项目推荐