「エージェントが動かなくなりました」という報告を受けて調べてみると、外部APIの一時的なレート制限に引っかかって、リトライもせずにそのまま停止していた――個人的にこの経験をしてから、エージェントのエラーリカバリ設計を根本から見直すことにしました。
人間が操作するアプリケーションなら「エラーが出たのでもう一度お試しください」で済む。しかしAIエージェントは自律的に動き続けるものです。エラーが発生したとき、自分で判断して復旧できなければ、エージェントを使う意味が半減します。
ここではAIエージェントパイプラインで発生する典型的な障害パターンを整理し、それぞれに対応するリカバリ戦略を実装コード付きで解説します。
エージェント特有の3つの障害パターン
Webアプリケーションでの一般的なエラーハンドリングとは異なり、AIエージェントには固有の障害パターンがあります。
パターン1:幻覚ループ。エージェントがツール呼び出しの結果を誤解釈し、存在しないリソースに何度もアクセスを試みる。たとえばファイル一覧の取得結果を「ファイルが1000個ある」と誤認し、存在しないファイルパスへのアクセスを繰り返すようなケースです。
パターン2:ツール連鎖失敗。ステップAの出力をステップBの入力にする多段パイプラインで、途中のステップが失敗するとそれ以降の全ステップが連鎖的に崩れます。単純なリトライでは解決しません。
パターン3:コンテキスト溢れ。長時間動作するエージェントがコンテキストウィンドウの上限に達し、初期の指示や重要な中間結果を「忘れる」。タスクの方向性がずれたまま動作し続ける。
それぞれに対する防御策を見ていこう。
指数バックオフ付きスマートリトライ
単純なリトライでは不十分です。APIのレート制限に対しては指数バックオフが必要ですし、幻覚ループに対してはリトライ自体を止める判断が必要になります。
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
retryableErrors: Set<string>;
// 同じエラーが連続した場合に早期打ち切りする閾値
sameErrorThreshold: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 30000,
retryableErrors: new Set([
"rate_limit_exceeded",
"server_error",
"timeout",
"connection_reset",
]),
sameErrorThreshold: 3,
};
async function withSmartRetry<T>(
fn: () => Promise<T>,
config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<T> {
let lastErrorCode = "";
let sameErrorCount = 0;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error: unknown) {
const errorCode = (error as { code?: string }).code ?? "unknown";
// リトライ不可能なエラーは即座に投げる
if (\!config.retryableErrors.has(errorCode)) {
throw error;
}
// 同じエラーの連続検知(幻覚ループの兆候)
if (errorCode === lastErrorCode) {
sameErrorCount++;
if (sameErrorCount >= config.sameErrorThreshold) {
throw new Error(
`同一エラーが${sameErrorCount}回連続: ${errorCode}(幻覚ループの可能性)`
);
}
} else {
sameErrorCount = 1;
lastErrorCode = errorCode;
}
if (attempt === config.maxRetries) throw error;
// 指数バックオフ + ジッター
const delay = Math.min(
config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500,
config.maxDelayMs
);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("unreachable");
}ポイントは sameErrorThreshold です。同じエラーコードが3回連続した場合、それはリトライで解決する問題ではなく、エージェントが同じ誤った判断を繰り返している兆候と判断して早期に打ち切る。これが幻覚ループへの第一の防御線になります。
フォールバックチェーンで連鎖失敗を防ぐ
多段パイプラインでは、各ステップに代替手段(フォールバック)を用意しておく点が肝心です。
type StepFn<TIn, TOut> = (input: TIn) => Promise<TOut>;
interface PipelineStep<TIn, TOut> {
name: string;
primary: StepFn<TIn, TOut>;
fallbacks: StepFn<TIn, TOut>[];
validate: (output: TOut) => boolean;
}
async function executeStep<TIn, TOut>(
step: PipelineStep<TIn, TOut>,
input: TIn
): Promise<{ output: TOut; usedFallback: boolean; method: string }> {
// プライマリを試行
try {
const output = await withSmartRetry(() => step.primary(input));
if (step.validate(output)) {
return { output, usedFallback: false, method: "primary" };
}
} catch {
// フォールバックへ
}
// フォールバックを順に試行
for (let i = 0; i < step.fallbacks.length; i++) {
try {
const output = await withSmartRetry(() => step.fallbacks[i](input));
if (step.validate(output)) {
return { output, usedFallback: true, method: `fallback_${i}` };
}
} catch {
continue;
}
}
throw new Error(`ステップ "${step.name}" の全手段が失敗`);
}
// 使用例:Webページの内容取得ステップ
const fetchContentStep: PipelineStep<string, string> = {
name: "fetch_content",
primary: async (url) => {
// 直接フェッチ
const res = await fetch(url);
return res.text();
},
fallbacks: [
async (url) => {
// キャッシュから取得
const cached = await cacheStore.get(url);
if (\!cached) throw new Error("キャッシュなし");
return cached;
},
async (url) => {
// 簡易版のコンテンツで代替
return `[コンテンツ取得失敗] URL: ${url} — 要約なしで処理を続行`;
},
],
validate: (content) => content.length > 0,
};3番目のフォールバックに注目してほしい。コンテンツ取得が完全に失敗しても、「取得失敗」というメタ情報を渡してパイプラインを止めありません。エージェントはこのメタ情報を受け取って、取得できた他のデータだけで処理を続行します。完璧な結果でなくても、止まるよりはましだという割り切りが、エージェントパイプラインの設計では重要になります。
サーキットブレーカーで障害の波及を止める
外部サービスが長時間ダウンしているとき、リトライを続けることはリソースの無駄遣いであり、タイムアウトの連鎖でパイプライン全体が遅延する原因になります。
enum CircuitState {
CLOSED = "closed", // 正常(リクエスト通過)
OPEN = "open", // 遮断(即座にエラー返却)
HALF_OPEN = "half_open" // 試行(1リクエストだけ通す)
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private readonly name: string,
private readonly failureThreshold: number = 5,
private readonly recoveryTimeMs: number = 60_000,
private readonly halfOpenSuccessThreshold: number = 2
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new Error(`CircuitBreaker [${this.name}]: OPEN — リクエスト遮断中`);
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenSuccessThreshold) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
}
}
}
// 外部サービスごとにサーキットブレーカーを作成
const openaiBreaker = new CircuitBreaker("openai_api", 3, 30_000);
const searchBreaker = new CircuitBreaker("search_api", 5, 60_000);サーキットブレーカーは3つの状態を持ちます。正常時は CLOSED(リクエストが通過する)、失敗が閾値を超えると OPEN(即座にエラーを返す)、一定時間後に HALF_OPEN(テスト的に1〜2リクエストを通す)に遷移します。
エージェントパイプラインでは、外部サービスごとに個別のサーキットブレーカーを作成するのがベストプラクティスです。検索APIがダウンしても、それ以外のツール(ファイル操作やコード実行)は正常に使い続けられます。
障害注入テストで耐障害性を検証する
リカバリ機構を実装したら、それが実際に機能するかを検証する必要があります。障害注入(Chaos Engineering)の考え方をエージェントテストに応用します。
class FaultInjector {
private rules: Map<string, { probability: number; errorType: string }> = new Map();
addRule(toolName: string, probability: number, errorType: string): void {
this.rules.set(toolName, { probability, errorType });
}
shouldInjectFault(toolName: string): { inject: boolean; errorType?: string } {
const rule = this.rules.get(toolName);
if (\!rule) return { inject: false };
return Math.random() < rule.probability
? { inject: true, errorType: rule.errorType }
: { inject: false };
}
}
// テスト例
async function testPipelineResilience(): Promise<void> {
const injector = new FaultInjector();
// 検索APIが50%の確率でタイムアウトする状況をシミュレート
injector.addRule("web_search", 0.5, "timeout");
// ファイル書き込みが20%の確率で失敗する状況
injector.addRule("file_write", 0.2, "disk_full");
const results = { succeeded: 0, failed: 0, usedFallback: 0 };
for (let i = 0; i < 100; i++) {
try {
const result = await runPipelineWithInjector(injector);
results.succeeded++;
if (result.usedFallback) results.usedFallback++;
} catch {
results.failed++;
}
}
console.log(`成功: ${results.succeeded}/100`);
console.log(`フォールバック使用: ${results.usedFallback}`);
console.log(`完全失敗: ${results.failed}`);
// 成功率90%以上を要求
if (results.succeeded < 90) {
throw new Error("耐障害性テスト失敗: 成功率が90%を下回った");
}
}このテストでは、100回のパイプライン実行のうち90%以上が成功することを確認しています。検索APIが半分の確率で落ちる過酷な条件でも、フォールバックとリトライが正しく機能すれば、パイプライン全体としての成功率は90%を超える。
実際にこのテストを導入したことで、「フォールバックの validate 関数が常に false を返すバグ」や「サーキットブレーカーの recoveryTime が短すぎてAPIが復旧する前に HALF_OPEN になる」といった問題を事前に発見できました。
エージェントが本番で止まるのは、エラーが発生したときではありません。エラーからの復旧に失敗したときです。この記事で紹介した3つのパターン――スマートリトライ、フォールバックチェーン、サーキットブレーカー――を組み合わせることで、個々の障害がパイプライン全体の停止に波及しない設計が実現できます。次のステップとして、自分のエージェントパイプラインで最も頻繁に発生しているエラーを洗い出し、そこにフォールバックを1つ追加することから始めてみてほしい。
この記事はプレミアムコンテンツの見本として全文を無料公開しています。Antigravity Lab のプレミアム記事では、このような本番運用に直結する実装パターンを、動作するコード付きで詳しく解説しています。すべてのプレミアム記事を読むには、メンバーシップへの登録をご検討ください。