Agentive
AIエージェント活用

AIエージェントのデバッグ手法 — ログ分析からプロンプト修正まで

約6分で読めます

AIエージェントが期待通りに動作しないとき、従来のソフトウェアデバッグとは異なるアプローチが必要になる。コードのバグではなくプロンプトの曖昧さが原因であったり、コンテキストの欠落がエラーを引き起こしたりする。本記事では、AIエージェント特有のデバッグ手法を、構造化ログの設計からプロンプトの改善まで体系的に解説する。

構造化ログの設計と実装

AIエージェントのデバッグで最も重要なのは、適切なログ設計である。各ステップで入力・出力・判断理由を記録し、問題が発生した際にトレースできるようにする。

interface AgentLogEntry {
  timestamp: string;
  sessionId: string;
  step: number;
  action: string;
  input: {
    prompt: string;
    contextLength: number;
    tools: string[];
  };
  output: {
    response: string;
    tokensUsed: { input: number; output: number };
    toolCalls: Array<{ name: string; args: unknown; result: unknown }>;
  };
  metadata: {
    model: string;
    temperature: number;
    latencyMs: number;
    stopReason: string;
  };
}

class AgentLogger {
  private logs: AgentLogEntry[] = [];
  private sessionId: string;

  constructor(sessionId: string) {
    this.sessionId = sessionId;
  }

  log(entry: Omit<AgentLogEntry, "timestamp" | "sessionId">): void {
    this.logs.push({
      ...entry,
      timestamp: new Date().toISOString(),
      sessionId: this.sessionId,
    });
  }

  getTrace(): AgentLogEntry[] {
    return [...this.logs];
  }

  findFailures(): AgentLogEntry[] {
    return this.logs.filter(
      (l) => l.metadata.stopReason === "error" || l.output.response.includes("ERROR")
    );
  }
}

ログレベルの使い分け

  • DEBUG: プロンプト全文、API応答の生データ、ツール実行の詳細
  • INFO: エージェントの各ステップ完了、ツール呼び出し結果の要約
  • WARN: リトライ発生、予期しない出力形式、性能劣化
  • ERROR: APIエラー、パース失敗、ガードレール違反

プロンプトデバッグの体系的手法

AIエージェントの問題の大半はプロンプトに起因する。以下の手順で原因を特定する。

1. 出力の分類

まず問題の出力を以下のカテゴリに分類する。

type OutputIssue =
  | { type: "wrong_format"; expected: string; actual: string }
  | { type: "hallucination"; claim: string; evidence: string }
  | { type: "refusal"; reason: string }
  | { type: "incomplete"; missingFields: string[] }
  | { type: "wrong_tool"; calledTool: string; expectedTool: string }
  | { type: "loop"; iterations: number; pattern: string };

function classifyIssue(expected: unknown, actual: unknown): OutputIssue {
  // 出力形式の不一致
  if (typeof expected !== typeof actual) {
    return {
      type: "wrong_format",
      expected: typeof expected,
      actual: typeof actual,
    };
  }
  // 以下、各種分類ロジック
  return { type: "incomplete", missingFields: [] };
}

2. プロンプトの差分テスト

問題のプロンプトを段階的に変更し、どの要素が問題を引き起こしているかを特定する。

async function promptABTest(
  basePrompt: string,
  variations: Record<string, string>,
  testInput: string,
  evaluator: (output: string) => { score: number; issues: string[] }
): Promise<Record<string, { score: number; issues: string[] }>> {
  const results: Record<string, { score: number; issues: string[] }> = {};

  for (const [name, prompt] of Object.entries(variations)) {
    const output = await runAgent(prompt, testInput);
    results[name] = evaluator(output);
  }

  // 結果をスコア順にソート
  const sorted = Object.entries(results).sort(
    ([, a], [, b]) => b.score - a.score
  );
  console.table(sorted.map(([name, r]) => ({ name, ...r })));

  return results;
}

コンテキストウィンドウのデバッグ

エージェントが途中で品質が劣化する場合、コンテキストウィンドウの枯渇が原因であることが多い。

コンテキスト使用量の監視

function monitorContext(
  messages: Array<{ role: string; content: string }>,
  maxTokens: number
): { used: number; remaining: number; warningLevel: string } {
  // 簡易的なトークン推定(1文字 = 約1.5トークンとして概算)
  const estimatedTokens = messages.reduce(
    (sum, m) => sum + Math.ceil(m.content.length * 1.5),
    0
  );

  const remaining = maxTokens - estimatedTokens;
  const usageRatio = estimatedTokens / maxTokens;

  let warningLevel = "OK";
  if (usageRatio > 0.9) warningLevel = "CRITICAL";
  else if (usageRatio > 0.7) warningLevel = "WARNING";
  else if (usageRatio > 0.5) warningLevel = "NOTICE";

  return { used: estimatedTokens, remaining, warningLevel };
}

ツール呼び出しのデバッグ

エージェントがツールを正しく使えない場合の診断手法。

ツール使用パターンの分析

function analyzeToolUsage(logs: AgentLogEntry[]): {
  toolFrequency: Record<string, number>;
  failureRate: Record<string, number>;
  avgLatency: Record<string, number>;
  unusedTools: string[];
} {
  const toolCalls = logs.flatMap((l) => l.output.toolCalls);
  const availableTools = new Set(logs.flatMap((l) => l.input.tools));
  const usedTools = new Set(toolCalls.map((tc) => tc.name));

  const toolFrequency: Record<string, number> = {};
  const failures: Record<string, number> = {};
  const latencies: Record<string, number[]> = {};

  for (const call of toolCalls) {
    toolFrequency[call.name] = (toolFrequency[call.name] || 0) + 1;
    if (call.result === null || call.result === undefined) {
      failures[call.name] = (failures[call.name] || 0) + 1;
    }
  }

  return {
    toolFrequency,
    failureRate: Object.fromEntries(
      Object.entries(failures).map(([k, v]) => [k, v / (toolFrequency[k] || 1)])
    ),
    avgLatency: {},
    unusedTools: [...availableTools].filter((t) => !usedTools.has(t)),
  };
}

ループ検出と自動回復

エージェントが同じアクションを繰り返すループ状態の検出と自動回復は、安定運用に不可欠である。

class LoopDetector {
  private actionHistory: string[] = [];
  private readonly maxHistory = 20;
  private readonly loopThreshold = 3;

  recordAction(action: string): { isLoop: boolean; pattern?: string } {
    this.actionHistory.push(action);
    if (this.actionHistory.length > this.maxHistory) {
      this.actionHistory.shift();
    }

    // パターンマッチングでループを検出
    for (let patternLen = 1; patternLen <= 5; patternLen++) {
      const recent = this.actionHistory.slice(-patternLen * this.loopThreshold);
      if (recent.length < patternLen * this.loopThreshold) continue;

      const pattern = recent.slice(0, patternLen);
      let isLoop = true;
      for (let i = 1; i < this.loopThreshold; i++) {
        const chunk = recent.slice(i * patternLen, (i + 1) * patternLen);
        if (JSON.stringify(chunk) !== JSON.stringify(pattern)) {
          isLoop = false;
          break;
        }
      }

      if (isLoop) {
        return { isLoop: true, pattern: pattern.join(" -> ") };
      }
    }

    return { isLoop: false };
  }
}

デバッグ用ダッシュボードの設計

エージェントの状態をリアルタイムで監視するダッシュボードの主要メトリクスは以下の通りである。

メトリクス説明警告閾値
ステップ数/タスク1タスクあたりの平均ステップ数20以上
ツール失敗率ツール呼び出しの失敗率10%以上
コンテキスト使用率コンテキストウィンドウの使用率80%以上
リトライ率APIリトライの発生率5%以上
平均レイテンシ1ステップあたりの応答時間10秒以上

リプレイデバッグ

本番環境で発生した問題を再現するために、エージェントの全入出力を記録し、同じ条件で再実行する手法。

class AgentReplay {
  static async record(
    sessionId: string,
    agent: Agent
  ): Promise<RecordedSession> {
    const recording: RecordedSession = {
      sessionId,
      startTime: new Date().toISOString(),
      steps: [],
    };

    agent.on("step", (step) => {
      recording.steps.push({
        input: step.input,
        output: step.output,
        toolCalls: step.toolCalls,
        timestamp: new Date().toISOString(),
      });
    });

    return recording;
  }

  static async replay(
    recording: RecordedSession,
    agent: Agent
  ): Promise<ReplayResult> {
    const differences: Array<{
      step: number;
      field: string;
      original: unknown;
      replayed: unknown;
    }> = [];

    for (let i = 0; i < recording.steps.length; i++) {
      const original = recording.steps[i];
      const replayed = await agent.executeStep(original.input);

      if (JSON.stringify(replayed.output) !== JSON.stringify(original.output)) {
        differences.push({
          step: i,
          field: "output",
          original: original.output,
          replayed: replayed.output,
        });
      }
    }

    return { differences, isReproducible: differences.length === 0 };
  }
}

ベストプラクティス

  1. ログは構造化して保存する: 自由形式のテキストログではなく、JSON構造化ログを使う
  2. プロンプトのバージョン管理: プロンプトの変更履歴をgitで管理し、A/Bテストの結果と紐付ける
  3. 再現可能な環境を作る: 温度パラメータを0に設定し、シード値を固定することで再現性を確保する
  4. 段階的に問題を絞り込む: システムプロンプト、ユーザー入力、ツール定義を個別に検証する

関連記事

A

Agentive 編集部

AIエージェントを実際に使い倒す個人開発者。サイト制作の自動化を実践しながら、その知見を発信しています。