graphify 语义抽取契约全解:extraction-spec.md 子代理提示词规范深度剖析
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
本文以 graphify 仓库中的 extraction-spec.md 为唯一主体,完整拆解这份"语义抽取子代理提示词规范"的全部规则:三级置信度分类、离散置信度评分量表、节点 ID 确定性规则、source_file逐字路径约束与 JSON 输出 Schema。读完你将理解 graphify 如何让 LLM 子代理产出的图片段与 AST 抽取器严格对齐、并能被增量更新与提示词版本化缓存正确复用,并能据此为自己的多代理抽取管线设计类似的输出契约。
一、extraction-spec.md 在 graphify 管线中的位置
graphify 的抽取分两条路:Part A 用本地确定性 AST 解析器处理代码的结构性事实(import、定义、跨文件引用),Part B 用 LLM 子代理处理"AST 找不到"的语义关系——文档、论文、图片中的概念、引用、设计意图。extraction-spec.md 就是 Part B 发给每一个语义子代理的提示词本体。文档开头的定位说明非常明确:
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 时,主代理才在 Step 3 Part B 加载它;纯代码语料会跳过 Part B,永远不读这个文件。这与 skill-agents.md 中 Part B 的"Fast path"规则完全对应——检测到零文档、零论文、零图片时直接跳到 Part C 合并阶段,且要先写一个空的.graphify_semantic.json占位,否则合并阶段会因无条件读取该文件而抛FileNotFoundError。
该提示词以**逐字(verbatim)**方式下发给每个子代理,其中 5 个占位符由主代理替换:
FILE_LIST:本 chunk 内子代理要读的文件清单;CHUNK_NUM/TOTAL_CHUNKS:chunk 编号与总数;DEEP_MODE:是否处于--mode deep模式;CHUNK_PATH:子代理必须把结果 JSON 写到的绝对路径。
值得注意的工程事实:这份规范并不是手写一份就完事。所有 18 个宿主(claude、codex、opencode、kilo、copilot、claw、droid、trae、kiro、pi、antigravity、windows、kimi、amp、gemini 等)的 skill 文件都由 tools/skillgen/ 下的单一源生成,每份宿主的references/extraction-spec.md(如 graphify/skills/agents/references/extraction-spec.md、graphify/skills/claude/references/extraction-spec.md)都是同一契约的渲染副本,避免多宿主各自漂移。
二、提示词骨架:三条置信度等级与三类文件的处理规则
提示词正文第一句就立下输出铁律:只输出符合 Schema 的合法 JSON——无解释、无 markdown 围栏、无前言。随后定义了每条边的三级置信度分类:
| 等级 | 含义 | 判定标准 |
|---|---|---|
EXTRACTED | 关系在源中显式存在 | import、call、citation、"see §3.2" 这类明写关系 |
INFERRED | 合理推断 | 共享数据结构、隐含依赖 |
AMBIGUOUS | 不确定 | 标记待审,不得省略 |
AMBIGUOUS的定位很关键:不确定不是"丢弃"的理由,而是必须落图、供人工审查。这一条贯穿了后文的评分规则——拿不准的边宁可降级为AMBIGUOUS,也不允许给出低于 0.4 的分数。
2.1 代码文件:只补 AST 的盲区,绝不重复劳动
对代码文件,规范要求聚焦于AST 找不到的语义边(调用关系、共享数据、架构模式),并有一条明确的禁则:不要重新抽取 import——AST 已经拿到了。这确立了 graphify"确定性解析 + LLM 补语义"的分工边界。
针对calls边,规范给出了两条硬性方向规则:
- 方向不可反:
source必须是调用方(发起调用的函数/类),target必须是被调方; calls边不得跨语言:Python 函数不能callsJS/TS/Go/Rust/Java 符号,反之亦然——跨语言调用边被定性为"phantom artifacts(幽灵产物)",严禁产出。
2.2 文档/论文文件:rationale 是属性,不是节点
对文档与论文,规范要求抽取命名概念、实体与引用。其中最容易做错的点是rationale(决策理由、权衡、设计意图)的处理方式:
- rationale 必须存为相关概念节点上的
rationale属性,不得单独创建 rationale 节点或 fragment 节点; - 只有自身就是命名实体或概念的东西才建节点;
file_type是概念类节点(思想、原则、机制、设计模式)的类型标记;file_type只允许恰好六个取值:code、document、paper、image、rationale、concept,其他值一律非法、会被拒绝。
这条规则在原生后端提示词 graphify/llm.py(_EXTRACTION_SYSTEM,约 L478–L508)中同样出现,两条抽取路径共用同一套 Schema 约束。
2.3 图片文件:用视觉理解"图是什么",而不是 OCR
图片文件必须用视觉能力理解图片本身是什么,而不是做 OCR。规范按图片类型给出了六类抽取要点:
- UI 截图:布局模式、设计决策、关键元素、用途;
- 图表:度量、趋势/洞察、数据来源;
- 推文/帖子:主张作为节点、作者、提及的概念;
- 示意图:组件与连接关系;
- 研究图:它证明了什么、方法、结果;
- 手写/白板:想法与箭头,读不确定的部分标
AMBIGUOUS。
2.4 DEEP_MODE:激进推断,但仍受约束
当以--mode deep运行时,规范指示子代理对INFERRED边采取更激进的策略——间接依赖、共享假设、潜在耦合都可以提边,但不确定的必须标AMBIGUOUS而不是省略。对比原生后端的 deep 后缀 graphify/llm.py(_DEEP_EXTRACTION_SUFFIX,约 L510–L516)可以看到同一设计意图:deep 模式只放行"具体架构信号"(共享数据契约、显式生命周期耦合、多步流程依赖),并抑制宽泛的概念相似边。
2.5 semantically_similar_to:无结构链接的跨切面相似
若同一 chunk 内两个概念解决同一问题或表达同一思想,但不存在任何结构链接(无 import、无 call、无 citation),应添加一条标记为INFERRED的semantically_similar_to边,confidence_score反映相似程度(0.6–0.95)。规范给出了三个示例:
- 两个都校验用户输入却互不调用的函数;
- 代码中的类与论文中的概念描述同一算法;
- 两个以不同方式处理同一失败模式的错误类型。
并强调克制:只在相似性确实非显而易见且跨切面时才添加,对"显而易见相似"的对象不要建边。
2.6 超边(hyperedges):成组关系的一等公民
当 3 个及以上节点共同参与一个仅靠成对边无法表达的共享概念、流程或模式时,向顶层hyperedges数组添加超边。规范给出三类示例:
- 实现同一协议/接口的所有类;
- 认证流程中的全部函数(即使它们并非两两互调);
- 论文章节中构成一个完整思想的全体概念。
使用原则是" sparingly"——只有当成组关系提供了成对边之外的信息时才加,且每个 chunk 最多 3 条超边。
2.7 YAML frontmatter 溯源字段透传
若文件带 YAML frontmatter(--- ... ---),其中的source_url、captured_at、author、contributor必须复制该文件产出的每一个节点上。这是图谱级溯源(provenance)的基础:一个概念节点最终能回答"出自哪个 URL、谁采集的、何时"。
三、离散置信度评分量表:为什么禁止 0.5
规范对confidence_score的规定是全文最"反直觉"的部分——它是离散量表而非连续区间:
| 边等级 | 分值规则 |
|---|---|
EXTRACTED | 恒为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:推测但可信(仅表面共现)。
规范还罕见地写入了"为什么":模型对离散量表的遵循度显著高于连续区间;生产环境观测到双峰分布(>50% 集中在 0.5,>40% 集中在 0.85+),说明区间式引导正在被坍缩成二值选择。最后一条兜底:若没有一档合适,把边标为AMBIGUOUS,而不是选 0.4 或更低。
这条规则在代码里被逐字执行并测试。graphify/export.py 中为缺失分数的边提供了回退值:
_CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.55, "AMBIGUOUS": 0.2}注释解释了历史:旧版 INFERRED 默认 0.5,恰恰是规范明文禁止的值,且不在离散集合中;现在改为取量表下限0.55——"缺失分数代表关于强度的证据缺失,诚实的回退应是量表允许的最弱值,而不是读起来像抛硬币的中点"。tests/test_inferred_confidence_rubric.py 进一步把这条契约焊死:断言默认值 ≠ 0.5、默认值 ∈ 量表集合,并扫描extract.py、symbol_resolution.py、extractors/engine.py、extractors/resolution.py四个 AST 发射点,确保没有任何模块硬编码量表外的字面量(如历史上的 0.8 和 0.5)。也就是说,LLM 子代理和确定性 AST 抽取器被要求遵守同一把评分尺,测试保证两者不会漂移。
四、节点 ID 确定性规则:与 AST 抽取器逐字节对齐
节点 ID 规则是整份规范中最长、最严苛的一段,因为ID 是 LLM 片段与 AST 片段在合并时唯一能拼到一起的键。规则要点:
- 小写,只允许
[a-z0-9_],无点、无斜杠; - 格式
{stem}_{entity}:stem是完整的仓库相对路径去掉扩展名,保留每一级路径段、各段小写并把非字母数字替换为_后拼接;entity是符号名做同样归一化; - 顶层文件(无父目录,如
setup.py)只用文件名字干:setup_my_func; - 禁止追加 chunk 号、序号或任何后缀(不允许
_c1、_c2、_chunk2)。ID 必须只由 label 确定性产生——同一实体无论落在哪个 chunk 处理,必须产出同一 ID。
规范给出四个 worked example(测试会逐条解析并验证它们,见下文):
| 文件 + 符号 | 生成的 ID |
|---|---|
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 |
规范同时锁死了两个错误形态作为反例:只用文件名(session_validatetoken)或只带直接父目录(auth_session_validatetoken)都会与 AST 抽取器产出的 ID 不一致,从而制造"孤儿幽灵重复节点"。
4.1 为什么必须是"完整仓库相对路径"
CHANGELOG.md 记录了这次演进:早期版本 ID 的 stem 只取"直接父目录 + 文件名",导致不同目录下的同名文件碰撞成同一个"最后写入者赢"的节点并静默丢图内容(docs/v1/api/README.md与docs/v2/api/README.md都会坍缩成api_readme)。修复后 stem 变为完整仓库相对路径(docs_v1_api_readmevsdocs_v2_api_readme),并且 AST 抽取器、LLM 系统提示、本规范文件与两处手写的 stem 辅助函数全部对齐到同一条规则——正是为了消灭 #1509 那类"AST 与 LLM 两套 ID 各说各话"的幽灵重复。规范还给了迁移建议:若项目是在旧的"直接父目录"格式下构建的,用户应运行graphify extract --force干净重建。
4.2 源码层面的三方对齐与漂移守卫
graphify/ids.py 是节点 ID 归一化的单一事实源,模块 docstring 开宗明义:三个独立的 ID 生产者必须达成一致,否则同一个体会被裂解成互不相连的幽灵节点——① AST 抽取器(extract._make_id),② 语义子代理(LLM,遵循本规范),③ 图构建器(build._normalize_id,当 LLM 输出的 ID 标点或大小写略有出入时重对齐边端点)。
normalize_id(graphify/ids.py)的实现细节值得注意:先对casefold+ NFKC迭代到不动点(两者不可交换,单趟不够,例如土耳其语İslemYap会产生含组合字符 U+0307 的中间结果),再把[^\w]+连续段替换为单个下划线(re.UNICODE保证 CJK/西里尔/阿拉伯字母存活)、折叠重复下划线、去首尾下划线,并保证幂等。make_id(graphify/ids.py)则把各段拼合后走同一归一化,与构建器产出完全相同。
更妙的是,仓库为这份手写散文设置了自动化漂移守卫:tests/test_extraction_spec_ids.py 用正则`([^`]+)`\s*\+\s*`([^`]+)`\s*→\s*`([^`]+)`从所有宿主的extraction-spec.md(含graphify/skills/与tools/skillgen/fragments/)中解析出每一个 worked example,然后调用真实生产函数_make_id(_file_stem(Path(path)), entity)复现并断言一致(tests/test_extraction_spec_ids.py)。这意味着规范示例被改成错误值、或 ID 函数改动导致文档示例失效,两种漂移都会让 CI 失败。测试还额外锁定反例:_make_id("session", "ValidateToken")(仅文件名)与_make_id("auth", "session", "ValidateToken")(仅直接父目录)都必须 ≠ 正确值(tests/test_extraction_spec_ids.py),确保"警告过错的形态"本身不会过期。
五、source_file 逐字路径与 CHUNK_PATH 写入:增量更新的最后一道细节
5.1 source_file 必须逐字照抄 FILE_LIST
规范要求每个节点、每条边、每个超边的source_file都设置为"起源文件在 FILE_LIST 中出现时的路径"——逐字、绝对;不得缩短为 basename、不得重新相对化、不得剥离任何目录前缀、不得改动分隔符(引擎会在下游归一化分隔符并相对化到构建根)。
这条看似苛刻的约束服务于增量更新:全量构建与graphify extract --update必须落在同一套节点键基准上,build_merge的replace-on-re-extract(重抽取时替换既有节点而非累积重复)才能匹配到已有节点。CHANGELOG.md 中的 #1366 正是这条契约的实战注脚:曾因--update运行中source_file基准漂移,变更文件的节点被误判为"已删除文件"的残留而遭剪除;修复方案就是"全量构建也传root=给build_from_json,且抽取规范把source_file钉死在逐字路径上",使全量构建与增量更新永不漂移。
5.2 CHUNK_PATH:必须用 Write 工具写到绝对路径
提示词结尾规定:子代理用 Write 工具把 JSON 写到确切的绝对路径CHUNK_PATH,并解释了原因——相对路径会被 Write 相对一个未定义的 cwd 解析,文件会被静默丢失。skill-agents.md 给出了主代理侧的配套动作:派单前PROJECT_ROOT=$(pwd),为 chunk N 派生CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"。Step B3 的收集逻辑也把"chunk 文件是否落盘"当作子代理成功的唯一信号:文件缺失通常意味着子代理被派成了只读类型,此时应打印警告提示改用 general-purpose 代理,而不是静默跳过;若超过半数 chunk 失败则整体停摆并要求重跑。
5.3 输出 Schema 全貌
规范给出的完整 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 }注意两点工程细节:tokens在子代理输出中恒为占位 0,真实值由主代理从 Agent 工具结果的usage字段读回、合并前写回 chunk JSON(skill-agents.md 的 Step B3 给出了合并脚本);relation枚举中rationale_for与"rationale 存属性不建节点"并存——前者用于显式表达"A 是 B 的理由"这类关系边,后者约束节点建模方式,二者不冲突。
六、提示词版本化:规范文件本身就是缓存命名空间
这份提示词还有第二个、隐藏的身份:语义缓存的归属凭证。skill-agents.md 的 Step B0 要求把SPEC_PATH(本规范文件的绝对路径)同时传给缓存读取check_semantic_cache与写入save_semantic_cache。graphify/cache.py 的prompt_fingerprint对提示词文本做 CRLF→LF 归一化、逐行 rstrip、去尾部空白后取 sha256 前缀,作为缓存命名空间cache/semantic/p{fingerprint}/。这带来两个可验证的性质(源自 #1939):
- 升级 graphify改了提示词:旧提示词产出的缓存条目全部失配,被重新抽取而非静默重放——此前曾因缓存键只有
sha256(内容+路径)而缺少提示词分量,导致"跑完返回 0、cost.json 看起来便宜、图里却混着两代提示词的抽取结果"; - 升级未触碰提示词:缓存条目跨版本存活,不浪费重新计费。
而 CRLF 归一化解决的是 Windows 检出与 LF 写缓存之间"同一份规范、两个指纹"的问题——否则每次 Windows 运行都会伪装成提示词变更而触发全量重抽。_resolve_prompt_fp(graphify/cache.py)还刻意把"提示词文本"与"含提示词的文件路径"设计为两个独立参数,因为把路径字符串本身当文本去哈希会得到"稳定、可信、但什么都没跟踪到"的错误指纹——对缓存而言,静默错配比直接失败更危险;不可读的规范文件会降级到无版本命名空间并显式警告,而不是让一次抽取因缓存问题而中止。
七、两条抽取路径的契约收敛
graphify 存在第二条原生后端路径:graphify extract --backend <gemini|claude|claude-cli|openai|kimi|…>直接由 Python 调用 LLM,其系统提示是 graphify/llm.py 的_EXTRACTION_SYSTEM。对比两份提示词可以看到同一契约的多处镜像:同样的"只输出合法 JSON"、同样的三级置信度定义、同样的六值file_type枚举、同样的"rationale 存属性不建节点"、同样的超边规则(含"每 chunk 最多 3 条")、同样的节点 ID 规则与边方向规则(calls的 source 恒为调用方)。CHANGELOG.md 记录了两者曾漂移的代价:原生提示词只给了"hyperedges":[]的空 Schema 示例、从未解释什么是超边,导致所有原生后端静默产出零超边,而 skill 路径正常产出——修复方式就是把"3 个及以上节点共同参与"的指令与填充后的 Schema 示例同步进原生提示词,使两条路径对同一语料给出一致的超边行为。
从源码结构看,这种"一份契约、两条执行路径、测试保证不漂移"的模式(规范文件 ↔ 原生提示词 ↔ AST 抽取器 ↔ 构建器归一化 ↔ 缓存命名空间)正是 graphify 能把 LLM 的不确定性关进笼子、让图谱结果可复现、可增量、可审计的核心机制。
八、小结
extraction-spec.md 表面上是一份子代理提示词,实质是 graphify 语义抽取管线的输出契约,其每条规则都对应一类真实事故:
- 三级置信度 +
AMBIGUOUS不省略 → 不确定性显式化,进入图而非消失; - 离散评分量表(禁 0.5)→ 对抗模型对连续区间的坍缩行为,并被 graphify/export.py 与 tests/test_inferred_confidence_rubric.py 在 AST 侧同步执行;
- 完整仓库相对路径的节点 ID → 消灭同名文件碰撞与 AST/LLM 幽灵重复,由 graphify/ids.py 实现、tests/test_extraction_spec_ids.py 用正则解析规范原文逐条验证;
source_file逐字路径 → 保证全量构建与--update增量运行共用同一节点键基准,支撑 replace-on-re-extract;CHUNK_PATH绝对路径落盘 → 以"文件存在"作为子代理成功的唯一信号,杜绝静默丢失;- 提示词指纹命名空间 → 提示词一变缓存即失效,未变则跨版本复用。
如果你在设计自己的"AST + LLM 子代理"混合抽取管线,这份规范几乎是可直接抄录的契约模板:把输出 Schema、ID 规则、评分量表、路径纪律与版本归属全部写进提示词,再用测试把提示词散文和代码实现对账——graphify 的做法证明,对 LLM 的输出约束,必须精确到"逐字节"的程度。
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考