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 };
}
}
ベストプラクティス
- ログは構造化して保存する: 自由形式のテキストログではなく、JSON構造化ログを使う
- プロンプトのバージョン管理: プロンプトの変更履歴をgitで管理し、A/Bテストの結果と紐付ける
- 再現可能な環境を作る: 温度パラメータを0に設定し、シード値を固定することで再現性を確保する
- 段階的に問題を絞り込む: システムプロンプト、ユーザー入力、ツール定義を個別に検証する
関連記事
- AIエージェントのオブザーバビリティ - 監視基盤の設計手法
- AIエージェントのエラー回復設計 - 障害からの自動復旧パターン
- AIエージェント向けプロンプトエンジニアリング - プロンプト設計の体系的手法
- AIエージェントのテスト戦略 - 自動テストの構築方法
A
Agentive 編集部
AIエージェントを実際に使い倒す個人開発者。サイト制作の自動化を実践しながら、その知見を発信しています。