複数のAIモデルを扱えるツールは増えた。しかしそれらの多くは、チャットやターミナルの単位でモデルを切り替える仕組みであって、モデル同士を協働させる仕組みではない。前の回答をコピーし、文脈を補い、次のモデルへ渡す。統合しているのは結局のところ人間である。
一方で、複数エージェントを協働させる基盤も存在する。ただしその多くはAPI呼び出しを前提としている。APIキーを用意し、トークン単位で課金され、その基盤の作法に合わせて実装することになる。すでにClaude、Codex、Geminiにサブスクリプションを払っているのに、協働させようとした瞬間に別建てで課金し直すことになる。しかも一度組み上げれば、その基盤から離れにくくなる。
この2つを避けたかった。そこで採った方針は単純である。
APIを呼ぶのではなく、すでに契約しているCLIをプロセスとして起動し、統率する。
作ったのは gc(group chat)というシステムだ。Discordに依頼を書くと、指定した順にAIエージェントが作業を引き継ぐ。本記事では、その設計思想とアーキテクチャを扱う。実装中に踏んだCLI固有の罠については、分量の都合で別記事にまとめる。
1. 何ができるか
Discordの専用チャンネルに、担当と指示を書く。
@gemini SQLiteのWALモードの利点を artifacts/notes.md に3行で書いて
@gpt それを読んで1行に要約して artifacts/summary.md に書いて
返ってくるのはこうだ。
AIgc: ジョブ 20260829-170009-baa38f を受け付けました(経路: gemini → gpt)
gemini: hop1 / gemini
SQLiteのWALモードの利点を調査し、notes.md に3行でまとめました。
次の処理: notes.md を読んで1行に要約し、summary.md に書いてください。
成果物: artifacts/notes.md [notes.md 607 bytes]
gpt: hop2 / gpt
要約を summary.md に書きました。
成果物: artifacts/summary.md [summary.md 196 bytes]
エージェントごとに別の名前とアイコンで発言し、作成したファイルはその場に添付される。Discordが動く環境であれば場所を選ばない。実際、最初の受け入れテストはiPhoneから行った。
現在の構成は4体である。
| Discord上の名前 | 実体 | 役割 |
|---|---|---|
opus | Claude Opus 5 | 計画、難しい判断、最終統合 |
sonnet | Claude Sonnet 5 | 実装、編集、日常作業 |
gemini | Gemini 3.1 Pro | 調査、長文資料、別視点でのレビュー |
gpt | Codex gpt 5.6 | コード実装、テスト、技術レビュー |
重要なのは、モデルを並べたことではなく、引き継ぎがシステムの一級の操作になっていることだ。誰が何を完了し、何を残作業として渡したかが記録され、成果物の実在が検証される。人間が手で運んでいたときには、そのどれも残っていなかった。
2. 設計の核 ― APIではなくCLIを統率する
この選択は、単なる実装上の都合ではない。以降のほぼすべての設計がここから導出される。
APIではなくCLIを起動することで得られるものは3つある。
- サブスクリプションのまま使える。 追加のAPIキーもトークン課金も発生しない
- ツールに縛られない。 各ベンダーのCLIは独立して更新され、こちらは呼び出し方を合わせるだけでよい
- ベンダーが用意した実行環境をそのまま使える。 サンドボックス、権限モデル、セッション管理を自前で作らずに済む
代償もはっきりしている。相手はHTTPリクエストではなく、プロセスである。以下は、この方式を選んだことで必要になった設計と、その根拠である。
| 設計上の決定 | その根拠 |
|---|---|
| 使用量を回数と実行時間で管理する | サブスクには「1回いくら」という価格が存在しないため |
| アダプタ層を設ける | CLIごとにフラグ、セッションの扱い、出力形式が異なるため |
| プロセスグループ単位で起動し、タイムアウトで終了させる | 応答しないプロセスを外から確実に止める手段が他にないため |
| 環境変数を絞ってから起動する | 親の環境を継承すると、CLIが作業場所を取り違えるため |
| 成果物の実在を毎回確認する | 終了コードが0でも、期待した場所にファイルがあるとは限らないため |
APIを叩く実装なら、これらの多くは考えなくてよい。CLIを選んだ時点で、プロセス管理と検証はこちらの責任になる。 それを引き受ける代わりに、課金とロックインから自由になる、という取引だ。
3. オーケストレーションの4層と、いまの位置

この種のシステムを作ると、「AIが次の担当を自分で決める」方向に進みたくなる。マルチエージェントらしい絵になるからだ。
gcの初期版では、それを意図的に実装していない。
理由は、暴走したときに止められないからである。3体が互いを呼び合えば、こちらが寝ている間にサブスクの枠を使い切り、作業ディレクトリが荒れる。そういう朝を迎えたくない。
代わりに置いたのが、上の4層という整理だ。
| 層 | 役割 | いまの担当 |
|---|---|---|
| 意図理解層 | 「何がしたいか」を解釈する | 人間 |
| 計画層 | 役割分担と実行順序を決める | 人間(メンションの並びとして表現) |
| 実行層 | 経路どおりに起動し、文脈と成果物を引き継ぐ | gc |
| 保護層 | 上限・タイムアウト・停止・分離・監査 | gc |
つまりgcは、オーケストレーションの実行基盤であって、指揮者ではない。 指揮者はいまのところ人間が務めている。
この順序で作ったのには理由がある。
マルチエージェントで難しいのは「誰に何をやらせるか決めること」ではない。決めたとおりに確実に実行し、失敗を検知し、確実に止めることである。
計画層を先に作れば、動くデモは早くできる。しかし実行層と保護層が薄いままでは、動くけれど任せられないものになる。止められないものは、結局のところ使えない。
4. アーキテクチャ

処理の流れは一本道である。
gateway.py はDiscordの入出力だけを担当し、モデルの知識を持たない。受信側では6つの条件(サーバー、チャンネル、投稿者、Bot判定、Webhook判定、添付の有無)をすべて満たすメッセージだけを受理する。送信側では2000字での分割、コードブロックをまたぐ場合の再オープン、レート制限時の待機、再試行による重複投稿の防止を扱う。
router.py が中核だ。メンションを出現順に解析して経路を確定し、ホップごとにエージェントを起動する。このとき、依頼の原文をそのまま全員に渡すのではなく、メンションの位置で指示を分割する(後述)。実行後は構造化出力を検証し、申告された成果物が実在するかを確認する。
adapters/ が各CLIの差異を吸収する。フラグの綴りも、セッションの継続方法も、構造化出力の返し方も、3つのCLIですべて異なる。ここを厚くしたおかげで、router以降はCLIの違いを意識しない。
guards.py はパスとコマンドを検査する。ディレクトリからの脱出、シンボリックリンク経由の回避、禁止領域への到達、そして危険なコマンドの実行を止める。
db.py はSQLiteでジョブ、キュー、セッション、成果物、イベント、日次の使用量を永続化する。
ジョブDBは、実質的にワークフローエンジンである
作ってから気づいたのではなく、ジョブを永続化して状態機械で管理すると決めた時点で、それはワークフローエンジンだった。
いまDBが持っているものを並べると、その性格がはっきりする。
- 状態機械(
queued → running → waiting_model → completed / failed / timed_out / cancelled / failed_recovery) - ホップ単位の進捗と担当
- 成果物の系譜(どのホップが何を作ったか)
- タイムスタンプ付きのイベントログ
- 使用量の集計
これは進捗ダッシュボードにも、事後監査にも、そのまま使えるデータモデルだ。
規模
| 実装 | 3,746行(Python) |
| テスト | 2,048行 / 200件 |
| 外部ライブラリ | Discord接続用の1つのみ。他はすべて標準ライブラリ |
| 制作期間 | 1日 |
外部依存を増やさなかったのは意図的である。エージェントにパッケージ導入を禁止している以上、システム自身の依存も最小にしておきたかった。JSON Schemaの検証も手書きした。
5. 統制の設計
保護層に何を入れたか。4つある。
5.1 経路と上限は人間が握る
- 実行するのは、利用者が明示した順番だけ
- 最大3ホップ、1モデル10分、ジョブ全体35分で必ず止まる
- AIが出力の中で「次は◯◯へ」と提案しても、記録と表示はするが実行経路には採用しない
将来的に自律引き継ぎを入れる場合も、エージェント単位で1体ずつ有効化し、上限とタイムアウトは維持する方針である。
5.2 指示をホップ単位に分ける
複数エージェントへの依頼を、原文のまま全員に渡すと分業が壊れる。実際に起きた例を挙げる。
@sonnet plan.md を作って @gpt それをレビューして
これを投げたところ、sonnetが plan.md と review.md の両方を作って返してきた。 gptの担当分まで終わらせていたのである。
当然だった。全ホップに原文を渡していたので、sonnetからは「レビューして」という指示も見えていた。真面目なエージェントほど全部やってしまう。
そこでメンションの位置を境界として指示を切り出し、各エージェントには自分の担当分を「あなたへの指示」として渡す。原文全体は文脈として別枠で添え、「他の担当の分は実行しないこと」と明記する。
5.3 AIの完了報告を、そのままは信じない
これが設計上もっとも重要な判断だった。
あるエージェントに「artifacts/smoke.md を作って」と指示し、「作成しました」という応答を得た。ジョブディレクトリを見ると、ファイルがない。探すと、まったく別の場所にあった。
/private/tmp/claude-501/-Users-hiro-Documents-cloude-4c7d3abd-...-claude_smoke2/artifacts/smoke.md
原因は環境変数だった。親プロセスから継承した変数を見て、CLIが自前の作業ディレクトリを作り、相対パスをそちら基準で解決していた。エージェントは嘘をついていない。指定した場所とは違う場所に、確実に作っていた。
以来、成否の判定はこの4条件の組み合わせで行っている。
- 構造化出力に必要な内容が揃っていること
- 申告された成果物が実在すること
- その成果物がジョブの
artifacts/配下にあること - タイムアウトしていないこと
「AIがそう報告した」ことと「システム上で確認できた」ことを分ける。 これが原則になった。
5.4 既存システムから分離する
同じMac上では、別のDiscordゲートウェイが24時間稼働している。gcの導入や障害がそちらへ波及してはならない。
プロセス、launchd設定、ログ、作業ディレクトリ、Bot、チャンネルをすべて分けた。作業の前後には、既存システムのPID、launchdの登録状態、ログ更新時刻を読み取り専用で記録し、変化がないことを確認する。
この確認が効いた場面がある。調べると、既存ゲートウェイの起動コマンドには --replace が付いていた。つまり別のプロセスが同じコマンドを実行した瞬間、稼働中のゲートウェイが置き換えられて停止する。 エージェントがバージョン確認のつもりで叩くだけで事故になる。そのコマンド名を、システム全体の禁止リストに入れた。
テストが見つけたもの
200件のテストが見つけたのは、他人のバグではなく自分で書いたバグ5つだった。
- 禁止コマンド検査の抜け穴 3件(パイプ経由、バッククォート内、改行区切り)
- DBの並行性の問題 1件(同一接続をスレッド間で共有していた)
- 停止処理の競合 1件(「停止した」と返しながらプロセスが走り続ける)
いずれもコードを読んでいて気づいたものではない。実際にプロセスを起動し、実際に強制終了シグナルを送って初めて出てきた。外部CLIを組み合わせるシステムでは、正常系の応答だけを試しても意味がない。
6. これから
6.1 並行実行・条件分岐・ループ
いまの経路は一次元の列である。将来はこれを有向グラフにしたい。ただし「経路をDAGにするだけ」では済まない。
| 拡張 | 必要になるもの | いまの設計との衝突 |
|---|---|---|
| 並行実行 | ノード単位の同時実行、全親の完了待ち | 同時実行数1の撤廃。停止対象が複数のプロセスグループになる |
| 条件分岐 | 分岐条件の評価器 | 判定にAIの出力を使えば「報告を信じない」原則と正面衝突する |
| ループ | 終了条件と最大反復回数 | 暴走防止の要。ホップ上限の考え方を引き継ぐ必要がある |
特に条件分岐が難しい。AIの申告を信じないと決めたシステムが、分岐の判断ではAIを信じざるを得ない。ここをどう設計するかは、まだ答えが出ていない。
並行実行にも別の問題がある。並行度がそのまま使用量のリスクになる。 いまの上限(1日60回・合計3時間)は逐次実行を前提とした数字であり、並行化すれば見直しが要る。
6.2 ダッシュボード
前述のとおり、ジョブDBはワークフローエンジンのデータモデルを持っている。ならば可視化は自然な発展だ。
ただし価値の中心は進捗管理ではなく、事後監査にあると考えている。どのジョブがどのファイルを書き換えたか、どのコマンドが検査で弾かれたか、どの権限拒否が記録されたか。エージェントに読み書きさせる範囲を広げるほど、「何をされたかを後から追えること」の重みが増す。必要な情報はイベントテーブルにすでに入っている。
6.3 そして、蓄積
これが一番大きな課題である。
いまのgcは、セッションをジョブ単位・エージェント単位で切っている。同じジョブ内で同じ担当が二度登場すれば記憶は続くが、ジョブをまたげば切れる。エージェント同士も、引き継ぎ文と成果物のパス以外は共有しない。別案件の文脈が混ざるのを防ぐための設計だ。
裏返せば、毎回ゼロから始めている。
蓄積の材料はある。全ジョブの引き継ぎ内容も、成果物も、検証で見つかった問題点も、すべてDBとファイルに残っている。欠けているのは取り出す仕組みだけだ。
しかし蓄積を入れると、この記事の主題と正面から衝突する。検証されていない知見を貯めることは、間違いを固着させることだからだ。 一度誤った知見が入り込むと、以後のすべてのジョブがそれを前提に動く。しかもAIは、書いてあることを自信を持って引用する。
現時点で有力だと考えているのは、AI専用の記憶を作らないという方針だ。蓄積先を人間が読み書きできる場所(自分の場合はObsidianのノート)に置き、残すかどうかを人間が決める。ノートを残す・消すという行為が、そのまま承認の仕組みになる。 ベクトルDBに放り込んで検索させる方式より地味だが、この設計の原則とは整合する。
オーケストレーションの次の課題は、並行実行ではなく蓄積である。そう考えている。
7. まとめ
gcは、Discordを入口として複数のAIエージェントを順に動かし、成果物を引き継ぐシステムだ。設計の中心に置いたのは3点である。
- APIではなく、すでに契約しているCLIをプロセスとして統率する。 サブスクのまま使え、ツールに縛られない。代わりにプロセス管理と検証は自分の仕事になる
- 実行経路と上限は人間が握る。 指揮者を自動化する前に、確実に実行し確実に止める基盤を作る
- AIの報告ではなく、構造化出力と成果物の実在で完了を判定する。 「そう報告した」と「確認できた」を分ける
1日で書いた5,794行のうち、3分の1はテストである。そのテストが見つけたのは、自分で書いたバグ5つだった。外部のCLIを組み合わせて何かを作るなら、正常系ではなく、空応答・誤った保存先・認証待ち・通知失敗・停止競合まで含めて試すことを勧める。
実装中に踏んだCLI固有の罠は、それ自体が分量のある話になった。別記事にまとめる。
関連記事: 「CLIオーケストレーションで踏んだ罠カタログ」(執筆予定)


コメント