kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

サブエージェントのジレンマを解決する:外部状態ファイルパターンによる独立性とフィードバックの両立

こんにちは。認証グループのいまひろです。

認証グループでは、Jiraの課題として起票されたユーザーストーリーに対して、Gherkin形式の確認テストの作成からプロダクトコードの実装までを一つのClaude Codeのカスタムコマンドで自動化しています。現在は単一のエージェントとして処理していますが、以下の課題に直面しています。

  • コンテキストの混在:テスト作成者と実装者の視点が混ざり、品質が低下する
  • コンテキストウィンドウの圧迫:複数リポジトリを扱うと情報が溢れる

この課題を解決するため、Claude Codeのサブエージェント機能の導入を試みましたが、機能的に一長一短な部分があり、採用に踏み切れていないのが現状です。

今回は、現状のサブエージェントの標準機能では解決できない制約と、それを改善するためのアプローチについてご紹介したいと思います。

サブエージェント導入の試みと挫折

従来の単一エージェント構成では、すべての視点が混在し、単一コンテキストであるが故のバイアスが生まれがちでした。

単一エージェント

一方、サブエージェントは、メインのClaude Codeセッションから別のエージェント(カスタムエージェント)を呼び出す機能となり、各サブエージェントは独立したコンテキストを持ち、特定の役割に特化して動作します。

そのため、サブエージェントを導入することにより、単一エージェントが抱えていたコンテキストの混在およびコンテキストウィンドウの圧迫の課題が解決されることを期待していました。

期待された利点

1. 独立したコンテキストによる視点の明確な分離

サブエージェント構成では:

親コマンド
├─ テスト作成エージェント(独立したコンテキスト)
├─ 実装エージェント(独立したコンテキスト)
└─ レビューエージェント(独立したコンテキスト)

それぞれが独立した視点を保持するため、真の意味での多様な視点が実現され、プロダクトコードの品質向上につながることが期待されました。

2. コンテキストウィンドウの効率的な活用

認証グループでは、複数のリポジトリにまたがる開発が日常的です。単一エージェントでは、すべてのリポジトリ情報がコンテキストに載り、すぐに限界に達していました。

サブエージェントでは各エージェントが必要最小限のコンテキストだけを持つため、コンテキストウィンドウを効率的に活用できると期待されました。

致命的な問題:フィードバックループの断絶

実際にサブエージェントを導入して運用を試みたところ、重要な制約が明らかになりました。

メインセッションから呼び出されたサブエージェントは、応答を返すとコンテキストが失われます

親コマンド → サブエージェント起動 → 処理実行 → 結果返却 → コンテキスト破棄

この仕組みでは、以下のようなフィードバックループが実現できません。

  • テスト作成時の人間とのフィードバックループ:レビュー結果をコンテキストを維持した状態で同じエージェントが修正し、そのループを繰り返すことができない
  • 実装とレビューのフィードバックループ:レビューで指摘された問題をコンテキストを維持した状態で同じエージェントが修正し、そのループを繰り返すことができない

サブエージェント(標準)

結果として、サブエージェントの導入は一旦見送られました。独立したコンテキストという利点は魅力的でしたが、フィードバックループが実現できないという欠点があまりにも大きかったためです。

新たなアプローチ:外部状態ファイルパターン

サブエージェントの導入を見送ってから時間が経ち、新しいアプローチを試すことにしました。それが、外部ファイルによる状態管理です。

各エージェントが自身の状態を外部ファイルとして保持することで、コンテキストが失われても次回の呼び出し時に状態を復元でき、独立性を保ちながらフィードバックループを実現できると考えました。

従来: サブエージェント起動 → 処理 → 応答 → コンテキスト破棄 → 終了

新方式: サブエージェント起動 → 処理 → 状態ファイル保存 → 応答
        ↑                                              ↓
        └──────────── 状態ファイル読み込み ←───────────┘
        (再起動時に前回の続きから作業可能)

この仕組みにより、独立したコンテキストを維持したまま、何度でもエージェントを再起動してフィードバックを反映できるようになります。

外部状態ファイルパターン

実証:マルチエージェントしりとりシステム

この概念が本当に機能するか実証するため、複数のAIエージェントがしりとりを行う単純化されたシステム構成を作成し検証してみました。この構成は、認証グループの開発フローと本質的に同じ構造を持っています。

しりとりシステム          認証グループの開発フロー(想定)
─────────────────        ─────────────────────────────
プレイヤーエージェント    実装エージェント
    ↓                        ↓
状態ファイルに記録        状態ファイルに記録
    ↓                        ↓
審判エージェント          レビューエージェント
    ↓                        ↓
判定結果を記録            レビュー結果を記録
    ↓                        ↓
次回起動時に              次回起動時に
状態を読み込み修正        状態を読み込み修正

システム構成

親コマンド (siritori)
├── プレイヤーエージェント (siritorist-01, 02, 03)
│   └── 状態ファイル: ~/.claude/work/siritorist-{XX}.md
├── 審判エージェント (judge)
└── ゲーム状態ファイル: ~/.claude/work/siritori.md

実際のコード

クリックで詳細表示 カスタムエージェント(プレイヤー)(プレイヤーの人数分作成する)

---
name: siritorist-01
model: sonnet
color: yellow
---

## 役割
あなたはしりとりを得意とするエージェントsiritorist-01だ

## 使用ワードルール

- 日本語のひらがな5文字程度

## あなたの状態ファイル
@~/.claude/work/siritorist-01.md

## やること

1. 現在のしりとりのワードを確認する
2. あなたの状態ファイルを読み込み、既に使用した使用済みワードを確認する
3. 現在のワードの最後の文字から始まり、使用済みワードで使用されていない、ルールに従ったワードを1つ考える
4. 選んだワードの最後の文字から始まる、ルールに従ったワードを1つ考え、あなたの状態ファイルのNGワード欄に追記する(Writeツールを使用)
5. 選んだワードを呼び出し元に返す

## 注意

- **他のプレイヤーの状態ファイルは参照してはいけない**

カスタムエージェント(審判)

---
name: judge
model: sonnet
color: red
---

## 役割
あなたはしりとりの審判だ

## プレイヤー

- siritorist-01
- siritorist-02
- siritorist-03

## 使用ワードルール

- 日本語のひらがな5文字程度

## 状態ファイル
@~/.claude/work/siritori.md

## プレイヤーごとの状態ファイル

@~/.claude/work/{プレイヤー名}.md

## ジャッジの基準

ジャッジ待ちワードをジャッジする:

1. **ルール違反チェック**
   - 現在のワードの最後の文字から始まっていること
   - 「ん」で終わっていないこと
   - 使用ワードのルールを満たしていること

2. **重複チェック**
   - チャレンジしたプレイヤーの状態ファイルを参照し、使用済みワードに記載されたワードでないこと

3. **NGワードチェック**
   - 全てのプレイヤーの状態ファイルを参照し、NGワードに記載されたワードでないこと

いずれかに違反している場合は、チャレンジ失敗でそのプレイヤーは失格になる。

## やること

1. 回答ワードについて、ジャッジの基準に従って、返答されたワードが適切かどうかを判定する
2. ジャッジして問題ない場合:
   - 履歴に記録(✅マーク付き)(Writeツールを使用)
   - 回答ワードを現在のワードに更新(Writeツールを使用)
   - プレイヤーごとの状態ファイルの該当プレイヤーの状態ファイルの使用済みワードに回答ワードを追記(Writeツールを使用)
3. ジャッジして問題がある場合:
   - そのプレイヤーを失格状態にする(Writeツールを使用)
   - 履歴に記録(❌マークと失格理由付き)(Writeツールを使用)

カスタムコマンド

---
argument-hint: [しりとりの最初のワード]
description: しりとりコマンド
---

## 役割

親としてプレイヤーのサブエージェント同士でしりとりを行うコマンド

## プレイヤー

- siritorist-01
- siritorist-02
- siritorist-03

## 状態ファイル

@~/.claude/work/siritori.md

- プレイヤーのリストから参加するエージェントを読み込み順番をシャッフルしてリストアップされている
- 負けたプレイヤーは参加できないので各プレイヤーに状態を持たせている
- 次のプレイヤーが分かるように現在のプレイヤーを保持している
- しりとりの現在のワードを保持している

## プレイヤーごとの状態ファイル

@~/.claude/work/{プレイヤー名}.md

各プレイヤーが自分で管理する状態ファイル。使用済みワード、NGワードが記録されている。

## 処理手順

0. 状態ファイル、プレイヤーごとの状態ファイルが存在している場合は削除する
1. 状態ファイルの初期化を行う
   - プレイヤーのリストを読み込み、順番をシャッフルしてリストアップ
   - 各プレイヤーの状態を「参加中」で初期化
   - しりとりの最初のワードを現在のワードにセット
   - 進行中の場合も全てリセット
2. 各プレイヤーの状態ファイルを初期化を行う
   - 使用済みワードリストを初期化
   - NGワードリストを初期化
3. 次の順番のプレイヤーをサブエージェントとして起動し、しりとりのワードを待つ

**プロンプト:**

[ここから]
しりとりの回答を返すこと

**作業情報:**
- 現在のワード: {現在のワード}

**指示:**
1. エージェントの指示を実行する

**注意:**
- NGワードの記録は行う
- 使用済みワードの記録は行わない
[ここまで]

4. サブエージェントからの回答があったら、その内容を画面出力する
5. ジャッジ用のサブエージェントを起動し、判定結果を待つ

**プロンプト:**

[ここから]
しりとりの判定を行うこと

**作業情報:**
- 現在のプレイヤー: {現在のプレイヤー}
- 現在のワード: {現在のワード}
- 回答のワード:{回答のワード}

**指示:**
1. エージェントの指示を実行する
[ここまで]

6. サブエージェントからの判定があったら、その内容を画面出力する
7. 順番に従い失格していない次のプレイヤーをピックアップする
   - 順番が最後の場合は先頭に戻る
   - 失格したプレイヤーはスキップ
8. 失格していないプレイヤーが1人より多い場合は、手順3に戻る
9. 失格していないプレイヤーが1人になったら、勝者を表示して終了する

## 注意

- プレイヤーの状態ファイルへの書き込みは初期化時のみ

フィードバックループ実現の工夫

工夫点1:状態を個別に外部ファイルで管理

親は専用の状態ファイルで、現在のプレイヤー、ワード、ラウンド数などを記録します。親は自身の状態ファイルのみ参照し、プレイヤーや審判エージェントの初期化や呼び出しのみ行います。

各プレイヤーは専用の状態ファイルで、使用済みワードとNGワードを記録します。プレイヤーは自身の状態ファイルのみ参照し、他のプレイヤーや親が管理する状態ファイルは参照しないことでコンテキストの汚染を防ぎます。

また、審判エージェントはすべての状態ファイルを参照することで、適切にジャッジを行うことができます。

工夫点2:フィードバックループの実現

しりとりシステムでは、以下のようなフィードバックループが実現されています。

【ラウンド1】
親コマンド → プレイヤーA起動
           → 状態ファイル読み込み(初回なので空)
           → ワード選択 & 状態ファイル保存 → [コンテキスト破棄]

親コマンド → 審判起動
           → 判定 & 使用済みワード追記 → [コンテキスト破棄]

【ラウンド2】
親コマンド → プレイヤーA起動(再度)
           → 状態ファイル読み込み
             ✅ 使用済みワード・NGワードが見える
           → 前回の経験を踏まえてワード選択

これが実現する価値:

  • 独立したコンテキスト:プレイヤーは他プレイヤーの内部状態を知らない
  • フィードバックループ:前回の自分の行動と結果を保持して繰り返すことができる

工夫点3:情報の受け渡し方法の使い分け

コマンドからサブエージェントを起動する場合、サブエージェントへの情報伝達には、2つの方法があります。

パターン1:起動時のプロンプトで値を渡す

コマンド内でTaskツールを使用し明示的にサブエージェントを起動する際、プロンプトで情報を渡すことができます。

使用する情報の特徴:

  • 親コマンドが管理している情報
  • その時点でのみ必要な情報
  • 毎回変わる可能性がある情報

例:現在のワード、回答ワード、現在のプレイヤー

パターン2:状態ファイルから値を取得

使用する情報の特徴:

  • エージェント自身が管理する情報
  • 長期的に保持したい情報
  • フィードバックループで蓄積される情報

例:使用済みワード、NGワード

なぜこの使い分けが重要か

親がすべての状態を管理してプロンプトで渡すことも可能ですが、そうすると親のコンテキストが肥大化し、そもそもの課題解決の意図から大きくズレてしまいます。

各コンテキストが知るべき情報、保持すべき情報を適切に判断し設計することで、親コマンドはシンプルに保ちながら、エージェントは自律的に動作し、フィードバックループが実現されることになります。

実践的な判断フロー

1. この情報は親コマンドが管理している?
   YES → プロンプトで渡す候補 / NO → 状態ファイル候補

2. この情報は毎回変わる可能性がある?
   YES → プロンプトで渡す / NO → 状態ファイル候補

3. この情報はフィードバックループで蓄積される?
   YES → 状態ファイル / NO → プロンプトで渡す候補

4. この情報を親コマンドが知る必要がある?
   YES → プロンプトで渡す / NO → 状態ファイル

認証グループへの適用

このパターンは認証グループの開発フローにおいて下記のような適用が考えられます。

【テスト作成フェーズ】
親コマンド → テスト作成エージェント起動(初回)
           → Gherkinテスト生成 & 状態ファイル保存

人間 → 「認証エラー時のリトライ処理のテストが不足している」

親コマンド → テスト作成エージェント起動(2回目)
           ├─ 状態ファイル読み込み
           │   ✅ 前回作成したテスト一覧
           │   ✅ 人間からのフィードバック
           ├─ 独立したコンテキスト内で改善
           │   (実装の詳細は知らない)
           └─ リトライ処理のテストを追加

【実装〜レビュー〜修正フェーズ】
親コマンド → 実装エージェント起動(初回)
           → コード実装 & 懸念点を状態ファイルに記録

親コマンド → レビューエージェント起動
           → コードレビュー & 指摘事項を状態ファイルに記録

親コマンド → 実装エージェント起動(2回目)
           ├─ 状態ファイル読み込み
           │   ✅ 前回の実装とレビュー指摘
           ├─ 独立したコンテキスト内で修正
           │   (レビュアーの思考過程は知らない)
           └─ 指摘事項を修正

親コマンド → レビューエージェント起動(2回目)
           ├─ 状態ファイル読み込み
           │   ✅ 前回の指摘と修正内容
           └─ 新鮮な目で再評価

実現可能な2つのフィードバックループ

しりとりシステムでの検証により、以下の2つのフィードバックループが実現できることが確認できました。

1. 人間とエージェントのフィードバックループ

人間のフィードバックを状態ファイルに記録し、エージェント再起動時に読み込むことで、独立性を保ちながら人間の意図を正確に反映できます。

2. エージェント間のフィードバックループ

各エージェントが状態ファイルを通じて情報を共有し、再起動時に読み込むことで、各エージェントが独立性を保ちながら、反復的に品質を向上できます。

この設計パターンがもたらすもの

1. 独立性とフィードバックの両立

現状のサブエージェントの機能として、以下はトレードオフだと考えられていました。

独立したコンテキスト ⇄ フィードバックループ
(視点の多様性)      (反復的改善)

しかし、外部状態ファイルパターンにより、両方を同時に実現することが可能になりました。

独立したコンテキスト(視点の多様性)
        +
外部状態ファイル(情報の永続化)
        =
フィードバックループ(反復的改善)

2. 人間との協調

AIエージェントだけで完結するのではなく、人間がループに入ることが重要です。各エージェントは独立しているため、人間の意図を純粋に反映でき、バイアスがかかりません。

3. スケーラビリティ

この設計パターンは、エージェント数が増えても破綻しません。各エージェントが状態ファイルを通じて協調し、独立性を保ちながら全体として機能を完成させることができます。

まとめ

Claude Codeのサブエージェント機能は、独立したコンテキストという強力な特性を持ちます。しかし、最初の導入試みではフィードバックループの断絶という課題がありました。

今回、外部状態ファイルパターンのアプローチにより、この課題を解決できる可能性が見えてきました。

とは言え、各エージェントが適切なコンテキストを保持しながらフィードバックループへ参加させるためには、各エージェントの設計、状態管理の設計、プロンプトの設計等の細かい調整が必要で、少し難易度が高い印象を持っています。

ただ、こうした論理的な設計については生成AIが強みを発揮するはずなので、チームで実現したいプロセスについて、今回のしりとりシステムをベースに指示を出すことで、Claude Code自身が適切な構成でシステムを構築してくれるのではないかと期待しています。