AI開発

【第3部】CLI互換とワーカープールで並行実装を回す

株式会社Atsumell|8分で読めます
CLI互換とワーカープールで並行実装を回す設計

# 【第3部】CLI互換とワーカープールで並行実装を回す

> 連載「Slack AIエージェント基盤を作る」全8部の第3部。

第2部でCLIをサーバーに上げると、次に欲しくなるのは並行実行だ。調査、実装、テスト、ドキュメント更新を1本ずつ待つのはもったいない。第3部では、複数のAI CLIを共通のジョブとして扱い、ワーカープールで安全に並行実装を回す考え方を掘り下げる。

前後で読む

AIエージェントを開発現場で使い始めると、すぐに「1つずつ待つのがもったいない」という感覚が出てくる。調査、実装、テスト、ドキュメント更新、レビュー観点の洗い出し。これらは人間が順番にやると時間がかかるが、AIエージェントなら分割して同時に進められる。

この設計の特徴は、Slackからの依頼を複数のCLIエージェントへ渡し、ワーカープールで並行処理できるようにしている点だ。単にAIを呼ぶのではなく、CLI互換の実行層を作り、その上でタスクを分散する。AIエージェントによる並行実装を、デモではなく運用できる形に近づけている。

OpenAI Codexのようなコード作業向けエージェントが出てくると、1人の開発者が複数の作業を同時に監督する開発スタイルが現実味を持つ。ただし、そのためにはCLIごとの差を吸収する設計が必要だ。

なぜCLI互換レイヤーを作るのか

AI CLIは、それぞれ得意なことも操作体系も違う。あるCLIはコード編集に強く、別のCLIは長文の設計整理に向いている。引数、対話形式、出力の癖、終了条件も揃っていない。

Slack側の実装が特定CLIに密結合すると、後から別のエージェントを試しにくい。そこでCLI互換レイヤーを作る。

互換レイヤーが担うのは、次のような役割だ。

  • 依頼文をCLIごとの入力形式へ変換する
  • 実行ディレクトリを準備する
  • 標準出力とエラーログを収集する
  • 完了、失敗、キャンセルを同じイベントとして扱う
  • 成果物のパスや要約をSlackへ返せる形にする
  • タイムアウトや再試行を共通化する

この層があると、Slackのメッセージ処理は「どのCLIを使うか」を意識しすぎなくて済む。エージェントの選択を意図理解層に任せ、実行層は共通のジョブとして扱える。

ワーカープールで並行実装を制御する

並行実装で一番避けたいのは、勢いだけでジョブを増やして環境を壊すことだ。AIエージェントはCPU、メモリ、外部API、リポジトリの差分を使う。無制限に起動すると、コストも品質も読めなくなる。

ワーカープールは、同時実行数を管理するための仕組みだ。Slackから複数の依頼が来ても、すべてを即時実行せず、キューに積んで空きワーカーへ割り当てる。これにより、次の制御ができる。

  • 全体の同時実行数を制限する
  • プロジェクト単位で実行数を分ける
  • 重いタスクを後回しにする
  • 優先度の高い依頼を先に流す
  • 失敗したタスクだけ再実行する

開発タスクは、単純な非同期処理と違って副作用がある。複数のAIエージェントが同じファイルを同時に編集すると衝突する。そこで、タスクの分割単位も設計対象になる。調査と実装、テスト追加とドキュメント更新のように、衝突しにくい粒度へ分けるのが扱いやすい。

Slackからの依頼を分割する設計

たとえば「この機能を実装して、テストも書いて、記事用にまとめて」とSlackで頼まれたとする。人間なら頭の中で作業を分ける。AIエージェント基盤では、この分割を明示的に行う。

  • 仕様確認ワーカー
  • コード調査ワーカー
  • 実装ワーカー
  • テストワーカー
  • ドキュメントワーカー
  • 統合レビュー役

すべてを同時に走らせるわけではない。調査の結果を見てから実装へ進むタスクもある。実装が終わってからテストを書くタスクもある。ワーカープールは「並行できるところだけ並行する」ための制御点になる。

Slack Bolt for JavaScriptで受けたメッセージをジョブ化し、内部で依存関係を持たせると、Slack上では1つの依頼に見えるまま、裏側では複数のワーカーが動ける。この設計は、ユーザー体験を複雑にせず、実行だけを高速化する。

並行実装の品質を保つチェックポイント

AIエージェントによる並行実装は、速さだけを見ると失敗する。大切なのは、最後に統合できる形で走らせることだ。

品質を保つには、次のチェックポイントを置く。

  • 各ワーカーに渡す目的を短く固定する
  • 触ってよいファイル範囲を絞る
  • 生成物の形式を決める
  • 最終統合役を置く
  • テストや静的解析を最後に必ず通す
  • Slackへ返す報告は成果と未解決点を分ける

ここでいう統合役は、人間でもAIでもよい。複数のワーカーが出した結果を読み、矛盾や重複を取り除き、最終成果物として整える役割だ。OpenAIのコード生成ガイドが示すように、コード作業では要件、コンテキスト、評価の与え方が結果を左右する。並行実装では、この評価点がさらに大きくなる。

並行実装で先に決める境界

並行実装で先に決めるのは、最大worker数ではない。どの状態を共有せず、どの状態をスレッドに固定し、どの失敗ならretryしてよいかだ。ここを誤ると、worker数を増やした瞬間に認証、session、成果物、Slack返信が混ざる。

そのため、worker lockとthread lockを別物として説明する。worker lockはCLI homeを守る。thread lockは会話文脈を守る。event adapterは進捗表示を守る。thread affinityは継続実行を守る。これらを分けているから、別スレッドは並列化しながら、同じスレッドの「続き」は壊さずに扱える。

企業内で導入するなら、並列数の上限よりも、stale lockの復旧、認証競合の回避、resume sessionの移動禁止を先に決めるべきだ。

worker poolは「空きプロセスへ投げる」だけではない

並行実装で本当に難しいのは、同時実行数を増やすことではない。認証、セッション、ロック、Slackスレッドの文脈を壊さずに増やすことだ。

この基盤では、workerごとに独立したhomeを持たせる。各workerはCLIのconfig、認証状態、session cache、lockを分けて持つ。本番並列実行で同じ認証ファイルをコピーして共有すると、refresh tokenやsession状態が衝突するので避ける。

persistent-worker-root/
├── worker-0/
│   ├── cli-home/
│   ├── exec.lock/
│   └── heartbeat.json
├── worker-1/
│   ├── cli-home/
│   ├── exec.lock/
│   └── heartbeat.json
└── worker-n/
    ├── cli-home/
    ├── exec.lock/
    └── heartbeat.json

共有ドキュメントには機密値の名前を出さなくてもよい。構造としては「workerごとに独立認証」「worker homeの共有禁止」「本番ではshared auth copyを許可しない」という考え方が重要になる。

lockはfilesystem mkdirで取る

worker lockは、分散ロックサービスを入れなくても、まずはfilesystemの`mkdir`で十分機能する。`exec.lock`ディレクトリを作れたworkerだけが実行権を持つ。既に存在すれば使用中とみなす。

lock内には、少なくともworkerId、jobId、pid、startedAt、heartbeatAtを残す。

{
  "workerId": 3,
  "jobId": "job_...",
  "pid": 41234,
  "startedAt": "2026-05-31T10:00:00.000Z",
  "heartbeatAt": "2026-05-31T10:05:00.000Z"
}

CLIプロセスは長時間動くため、コンテナ停止やkillでlockだけが残ることがある。そこでheartbeatが一定時間更新されていなければstale lockとして回収する。旧形式lockのようにheartbeat情報がないものは、より短い猶予で回収する。これで「死んだlockでworkerが永久に詰まる」状態を避けられる。

thread affinityで会話文脈を守る

別スレッドなら並列に走らせたい。一方、同じSlackスレッドは直列化したい。ユーザーが同じスレッドで「続けて」「やっぱりこの条件も入れて」と言った場合、前の処理と同時に走ると成果物や文脈が壊れる。

そこで、Slack threadごとのsession directoryにworker idを保存する。次回同じthreadで実行するとき、そのworkerを優先する。resume対象のsession idがあっても、別workerのhomeに必要なsession状態がなければ継続できない可能性があるからだ。

ロック守る対象目的
worker lockworker home認証、config、session状態の競合防止
thread lockSlack thread会話文脈と実行順序の保護
artifact lock成果物出力先同名ファイルや途中生成物の破壊防止

Codex系CLIとClaude系CLIでstream event形式が違う場合は、adapterでClaude-likeなeventへ寄せる。これにより、既存のSlack progress reporterは大きく変えなくて済む。ユーザーから見れば「コマンド実行中」「画像生成中」「ファイル作成中」と表示され、裏側のCLI差分は隠せる。

互換レイヤーは移行用の薄いaliasではない

既存スキルが特定CLIを呼んでいる場合、互換ラッパは単なるコマンド名の置き換えでは済まない。引数、標準入力、画像添付、stream event、終了コード、session resume、worker割り当てまで吸収する必要がある。

この構成にしておくと、既存スキルは大きく変えずに、実行エンジンだけを差し替えられる。たとえば、元のCLIがstream-jsonを出す前提なら、Codex側のeventをClaude-likeなeventへ変換し、既存のSlack progress reporterへ渡す。Slack上では「コマンド実行中」「画像生成中」「成果物確認中」といった進捗が保たれる。

retryは可用性より文脈維持を優先する

worker poolでは、一時的な認証エラーやworkerの不調に対して、別workerへのretryを考えたくなる。ただし、resume sessionがある実行を安易に別workerへ移すと、前回の状態やCLI home内のsessionが見つからず、文脈が壊れる。

そのため、retryは新規実行に限定する。既存sessionを継続する場合は、同じthread affinityを優先する。これは一見保守的だが、Slackスレッド上で「続きやって」と言われたときに、過去の作業ディレクトリやsession状態を失わないための判断だ。

並行実装の性能はworker数だけでは決まらない。worker lock、thread lock、event adapter、retry条件を合わせて設計して初めて、複数のAIエージェントが同時に動いても壊れない。

関連記事

連載「Slack AIエージェント基盤を作る」

  1. 第1部 Slackを業務OSにする
  2. 第2部 CLIをサーバーに上げる
  3. 第3部 並行実装を回す
  4. 第4部 Slack発言をジョブ化する
  5. 第5部 意図理解で振り分ける
  6. 第6部 記憶を設計する
  7. 第7部 定期実行で運用する
  8. 第8部 安全性と観測性を固める
  9. まとめ Slack AIエージェント基盤の全体設計

あわせて読みたい既存記事

#AIエージェント#ワーカープール#CLI互換#並行実装#Codex

AIエージェントの並行実装基盤を設計しませんか?

Atsumellはworker分離、認証境界、成果物管理まで含めて、社内で使える並行実行設計を支援できます。

相談する