首页
/ graphify 语义抽取规范(extraction-spec):LLM 子代理如何生成可确定性合并的知识图谱碎片

graphify 语义抽取规范(extraction-spec):LLM 子代理如何生成可确定性合并的知识图谱碎片

2026-09-06 13:21:39作者:伍霜盼Ellen

graphify 在 Part A 用本地确定性 AST 解析代码后,还会对文档、论文、图片等"语义内容"启动第二条抽取管线:把每个文件分块(chunk),为每个块派发一个 LLM 子代理,并让它严格按照 references/extraction-spec.md 中规定的提示词模板、JSON Schema、节点 ID 规范和置信度评分标尺输出图谱碎片。本文以 graphify/skills/claude/references/extraction-spec.md 这份规范文件为主体,逐条拆解其中的抽取规则、评分标尺与 ID 规范,并结合 graphify/validate.pygraphify/ids.pygraphify/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_LISTCHUNK_NUMTOTAL_CHUNKSDEEP_MODECHUNK_PATH

各平台的 skill 文件(如 graphify/skill-claude.md、graphify/skill.md)都引用同一份 spec:

See references/extraction-spec.md for the exact subagent prompt (JSON schema, node-ID rules, confidence rubric, frontmatter, hyperedge, and vision rules).

"逐字传递 + 有限占位符"是这份规范可被机器验证的前提——如果提示词允许自由发挥,后面所有关于 ID 确定性、缓存命中和合并去重的机制都会失效。

主代理在派发子代理前的准备工作(见 graphify/skill.md Step B0–B3):

  1. Step B0 查缓存:调用 graphify.cache.check_semantic_cache(all_files, root=..., prompt_file='SPEC_PATH'),把 spec 文件本身作为缓存条目的归属依据(详见第七节);
  2. Step B1 分块:未命中缓存的文件按每块 20–25 个文件切分;每张图片独占一个块(视觉任务需要独立上下文);同目录文件尽量同块,提高跨文件关系抽取率;
  3. Step B2 并行派发:所有子代理必须在同一条消息里一次性全部派发才能并行;CHUNK_PATH 必须是绝对路径,形如 ${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json
  4. 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 rationale attribute on the relevant concept node — do NOT create a separate rationale node or fragment node.

设计决策的"为什么"(rationale)是文档中极有价值的语义,但规范强制要求把它作为节点的属性挂到相关概念节点上,而不是单独建节点——只有"本身是命名实体或概念"的东西才配拥有节点。

同时给出 file_type六值枚举硬约束

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.

这个"会被拒绝"不是警告,而是有对应实现的: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_to edge 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.pyattach_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 规范:确定性是整个合并机制的地基

这是规范中最长、约束最密的一段,核心规则可归纳为:

  1. 字符集:小写,仅 [a-z0-9_],无点、无斜杠;
  2. 格式{stem}_{entity},其中 stem 是完整的仓库相对路径去掉扩展名,保留所有路径段、各段小写且非字母数字字符替换为 _;entity 是同样归一化后的符号名;
  3. 必须使用全部目录层级,不能只用文件名或直接父目录——这保证不同目录下同名文件不会撞 ID;
  4. 禁止追加块号或序号后缀_c1_chunk2 之类一律禁止);
  5. 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 开门见山:

  1. The AST extractor (extract._make_id) — deterministic, per-language.
  2. The semantic subagents (LLM) — follow the node-ID spec in the skill prompt.
  3. 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_urlcaptured_atauthorcontributor 必须复制到该文件的每个节点上。这让图谱能回答"这个概念来自哪篇文章、谁写的、何时抓取"。

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)共八种:callsimplementsreferencescitesconceptually_related_toshares_data_withsemantically_similar_torationale_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.pyVALID_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 增量 --updatebuild_merge 的 replace-on-re-extract 能匹配旧节点而非累积重复
绝对路径 CHUNK_PATH 落盘 块文件作为唯一成功信号;缺失即判定子代理类型错误,防止静默丢结果
spec 文件内容本身 graphify/cache.py 的 prompt fingerprint:spec 变更即缓存失效重抽,未变即命中

对使用者而言,这意味着:纯代码仓库永远不触碰这份规范(跳过 Part B、零 LLM 成本);混合语料仓库的语义质量上限由这份 spec 决定;而若你要审计或复现某次构建中的语义边,graphify-out/.graphify_chunk_*.json 就是各子代理按此契约产出的原始证据。

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