graphify 语义抽取子代理规范:extraction-spec.md 的 Prompt 契约、节点 ID 规则与置信度量规
本文以 graphify 仓库中随各宿主 skill 一起发布的子代理提示词规范 extraction-spec.md 为主线,拆解它在 /graphify 流水线中的加载时机、对 LLM 语义抽取子代理的全部输出约束(JSON Schema、节点 ID 规则、置信度量规、超边与视觉规则),并结合 graphify/ids.py、graphify/extract.py、graphify/build.py 与 tests/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_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE 与 CHUNK_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.json。CHUNK_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+ValidateToken→src_auth_session_validatetokenlib/utils/helpers.py+parse_url→lib_utils_helpers_parse_urltests/test_foo.py+_helper→tests_test_foo_helperdocs/v1/api/README.md+getUser→docs_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。其中几个字段约束值得展开:
- 节点字段:
id、label、file_type、source_file、source_location,以及四个允许为 null 的前置元数据字段source_url、captured_at、author、contributor。规范规定:若文件带 YAML frontmatter(--- ... ---),这四个字段要复制到该文件的每一个节点上。 - 边的 relation 枚举:
calls | implements | references | cites | conceptually_related_to | shares_data_with | semantically_similar_to | rationale_for;每条边必带confidence、confidence_score、source_file,默认weight为1.0。 - 超边:
relation限participate_in | implement | form,confidence 只允许 EXTRACTED/INFERRED,成员数 ≥ 3,且每 chunk 最多 3 条——只有当"群体关系提供了成对边之外的信息"时才添加。
source_file 逐字规则(对每个节点、边、超边都适用):必须原样复制 FILE_LIST 中该条目的字符,不得缩成 basename、不得重新相对化、不得剥目录前缀、不得改分隔符——引擎在下游负责统一分隔符并按构建根相对化。规范给出的理由是:这样全量构建与增量 --update 才保持同一基准,build_merge 的 replace-on-re-extract 才能匹配到既有节点而不是累积重复节点。
file_type 六值约束(code、document、paper、image、rationale、concept,其他值"无效且会被拒绝")在代码侧有明确的容错设计。graphify/build.py 定义了 _FILE_TYPE_SYNONYMS,把 LLM 子代理常见的非法取值就近映射回合法值(markdown/text→document、tool/library→code、pattern/principle/framework 等→concept),兜底归为 concept(#840)。同理,超边成员列表的别名 members、node_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.md 与 graphify/skill-droid.md 配套。从源码结构看,这些逐宿主副本由 skillgen 工具从共享片段渲染而来:漂移守卫测试同时扫描 graphify/skills/ 与 tools/skillgen/fragments/ 两个根目录下的所有 extraction-spec.md(排除打包产物 build/ 与 expected/),并断言至少解析出 13 条节点 ID 示例,防止"守卫空转"。这意味着对规范的任何修改(例如调整量规档位或 ID 规则)都应当与 graphify/ids.py、extract.py 中的实现和上述测试一起变更,才能保持"提示词—AST 抽取器—构建器"三方的 ID 一致性。
8. 小结
extraction-spec.md 是 graphify 把"LLM 语义抽取"驯化成"可合并流水线组件"的契约文件,其要点可以归纳为:
- 加载纪律:只在 Step 3 Part B 且存在 doc/paper/image chunk 时加载,纯代码语料永远不读它;缓存条目以该文件的绝对路径为归属键,提示词一变即触发重抽取。
- 输出纪律:只输出符合固定 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 兜底)。 - 身份纪律:节点 ID = 完整仓库相对路径干 + 归一化符号名,与 AST 抽取器逐字符一致,禁止 chunk 后缀,保证增量更新时 replace-on-re-extract 命中原节点。
- 语义分工:代码文件只补 AST 找不到的语义边(不重抽 import,
calls方向为 caller→callee 且禁止跨语言);文档/论文抽取概念、实体与引用,rationale 作为属性而非独立节点;图片按六种类型做"理解而非 OCR"的视觉抽取;--mode deep时激进产生 INFERRED 边但把不确定的标为 AMBIGUOUS。 - 代码兜底:
file_type同义词映射、超边成员别名归一、跨语言幻影边过滤、ID 漂移守卫测试,确保提示词失手时构建管线仍可收敛而不是崩溃或累积幽灵节点。
对读者而言,这份规范同时也是很好的"面向 LLM 的工程化提示词"样本:用离散量规替代连续区间、用逐字复制规则隔离路径歧义、用可执行测试锁定自然语言示例——这三点在任何"LLM 输出要被确定性系统消费"的场景中都值得借鉴。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00