0xMovez AI さんの「Graph Engineering with Claude: 14-Step roadmap from 0 to graph architect」(2026-07-25 公開、2026-07-30 取得)を読みました。直線的に走らせているエージェント運用を、処理の単位(節点)とデータが流れる経路(辺)からなるグラフとして設計し直す、という記事です。

主張のうち2つを手元の環境で実際に動かして確かめたので、その結果まで書きます。

記事が言っていること

14段の構成です。前半は基礎(節点は1エージェント1ジョブ、辺は実際にデータが流れる依存だけ)、中盤は型(並列展開、全部揃ってから進む待ち合わせ、展開→縮約→統合のダイヤモンド、辺の上に検証者を置く)、終盤が今回試した2つです。

Step 12(節点ごとにモデルを階層化する):

Not every node needs your best model. (中略) Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives.

〔訳〕すべての節点に最良のモデルが要るわけではない。退屈な節点は安いモデルで走らせ、高価なトークンは判断が実際に住んでいる場所に使え。

Step 14(良い実行を再実行可能な定義として保存する):

When a run is good, press s to save its script into .claude/workflows/ - version-controlled, re-runnable by name, a graph anyone who clones the repo can launch.

〔訳〕良い実行ができたら s を押してそのスクリプトを .claude/workflows/ に保存する。バージョン管理され、名前で再実行でき、リポジトリをクローンした誰もが起動できるグラフになる。

正直に書いておくと、この記事には外部の一次資料へのリンクが1本もありません(本文中のリンクは画像だけで、あとは著者の LinkedIn の URL が生テキストで書かれているのみです)。具体的なモデル名も出てきません。設計論としては筋が通っていると感じましたが、主張の裏付けは記事の中では取れない、という素材です。だから自分で動かして確かめることにしました。

手元にどう効きそうか

ここは推測です。

階層化は、効く度合いが仕事の分布次第では? 展開する側の節点数が多く、しかも1個ずつが機械的な仕事(抽出・分類)であるほど効くはずです。逆に節点が数個しかない小さいグラフなら、差は誤差の範囲かもしれません。

保存の価値は、再現性の置き場が変わることでは? プロンプトで毎回同じ手順を指示すると、モデルの解釈が実行ごとに揺れます。オーケストレーションがスクリプト(コード)として保存されるなら、揺れる部分がエージェントの中身だけに閉じ込められます。

試した結果

環境は Claude Code の Workflow 機構(記事が「dynamic workflows」と呼んでいるもの)です。題材は、以前の記事で扱った MCP 仕様 2026-07-28 の major change 9件を「サーバ運用者への影響度」で分類する仕事にしました。結果の正否を自分で検算できるからです。

構成は記事の言うダイヤモンド型の最小形です。分類3節点(3件ずつ担当、reasoning effort を low に指定)→ 統合1節点(指定なし=セッション既定)。

オーケストレーションスクリプトの骨格はこうです(スキーマ定義と入力データの配列は省略しています。agent() が subagent を1体起こし、parallel() が同時に走らせ、schema を渡すと返り値が検証済みの JSON になります)。

phase('Classify')
const classified = await parallel(GROUPS.map((g, i) => () =>
  agent(`次の MCP 仕様 2026-07-28 の変更項目それぞれについて、リモート MCP サーバを
運用する側への影響度(high/medium/low)を判定し、理由を一行で述べよ。
変更項目: ${JSON.stringify(g)}`,
    { label: `classify:${i + 1}`, effort: 'low', schema: SCHEMA })))

phase('Merge')
const all = classified.filter(Boolean).flatMap(r => r.items)
const merged = await agent(`次の分類結果(MCP 2026-07-28 の変更のサーバ運用者影響)を
統合し、high のものから順に並べ、全体を3行で総括せよ: ${JSON.stringify(all)}`,
  { label: 'merge' })
return { count: all.length, items: all, summary: merged }

実行は Claude Code の Workflow ツールにこのスクリプトを渡すだけです(シェルコマンドではなく、セッション内のツール呼び出しです)。以降の実行記録の数値は、完了時に返る usage ブロックの値をそのまま書き写しています。

変更9件 3件×3グループ 分類 1(effort: low) 出力 478 トークン 分類 2(effort: low) 出力 592 トークン 分類 3(effort: low) 出力 686 トークン 統合(既定) 出力 935 トークン

1回目の実行: 保存は勝手に起きていた

4節点が完走しました。完了通知の usage はこうです。

agent_count: 4, duration_ms: 29314, subagent_tokens: 100564

subagent_tokens は入力も含めた4節点の総消費です(後述の節点別の数値は出力トークンだけなので、足しても一致しません)。分類結果は9件とも返り、統合節点が high 5件・medium 3件・low 1件に並べ直しました(内容も妥当でした。セッション削除と GET ストリーム削除が high 側なのはその記事の実感とも合います)。

ここで記事の Step 14 を補足します。保存先は2つあって、役割が違いました。

ひとつめは自動で保存される控えです。実行した時点でオーケストレーションスクリプトが毎回ここに保存され、実行結果からパスが返ってきます。

~/.claude/projects/<プロジェクト名>/<セッションID>/workflows/scripts/<名前>-<実行ID>.js

ホームディレクトリ配下のセッション記録置き場で、リポジトリの外です。git 管理されず他の人からも見えませんが、「この実行をもう一度再現・再開する」にはこれで足ります。ファイル名に実行ごとに採番される ID が入っているので、過去の実行の残骸ではありません。この保存に操作は要りませんでした。

ふたつめが、元記事の「s を押して保存する」が指しているリポジトリ直下の .claude/workflows/<ワークフロー名>.js です。これも実際にやってみました(対話 UI 側の操作なので、ここだけ人間の手です)。/workflows で実行一覧を開き、保存したいワークフローの上で s を押すと:

Dynamic workflow saved to <リポジトリ>/.claude/workflows/pattern-map-control.js.
Invoke as /pattern-map-control or Workflow({name: "pattern-map-control"}) in future sessions.

リポジトリの中なので git でコミットでき、以後は名前(/pattern-map-control)で起動できます。クローンした人も同じワークフローを使える、という元記事の記述どおりの置き場です。

つまり自分のセッション用の控えは勝手に作られ、チームに配る正式版への昇格だけが明示的な操作、という二段構えです。この記事のキャッシュ付き再実行は、すべてひとつめの控えを指定して行っています。

再実行: 7ミリ秒、追加トークン0

保存されたスクリプトのパスと実行 ID(resumeFromRunId)を指定して、同じワークフローをもう一度起動しました。再実行の usage はこうです。

agent_count: 4, duration_ms: 7, subagent_tokens: 0

1回目の 29,314 ミリ秒・100,564 トークンに対して、7 ミリ秒・追加トークン0。返ってきた分類結果と総括は1回目と同一でした。スクリプトが変わっていないので、全節点がキャッシュされた結果をそのまま返した形です。「良い実行を再実行可能な定義として保存する」は、私の環境では保存にキャッシュ付き再開まで付いてくる、記事の主張より一段強い形で実在しました。スクリプトを一部書き換えた場合に変わっていない節点だけキャッシュが効くかどうかは、後段の25節点の実験で確かめます。

階層化: モデルではなく effort でなら試せた

節点ごとの出力トークンは、各節点の実行ログ(transcript)から取れます。

$ jq -c 'select(.message.usage != null) |
    {model: .message.model, out: .message.usage.output_tokens}' agent-*.jsonl | tail -1

これを4節点ぶん並べると、分類3節点が 478 / 592 / 686、統合節点が 935 で、モデルは4節点とも claude-fable-5(セッション既定)でした。判断を集約する節点が最も出力する、という記事の前提と合う分布です。ただしここは少し循環しています。分類側には自分で effort: low を指定しているので、分布の一部は自分の設定の結果でもあります。

正確に書くと、この4節点で試したのはモデルの階層化ではありません。節点ごとに変えたのは reasoning effort(思考の深さ)だけです。モデル自体の差し替えは、次の実験でやります。

25節点に広げ、モデルを実際に差し替えて比べた

ここまでは4節点の小さい話だったので、規模を一桁上げて、元記事の主張そのもの(節点単位のモデル差し替え)を対照付きで試しました。

題材は手元のパターンカタログの24ページです。1ページ1節点で読ませて「パターン名・中心主張一文・効く局面(before-run / during-run / after-run)」を抽出し、統合1節点が全体を分類して総括する、計25節点の展開です。抽出節点のスクリプトはこうです(プロンプト後半と SCHEMA・SLUGS の定義は省略しています。抽出項目は上に書いた3つです)。model: 'haiku' の1オプションだけが差し替えの実体です。

const extracted = await parallel(SLUGS.map(s => () =>
  agent(`ファイル ${DIR}${s}.md を Read で読み、次を抽出せよ: …`,
    { label: `extract:${s}`, model: 'haiku', schema: SCHEMA })))

これを、抽出24節点を model: 'haiku' にした階層化ありと、指定を外してセッション既定(claude-fable-5)のままにした対照の2回走らせました。usage はこうです。

階層化あり: agent_count: 25, duration_ms: 67604, subagent_tokens: 724279
対照:       agent_count: 25, duration_ms: 43354, subagent_tokens: 870967

節点が申告どおりのモデルで動いたことは、各節点の実行ログのモデル名で確認しました。

$ for f in wf_051b2590-*/agent-*.jsonl; do
    jq -r 'select(.message.model != null) | .message.model' "$f" | tail -1
  done | sort | uniq -c
   2 claude-fable-5
  24 claude-haiku-4-5-20251001

(claude-fable-5 が2件あるのは、統合節点を後述の部分キャッシュ実験でもう一度走らせたためです。対照側は25節点すべて claude-fable-5 でした。)

読み取れたことを順に書きます。

トークンの総量の差は2割弱にとどまりました(724,279 対 870,967)。同じファイルを読んで同じスキーマで返す仕事なので、当然といえば当然です。つまり階層化の節約は量ではなく単価で効きます。トークン単価はモデルごとに違うので、差額は各モデルの料金表を掛けて初めて出ます(この記事では金額の試算はしません)。

時間はむしろ階層化ありのほうが遅かったです(67.6秒 対 43.4秒)。1回ずつしか走らせていないので、並列スロットの割り当てなど偶然の揺れかもしれません。「安いモデル=速い」を無条件に期待してはいけない、くらいに読むのが安全だと思います。

品質は、24件とも両方成功しましたが、差は見えました。どちらのモデルも全節点がスキーマどおりに返し、パターン名の取り違えもゼロです。ただし中心主張の一文は、対照(claude-fable-5)のほうが一貫して長く、原文の留保(「〜とは主張しない」「ただし〜」)まで拾っています。局面の判定は 24件中4件で両者が食い違いました(例: 「読み捨ての隔離」を haiku は during-run、fable は before-run と判定。どちらも通る余地のある境界例です)。機械的な抽出はどちらでも成立し、微妙な判断ほど差が出る。元記事の「退屈な節点は安いモデルで」という切り方と整合する結果でした。

いちばん面白かったのは統合節点のつまずき方です。統合はどちらの実行でもセッション既定モデルでしたが、どちらも件数の集計でつまずきました(片方は局面別の内訳を数え違えて途中で自己訂正し、もう片方は本文に書いた件数と内訳の並びが食い違ったまま)。分類の中身は妥当なのに、数を数える仕事だけ落とす。これは元記事の Step 4(配線はコードで、辺は無料)がそのまま効く場面で、件数は統合節点に数えさせず、スクリプト側で all.length を集計して渡すべきでした。モデルの階層より先に、そもそもモデルにやらせない部分を切り出すほうが効く、という教訓です。

部分キャッシュ再実行も、ここで確かめました。統合節点のプロンプトだけ書き換えて(総括を5行→3行+件数)、実行 ID 付きで再起動すると:

agent_count: 25, duration_ms: 18778, subagent_tokens: 27056

抽出24節点はキャッシュから同一結果が返り、統合1節点だけが実行されました。724,279 トークンの実行を、末端の1節点ぶん(27,056 トークン)で修正できたことになります。グラフの下流だけ直して上流を再利用する、という元記事の設計論がコスト面で最も効くのは、たぶんこの形です。

この実験で確かめられていないこと

  • 各実験は1回ずつです。特に実行時間の比較(67.6秒 対 43.4秒)は、複数回まわせば逆転するかもしれません
  • 品質の比較は目視です。主張一文の忠実さや局面判定の妥当性は私が読み比べただけで、定量的な評価基準は置いていません
  • 環境が1つです。記事も私の実測も Claude Code の機構に密着した話で、他のオーケストレーション環境に一般化できるかは分かりません

まとめ

  • 元記事は設計論として筋が通っていますが、一次資料へのリンクが無いので、主張は自分で動かして確かめる素材だと思って読むのがよさそうです
  • 「良い実行を再実行可能な定義として保存する」は実在しました。しかも手元では保存が既定で、再実行はキャッシュにより 7 ミリ秒・追加トークン0でした
  • 「節点ごとの階層化」は 25節点の対照実験で実測しました。トークン総量は2割弱しか変わらず(724,279 対 870,967)、節約は量ではなく単価で効きます。機械的な抽出は安いモデルでも全件成立し、留保の拾い方と境界例の判定(24件中4件)に差が出ました(品質差の評価は目視です)
  • 差が最も大きかったのは階層化ではなく再利用でした。統合節点だけ書き換えた再実行は 27,056 トークンで済み、初回の 724,279 トークンに対して 26分の1です。あわせて、件数の集計のような決定的な仕事は統合節点に数えさせず、スクリプト側のコードでやるべきでした(両実行とも統合節点が集計でつまずきました)

複数回の反復で時間差が再現するか、品質差の定量評価、他のオーケストレーション環境への一般化は、まだ試せていません。