首页
/ graphify 语义抽取子代理规范:extraction-spec.md 的 Prompt 契约、节点 ID 规则与置信度量规

graphify 语义抽取子代理规范:extraction-spec.md 的 Prompt 契约、节点 ID 规则与置信度量规

2026-09-06 15:13:11作者:邬祺芯Juliet

本文以 graphify 仓库中随各宿主 skill 一起发布的子代理提示词规范 extraction-spec.md 为主线,拆解它在 /graphify 流水线中的加载时机、对 LLM 语义抽取子代理的全部输出约束(JSON Schema、节点 ID 规则、置信度量规、超边与视觉规则),并结合 graphify/ids.pygraphify/extract.pygraphify/build.pytests/test_extraction_spec_ids.py 等源码,说明这些"提示词里的文字约定"是如何被代码侧的归一化函数、漂移守卫测试和容错逻辑兜住、最终保证 AST 结果与语义结果能无损合并的。

1. 规范文件在流水线中的位置:只有语义抽取才会读取它

graphify 的构建流程在 SKILL 文件中被组织为 Step 3 的三个部分:Part A 对代码文件做确定性 AST 抽取,Part B 用并行子代理做语义抽取,Part C 把两者合并。graphify/skill-droid.md 中明确要求"Run Part A (AST) and Part B (semantic) in parallel",两者操作不同文件类型、可以同时在一条消息里启动,大语料上可节省 5–15 秒。

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.

也就是说,它是一份按需加载的提示词模板,只在语料里存在文档、论文或图片块时才被读取。每个语义子代理收到的都是同一份提示词的逐字副本,仅替换五个占位符:FILE_LISTCHUNK_NUMTOTAL_CHUNKSDEEP_MODECHUNK_PATH。宿主 skill(如 graphify/skill-droid.md)对应写的是:"Pass each subagent that prompt verbatim with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH substituted"。

围绕这份规范,宿主 skill 的 Step B0–B3 还定义了完整执行协议:

  • Step B0(缓存检查):调用 graphify.cache.check_semantic_cache(files, root=..., prompt_file='SPEC_PATH')。这里的 SPEC_PATH 是该 extraction-spec.md 的绝对路径。注释解释得很直白:缓存条目归属于"产生它的那份提示词"——graphify 升级改变了提示词,旧条目就会被重新抽取而不是直接回放;提示词未变则命中缓存(#1939)。见 graphify/skill-droid.md
  • Step B1(分块):按 20–25 个文件一块切分;每个图片单独一块(视觉需要独立上下文);同一目录的文件尽量分在一块,提高跨文件关系被抽出的概率。
  • Step B2(派发):用 Task 工具在同一条响应里并行派发所有子代理,结果分别写入 graphify-out/.graphify_chunk_NN.jsonCHUNK_PATH 必须是绝对路径,因为 Write 工具把相对路径解析到一个未定义的 cwd,文件会被静默丢失——这正是规范文件末尾专门警告的原因(见第 6 节)。
  • Step B3(收集、缓存、合并):以"chunk 文件出现在磁盘上"为成功信号;文件缺失提示子代理可能被派成了只读类型;失败或非法 JSON 只告警跳过、不中止;超过一半 chunk 失败才停下要求用户改用 general-purpose 代理重跑。

2. 完整的子代理 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.

Files (chunk CHUNK_NUM of TOTAL_CHUNKS):
FILE_LIST

Rules:
- EXTRACTED: relationship explicit in source (import, call, citation, "see §3.2")
- INFERRED: reasonable inference (shared data structure, implied dependency)
- AMBIGUOUS: uncertain - flag for review, do not omit

Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns).
  Do not re-extract imports - AST already has those.
Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made,
  trade-offs, design intent): store as a `rationale` attribute on the relevant concept node — do NOT
  create a separate rationale node or fragment node. Only create a node for something that is itself
  a named entity or concept. Use `file_type:"rationale"` for concept-like nodes (ideas, principles,
  mechanisms, design patterns). `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.
Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling),
  target MUST be the callee. Never reverse this direction. `calls` edges MUST stay within one language:
  a Python function cannot `calls` a JS/TS/Go/Rust/Java symbol and vice versa — cross-language call
  edges are phantom artifacts, never emit them.
Image files: use vision to understand what the image IS - do not just OCR.
  UI screenshot: layout patterns, design decisions, key elements, purpose.
  Chart: metric, trend/insight, data source.
  Tweet/post: claim as node, author, concepts mentioned.
  Diagram: components and connections.
  Research figure: what it demonstrates, method, result.
  Handwritten/whiteboard: ideas and arrows, mark uncertain readings AMBIGUOUS.

DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps,
  shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.

Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea
  without any structural link (no import, no call, no citation), add a `semantically_similar_to`
  edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples:
- Two functions that both validate user input but never call each other
- A class in code and a concept in a paper that describe the same algorithm
- Two error types that handle the same failure mode differently
Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them
  for trivially similar things.

Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that
  is not captured by pairwise edges alone, add a hyperedge to a top-level `hyperedges` array. Examples:
- All classes that implement a common protocol or interface
- All functions in an authentication flow (even if they don't all call each other)
- All concepts from a paper section that form one coherent idea
Use sparingly — only when the group relationship adds information beyond the pairwise edges.
  Maximum 3 hyperedges 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 edges: confidence_score = 1.0 always
- INFERRED edges: pick exactly ONE value from this set — never 0.5:
    0.95  direct structural evidence (shared data structure, named cross-file reference).
    0.85  strong inference (clear functional alignment, no direct symbol link).
    0.75  reasonable inference (shared problem domain + similar shape, requires interpretation).
    0.65  weak inference (thematically related, no shape evidence).
    0.55  speculative but plausible (surface-level co-occurrence only).
  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. If no value above fits, mark
  the edge AMBIGUOUS rather than picking 0.4 or below.
- AMBIGUOUS edges: 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 path segment kept and joined with _
  (each segment lowercased with non-alphanumeric chars replaced by _), and entity is the symbol name
  similarly normalized. Use every directory level, not just the immediate parent — this keeps same-named
  files in different directories distinct. Examples: src/auth/session.py + ValidateToken →
  src_auth_session_validatetoken; lib/utils/helpers.py + parse_url → lib_utils_helpers_parse_url;
  tests/test_foo.py + _helper → tests_test_foo_helper; docs/v1/api/README.md + getUser →
  docs_v1_api_readme_getuser. Top-level files (no parent dir, e.g. setup.py) use just the filename
  stem: setup_my_func. This must match the ID the AST extractor generates — using just the filename
  (e.g. session_validatetoken) or only the immediate parent (e.g. auth_session_validatetoken) will
  create orphan ghost-duplicate nodes. If you are re-extracting a project built under the old
  immediate-parent format, the user should run `graphify extract --force` to rebuild cleanly.
  CRITICAL: never append chunk numbers, sequence numbers, or any suffix to an ID (no _c1, _c2,
  _chunk2, etc.). IDs must be deterministic from the label alone — the same entity must always produce
  the same ID regardless of which chunk processes it.

Generate the extraction JSON matching this schema exactly:
{"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 (every node, edge, and hyperedge): set source_file to the path of the originating
  file EXACTLY as it appears in FILE_LIST — verbatim and absolute. Do NOT shorten to a basename, do
  NOT re-relativize, do NOT strip any directory prefix, and do NOT change separators (the engine
  canonicalizes separators and relativizes against the build root downstream). Copy the FILE_LIST
  entry character-for-character. This keeps the full build and incremental --update on the same base,
  so build_merge's replace-on-re-extract matches the existing node instead of accumulating a duplicate.

Then write the JSON to disk using the Write tool at this exact absolute path (no relative paths —
  Write resolves relative paths against an undefined cwd and the file will be silently lost):
CHUNK_PATH

整份提示词的设计意图可以概括为一句话:把 LLM 的输出面压缩到与确定性 AST 流水线完全可合并的最小集合——固定 JSON 结构、固定 ID 算法、固定分数档位,其余一切(解释、围栏、前言)都被禁止。

3. 边置信度:三档定性标签 + 离散打分量规

规范把每条边分为三档:

标签 含义 confidence_score
EXTRACTED 关系在源文中显式存在(import、call、citation、"see §3.2") 恒为 1.0
INFERRED 合理推断(共享数据结构、隐含依赖) 只能从离散集合 {0.95, 0.85, 0.75, 0.65, 0.55} 中取一个值,永不允许 0.5
AMBIGUOUS 不确定——标记供人工审查,不得省略 0.1–0.3

五个 INFERRED 档位各自的语义在规范里逐一给出:0.95 对应"直接结构证据(共享数据结构、具名跨文件引用)",0.85 对应"强推断(功能对齐明显但无直接符号链接)",0.75 对应"合理推断(同一问题域 + 形状相似,需要解释)",0.65 对应"弱推断(主题相关但无形状证据)",0.55 对应"推测但可信(仅有表层共现)"。

规范还解释了这个离散量规的设计动机:"Models follow discrete rubrics better than continuous ranges"——生产环境观察到置信度分布呈双峰(>50% 落在 0.5,>40% 落在 0.85 以上),说明给区间引导时模型会把它塌缩成二选一。因此规则改为:五个档位都不匹配时,宁可将边标为 AMBIGUOUS,也不允许出现 0.4 及以下的 INFERRED 分数。

AST 侧同样遵守这份量规。graphify/extract.py 中,AST 解析器为跨文件间接调用发出 INFERRED indirect_call 边时固定使用 0.85,注释写明原因:"0.85, not 0.8: the rubric in references/extraction-spec.md is a discrete set {0.55, 0.65, 0.75, 0.85, 0.95} and 0.8 is not in it"(#2813)。graphify/extract.py 中直接 calls 边同理:有 import 证据则升级为 EXTRACTED / 1.0,否则 INFERRED / 0.85。这说明 extraction-spec.md 不只是给 LLM 看的文字,它是整个抽取层共享的数值契约——确定性代码与 LLM 输出共用同一把尺子,合并后的 graph.json 中 confidence_score 才有可比的含义。

4. 节点 ID 规范:防止图"裂开"的关键约定

ID 规则是整份规范中最重的一段,因为它同时约束两个独立的生产者:

  • 格式:小写、仅 [a-z0-9_]、无点无斜杠;形如 {stem}_{entity},其中 stem去掉扩展名的完整仓库相对路径,保留每一个目录层级、逐段小写、非字母数字字符替换为 _,段间用 _ 连接;entity 做同样归一化。
  • 示例(原样继承自规范文件):
    • src/auth/session.py + ValidateTokensrc_auth_session_validatetoken
    • lib/utils/helpers.py + parse_urllib_utils_helpers_parse_url
    • tests/test_foo.py + _helpertests_test_foo_helper
    • docs/v1/api/README.md + getUserdocs_v1_api_readme_getuser
    • 顶层文件(无父目录,如 setup.py)只用文件干:setup_my_func
  • 两条禁忌:只取文件名(session_validatetoken)或只取直接父目录(auth_session_validatetoken)都会制造孤儿"幽灵重复"节点;ID 严禁附加 chunk 编号等任何后缀(_c1_chunk2 之类),因为 ID 必须仅由 label 决定、完全确定——同一实体无论落在哪个 chunk 处理,都必须生成同一个 ID。对用旧"直接父目录"格式构建过的项目,规范给出的补救手段是运行 graphify extract --force 重建。

代码侧的对应实现graphify/ids.py,其模块 docstring 开宗明义:三个独立生产者(AST 抽取器的 extract._make_id、遵循本规范的语义子代理、图构建器的 build._normalize_id)必须在节点 ID 上达成一致,否则"图会把单一实体裂成互不相连的幽灵节点"。归一化配方是:casefold 后接 NFKC 归一化并迭代到不动点(因为 İ 会展开成基础字母加组合符 U+0307,两步不交换、单趟处理不稳定),再把 [^\w]+ 连续替换为单个 _、折叠重复下划线、去掉首尾下划线。核心函数见 graphify/ids.py

漂移守卫:由于规范是手写的提示词文本,可能悄悄与代码脱节。tests/test_extraction_spec_ids.py 专门解析 graphify/skills/tools/skillgen/fragments/ 下所有 extraction-spec.md 中的反引号示例(正则匹配 `path` + `entity` → `id` 格式),并用生产函数 _make_id(_file_stem(Path(path)), entity) 逐条复算,两边不一致即测试失败——"规范示例被改错"或"ID 函数变了但示例没跟上"都会被抓到(见 tests/test_extraction_spec_ids.py)。该测试还锁定反例:文件名干形式与直接父目录形式确实与正确 ID 不同,防止警示语过期。

5. JSON Schema、source_file 规则与 file_type 六值约束

Schema 顶层固定为 nodes / edges / hyperedges / input_tokens / output_tokens。其中几个字段约束值得展开:

  • 节点字段idlabelfile_typesource_filesource_location,以及四个允许为 null 的前置元数据字段 source_urlcaptured_atauthorcontributor。规范规定:若文件带 YAML frontmatter(--- ... ---),这四个字段要复制到该文件的每一个节点上
  • 边的 relation 枚举calls | implements | references | cites | conceptually_related_to | shares_data_with | semantically_similar_to | rationale_for;每条边必带 confidenceconfidence_scoresource_file,默认 weight1.0
  • 超边relationparticipate_in | implement | form,confidence 只允许 EXTRACTED/INFERRED,成员数 ≥ 3,且每 chunk 最多 3 条——只有当"群体关系提供了成对边之外的信息"时才添加。

source_file 逐字规则(对每个节点、边、超边都适用):必须原样复制 FILE_LIST 中该条目的字符,不得缩成 basename、不得重新相对化、不得剥目录前缀、不得改分隔符——引擎在下游负责统一分隔符并按构建根相对化。规范给出的理由是:这样全量构建与增量 --update 才保持同一基准,build_merge 的 replace-on-re-extract 才能匹配到既有节点而不是累积重复节点。

file_type 六值约束codedocumentpaperimagerationaleconcept,其他值"无效且会被拒绝")在代码侧有明确的容错设计。graphify/build.py 定义了 _FILE_TYPE_SYNONYMS,把 LLM 子代理常见的非法取值就近映射回合法值(markdown/textdocumenttool/librarycodepattern/principle/framework 等→concept),兜底归为 concept(#840)。同理,超边成员列表的别名 membersnode_ids 会在入口被归一到规范键 nodes,成员若是对象则强转为标量 id(见 graphify/build.py)。提示词负责"事前约束",构建器负责"事后兜底",两层缺一不可。

跨语言 calls 禁令也有代码护栏。 规范断言"跨语言调用边是幻影伪迹,永远不要输出";graphify/build.py 中的 _EDGE_LANG_FAMILY 表按"真实互操作"分组语言(JS/TS 共享模块图、C/C++/ObjC 共享编译单元、JVM 系共享字节码),在边循环中丢弃伪跨语言 calls 边,同时放行合法的 TS→JS 导入或 C 实现→头文件调用。

6. 写入路径:为什么 CHUNK_PATH 必须是绝对路径

提示词最后一段要求子代理用 Write 工具把 JSON 写到 CHUNK_PATH 这个精确的绝对路径,理由写在规范原文里:"Write resolves relative paths against an undefined cwd and the file will be silently lost"。这与宿主 skill 的 Step B2 完全呼应:graphify/skill-droid.md 要求派发前先从 pwd 推导 CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"(注意是项目根而非 .graphify_root 的扫描目录,#1392),且"文件出现在磁盘上"是 Step B3 判定子代理成功的唯一信号——缺失即提示"子代理可能以只读方式派发",不允许静默跳过。

7. 规范文本的可再生性与多宿主分发

仓库中每个宿主目录(droid、codex、claude、copilot 等十余个)都带有一份内容一致的 extraction-spec.md,如本文主线的 graphify/skills/droid/references/extraction-spec.mdgraphify/skill-droid.md 配套。从源码结构看,这些逐宿主副本由 skillgen 工具从共享片段渲染而来:漂移守卫测试同时扫描 graphify/skills/tools/skillgen/fragments/ 两个根目录下的所有 extraction-spec.md(排除打包产物 build/expected/),并断言至少解析出 13 条节点 ID 示例,防止"守卫空转"。这意味着对规范的任何修改(例如调整量规档位或 ID 规则)都应当与 graphify/ids.pyextract.py 中的实现和上述测试一起变更,才能保持"提示词—AST 抽取器—构建器"三方的 ID 一致性。

8. 小结

extraction-spec.md 是 graphify 把"LLM 语义抽取"驯化成"可合并流水线组件"的契约文件,其要点可以归纳为:

  1. 加载纪律:只在 Step 3 Part B 且存在 doc/paper/image chunk 时加载,纯代码语料永远不读它;缓存条目以该文件的绝对路径为归属键,提示词一变即触发重抽取。
  2. 输出纪律:只输出符合固定 Schema 的 JSON;边必带三档置信标签与离散分数的 confidence_score(EXTRACTED=1.0;INFERRED∈{0.95, 0.85, 0.75, 0.65, 0.55};AMBIGUOUS∈[0.1, 0.3],永不用 0.5 兜底)。
  3. 身份纪律:节点 ID = 完整仓库相对路径干 + 归一化符号名,与 AST 抽取器逐字符一致,禁止 chunk 后缀,保证增量更新时 replace-on-re-extract 命中原节点。
  4. 语义分工:代码文件只补 AST 找不到的语义边(不重抽 import,calls 方向为 caller→callee 且禁止跨语言);文档/论文抽取概念、实体与引用,rationale 作为属性而非独立节点;图片按六种类型做"理解而非 OCR"的视觉抽取;--mode deep 时激进产生 INFERRED 边但把不确定的标为 AMBIGUOUS。
  5. 代码兜底file_type 同义词映射、超边成员别名归一、跨语言幻影边过滤、ID 漂移守卫测试,确保提示词失手时构建管线仍可收敛而不是崩溃或累积幽灵节点。

对读者而言,这份规范同时也是很好的"面向 LLM 的工程化提示词"样本:用离散量规替代连续区间、用逐字复制规则隔离路径歧义、用可执行测试锁定自然语言示例——这三点在任何"LLM 输出要被确定性系统消费"的场景中都值得借鉴。

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