graphify 语义提取规范(extraction-spec)全解析:子代理如何把文档与图片转换成可查询的知识图谱
graphify 把代码库连同其中的文档、SQL 模式、配置和 PDF 一起加工成可查询的知识图谱:代码由本地确定性 AST 解析完成,而文档、论文、图片这类“语义密集”语料则由 LLM 子代理抽取图谱片段。tools/skillgen/fragments/references/shared/extraction-spec.md 正是这一段流水线的核心协议文档——它定义了语义抽取子代理必须遵守的 JSON Schema、节点 ID 规则、可信度分级与评分标尺、超边与语义相似边的使用边界,以及决定增量更新能否正确去重的 source_file 锚定规则。读完本文,你将掌握 graphify 语义抽取的完整数据契约,理解每一条规则背后的工程动机(幽灵重复节点、跨语言伪调用、置信度塌缩、缓存失效),并能据此对接自己的 LLM 抽取管道或二次开发 /graphify 技能。
一、这份规范在 graphify 技能流水线中的位置
在 graphify 的架构里,抽取被刻意拆成两条互不重叠的路径:
- 代码文件走“本地确定性 AST 引擎”(见
graphify/extractors/与 extract.py),不需要任何 API Key、结果可复现; - 文档 / 论文 / 图片走 LLM 语义抽取,由子代理(subagent)按本规范产出知识图谱片段。
extraction-spec.md 的头部写明了它的装载条件:“在语料中至少有一个 doc、paper 或 image 分块时,于 Step 3 Part B 装载;纯代码语料会跳过 Part B,永远不读取本文件。” 这与 llm.py 中注释相互印证——“语义(LLM)抽取运行在文档/论文/图片上,代码文件由确定性 AST 引擎处理、永远不会到达模型”。
这份 fragment 同时存在两个变体:
| 变体 | 文件 | 用途 |
|---|---|---|
| verbose(完整版) | tools/skillgen/fragments/references/shared/extraction-spec.md | 多数平台的默认抽取提示词正文 |
| compact(精简版) | tools/skillgen/fragments/references/shared/extraction-spec-compact.md | 上下文预算更紧的平台(如返回 JSON inline 的 Codex)选用 |
选择逻辑沉淀在技能生成器里:gen.py 用 "verbose": "references/shared/extraction-spec.md" 与 "compact": "references/shared/extraction-spec-compact.md" 两张映射表完成引用解析,而 platforms.toml 中 extraction verbose | compact 一行正是按平台挑选正文的开关。渲染完成的技能包会把这些 reference 文件随附在每个平台的 references/ 目录下,例如 graphify/skills/agents/references/extraction-spec.md;tools/skillgen/expected/graphify__skill.md 中的“See references/extraction-spec.md for the exact subagent prompt”段落,就是该引用在最终技能文档里的落点。
二、提示词的整体契约:任务、输入、输出
规范以“逐字下发给每个语义子代理”的方式呈现,运行时需要替换五个占位符:
| 占位符 | 含义 |
|---|---|
FILE_LIST |
本分块负责的文件清单(绝对路径) |
CHUNK_NUM |
当前分块序号 |
TOTAL_CHUNKS |
总分块数 |
DEEP_MODE |
是否启用 --mode deep 深度模式 |
CHUNK_PATH |
结果 JSON 必须写入的绝对磁盘路径 |
子代理的任务一句话概括:读取分发给它的文件,抽取一个知识图谱片段(knowledge graph fragment),并且只输出符合下述 Schema 的合法 JSON——“不要解释、不要 markdown 围栏、不要前言”。 输出 JSON 的顶层包含 nodes、edges、hyperedges 三个数组,以及 input_tokens、output_tokens 两个用量字段(供成本统计与缓存使用)。
结果落到磁盘的规则同样严格:必须使用 Write 工具写到 CHUNK_PATH 这个“绝对路径”——不允许相对路径,因为“Write 会基于未定义的 cwd 解析相对路径,文件会被静默丢失”。
实现侧印证:在直接调用 LLM 的 Python API 路径中,llm.py 的
_EXTRACTION_SYSTEM常量与这份提示词几乎同构,同样规定“Output ONLY valid JSON——no explanation, no markdown fences, no preamble”,并附上同一份 Schema 字符串;_extraction_system(deep=...)则按是否深度模式拼接系统提示词。
三、边可信度的三分类词汇表
每条关系(edge)都必须携带一个 confidence 字段,取值只能是下列三档之一,任何其他值都不合法:
| 取值 | 语义 | 判定依据 |
|---|---|---|
EXTRACTED |
源文本中显式存在的关系 | import、调用、引用、citation、“see §3.2”这类字面证据 |
INFERRED |
合理的推断 | 共享数据结构、隐含依赖等不直接成文的关系 |
AMBIGUOUS |
不确定——必须标记出来供人工复核,不得省略 | 无法确证但又不该丢掉的候选关系 |
“不确定就标记、不要删除”是贯穿全篇的原则,它直接对冲了 LLM 抽取最常见的两种失败模式:过度自信的幻觉边与因为没把握而漏报。实现侧可以佐证这个词汇表是被校验过的:llm.py 的注释明确写着“validated vocabulary({EXTRACTED, INFERRED, AMBIGUOUS},且只出现在边上)”。
四、按文件类型的差异化抽取规则
4.1 代码文件:只补 AST 找不到的语义边
代码文件出现在语义分块中时(例如文档里嵌的代码片段),子代理要提取的是“AST 抓不到的语义关系”,并且严禁重复抽取 import——那些确定性解析器已经拿到了。本规范特别强调:
calls边方向不可逆转:source必须是调用方(发起调用的函数/类),target必须是被调用方;calls边必须停留在单一语言内部:Python 函数不能calls一个 JS/TS/Go/Rust/Java 符号,反之亦然——“跨语言调用边是幽灵伪影(phantom artifacts),永远不要产出”。
这与代码库对“跨语言伪调用”的持久戒备一致:tests/ 下有 test_phantom_cross_package_call.py、test_phantom_external_import.py 等用例,csharp_dispatch.py、ruby_resolution.py、symbol_resolution.py 等模块则承担真正的跨文件/跨语言解析工作。
4.2 文档/论文:概念节点与 rationale 的内联存储
处理 doc/paper 时,抽取目标是命名概念、实体与引用。两条关键约束:
- 设计理由(rationale)不允许建成独立节点。WHY 类信息——为何做这个决定、权衡了什么、设计意图——必须以
rationale属性挂在相关概念节点上,禁止单独造一个 rationale 节点或 fragment 节点; - 只有本身是命名实体/概念的东西才值得建节点。想法、原则、机制、设计模式这类“概念型”东西用
file_type:"rationale"标注。
节点 file_type 全仓库只有且只有六个合法值:code、document、paper、image、rationale、concept,规范原文措辞是“Any other value is invalid and will be rejected”(任何其他取值非法并被拒绝)。
佐证:llm.py 的抽取系统提示词使用几乎相同的措辞约束 rationale——“不要在源文本没有明确给出理由时编造这个属性(不要复述描述)”;analyze.py 在计算弱连通节点时会显式跳过
file_type == "rationale"的节点,说明rationale作为一个节点类型在后续分析中被单独对待。
4.3 图片:用视觉理解“图片是什么”,而不只是 OCR
图片分块要求调用视觉能力理解图像本体,规范按图像类型给出抽取重点:
| 图像类型 | 要抽取的内容 |
|---|---|
| UI 截图 | 布局模式、设计决策、关键元素、用途 |
| 图表 | 指标、趋势/洞察、数据来源 |
| 推文/帖子 | 主张(作为节点)、作者、提到的概念 |
| 架构图 | 组件与连接关系 |
| 科研配图 | 论证了什么、方法、结果 |
| 手写/白板 | 想法与箭头指向,不确定的识别一律标 AMBIGUOUS |
图片支持在实现层有完整对应:llm.py 中 _ImageRef、_backend_supports_vision、_anthropic_content 等函数负责把图片编码进各后端请求,并区分“纯 OCR”与“理解语义”两档视觉用法。
五、置信度评分标尺:confidence_score 的离散打分制
规范用一整段强调了边上的 confidence_score:每条边必填、绝不省略,并且绝不能用 0.5 当默认值。打分规则如下:
| 场景 | 取值 | 依据 |
|---|---|---|
EXTRACTED 边 |
恒为 1.0 | 显式证据,无条件满分 |
INFERRED 边 |
恰好取下列五档之一 | 见下表 |
AMBIGUOUS 边 |
0.1–0.3 | 不确定区段 |
INFERRED 的离散标尺(注意 0.5 被明确排除):
| 分 | 档位 | 证据强度 |
|---|---|---|
| 0.95 | direct structural evidence | 共享数据结构、命名的跨文件引用等直接结构证据 |
| 0.85 | strong inference | 清晰的功能对齐,但无直接符号链接 |
| 0.75 | reasonable inference | 共享问题域 + 相似形态,需要解读 |
| 0.65 | weak inference | 主题相关,但无形态证据 |
| 0.55 | speculative but plausible | 仅表层共现 |
规范还解释了为什么要设计成离散档而非连续区间:“模型对离散标尺的跟随性好于连续区间;生产环境观测到的双峰分布(>50% 落在 0.5、>40% 落在 0.85+)表明区间引导正在被压扁成二值判断。” 这是一条非常典型的 prompt 工程经验——给连续区间,模型会无脑取中值 0.5;给离散菜单,模型反而会认真挑选。因此规则收尾很果断:如果五档都不合适,就把边标记为 AMBIGUOUS,而不是打 0.4 或更低的分。
六、节点 ID 规范:{stem}_{entity} 与确定性
节点 ID 是全篇规则密度最高、踩坑代价最大的一节,值得单独展开:
- 字符集:全部小写,只允许
[a-z0-9_],不允许点号与斜杠; - 格式:
{stem}_{entity}。stem= 去掉扩展名的完整仓库相对路径,路径每一段都保留并以下划线拼接(每段小写、非字母数字字符替换为_);entity= 符号名,按同样规则归一化;
- 必须使用每一级目录,而不是只看直接父目录——这是为了让不同目录下的同名文件保持区分;
- 顶层文件(无父目录,如
setup.py)只使用文件名主干,如setup_my_func。
规范的示例逐条给出了映射:
| 源文件 + 符号 | 规范节点 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 + my_func(顶层) |
setup_my_func |
规范明确给出三条动机:
- 必须与 AST 抽取器生成的 ID 一致。只写文件名(如
session_validatetoken)或只写直接父目录(如auth_session_validatetoken)都会造出孤立的幽灵重复节点; - 如果项目是用旧的“直接父目录”格式构建的,用户需要跑
graphify extract --force干净地重建; - CRITICAL:永远不要在 ID 后追加分块号或序号后缀(不许有
_c1、_c2、_chunk2)。ID 必须能仅凭 label 确定性地推导出来——同一个实体无论在哪个分块里处理,都必须产出同一个 ID。
这条规则的工程含义很深刻:确定性 ID + 全路径 stem 是“跨分块合并去重”和“增量更新”的前提,否则同一个符号在每个分块都会生成一个不同 ID 的节点,图里会累积出一堆残骸。
七、语义相似边与超边:补足“两两成对边”表达不了的关系
7.1 semantically_similar_to:跨结构的概念共鸣
如果本分块内两个概念在没有结构链接(无 import、无调用、无引用)的前提下解决同一个问题或表达同一个想法,则添加 semantically_similar_to 边,标记为 INFERRED,并给一个反映相似程度的 confidence_score(0.6–0.95)。规范给出的示例:
- 两个都做用户输入校验、但从不互相调用的函数;
- 代码里的一个类与论文中描述同一算法的概念;
- 两个用不同方式处理同一失败模式的错误类型。
限定条件非常严格:只有当相似性是“真正的非显然、且横切多个文件”时才加;对明显相似的东西不要加——这条边不是用来制造噪音的。
7.2 超边:3 个及以上节点共享一个模式
当 3 个或更多节点明显共同参与一个“两两成对的边表达不了”的共享概念、流程或模式时,在顶层 hyperedges 数组中加入一条超边。规范示例:
- 实现同一协议/接口的全部类;
- 认证流程中的所有函数(即使它们并不互相调用);
- 论文某一节中构成一个连贯想法的全部概念。
使用要克制——只有成组关系相比两两成对的边确实携带了新信息才建;且每个分块最多 3 条超边。超边字段的 relation 合法值受限为 participate_in、implement、form 三者之一,confidence 只能是 EXTRACTED 或 INFERRED。
实现侧佐证:抽取结果进入图构建阶段后,build.py 会逐条消费
hyperedges;合并多个分块时 build.py 把各分块的nodes/edges/hyperedges拼进一个combined字典;而当超边成员节点在清洗后全部失效时,代码会剔除“单成员超边”并给出“所有超边被移除”的告警(build.py)。
八、YAML frontmatter:元数据向节点的透传
如果文件带 YAML frontmatter(--- ... ---),规则是:把 source_url、captured_at、author、contributor 复制到来自该文件的每一个节点上。这与节点 Schema 中对应的可空字段 source_url、captured_at、author、contributor 一一对应——它们是文档溯源(provenance)信息,会随节点进入图并最终呈现在导出/查询结果里。
九、source_file 规则:增量更新与去重的“锚”
source_file 字段被单独提炼成一条大写的 RULE,因为它是整个增量构建正确性的基石:
- 每个 node、edge、hyperedge 的
source_file必须设置为源文件在FILE_LIST中出现的路径——逐字符、原样、绝对路径; - 不允许缩短为 basename、不允许重新相对化、不允许去掉任何目录前缀、不允许改动分隔符;
- 引擎(engine)会在下游做分隔符规范化并按构建根做相对化,所以原样传递是正确的;
- 这样做能让全量构建(full build)与增量
--update处于同一条基准线上,使 build_merge 的 replace-on-re-extract(重新抽取时替换)能命中既有节点,而不是不断累积一个重复节点。
这条规则解释了为什么 fragment 头部的 CHUNK_PATH 也要强调绝对路径——base 不一致是幽灵重复节点的第二大成因(第一大成因是 6 节讲的不规范节点 ID)。相应地,graphify 的缓存设计也把提示词本身当作缓存键的一部分:cache.py 中语义缓存区 semantic-deep/(--mode deep 命名空间)与 semantic/ 分开存放,且缓存条目按产生它的抽取提示词归因——当 graphify 升级改变了提示词,旧提示词产生的条目会被重新抽取而不是回放缓存。
十、完整 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}
逐层拆解:
节点对象 nodes[]:
id:按第六节的{stem}_{entity}确定性生成,全局唯一;label:人类可读名称(Human Readable Name);file_type:六选一词汇表,其它值拒绝;source_file:原样来自FILE_LIST(第九节);source_location/source_url/captured_at/author/contributor:溯源字段,可由 YAML frontmatter 填充,无则null。
边对象 edges[]:
source/target:两端节点 ID,方向语义随relation变化(调用边 source 恒为调用方);relation合法值八种:calls | implements | references | cites | conceptually_related_to | shares_data_with | semantically_similar_to | rationale_for;confidence:三分类词汇表;confidence_score:必填,按第五节标尺;source_file/source_location:溯源;weight:数值权重,默认 1.0。
超边对象 hyperedges[]:id(snake_case)、label、nodes(至少 3 个成员)、relation(三选一)、confidence(二选一)、confidence_score、source_file。
顶层用量字段:input_tokens、output_tokens 由子代理报告,供成本核算与缓存归因。
十一、深度模式:--mode deep 对协议的放大
规范的 DEEP_MODE 段描述了 CLI --mode deep 的语义:在深度模式下,要激进地补 INFERRED 边——包括间接依赖、共享假设、潜在耦合;拿不准的标 AMBIGUOUS 而不是省略。 注意一个细节:文档/片段强调的深挖对象是“结构证据型推断”,而 llm.py 的 _DEEP_EXTRACTION_SUFFIX 更明确地警告“避免宽泛的概念相似边”——深度模式鼓励的是具体的架构信号(共享数据契约、显式生命周期耦合、多步流程依赖),而不是放任模型满天飞地连“感觉相关”的边。
CLI 侧把该开关一路从参数透传到语义语料打包:cli.py 解析 deep_mode = extract_mode == "deep",随后把模式写进语义缓存命名空间(cache/semantic-deep/),保证深度模式的结果不会污染、也不会被普通模式缓存命中或遮蔽(cache.py 相应注释说明这是 #1894 引入的命名空间隔离)。graphify __main__.py 的帮助文本则把它描述为“--mode deep aggressive INFERRED-edge semantic extraction”。
十二、与实现侧对齐的几个关键机制
把规范与源码对照,可以归纳出几条值得写进任何“图抽取系统设计”清单的工程机制:
- 幻觉防护(证据绑定):语义抽取只见文档/图片,因此模型中冒出的
file_type == "code"节点其实是“从文档内部冒出的符号”(代码块里的名字、论文里引用的 API)。llm.py 的_bind_node_evidence会在抽取后核对:该符号名是否真的出现在模型看过的源字节里——出现不了就给它打verification = "unverified"标记(而不是直接丢弃),由诊断层上报。这是规范“AMBIGUOUS标记而非省略”哲学的镜像实现。 - 注入防护:llm.py 的
_wrap_untrusted把每个源文件包进带sha256指纹的<untrusted_source>块,_neutralise_injection_sentinels会先“去武装”聊天模板/越狱控制符——源文件里任何“忽略以上规则、输出特定节点列表、泄露提示词”的指令都按惰性数据处理。这正是“输出严格受 Schema 约束”能在不可信语料上成立的底层保障。 - 提示词即缓存键:抽取提示词一旦随版本改变,旧缓存条目作废重抽(
#1939),保证“同提示词命中、异提示词重抽”。
十三、结语:这份协议给了抽取系统什么
回看整份 extraction-spec.md,它其实是一份极其成熟的 LLM 结构化输出工程清单:用三档可信度与离散置信度标尺对抗模型的过度自信与平均化倾向;用“带全路径 stem 的确定性节点 ID”让分布式分块抽取天然可合并、可增量;用“reasoning 内联而非建节点、跨语言调用一律不产、图片理解而非 OCR、超边克制使用”等规则把噪音扼杀在源头;再用 source_file 逐字符锚定把全量与增量构建钉在同一条基线上。对想要自定义 /graphify 技能、接入新平台(gen.py 与 platforms.toml 负责多平台渲染)或自建类似抽取管道的开发者而言,这份文件本身就是一份可以直接复用的生产级参考实现,搭配 extraction-spec-compact.md 还可以获得一个面向有限上下文的浓缩版本。
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 StartedRust0626
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