graphify 并行语义抽取的 Step B2 全量子代理分发协议:单条消息并行调度、general-purpose 落盘契约与 chunk 路径规范
导读
graphify 的 /graphify skill 在把文档、论文、图片转换为知识图谱的语义抽取阶段(Step 3 Part B),依赖宿主 Agent(如 Claude Code)内置的 Agent tool 把内容分片交给子代理并行抽取。Step B2 正是这套并行分发的核心指令:它要求在单条消息内一次性派发全部子代理,强制使用可写盘的 general-purpose 类型,并把每个 chunk 的产出以 JSON 落盘到绝对路径 graphify-out/.graphify_chunk_NN.json。读完本文,你将理解 graphify 如何设计「并行 × 落盘 × 逐 chunk 收敛」的子代理编排,掌握派发前如何推导 CHUNK_PATH、如何替换 FILE_LIST / CHUNK_NUM / TOTAL_CHUNKS / DEEP_MODE 五个槽位,以及在 Step B3 如何以「chunk 文件是否存在」作为成功信号完成收集、缓存与合并。
本文以源码碎片 agent-tool-disk.md 为主线,结合其宿主模板 core/core.md、抽取子代理提示词 extraction-spec.md 以及渲染器 gen.py 展开讲解。
Step B2 在 graphify 语义抽取管线中的定位
graphify 的语义抽取位于 skill 运行流程的 Step 3 Part B,它与确定性的 AST 抽取(Part A)并行执行。整条管线的分工如下:
| 阶段 | 处理对象 | 手段 | 产物 |
|---|---|---|---|
| Step 2 detect | 语料分类 | graphify.detect |
.graphify_detect.json |
| Part A 结构抽取 | code 文件 | AST,确定性、零成本 | .graphify_ast.json |
| Part B 语义抽取 | document / paper / image | LLM(Gemini 或宿主 Agent 自身,或派生子代理) | .graphify_semantic.json |
| Part C 合并 | AST + 语义 | 按节点 id 去重 | .graphify_extract.json |
| Step 4+ | 建图 | build_from_json + 聚类 |
graph.json / GRAPH_REPORT.md / graph.html |
关键前提来自 core/core.md 的两个判断:
- 纯代码语料直接跳过 Part B。代码已被 AST 覆盖,语义子代理无事可做。此时需要先写入一个空的
.graphify_semantic.json(否则 Part C 的无条件读取会触发FileNotFoundError),然后直接进入 Part C。 - 只有当语料中存在 doc、paper 或 image 时才进入 Step B。这就是 Step B2 末尾那句「Load it only here, only when at least one chunk holds a doc, paper, or image」的含义——纯代码语料永远不会去读取 extraction-spec。
因此,Step B2 是 graphify 在「非纯代码语料」上实现可并行、可缓存、可续跑语义抽取的枢纽节点。
并行原则:一次消息派发全部子代理,杜绝串行
Step B2 的标题即点明了硬性约束:Dispatch ALL subagents in a single message。碎片 agent-tool-disk.md 给出的原因是实现层面的——只有同一条 response 里多次调用 Agent tool,子代理才会并行运行;如果「派发一个、等待、再派发一个」,本质就退化成了串行,且比逐文件自读慢 5~10 倍,违背了 Part B 强制使用 Agent tool 的设计初衷。
以 3 个 chunk 为例,core/core.md 与 skill 成品 skill.md 中的编排骨架是:
[Agent tool call 1: files 1-15, subagent_type="general-purpose"]
[Agent tool call 2: files 16-30, subagent_type="general-purpose"]
[Agent tool call 3: files 31-45, subagent_type="general-purpose"]
三个调用必须在同一条消息中发出,而不是三条独立消息。分块规模由前序的 Step B1 决定(见下文),这里 15 个文件/块只是示意——真实分块是 每块 20~25 个文件,图片单独成块。
子代理类型:为什么必须用 general-purpose 而禁用 Explore
这是 Step B2 中带有「IMPORTANT」强调的最关键规则:
始终使用
subagent_type="general-purpose"。不要使用Explore——它是只读的,无法把 chunk 文件写到磁盘,会在静默状态下丢失抽取结果。
原因是 agent 的权限模型:Explore 类型的子代理只有读取能力,而 general-purpose 具备宿主授予的 Write 与 Bash 访问权,这是把抽取 JSON 落到磁盘的硬性前提。此处「静默丢失」是重点——若子代理无法写盘,调用看起来成功了、实际产物不存在,主流程如果缺乏校验就会带着缺失数据继续建图。
Step B3 因此把「文件是否存在」当作成功信号(详见下文),并在文件缺失时给出针对性诊断:「chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent.」,而不是静默跳过。若超过一半的 chunk 失败或缺失,则直接停止并提示用户用 general-purpose 重跑。
派发前的准备:Step B0 缓存检查与 Step B1 分块规则
Step B2 不是孤立指令,它依赖两个前序步骤产出的输入:
Step B0 —— 抽取缓存检查(去重免抽取)
派发前先用 graphify.cache 的 check_semantic_cache 检查哪些文件已有缓存结果,返回 (cached_nodes, cached_edges, cached_hyperedges, uncached_files)(其函数定义见 cache.py)。只有 graphify-out/.graphify_uncached.txt 里列出的文件才值得派发;全部命中时直接跳到 Part C。该调用传入 prompt_file='SPEC_PATH'(extraction-spec 的绝对路径),使缓存条目与产出它的提示词版本绑定——graphify 升级改版提示词后,旧条目会被重新抽取而非直接回放(对应 issue #1939)。如果全部未命中,还要删除可能遗留的旧 .graphify_cached.json,避免 Part C 合并到上一次运行的陈旧缓存(#1392)。
Step B1 —— 分块
从 .graphify_uncached.txt 读取文件后,Step B1 按三条规则切块:
- 每块 20~25 个文件;
- 每张图片单独成块(视觉任务需要独立上下文);
- 同一目录的文件尽量聚在同一块,让相关产物落在同一 chunk 中,更容易抽取到跨文件关系。
Step B2 实操:CHUNK_PATH 的绝对路径推导与提示词槽位替换
进入 Step B2 时,主代理需要为每个 chunk 预先推导 CHUNK_PATH,且必须是绝对路径(相对路径会被 Write 工具解析到不确定的 cwd,导致文件静默丢失)。碎片中给出的推导方法是:
PROJECT_ROOT=$(pwd) # cwd —— Part C 在此 glob graphify-out/(不是 .graphify_root/scan 目录,#1392)
# 然后对 chunk N:CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"
注意注释中的两个工程细节:
PROJECT_ROOT是当前工作目录,即运行命令的项目根目录,而不是 detect 阶段可能涉及到的.graphify_root/scan目录——chunk 产物统一落在根下的graphify-out/,这样 Part C 的sorted(glob.glob('graphify-out/.graphify_chunk_*.json'))才能把全部块收集齐。- 文件名采用两位十进制编号(
.graphify_chunk_01.json、.graphify_chunk_02.json…),保证 glob 排序后按 chunk 序号稳定合并。
替换进子代理提示词的五个槽位
每个子代理收到的提示词中需要替换五个变量:
| 槽位 | 含义 |
|---|---|
FILE_LIST |
该 chunk 负责的源文件清单 |
CHUNK_NUM |
当前 chunk 序号(从 1 起) |
TOTAL_CHUNKS |
总块数 |
DEEP_MODE |
是否 --mode deep(true/false),在 Step 3 开始时就必须从原始调用记录并一路传递 |
CHUNK_PATH |
上面推导出的绝对落盘路径 |
其中 DEEP_MODE 需要在 Step 3 开始时先从原始调用中判断 --mode deep 是否出现并贯穿整个派发,不能在中途丢失。deep 模式会要求子代理「激进地添加 INFERRED 边」,并把拿不准的关系标为 AMBIGUOUS 而不是省略(见 extraction-spec.md)。
提示词本身:verbatim 透传 extraction-spec
碎片明确要求:extraction-spec(含 JSON schema、node-ID 规则、confidence rubric、frontmatter、hyperedge、vision 规则)只在 Step B2 这一处、且仅在至少一个 chunk 持有 doc/paper/image 时加载。加载后要把这份提示词**逐字(verbatim)**传给每个子代理——保证同一语料的不同 chunk 遵循同一套 ID 生成规则与置信度标准,是后续按 id 去重合并不产生幽灵节点的前提。该提示词的源碎片位于 extraction-spec.md,渲染后的成品副本在各平台的 references 目录下,例如 graphify/skills/claude/references/extraction-spec.md(部分平台使用 compact 精简版)。
落盘契约与 Step B3 的收敛逻辑
子代理把 JSON 写入 CHUNK_PATH 后,由 Step B3 统一收敛,收敛过程构成一条明确的「成功/失败判定链」:
- 成功信号 = 文件存在:检查
graphify-out/.graphify_chunk_NN.json是否在磁盘上。存在且为含nodes/edges的合法 JSON → 计入合并并写入缓存。 - 文件缺失 = 只读子代理:大概率派发成了
Explore类型,打印警告并要求用 general-purpose 重跑对应 chunk,不静默跳过。 - 失败或非法 JSON = 跳过该块但不停机:打印警告后继续,允许部分失败的部分结果参与合并。
- 过半失败即中止:超过一半 chunk 缺失/失败时,停止并提示用户确认
subagent_type="general-purpose"。 - Token 回填:每次 Agent 调用完成后,从工具结果的
usage字段读取真实 token 计数并写回 chunk JSON(chunk JSON 自身永远是占位 0),保证最终 token 统计与成本追踪(graphify-out/cost.json)不虚报。 - 合并去重:所有 chunk 汇聚成
.graphify_semantic_new.json→ 经save_semantic_cache(见 cache.py)存入缓存 → 与.graphify_cached.json合并、按节点 id 去重后写入.graphify_semantic.json→ 清理临时文件(.graphify_cached.json、.graphify_uncached.txt、.graphify_semantic_new.json)。
这套「落盘即提交」的设计让 Part B 天然具备可续跑性:任何一个 chunk 失败,下一次运行通过缓存检查只会重派失败的未缓存文件,而不会重复计费已成功的块。
给子代理的抽取红线(来自 extraction-spec 正文)
Step B2 逐字透传的提示词内含多条影响产物质量的硬规则,主代理分发时应理解其约束:
- 节点 ID 必须确定性:格式为
{stem}_{entity},stem是完整仓库相对路径去掉扩展名、逐段小写归一化(如src/auth/session.py的ValidateToken→src_auth_session_validatetoken);严禁追加 chunk 序号等任何后缀(_c1、_chunk2均为非法),否则同一实体在不同 chunk 会产生幽灵双胞胎节点。 calls边方向与语言边界:source 必须是调用方、target 必须是 callee,绝不反向;calls边严禁跨语言(Python 函数不能callsJS/TS/Go/Rust/Java 符号),跨语言调用是幻影伪影。confidence_score必填且遵守离散评分:EXTRACTED 恒为 1.0;INFERRED 只能从{0.95, 0.85, 0.75, 0.65, 0.55}中取值,禁用 0.5 兜底;AMBIGUOUS 用 0.1~0.3。- hyperedge 克制:≥3 个节点共同参与才使用,每 chunk 最多 3 条。
source_fileverbatim 透传:取FILE_LIST中的原样绝对路径,不缩短、不重相对化,保证与增量--update的同基合并。- 视觉理解而非 OCR:图片要描述它「是什么」(UI 截图的布局/设计意图、图表的指标/趋势、白板草图的连线并标注 AMBIGUOUS 等)。
这些规则共同保证了多个并行子代理各自产出的图片段能在 Part C / 建图阶段被安全拼接。
从碎片到成品:Step B2 在 skillgen 中如何渲染进各平台 skill
agent-tool-disk.md 本质是 skillgen 渲染器的一个可插拔碎片,被 @@DISPATCH@@ 槽位注入宿主模板。渲染逻辑位于 gen.py:_render_core() 读取 core/core.md 后,把 @@DISPATCH@@ 替换为该平台在 platforms.toml 中声明的 dispatch 碎片,替换完成后还会校验正文中没有遗留的 @@槽位@@ 占位符。
各平台对该碎片的选用如下(platforms.toml):
| dispatch 碎片 | 使用平台 | 差异要点 |
|---|---|---|
agent-tool-disk |
claude、kilo、copilot、claw、kiro、pi | 标准「Agent tool + 磁盘落盘」路径,正文即本文分析的 Step B2 |
agent-tool-disk-powershell |
windows | 同一编排的 PowerShell 变体(shell = powershell 平台) |
codex-agenttask |
codex | Codex 的 AgentTask 风格派发 |
task-tool-disk / task-tool-disk-trae |
droid、amp、agents / trae | Task tool + 磁盘收集结果(trae 还带无 PreToolUse hook 的提示) |
opencode-mention / manual-paste |
opencode / vscode | 宿主能力不同时的替代派发 |
同时 core/core.md 提到,若 GEMINI_API_KEY/GOOGLE_API_KEY 已设置,语义抽取可改用 graphify.llm.extract_corpus_parallel(files, backend="gemini"),此时不再走子代理派发——Step B2 的 parallel 协议服务于「宿主 Agent 自身即 LLM」的默认路径。skillgen 的生成校验(--check、expected/ 快照比对)保证这些碎片与各平台成品 skill.md、skill-claude.md 等逐字节一致,碎片一旦被手工改动即触发漂移报警,相关测试可参见 tests/test_skillgen.py 与平台一致性测试 tests/test_agents_platform.py。
小结:把 Step B2 的五个要点带进实际运行
- 一次消息、全部派发:多 chunk 的 Agent 调用必须同处一条 response,否则退化为串行,失去并行提速意义。
- 类型即权限:子代理一律
general-purpose;Explore只读、写不了盘、会静默丢结果。 - 绝对路径落盘:
CHUNK_PATH由$(pwd)+graphify-out/.graphify_chunk_0N.json推导,绝不能用相对路径。 - 提示词逐字一致:extraction-spec 仅在有 doc/paper/image 时加载,原样传给每个子代理并替换五个槽位;deep 模式全程透传。
- 以磁盘文件为真相:Step B3 以 chunk 文件是否存在作为成功信号,缺失即警告重跑、过半失败即中止,保证任何一次部分失败都能被显式发现而不是被静默合并进图里。
理解这套协议后,无论是排查 /graphify 在大型混合语料上「结果凭空消失」的问题,还是为自有 Agent 编排设计类似的并行抽取流水线,都能直接复用这里的「并行派发 + 落盘提交 + 文件即信号 + 逐字同规格提示词」模式。
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