graphify 语义抽取规范(extraction-spec):LLM 子代理如何生成可确定性合并的知识图谱碎片
graphify 在 Part A 用本地确定性 AST 解析代码后,还会对文档、论文、图片等"语义内容"启动第二条抽取管线:把每个文件分块(chunk),为每个块派发一个 LLM 子代理,并让它严格按照 references/extraction-spec.md 中规定的提示词模板、JSON Schema、节点 ID 规范和置信度评分标尺输出图谱碎片。本文以 graphify/skills/claude/references/extraction-spec.md 这份规范文件为主体,逐条拆解其中的抽取规则、评分标尺与 ID 规范,并结合 graphify/validate.py、graphify/ids.py、graphify/cache.py 等源码,说明每一条提示词约束背后对应的工程机制——读完后你能理解:为什么一份"给 LLM 的提示词"能产出可增量合并、可去重、可验证的图谱数据,以及如果你要自定义或审计这条管线,哪些地方是不能碰的硬约束。
一、规范文件的定位:语义管线的第二道防线
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.
也就是说,该文件是 Step 3 Part B(语义抽取阶段)才加载的子代理提示词。graphify 的完整构建分为两条线:
- Part A(AST 管线):本地确定性解析器处理纯代码文件,产出 imports、calls 等结构性边——这部分是确定性的,不经过 LLM;
- Part B(语义管线):只有当语料中至少存在一个 doc、paper 或 image 块时才触发,主代理把本规范文件的内容**逐字(verbatim)**传给每个语义子代理,仅替换五个占位符:
FILE_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE、CHUNK_PATH。
各平台的 skill 文件(如 graphify/skill-claude.md、graphify/skill.md)都引用同一份 spec:
See
references/extraction-spec.mdfor the exact subagent prompt (JSON schema, node-ID rules, confidence rubric, frontmatter, hyperedge, and vision rules).
"逐字传递 + 有限占位符"是这份规范可被机器验证的前提——如果提示词允许自由发挥,后面所有关于 ID 确定性、缓存命中和合并去重的机制都会失效。
主代理在派发子代理前的准备工作(见 graphify/skill.md Step B0–B3):
- Step B0 查缓存:调用
graphify.cache.check_semantic_cache(all_files, root=..., prompt_file='SPEC_PATH'),把 spec 文件本身作为缓存条目的归属依据(详见第七节); - Step B1 分块:未命中缓存的文件按每块 20–25 个文件切分;每张图片独占一个块(视觉任务需要独立上下文);同目录文件尽量同块,提高跨文件关系抽取率;
- Step B2 并行派发:所有子代理必须在同一条消息里一次性全部派发才能并行;
CHUNK_PATH必须是绝对路径,形如${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json; - Step B3 收集合并:以"块文件落在磁盘上"为成功信号,合并所有
.graphify_chunk_*.json为.graphify_semantic_new.json。
二、提示词模板与三条置信度总规则
规范内嵌的提示词模板要求子代理扮演 "graphify extraction subagent",读取 FILE_LIST 列出的文件,只输出符合 Schema 的合法 JSON——无解释、无 markdown 围栏、无前言。核心指令如下:
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
接下来是三条置信度(confidence)分层总规则,它们决定了每条边在后续合并与查询中的权重:
| 层级 | 含义 | 处置要求 |
|---|---|---|
EXTRACTED |
关系在源文件中显式存在(import、调用、引用、"see §3.2" 这类显式指引) | 直接输出,confidence_score = 1.0 |
INFERRED |
合理推断(共享数据结构、隐含依赖) | 必须从离散评分集合中选值(见第五节) |
AMBIGUOUS |
不确定 | 必须保留并标记待审,不得省略 |
"AMBIGUOUS 不得省略"是一个值得注意的设计:图谱构建方宁愿保留低置信度边供人工审查,也不愿丢失可能的关系。这与 graphify/validate.py 的校验逻辑一致——VALID_CONFIDENCES = {"EXTRACTED", "INFERRED", "AMBIGUOUS"},出现其他取值(如小写 inferred)会直接被校验器拒绝。
DEEP_MODE 占位符对应 graphify 的 --mode deep 参数。规范对 deep 模式的指令是:"be aggressive with INFERRED edges - indirect deps, shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting." 即深模式下鼓励更激进地推断间接依赖、共享假设与潜在耦合,但所有不确定项必须落到 AMBIGUOUS 而不是丢弃。
三、按文件类型分派的抽取规则
规范对不同 FILE_LIST 来源施加了完全不同的抽取策略:
3.1 代码文件:只补 AST 找不到的语义边
Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns). Do not re-extract imports - AST already has those.
这条规则明确了 AST 管线与 LLM 管线的职责边界:import 边已经由 Part A 确定性产出,LLM 再抽一遍只会引入重复甚至冲突。LLM 只负责 AST 能力之外的语义边:跨文件的调用关系、共享数据、架构模式。
两条硬性约束:
calls边方向:source 必须是调用方(caller),target 必须是被调方(callee),"Never reverse this direction";calls边语言边界:调用边必须留在单一语言内——Python 函数不能calls一个 JS/TS/Go/Rust/Java 符号,反之亦然。规范直言跨语言调用边是 "phantom artifacts"(幻影产物),永远不得输出。这一条针对的是 LLM 常见的"同名函数臆测调用"错误:不同语言中恰好同名并不能构成调用关系。
3.2 文档/论文文件:概念、实体与 rationale 的存放位置
Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made, trade-offs, design intent): store as a
rationaleattribute on the relevant concept node — do NOT create a separate rationale node or fragment node.
设计决策的"为什么"(rationale)是文档中极有价值的语义,但规范强制要求把它作为节点的属性挂到相关概念节点上,而不是单独建节点——只有"本身是命名实体或概念"的东西才配拥有节点。
同时给出 file_type 的六值枚举硬约束:
file_typeMUST be one of exactly these six values:code,document,paper,image,rationale,concept. Any other value is invalid and will be rejected.
这个"会被拒绝"不是警告,而是有对应实现的:graphify/validate.py 定义了
VALID_FILE_TYPES = {"code", "document", "paper", "image", "rationale", "concept"}
validate_extraction() 会逐节点检查 file_type,非法值产生形如 invalid file_type 'xxx' - must be one of [...] 的错误并整体拒绝该 JSON。
3.3 图片文件:理解"图是什么",而不是 OCR
Image files: use vision to understand what the image IS - do not just OCR.
规范按图片类别给出了具体的抽取目标:
| 图片类别 | 应抽取的内容 |
|---|---|
| UI 截图 | 布局模式、设计决策、关键元素、用途 |
| 图表(chart) | 指标、趋势/洞见、数据来源 |
| 推文/帖子 | 主张作为节点、作者、提到的概念 |
| 示意图(diagram) | 组件与连接 |
| 研究图(figure) | 它证明了什么、方法、结果 |
| 手写/白板 | 想法与箭头,不确定读法标记 AMBIGUOUS |
这解释了 Step B1 中"每张图片独占一个块"的原因:视觉理解需要独立、完整的上下文窗口,且手写内容的置信度必须降级处理。
四、语义相似度边与超边:两种类二元关系的扩展
4.1 semantically_similar_to 边
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_toedge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95).
规范给出的三个示例场景:
- 两个函数都做用户输入校验但互不调用;
- 代码中的类与论文中的概念描述同一算法;
- 两种错误类型以不同方式处理同一失效模式。
同时给出抑制条件:"Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things." 即相似度必须是非显然的、跨切面的,避免把"两个都叫 helper 的函数"连成一片噪声。
4.2 超边(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 ... Use sparingly — only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk.
规范列出的典型超边场景:
- 所有实现同一协议/接口的类;
- 一个认证流程中的所有函数(即使它们并非两两互相调用);
- 论文章节中构成同一连贯思想的一组概念。
"每块最多 3 条超边"是防止 LLM 过度泛化的硬上限。超边与节点/边一样受 Schema 约束,其 relation 取值限定为 participate_in | implement | form。在下游,graphify/export.py 的 attach_hyperedges() 会把超边存入图对象的元数据(G.graph["hyperedges"]),并在增量更新中按 id 去重。
五、置信度评分标尺:为什么禁止 0.5 与连续区间
规范对 confidence_score 的要求是全篇最"反直觉"也最工程化的一段:
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; ... If no value above fits, mark the edge AMBIGUOUS rather than picking 0.4 or below.
- AMBIGUOUS edges: 0.1-0.3
规范自己给出了采用离散集合而非连续区间的理由:生产环境中 LLM 输出的置信度呈双峰分布(>50% 挤在 0.5,>40% 挤在 0.85+),说明"区间指导"实际被模型坍缩成了二元选择——那么不如直接给出离散标尺。每个分数档都有明确的证据语义(见上表),"套不进任何一档"的正确做法是标 AMBIGUOUS,而不是给 0.4 或更低的分。
这个标尺不只是写给 LLM 看的,AST 管线同样以它为契约。例如 graphify/extract.py 中发射间接调用边时特意选择 0.85 而非 0.8:
# 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.
"confidence_score": 0.85,
同理,graphify/export.py 为缺失分数的边提供了兜底默认值:
_CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.55, "AMBIGUOUS": 0.2}
注释明确提到:INFERRED 的旧默认值 0.5 正是 extraction-spec 用文字禁止的值("never use 0.5 as a default"),现改为标尺最低档 0.55——"缺失分数代表强度证据缺失,诚实的兜底是标尺允许的最弱值,而不是看起来像抛硬币的中点值"。规范、AST 发射点、导出兜底三处对同一套标尺保持一致,这正是"提示词即契约"的体现。
六、节点 ID 规范:确定性是整个合并机制的地基
这是规范中最长、约束最密的一段,核心规则可归纳为:
- 字符集:小写,仅
[a-z0-9_],无点、无斜杠; - 格式:
{stem}_{entity},其中 stem 是完整的仓库相对路径去掉扩展名,保留所有路径段、各段小写且非字母数字字符替换为_;entity 是同样归一化后的符号名; - 必须使用全部目录层级,不能只用文件名或直接父目录——这保证不同目录下同名文件不会撞 ID;
- 禁止追加块号或序号后缀(
_c1、_chunk2之类一律禁止); - ID 必须由 label 确定性生成——同一实体无论被哪个块处理,必须产生同一 ID。
规范给出了四个完整示例:
| 路径 + 符号 | 生成的 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 |
顶层文件(无父目录,如 setup.py)只用文件名主干:setup_my_func。
规范同时警告了两类历史错误形态:只用文件名(session_validatetoken)和只用直接父目录(auth_session_validatetoken)都会产生"orphan ghost-duplicate nodes"(孤儿幽灵重复节点);如果项目是在旧的 immediate-parent 格式下构建的,应运行 graphify extract --force 干净重建。
6.1 源码侧的三方一致性
这套 ID 规范之所以如此严苛,是因为三个独立的生产者必须对同一实体生成相同 ID,否则一个符号会被图裂成互不相连的幽灵节点。graphify/ids.py 的模块 docstring 开门见山:
- The AST extractor (
extract._make_id) — deterministic, per-language.- The semantic subagents (LLM) — follow the node-ID spec in the skill prompt.
- The graph builder (
build._normalize_id) — reconciles edge endpoints when the LLM emits IDs with slightly different punctuation or casing than the AST.
该模块把归一化配方集中在单处:迭代执行 casefold + NFKC 直至不动点(处理土耳其语 İ 这类 casefold 会展开成"基础字母 + 组合标记"的 Unicode 陷阱),再用 re.UNICODE 的 [^\w]+ 过滤非词字符(CJK/西里尔/阿拉伯/带音符拉丁字母得以保留),折叠重复下划线。docstring 中列举的 #811(Unicode 坍缩)、#550(同名文件冲突)、#1033(AST 与 LLM 文件节点不匹配)、#2614(土耳其语标识符)等问题,全部源于历史上"ID 配方被复制粘贴、靠镜像 docstring 保持同步"的架构——现在三个生产者共享同一份配方。
6.2 用测试锁死"文档示例 = 代码行为"
更巧妙的是 tests/test_extraction_spec_ids.py:这是一个漂移守卫,它用正则 `path` + `entity` → `id` 直接从每一份随包发布的 extraction-spec.md(包括 skillgen 渲染出的各平台副本)中解析出所有节点 ID 示例,然后断言真实生产函数 _make_id(_file_stem(path), entity) 对每个示例复现出文档承诺的值:
_EXAMPLE_RE = re.compile(r"`([^`]+)`\s*\+\s*`([^`]+)`\s*→\s*`([^`]+)`")
...
got = _ast_symbol_id(path, entity)
assert got == expected, (
f"node-ID spec drift: spec says `{path}` + `{entity}` → `{expected}`, but ..."
)
测试还锁死了规范里的"反例":_make_id("session", "ValidateToken")(只用文件名)与 _make_id("auth", "session", "ValidateToken")(只用直接父目录)必须都不等于正确值 src_auth_session_validatetoken。这意味着:spec 文档的示例被改错、或 ID 函数被改动导致文档示例失效,CI 都会失败——"提示词文档"与"生产代码"被测试强制绑定,二者无法静默漂移。
七、YAML frontmatter 继承与 source_file 逐字匹配规则
两条关于"数据出处"的规则保证增量更新可以精确对账:
Frontmatter 继承:如果源文件带 YAML frontmatter(--- ... ---),其中的 source_url、captured_at、author、contributor 必须复制到该文件的每个节点上。这让图谱能回答"这个概念来自哪篇文章、谁写的、何时抓取"。
source_file 逐字规则(规范原文强调程度最高的一条):
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.
理由写在括号里:保持"完整构建"与"增量 --update"在同一基准上,这样 build_merge 的 replace-on-re-extract 才能匹配已有节点,而不是累积重复节点。换言之,LLM 如果"好心"把绝对路径改写成相对路径或截成文件名,增量合并时就会找不到旧节点,导致同一个概念在图里长出两个分身。
对应的 Schema 中,每个 node、edge、hyperedge 都携带 source_file(以及 source_location 可选定位),而 graphify/validate.py 将其列为节点与边的必填字段之一:
REQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"}
REQUIRED_EDGE_FIELDS = {"source", "target", "relation", "confidence", "source_file"}
校验器还会检查每条边的 source/target 是否都能在本文件的节点集中找到,悬空端点(dangling endpoint)会报错——这正好呼应规范"AMBIGUOUS 不省略、但端点必须存在"的组合要求。
八、输出 JSON 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
}
边关系(relation)共八种:calls、implements、references、cites、conceptually_related_to、shares_data_with、semantically_similar_to、rationale_for——分别对应调用、接口实现、引用、论文引用、概念关联、共享数据、语义相似、决策理由,覆盖了代码边与文档边的全部语义空间。
末尾的 input_tokens / output_tokens 在块文件中是占位零值:主代理在 Step B3 合并前,会从 Agent 工具结果的 usage 字段读出真实 token 数回写到块 JSON 中,再汇总进 .graphify_semantic_new.json 用于成本统计。
九、落盘规则:为什么 CHUNK_PATH 必须是绝对路径
提示词模板的最后一句:
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 工具链的经典陷阱:子代理的 Write 工具若拿相对路径,会在一个未定义的 cwd 上解析,文件被写到你找不到的地方,且静默丢失。规范因此强制主代理在派发前用 PROJECT_ROOT=$(pwd) 推导绝对路径(${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json),子代理只认这一个路径。
落盘文件同时充当成功信号:Step B3 只认磁盘上的 .graphify_chunk_NN.json,不认子代理的口头回复。skill 文件据此定义了明确的失败处置——块文件缺失时提示"可能是以只读 Explore 型子代理派发的,请改用 general-purpose 重跑",而不是静默跳过;超过一半的块失败则整体中止并要求用户重跑。
十、规范文件即缓存版本:prompt fingerprint 机制
这份 spec 还有一个隐藏身份:它是语义缓存的版本锚点。graphify/cache.py 中的 prompt_fingerprint() 会对提示词文件内容做 SHA-256(取前 12 位十六进制):
def prompt_fingerprint(prompt: "str | Path") -> str:
"""...the skill path's references/extraction-spec.md...
Line endings and trailing whitespace are normalized before hashing: the same
spec file checked out with CRLF on Windows must not fingerprint differently
from the LF checkout..."""
缓存条目按(内容哈希, 提示词指纹)归属,于是:
- graphify 升级修改了 spec 提示词 → 旧提示词产出的缓存条目自动失效,文件被重新抽取而非回放;
- 提示词未变 → 缓存继续命中,不重复计费 LLM;
- Windows 的 CRLF 检出与 LF 检出在哈希前先归一化换行与行尾空白,避免"每次 Windows 运行都像提示词变了"而全额重抽(对应问题 #1939)。
skill 侧 Step B0 的 check_semantic_cache(..., prompt_file='SPEC_PATH') 正是把本文件路径传进去完成归属的。也就是说,extraction-spec.md 的任何实质性编辑都会触发一次性的全量语义重抽取——这是修改该文件前必须知道的成本。
十一、小结:一份提示词为何能支撑增量、可去重的图谱构建
回看整份 extraction-spec.md,它表面是"给 LLM 的提示词",实际是 graphify 语义管线的输出契约,每条规则都对应一个下游机制:
| 规范约束 | 对应工程机制 |
|---|---|
file_type 六值枚举、confidence 三档 |
graphify/validate.py 的 VALID_FILE_TYPES / VALID_CONFIDENCES 校验,非法值整体拒绝 |
| 离散置信度标尺(禁用 0.5) | graphify/extract.py 的 AST 发射点与 graphify/export.py 的兜底默认值共用同一标尺 |
| 全路径 stem 节点 ID + 无块号后缀 + 确定性 | graphify/ids.py 三方共享的 normalize_id / make_id,保证 AST、LLM、builder 不产生幽灵重复节点 |
| 文档示例与代码同步 | tests/test_extraction_spec_ids.py 解析 spec 示例并断言生产函数可复现,CI 锁死漂移 |
source_file 逐字匹配 FILE_LIST |
增量 --update 时 build_merge 的 replace-on-re-extract 能匹配旧节点而非累积重复 |
绝对路径 CHUNK_PATH 落盘 |
块文件作为唯一成功信号;缺失即判定子代理类型错误,防止静默丢结果 |
| spec 文件内容本身 | graphify/cache.py 的 prompt fingerprint:spec 变更即缓存失效重抽,未变即命中 |
对使用者而言,这意味着:纯代码仓库永远不触碰这份规范(跳过 Part B、零 LLM 成本);混合语料仓库的语义质量上限由这份 spec 决定;而若你要审计或复现某次构建中的语义边,graphify-out/.graphify_chunk_*.json 就是各子代理按此契约产出的原始证据。
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