首页
/ graphify 并行语义抽取的 Step B2 全量子代理分发协议:单条消息并行调度、general-purpose 落盘契约与 chunk 路径规范

graphify 并行语义抽取的 Step B2 全量子代理分发协议:单条消息并行调度、general-purpose 落盘契约与 chunk 路径规范

2026-09-07 10:04:45作者:胡易黎Nicole

导读

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 的两个判断:

  1. 纯代码语料直接跳过 Part B。代码已被 AST 覆盖,语义子代理无事可做。此时需要先写入一个空的 .graphify_semantic.json(否则 Part C 的无条件读取会触发 FileNotFoundError),然后直接进入 Part C。
  2. 只有当语料中存在 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.cachecheck_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"

注意注释中的两个工程细节:

  1. PROJECT_ROOT当前工作目录,即运行命令的项目根目录,而不是 detect 阶段可能涉及到的 .graphify_root/scan 目录——chunk 产物统一落在根下的 graphify-out/,这样 Part C 的 sorted(glob.glob('graphify-out/.graphify_chunk_*.json')) 才能把全部块收集齐。
  2. 文件名采用两位十进制编号.graphify_chunk_01.json.graphify_chunk_02.json…),保证 glob 排序后按 chunk 序号稳定合并。

替换进子代理提示词的五个槽位

每个子代理收到的提示词中需要替换五个变量:

槽位 含义
FILE_LIST 该 chunk 负责的源文件清单
CHUNK_NUM 当前 chunk 序号(从 1 起)
TOTAL_CHUNKS 总块数
DEEP_MODE 是否 --mode deeptrue/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 统一收敛,收敛过程构成一条明确的「成功/失败判定链」:

  1. 成功信号 = 文件存在:检查 graphify-out/.graphify_chunk_NN.json 是否在磁盘上。存在且为含 nodes/edges 的合法 JSON → 计入合并并写入缓存。
  2. 文件缺失 = 只读子代理:大概率派发成了 Explore 类型,打印警告并要求用 general-purpose 重跑对应 chunk,不静默跳过。
  3. 失败或非法 JSON = 跳过该块但不停机:打印警告后继续,允许部分失败的部分结果参与合并。
  4. 过半失败即中止:超过一半 chunk 缺失/失败时,停止并提示用户确认 subagent_type="general-purpose"
  5. Token 回填:每次 Agent 调用完成后,从工具结果的 usage 字段读取真实 token 计数并写回 chunk JSON(chunk JSON 自身永远是占位 0),保证最终 token 统计与成本追踪(graphify-out/cost.json)不虚报。
  6. 合并去重:所有 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.pyValidateTokensrc_auth_session_validatetoken);严禁追加 chunk 序号等任何后缀_c1_chunk2 均为非法),否则同一实体在不同 chunk 会产生幽灵双胞胎节点。
  • calls 边方向与语言边界:source 必须是调用方、target 必须是 callee,绝不反向;calls 边严禁跨语言(Python 函数不能 calls JS/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_file verbatim 透传:取 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.mdskill-claude.md逐字节一致,碎片一旦被手工改动即触发漂移报警,相关测试可参见 tests/test_skillgen.py 与平台一致性测试 tests/test_agents_platform.py

小结:把 Step B2 的五个要点带进实际运行

  • 一次消息、全部派发:多 chunk 的 Agent 调用必须同处一条 response,否则退化为串行,失去并行提速意义。
  • 类型即权限:子代理一律 general-purposeExplore 只读、写不了盘、会静默丢结果。
  • 绝对路径落盘CHUNK_PATH$(pwd) + graphify-out/.graphify_chunk_0N.json 推导,绝不能用相对路径。
  • 提示词逐字一致:extraction-spec 仅在有 doc/paper/image 时加载,原样传给每个子代理并替换五个槽位;deep 模式全程透传。
  • 以磁盘文件为真相:Step B3 以 chunk 文件是否存在作为成功信号,缺失即警告重跑、过半失败即中止,保证任何一次部分失败都能被显式发现而不是被静默合并进图里。

理解这套协议后,无论是排查 /graphify 在大型混合语料上「结果凭空消失」的问题,还是为自有 Agent 编排设计类似的并行抽取流水线,都能直接复用这里的「并行派发 + 落盘提交 + 文件即信号 + 逐字同规格提示词」模式。

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