AIコーディング 2026.06.11

並列Claudeエージェントの無限ループ問題|Next.js環境での原因と修正手順

タグ:Claude Code / エラー解決 / Next.js / エージェント / デバッグ

並列Claudeエージェントで起きる無限ループ問題とは

複数のClaudeエージェントを同時に実行する際に、Next.js環境でビルドプロセスが永遠に再コンパイルされ続ける現象が発生することがあります。開発サーバーが朝から晩まで「コンパイル中」の表示のままになり、実際には処理が進まないという事態です。

この問題は、並列エージェントの設計における「フィードバックループの構造」に起因しています。エージェントAが結果を出力 → エージェントBがそれを入力として受け取る → エージェントAが再度実行される、という連鎖が生じ、各エージェント呼び出しがファイル変更をトリガーするため、Next.jsの自動リロード機能が延々と反応し続けるわけです。

この問題で何が困るか・修正で何が変わるか

発生した場合の影響

  • 開発が完全に止まる: Next.jsのホットリロードが終わらず、ブラウザのプレビューが更新されない
  • CPU・メモリが枯渇: バックグラウンドで無限コンパイルが続くため、マシンのリソースを消費し続ける
  • デバッグが困難: エージェントの実際の動作が見えないまま時間だけが過ぎていく
  • 本番環境への懸念: 並列エージェントの実装に自信が持てず、本番デプロイに踏み切れない

修正後

  • 開発サーバーが安定: エージェント実行中でもNext.jsのコンパイルが適切に終了する
  • 処理時間が予測可能: エージェントの実行が終わる時点が明確になり、UI上の確認ができる
  • 本番運用が可能: フィードバックループを制御する実装パターンが身につく

前提環境・必要なもの

  • Node.js: 18.x以上
  • Next.js: 13.x以上(App Router推奨)
  • Claude API: 有効なAPIキー
  • 開発環境: Mac・Windows・Linux(動作は環境依存しない)
  • Claude Models: claude-3-5-sonnet、claude-3-opus対応

この問題はClaudeのモデル世代や機能に直接依存せず、エージェント設計の問題なので、ほぼすべての並列実装環境で再現する可能性があります。

無限ループが発生する3つのパターンと原因

パターン1: 相互参照型エージェント(最も一般的)

// ❌ 無限ループになりやすいコード
async function agentA(input) {
  const result = await claude.message({
    messages: [{ role: "user", content: `${input}を処理せよ` }],
  });
  // agentBの結果を待たずに返す
  return agentB(result.content);
}

async function agentB(input) {
  const result = await claude.message({
    messages: [{ role: "user", content: `${input}を検証せよ` }],
  });
  // 再度agentAに戻す
  return agentA(result.content);
}

この場合、結果が満足できるまで相互に呼び出し続けます。ファイルシステムにログを書き込んだり、APIレスポンスをDBに保存したりすると、その変更がNext.jsのウォッチャー(file watcher)に検出され、リビルドが発火します。

パターン2: 複数ファイル同時更新

// ❌ 複数ファイルを一度に更新する場合
async function parallelAgents(task) {
  const promises = [
    agentForFrontend(task).then(result => 
      fs.writeFileSync('./src/components/Output.tsx', result)
    ),
    agentForBackend(task).then(result => 
      fs.writeFileSync('./src/api/route.ts', result)
    ),
  ];
  
  return Promise.all(promises);
}

複数のエージェントが同時にファイルを書き込むと、Next.jsの自動リロードが複数回トリガーされ、その間に新たなエージェント実行が始まり、さらにファイルが書き込まれるという連鎖が発生します。

パターン3: 終了条件の不明確さ

// ❌ ループを抜ける条件がない
async function iterativeAgent(draft) {
  const feedback = await claude.message({
    messages: [{ role: "user", content: `このコードをレビューして改善案を出せ: ${draft}` }],
  });
  
  if (feedback.content.includes("改善可能")) {
    // 終了条件がないため無限に繰り返される
    return iterativeAgent(feedback.content);
  }
  
  return draft;
}

改善案が出続ける限りループが続き、終了の基準が曖昧だと判断が宙ぶらりになります。

修正方法1: タイムアウトとイテレーション上限を設定

最も簡潔な修正方法は、エージェントの実行回数と実行時間に明確な制限を加えることです。

// ✅ 修正済みコード
async function agentA(input, iteration = 0, maxIterations = 3) {
  // 上限に達したら終了
  if (iteration >= maxIterations) {
    console.log("最大イテレーション数に達しました");
    return input;
  }

  const result = await claude.message({
    messages: [{ role: "user", content: `${input}を処理せよ` }],
  });

  // 条件を満たしたら終了
  if (result.content.includes("完了")) {
    return result.content;
  }

  // それ以外はagentBに渡す
  return agentB(result.content, iteration + 1, maxIterations);
}

async function agentB(input, iteration = 0, maxIterations = 3) {
  if (iteration >= maxIterations) {
    console.log("最大イテレーション数に達しました");
    return input;
  }

  const result = await claude.message({
    messages: [{ role: "user", content: `${input}を検証せよ` }],
  });

  if (result.content.includes("検証完了")) {
    return result.content;
  }

  return agentA(result.content, iteration + 1, maxIterations);
}

重要なポイント:

  • maxIterationsは3~5程度が目安(実装パターンに応じて調整)
  • 各イテレーションでカウンタをインクリメントして渡す
  • 上限到達時は現在の状態をそのまま返す

修正方法2: ファイル書き込みの非同期化とバッチ処理

ファイルシステムの変更をバッチ処理して、Next.jsのリビルドを1回に抑える方法です。

// ✅ 修正済みコード(バッチ処理版)
async function parallelAgentsWithBatch(task) {
  // エージェント実行と結果収集(ファイル書き込みなし)
  const [frontendResult, backendResult] = await Promise.all([
    agentForFrontend(task),
    agentForBackend(task),
  ]);

  // 結果をメモリに一時保存
  const updates = {
    "./src/components/Output.tsx": frontendResult,
    "./src/api/route.ts": backendResult,
  };

  // すべての書き込みを同期実行(Next.jsのリビルドは1回のみ)
  Object.entries(updates).forEach(([filePath, content]) => {
    fs.writeFileSync(filePath, content);
  });

  console.log("ファイル更新完了");
  return updates;
}

次のステップとして、ファイルシステムの変更を API ルートを経由して行う方法もあります。

// ✅ API ルート経由(より安全)
// src/app/api/update-files/route.ts
export async function POST(request) {
  const { updates } = await request.json();
  
  // サーバー側でファイル更新
  Object.entries(updates).forEach(([filePath, content]) => {
    fs.writeFileSync(
      path.join(process.cwd(), filePath),
      content
    );
  });

  return Response.json({ success: true });
}

// クライアント側(エージェント実行部分)
const updates = {
  "./src/components/Output.tsx": frontendResult,
  "./src/api/route.ts": backendResult,
};

await fetch("/api/update-files", {
  method: "POST",
  body: JSON.stringify({ updates }),
});

修正方法3: エージェント実行の責務分離

エージェント自体をステートレスにして、外部のオーケストレーション層で制御する方法です。こうすることで、ループ制御ロジックが明確になります。

// ✅ 責務分離版
// agents.ts(純粋なエージェント定義)
async function analyzeCode(code) {
  return claude.message({
    messages: [{ role: "user", content: `このコードを分析せよ:\n${code}` }],
  });
}

async function improveCode(analysis) {
  return claude.message({
    messages: [{ role: "user", content: `この分析に基づいて改善案を出せ:\n${analysis}` }],
  });
}

// orchestrator.ts(ループ制御)
async function codeImprovementWorkflow(initialCode) {
  let code = initialCode;
  let iteration = 0;
  const maxIterations = 3;
  const history = [];

  while (iteration < maxIterations) {
    console.log(`イテレーション ${iteration + 1}`);

    // ステップ1: 分析
    const analysis = await analyzeCode(code);
    history.push({ step: "analyze", result: analysis.content });

    // ステップ2: 改善案生成
    const improvement = await improveCode(analysis.content);
    history.push({ step: "improve", result: improvement.content });

    // ステップ3: 終了判定
    if (improvement.content.includes("十分な品質")) {
      console.log("目標品質に達しました");
      break;
    }

    code = improvement.content;
    iteration++;
  }

  return { finalCode: code, history };
}

このパターンでは、ループの構造が while 文で明確に見え、終了条件も一箇所で管理できます。

修正方法4: 状態管理ライブラリの活用

複雑な並列エージェント処理の場合、Zustand(軽量)または Redux(大規模)で状態を管理する方法もあります。

// ✅ Zustand を使った状態管理
import { create } from "zustand";

const useAgentStore = create((set, get) => ({
  iterations: 0,
  maxIterations: 3,
  status: "idle", // idle | running | completed | error
  results: [],

  startAgent: async (task) => {
    set({ status: "running", iterations: 0 });

    for (let i = 0; i < get().maxIterations; i++) {
      set({ iterations: i + 1 });

      const result = await claude.message({
        messages: [{ role: "user", content: task }],
      });

      set((state) => ({
        results: [...state.results, result.content],
      }));

      if (result.content.includes("完了")) {
        set({ status: "completed" });
        break;
      }
    }

    if (get().iterations >= get().maxIterations) {
      set({ status: "completed" });
    }
  },
}));

// UI コンポーネント
export default function AgentUI() {
  const { status, iterations, maxIterations, results, startAgent } = useAgentStore();

  return (
    <div>
      <button onClick={() => startAgent("タスク")}>実行開始</button>
      <p>{iterations} / {maxIterations} 完了</p>
      {status === "completed" && <p>処理終了</p>}
      <pre>{JSON.stringify(results, null, 2)}</pre>
    </div>
  );
}

デバッグの進め方

ステップ1: ログ出力を大量に仕込む

async function agentA(input, iteration = 0) {
  console.log(`[agentA] イテレーション開始: ${iteration}, 入力: ${input.substring(0, 50)}`);

  const startTime = Date.now();
  const result = await claude.message({
    messages: [{ role: "user", content: input }],
  });
  const duration = Date.now() - startTime;

  console.log(`[agentA] API呼び出し完了: ${duration}ms`);
  console.log(`[agentA] 出力: ${result.content.substring(0, 50)}`);

  return result.content;
}

ステップ2: ブラウザの DevTools でネットワークを確認

  1. F12 を開く
  2. Network タブを確認
  3. API呼び出しが何度も発生していないか確認
  4. 特に /api/... へのリクエストが重複していないかチェック

ステップ3: Next.js のビルドログを確認

# 本番ビルドでテスト(開発サーバーより詳細)
npm run build

エラーが出ていないか、ビルドが進むか確認します。

ステップ4: ローカル変数の状態を保存

const debugLog = [];

async function agentA(input) {
  debugLog.push({
    timestamp: new Date().toISOString(),
    function: "agentA",
    input,
  });

  // ... 処理 ...

  // 最後にダンプ
  if (iteration >= maxIterations) {
    console.log(JSON.stringify(debugLog, null, 2));
    fs.writeFileSync("./debug-log.json", JSON.stringify(debugLog, null, 2));
  }
}

つまずきやすいポイントと解決策

❌ よくある間違い1: 条件分岐が複雑すぎる場合

// ❌ 悪い例
if (result.includes("完了") || result.includes("終了") || 
    result.includes("done") || result.includes("OK")) {
  // ...
}

複数の条件でループを判定しようとすると、予期しないパターンで終了しないことがあります。

// ✅ 改善版
const completionKeywords = ["完了", "終了", "done", "OK"];
const isComplete = completionKeywords.some(keyword => result.includes(keyword));
if (isComplete) {
  // ...
}

❌ よくある間違い2: 非同期処理の順序を考慮しない

// ❌ 悪い例
agentA();
agentB();
writeFile(); // agentAとBが終わる前にファイルを書き込んでしまう
// ✅ 改善版
const resultA = await agentA();
const resultB = await agentB();
await writeFile(resultA, resultB); // 必ず await を使う

❌ よくある間違い3: Next.js のウォッチャーを無視する

next.config.js で監視対象を制限できます。

// next.config.js
module.exports = {
  onDemandEntries: {
    maxInactiveAge: 60 * 1000, // 60秒で未使用ページをアンロード
    pagesBufferLength: 5,
  },
  webpack: (config, { isServer }) => {
    config.watchOptions = {
      ignored: ['**/node_modules', '**/.git', '**/debug-*.json'],
    };
    return config;
  },
};

応用:本番環境への展開パターン

ローカルで修正が完了したら、以下の方法で本番環境に展開します。

Vercel へのデプロイ

git add .
git commit -m "Fix: 並列エージェントの無限ループ対策"
git push origin main

Vercel は自動デプロイが走り、ビルドログを確認できます。ビルド時間が異常に長くないか確認してください。

Docker を使った隔離実行

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .

# エージェント実行専用のスクリプト
RUN echo '#!/bin/sh\nnode scripts/agent.js' > /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

エージェント処理を独立したコンテナで実行することで、Next.js サーバーの影響を受けません。

docker build -t claude-agent .
docker run claude-agent

まとめ

並列エージェントの無限ループはフィードバックループ構造と Next.js の自動リロード機能の相互作用で発生します。修正には以下の方法が有効です。

  1. イテレーション上限の設定(最もシンプル)
  2. ファイル書き込みのバッチ処理(ファイルシステム関連)
  3. 責務分離とオーケストレーション(複雑な処理向け)
  4. 状態管理ライブラリの導入(大規模実装向け)

まずは小さい修正から始め、maxIterations を設定して、各イテレーション時に明確なログを出力する習慣をつけることをお勧めします。その後、必要に応じて責務分離や状態管理に進むと良いでしょう。


あわせて読みたい

参考ソース