複数Claude Maxアカウントのリソース振り分け機能

Issue
#329
作成日
2026-04-07
ステータス
設計完了
変更規模
新規2ファイル + 既存4ファイル小改修
設計者
W層 architect
フェーズ
Phase 0(実証→本番)

1. 概要

3つのClaude Maxアカウントを使い分け、rate limitの影響を最小化する仕組みを実装する。CLAUDE_CONFIG_DIR + キーチェーンによるアカウント分離は技術的に実証済み。

解決する問題

設計方針

2. アーキテクチャ図

変更箇所の全体像: bot.py → claude_runner.py → account_pool.py(新規) → Claude CLI subprocess
bot.py / router.py claude_runner.py run_claude() account_pool.py AccountPool acquire() / release() mark_rate_limited() 新規 Account A (CLI) Account B (CLI) Account C (CLI) pool_status.json AI Ops Monitor 読取専用で監視 watcher.py 改修 read config.py(設定追加) Rate Limit 発生時のフロー RateLimitError mark_rate_limited() 5分クールダウン acquire() 別アカウント取得 即リトライ実行 全アカウント 制限中 → CRITICAL通知

3. account_pool.py の設計

3.1 データモデル

@dataclass
class AccountInfo:
    name: str                  # 識別名(例: "account-a")
    config_dir: str            # CLAUDE_CONFIG_DIR のパス
    active_count: int = 0      # 現在実行中のプロセス数
    cooldown_until: float = 0  # rate limit クールダウン終了時刻(time.time()ベース、0=制限なし)
    total_runs: int = 0        # 累計実行回数(統計用)
    last_rate_limit: float = 0 # 最後にrate limit検知した時刻

3.2 AccountPool クラス

class AccountPool:
    """Claude Maxアカウントのプール管理

    アクティブプロセス数ベースのスコアリングで負荷分散し、
    rate limit発生時は該当アカウントをクールダウンして別アカウントに振り替える。
    """

    def __init__(self, accounts: list[dict]) -> None:
        """アカウント一覧から初期化
        accounts: [{"name": "account-a", "config_dir": "/path/to/.claude-a"}, ...]
        """

    def acquire(self, *, exclude: str | None = None) -> AccountInfo | None:
        """最小スコアのアカウントを取得し、active_count += 1
        exclude: 除外するアカウント名(rate limit後のリトライ時)
        Returns: AccountInfo(利用可能なアカウントがなければ None)
        """

    def release(self, name: str) -> None:
        """プロセス完了時に active_count -= 1"""

    def mark_rate_limited(self, name: str, cooldown_seconds: int = 300) -> None:
        """rate limit検知時にクールダウン期間を設定(デフォルト5分)"""

    def _score(self, account: AccountInfo) -> float:
        """アカウントのスコアを計算(低い = 優先)
        クールダウン中 → float('inf')
        それ以外 → active_count
        """

    def status(self) -> list[dict]:
        """全アカウントの現在状態を返す(ダッシュボード / AI Ops用)"""

    def persist(self) -> None:
        """pool_status.json にアトミック書出し"""

    def all_limited(self) -> bool:
        """全アカウントがクールダウン中かどうかを返す"""

3.3 スコアリングアルゴリズム

条件スコア説明
クールダウン中float('inf')rate limit中のアカウントは選択されない
通常active_count実行中プロセスが少ないアカウントを優先
同スコア時total_runs で破壊累計実行数が少ない方を選択(均等分散)
選択ロジック: min(available_accounts, key=lambda a: (a.active_count, a.total_runs))

3.4 pool_status.json の構造

{
  "accounts": [
    {
      "name": "account-a",
      "config_dir": "/Users/mnmladmin/.claude-a",
      "active_count": 2,
      "cooldown_until": null,
      "total_runs": 145,
      "last_rate_limit": null
    },
    {
      "name": "account-b",
      "config_dir": "/Users/mnmladmin/.claude-b",
      "active_count": 1,
      "cooldown_until": 1743984000.0,
      "total_runs": 132,
      "last_rate_limit": 1743983700.0
    },
    {
      "name": "account-c",
      "config_dir": "/Users/mnmladmin/.claude-c",
      "active_count": 0,
      "cooldown_until": null,
      "total_runs": 128,
      "last_rate_limit": null
    }
  ],
  "updated_at": 1743984001.0
}

保存先: logs/pool_status.json(既存の rate_limit_status.json と同階層)

4. 配分アルゴリズム — アカウント選択〜実行〜リトライのフロー

run_claude() 呼出 pool.acquire() 取得成功? 全アカウント制限中 NG 既存backoffで待機 env[CLAUDE_CONFIG_DIR] = account.config_dir OK Claude CLI subprocess実行 結果 pool.release() & return 成功 mark_rate_limited() 5分クールダウン設定 Rate Limit pool.release(現アカウント) pool.acquire(exclude=現) 別アカウントで即リトライ 別アカウントでリトライ 完了 pool.release() & raise 他エラー

5. claude_runner.py の変更

5.1 変更方針

_run_claude_inner() 内で、subprocess起動直前にアカウントを取得し、完了時(正常/異常問わず)にリリースする。rate limit検知時は別アカウントでリトライする。

5.2 変更箇所(擬似コード)

# _run_claude_inner() の変更イメージ(約30行追加)

from bot.account_pool import pool  # グローバルインスタンス

async def _run_claude_inner(..., *, _account_name: str | None = None, ...):
    # ① アカウント取得
    account = pool.acquire(exclude=_account_name) if pool.enabled else None

    # ② env構築時に CLAUDE_CONFIG_DIR を注入
    env = {k: v for k, v in os.environ.items() if k not in _env_exclude}
    if account:
        env["CLAUDE_CONFIG_DIR"] = account.config_dir

    try:
        # ... 既存のsubprocess実行ロジック(変更なし) ...
        pass
    finally:
        # ③ 必ずリリース
        if account:
            pool.release(account.name)

    # ④ RateLimitError 検知時: 別アカウントでリトライ
    # (既存のRateLimitError送出箇所を変更)
    if is_rate_issue and not result_text.strip():
        if account:
            pool.mark_rate_limited(account.name, cooldown_seconds=wait_seconds)
            # 別アカウントで即リトライ(1回のみ)
            if not _account_name:  # 初回のみリトライ
                return await _run_claude_inner(
                    ..., _account_name=account.name, ...
                )
        raise RateLimitError(wait_seconds=wait_seconds)

5.3 インターフェース変更

外部インターフェースの変更なし: run_claude() の引数・戻り値は一切変わらない。アカウント選択は内部で透過的に行われる。

内部関数 _run_claude_inner()_account_name: str | None 引数を追加(プライベート、リトライ時の除外用)。

6. Rate Limit 対応

6.1 検知〜リトライの流れ

ステップ処理担当
1Claude CLI が rate_limit_event を出力Claude CLI
2既存パースロジックで rate_limited=True を検知claude_runner.py(既存)
3pool.mark_rate_limited(name, wait_seconds) でクールダウン設定claude_runner.py(追加)
4pool.release(name) で現アカウントを解放claude_runner.py(追加)
5pool.acquire(exclude=name) で別アカウントを取得claude_runner.py(追加)
6a別アカウント取得成功 → _run_claude_inner() を再帰呼出claude_runner.py(追加)
6b全アカウント制限中 → 既存の RateLimitError をそのまま送出claude_runner.py(既存)

6.2 クールダウンの設計

パラメータ根拠
デフォルトクールダウン300秒(5分)Claude Max の rate limit ウィンドウは通常5時間。5分で十分に回復する実績あり
CLI提供の待機秒数優先使用rate_limit_inforetry_after / resetsAt がある場合はそれを使用
クールダウン上限1800秒(30分)過大な値を防ぐ安全弁
リトライ回数1回(別アカウントで即リトライ)2回以上のリトライは全アカウント制限のリスク。既存の Task.retry_count と組み合わせて上限管理

6.3 既存リトライ機構との関係

二重リトライの防止: アカウント切替リトライ(account_pool)と既存のタスクリトライ(Task.retry_count)は別レイヤー。

7. AI Ops 監視

7.1 監視対象

AI Ops Monitor(agents/ai-ops/monitor/watcher.py)に _check_pool_status() メソッドを追加。

async def _check_pool_status(self) -> None:
    """アカウントプールの状態を監視

    チェック項目:
    1. 全アカウントがクールダウン中 → CRITICAL通知
    2. 1アカウントが長時間(15分超)クールダウン中 → WARNING通知
    3. 特定アカウントのrate limit頻度が高い → INFO通知(偏り検出)
    """

7.2 通知条件

条件レベル通知先クールダウン
全アカウントが同時にクールダウン中 CRITICAL #bot-info 5分
1アカウントのクールダウンが15分超 WARNING #bot-info 30分
1アカウントが1時間に3回以上rate limit INFO #bot-info 1時間

7.3 データ読取

既存の reader.pyread_pool_status() メソッドを追加。logs/pool_status.json を読取専用でパースする。

# reader.py に追加
@dataclass(frozen=True)
class PoolAccountRecord:
    name: str
    config_dir: str
    active_count: int
    cooldown_until: float | None
    total_runs: int
    last_rate_limit: float | None

def read_pool_status(self) -> list[PoolAccountRecord]:
    """pool_status.json からアカウント状態を読取"""

8. config.py の変更

8.1 追加設定

class Settings(BaseSettings):
    # ... 既存設定 ...

    # アカウントプール設定
    # 形式: "name1:/path1,name2:/path2,name3:/path3"
    claude_accounts: str = ""
    # クールダウン秒数(デフォルト5分)
    pool_cooldown_seconds: int = 300
    # クールダウン上限(デフォルト30分)
    pool_cooldown_max: int = 1800

8.2 .env の設定例

# アカウントプール(カンマ区切り、各 "名前:config_dirパス")
CLAUDE_ACCOUNTS=account-a:/Users/mnmladmin/.claude-a,account-b:/Users/mnmladmin/.claude-b,account-c:/Users/mnmladmin/.claude-c

# クールダウン秒数(任意、デフォルト300)
# POOL_COOLDOWN_SECONDS=300
未設定時の挙動: claude_accounts が空文字の場合、AccountPool.enabled = False となり、従来どおり単一アカウントで動作する(後方互換性)。

9. 変更対象ファイル一覧

ファイル操作変更内容行数目安
bot/account_pool.py 新規 AccountPool クラス、pool_status.json 永続化、グローバルインスタンス初期化 〜120行
bot/claude_runner.py 改修 _run_claude_inner() にアカウント取得/解放/切替リトライを追加 +30行
bot/config.py 改修 claude_accounts, pool_cooldown_seconds, pool_cooldown_max の3設定を追加 +6行
agents/ai-ops/monitor/watcher.py 改修 _check_pool_status() メソッドを追加。メインループの asyncio.gather に追加 +40行
agents/ai-ops/monitor/reader.py 改修 PoolAccountRecord データクラス + read_pool_status() メソッド追加 +20行
.env 改修 CLAUDE_ACCOUNTS 環境変数を追加 +2行

10. 不確実要素・リスク

項目リスク対策
Rate limitの粒度 Anthropicの公開情報にrate limitがアカウント単位かIP/組織単位かの明記なし。
Phase 0で実測検証が必要。もしIP単位なら3アカウントでも効果なし。
→ 実測結果に基づき、Phase 1でIPローテーション等の代替策を検討
CLAUDE_CONFIG_DIRの互換性 Claude CLI のバージョンアップで挙動が変わる可能性あり。
→ 技術実証済み(Issue #329確認事項)。CLI更新時に動作確認を入れる
セッション継続(--resume) セッションIDはアカウント固有。rate limitリトライで別アカウントに切り替えると --resume が使えない。
→ リトライ時は新規セッションで実行(one-shot)。プロンプトに必要なコンテキストは含まれているため問題なし
active_countの不整合 プロセスkill等で release() が呼ばれないケース。
try/finally で確実にリリース。加えて ProcessRegistry との突き合わせで定期補正(AI Ops)
キーチェーンアクセス 3アカウント分のOAuthトークンがキーチェーンに格納されている前提。
→ 各アカウントの claude auth status で事前確認するスクリプトを用意

11. Phase 0 スコープ

11.1 Phase 0 でやること

  1. bot/account_pool.py 新規作成
  2. bot/claude_runner.py にアカウント選択ロジック組込み
  3. bot/config.py に設定追加
  4. .env に3アカウント設定
  5. 単体テスト: AccountPool の acquire/release/mark_rate_limited
  6. 統合テスト: 実際に3アカウントでCLI実行し、アカウント振り分けを確認
  7. 実測検証: 1アカウントを意図的にrate limitさせ、別アカウントへの切替が有効か確認

11.2 Phase 0 でやらないこと(Phase 1以降)

11.3 Phase 0 完了条件

12. セキュリティ考慮

入力バリデーション

機密情報

アトミック書込み

13. トレードオフ・代替案

検討した代替案

概要採用/却下理由
A. ラウンドロビン 順番にアカウントを割り当て 却下 負荷の偏りを考慮できない。長時間タスクがあると特定アカウントに偏る
B. スコアベース(採用) active_count最小のアカウントを選択 採用 実装がシンプルかつ負荷均等化に効果的。active_countは既にProcessRegistryで追跡中
C. 重み付きスコア active_count + rate_limit_history でスコア計算 却下 Phase 0では過剰。rate limitの実測データが不足している段階で重みを調整するのは時期尚早
D. 外部プロキシ HAProxy等でCLI実行をプロキシ 却下 Claude CLIはsubprocess実行のため、HTTPプロキシは不適。アーキテクチャの複雑化