AIエージェントはなぜ本番で壊れるのか
Antigravity で動くAIエージェントを開発環境でテストしているときは、すべてが順調に見える。しかし本番にデプロイした瞬間、想像もしなかった壊れ方をします。LLM APIが429を返す。レスポンスが途中で途切れます。トークン上限に達してエージェントが中途半端な状態で停止します。
私がこの問題に本格的に向き合ったのは、マルチエージェントパイプラインを本番投入して3日目の深夜2時だった。Gemini APIのレートリミットに引っかかり、5つのエージェントが連鎖的に停止。復旧に4時間かかった。
このガイドでは、その経験から構築した耐障害性パターンをすべて公開します。サーキットブレーカー、リトライ戦略、モデルフォールバック、チェックポイント設計——本番環境でAIエージェントを安定稼働させるために必要な実装パターンを、動作するコードとともに解説します。
リトライ戦略の設計 — Exponential Backoff と Jitter の正しい実装
AIエージェントのAPI呼び出しは、従来のWebサービスとは異なるリトライ戦略が必要です。LLM APIは応答に数秒〜数十秒かかるため、単純なリトライでは待ち時間が爆発的に増加します。また、複数エージェントが同時にリトライすると「リトライストーム」を引き起こし、API側の負荷をさらに悪化させる。
なぜ固定間隔リトライではダメなのか
固定間隔(例: 毎回3秒待機)でリトライすると、障害発生時に全エージェントが同じタイミングで再リクエストを投げる。これはいわゆる「Thundering Herd(群集雪崩)」問題で、APIの復旧をかえって遅らせる。
Exponential Backoff with Jitter(指数バックオフ+ジッター)が標準解です。待ち時間を指数的に増やしつつ、ランダムなゆらぎを加えることで、リトライのタイミングを分散させる。
// Antigravity プロジェクトで使えるリトライユーティリティ
// エージェントのAPI呼び出しをラップして自動リトライする
interface RetryConfig {
maxRetries: number; // 最大リトライ回数
baseDelayMs: number; // 初回待機時間(ミリ秒)
maxDelayMs: number; // 待機時間の上限
jitterFactor: number; // ジッター係数(0〜1)
retryableErrors: number[]; // リトライ対象のHTTPステータス
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 60000,
jitterFactor: 0.5,
retryableErrors: [429, 500, 502, 503, 504],
};
async function withRetry<T>(
operation: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const cfg = { ...DEFAULT_CONFIG, ...config };
let lastError: Error | null = null;
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
try {
return await operation();
} catch (error: unknown) {
lastError = error instanceof Error ? error : new Error(String(error));
// リトライ対象でないエラーは即座にスロー
const status = (error as { status?: number }).status;
if (status && \!cfg.retryableErrors.includes(status)) {
throw error;
}
if (attempt === cfg.maxRetries) {
break; // 最大リトライ回数に達した
}
// Exponential Backoff + Full Jitter
const exponentialDelay = cfg.baseDelayMs * Math.pow(2, attempt);
const cappedDelay = Math.min(exponentialDelay, cfg.maxDelayMs);
const jitter = cappedDelay * cfg.jitterFactor * Math.random();
const finalDelay = cappedDelay - (cappedDelay * cfg.jitterFactor / 2) + jitter;
console.warn(
`[Retry ${attempt + 1}/${cfg.maxRetries}] ` +
`Waiting ${Math.round(finalDelay)}ms before retry. ` +
`Error: ${lastError.message}`
);
await new Promise(resolve => setTimeout(resolve, finalDelay));
}
}
throw new Error(
`Operation failed after ${cfg.maxRetries} retries. Last error: ${lastError?.message}`
);
}
// 使用例: Gemini API呼び出しにリトライを適用
const response = await withRetry(
() => fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': process.env.GEMINI_API_KEY ?? '',
},
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
}),
{ maxRetries: 3, baseDelayMs: 2000 }
);
// 期待出力: 429エラー時に2s→4s→8sの間隔でリトライし、成功すればレスポンスを返す
リトライと冪等性
ここで見落としがちな問題があります。リトライするということは、同じ操作を複数回実行する可能性があるということです。LLMへの問い合わせ自体は冪等だが、エージェントが「ファイルに書き込む」「APIを呼ぶ」といった副作用を伴うアクションを実行する場合、リトライが重複実行を引き起こす。
対策は、アクション実行前にチェックポイントを記録し、リトライ時に「すでに実行済みか」を確認するパターンです。これについては後のセクションで詳しく解説します。
サーキットブレーカー — 連鎖障害を断ち切る
リトライだけでは、根本的にダウンしているサービスに対して無駄なリクエストを投げ続けることになります。サーキットブレーカーは、障害が一定回数続いたら自動的にリクエストを遮断し、サービスの復旧を待つパターンです。
3つの状態遷移
サーキットブレーカーには3つの状態があります。
Closed(閉): 正常運転。リクエストはそのまま通過します。失敗をカウントし、閾値に達したらOpenに遷移します。
Open(開): 遮断状態。リクエストを即座に拒否し、フォールバック処理を実行します。一定時間が経過したらHalf-Openに遷移します。
Half-Open(半開): 試験的に1件だけリクエストを通す。成功すればClosedに戻り、失敗すればOpenに戻る。
// AIエージェント用サーキットブレーカー実装
// モデルAPI・外部ツール呼び出しの障害を自動遮断する
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
interface CircuitBreakerConfig {
failureThreshold: number; // Open遷移の失敗回数閾値
resetTimeoutMs: number; // Open→Half-Open遷移までの待ち時間
halfOpenMaxAttempts: number; // Half-Openで試す最大リクエスト数
monitorWindowMs: number; // 失敗カウントのウィンドウ(古い失敗を忘れる)
}
class CircuitBreaker {
private state: CircuitState = 'CLOSED';
private failures: number[] = []; // タイムスタンプ配列
private lastOpenTime = 0;
private halfOpenAttempts = 0;
private consecutiveSuccesses = 0;
constructor(
private name: string,
private config: CircuitBreakerConfig = {
failureThreshold: 5,
resetTimeoutMs: 30000,
halfOpenMaxAttempts: 3,
monitorWindowMs: 60000,
}
) {}
async execute<T>(
operation: () => Promise<T>,
fallback?: () => Promise<T>
): Promise<T> {
// ウィンドウ外の古い失敗記録を除去
const now = Date.now();
this.failures = this.failures.filter(
t => now - t < this.config.monitorWindowMs
);
if (this.state === 'OPEN') {
if (now - this.lastOpenTime >= this.config.resetTimeoutMs) {
// タイムアウト経過 → Half-Openへ遷移
this.state = 'HALF_OPEN';
this.halfOpenAttempts = 0;
this.consecutiveSuccesses = 0;
console.info(`[CircuitBreaker:${this.name}] OPEN → HALF_OPEN`);
} else {
// まだOpen中 → フォールバック実行
console.warn(`[CircuitBreaker:${this.name}] Circuit OPEN — executing fallback`);
if (fallback) return fallback();
throw new Error(`Circuit breaker "${this.name}" is OPEN`);
}
}
if (this.state === 'HALF_OPEN' && this.halfOpenAttempts >= this.config.halfOpenMaxAttempts) {
// Half-Openの試行上限 → まだ不安定ならOpenに戻す
this.transitionToOpen();
if (fallback) return fallback();
throw new Error(`Circuit breaker "${this.name}" — half-open attempts exhausted`);
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
if (this.state === 'OPEN' && fallback) {
return fallback();
}
throw error;
}
}
private onSuccess(): void {
if (this.state === 'HALF_OPEN') {
this.consecutiveSuccesses++;
this.halfOpenAttempts++;
if (this.consecutiveSuccesses >= 2) {
// 連続成功 → Closedに復帰
this.state = 'CLOSED';
this.failures = [];
console.info(`[CircuitBreaker:${this.name}] HALF_OPEN → CLOSED (recovered)`);
}
}
}
private onFailure(): void {
this.failures.push(Date.now());
if (this.state === 'HALF_OPEN') {
this.transitionToOpen();
return;
}
if (this.failures.length >= this.config.failureThreshold) {
this.transitionToOpen();
}
}
private transitionToOpen(): void {
this.state = 'OPEN';
this.lastOpenTime = Date.now();
console.warn(
`[CircuitBreaker:${this.name}] → OPEN ` +
`(failures: ${this.failures.length}, timeout: ${this.config.resetTimeoutMs}ms)`
);
}
getState(): CircuitState {
return this.state;
}
}
// 使用例: Gemini API用とGemma 4ローカルモデル用を個別に管理
const geminiBreaker = new CircuitBreaker('gemini-api', {
failureThreshold: 3,
resetTimeoutMs: 30000,
halfOpenMaxAttempts: 2,
monitorWindowMs: 60000,
});
const gemmaBreaker = new CircuitBreaker('gemma-local', {
failureThreshold: 2, // ローカルモデルは障害が深刻な場合が多い
resetTimeoutMs: 10000, // 復旧が速いので短めに
halfOpenMaxAttempts: 1,
monitorWindowMs: 30000,
});
// 期待動作: geminiBreaker は60秒間に3回失敗するとOpen状態になり、
// 30秒後にHalf-Openで試行、2回連続成功でClosedに復帰する
なぜエージェントごとにサーキットブレーカーが必要なのか
Antigravity のマルチエージェント構成では、各エージェントが異なるAPIやツールに依存しています。1つのサーキットブレーカーで全体を管理すると、特定APIの障害が無関係なエージェントまで止めてしまう。依存先ごとにサーキットブレーカーを分離するのが鉄則です。
モデルフォールバック — AIの「プランB」を用意する
LLM APIの障害で最も効果的な対策は、別のモデルにフォールバックすることです。Antigravity は複数のAIモデルを切り替えて使える環境なので、この利点を最大限に活かす。
フォールバックチェーンの設計
モデルフォールバックでは「品質」と「可用性」のトレードオフが発生します。最高品質のモデルが使えないとき、どこまで品質を妥協するかを事前に決めておく必要があります。
// モデルフォールバックチェーン
// 高品質モデルから順に試行し、利用可能なモデルで応答する
interface ModelConfig {
id: string;
endpoint: string;
apiKeyEnv: string;
maxTokens: number;
qualityTier: 'premium' | 'standard' | 'fallback';
circuitBreaker: CircuitBreaker;
}
interface FallbackResult<T> {
data: T;
modelUsed: string;
qualityTier: string;
wasFallback: boolean;
attemptedModels: string[];
}
class ModelFallbackChain {
private models: ModelConfig[];
constructor(models: ModelConfig[]) {
// qualityTier順にソート(premium → standard → fallback)
const tierOrder = { premium: 0, standard: 1, fallback: 2 };
this.models = [...models].sort(
(a, b) => tierOrder[a.qualityTier] - tierOrder[b.qualityTier]
);
}
async generate(prompt: string): Promise<FallbackResult<string>> {
const attemptedModels: string[] = [];
for (const model of this.models) {
attemptedModels.push(model.id);
try {
const result = await model.circuitBreaker.execute(
async () => {
const apiKey = process.env[model.apiKeyEnv];
if (\!apiKey) throw new Error(`Missing API key: ${model.apiKeyEnv}`);
const response = await withRetry(
() => fetch(model.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model.id,
messages: [{ role: 'user', content: prompt }],
max_tokens: model.maxTokens,
}),
}),
{ maxRetries: 2, baseDelayMs: 1000 }
);
if (\!response.ok) {
const err = new Error(`API error: ${response.status}`);
(err as Error & { status: number }).status = response.status;
throw err;
}
const data = await response.json();
return data.choices?.[0]?.message?.content ?? data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
},
// サーキットブレーカーのフォールバックは次のモデルに委譲
undefined
);
return {
data: result,
modelUsed: model.id,
qualityTier: model.qualityTier,
wasFallback: attemptedModels.length > 1,
attemptedModels,
};
} catch (error) {
console.warn(
`[Fallback] ${model.id} failed (${model.qualityTier}): ${(error as Error).message}`
);
continue; // 次のモデルを試行
}
}
throw new Error(
`All models failed. Attempted: ${attemptedModels.join(' → ')}`
);
}
}
// 使用例: Gemini Pro → Gemma 4 ローカル → Gemini Flash の順にフォールバック
const chain = new ModelFallbackChain([
{
id: 'gemini-2.5-pro',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent',
apiKeyEnv: 'GEMINI_API_KEY',
maxTokens: 8192,
qualityTier: 'premium',
circuitBreaker: geminiBreaker,
},
{
id: 'gemma-4-local',
endpoint: 'http://localhost:11434/api/generate',
apiKeyEnv: 'OLLAMA_DUMMY_KEY', // ローカルはキー不要だがインターフェース統一のため
maxTokens: 4096,
qualityTier: 'standard',
circuitBreaker: gemmaBreaker,
},
{
id: 'gemini-2.0-flash',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent',
apiKeyEnv: 'GEMINI_API_KEY',
maxTokens: 4096,
qualityTier: 'fallback',
circuitBreaker: new CircuitBreaker('gemini-flash'),
},
]);
const result = await chain.generate('Explain the circuit breaker pattern');
console.log(`Model used: ${result.modelUsed} (${result.qualityTier})`);
console.log(`Was fallback: ${result.wasFallback}`);
// 期待出力(Gemini Pro が429エラーの場合):
// [Fallback] gemini-2.5-pro failed (premium): API error: 429
// Model used: gemma-4-local (standard)
// Was fallback: true
フォールバック時の品質低下を可視化する
フォールバックが発生していることをチームが認識できなければ、品質低下に気づかないまま運用が続く。フォールバック発生時にはメトリクスを記録し、ダッシュボードに表示する点が肝心です。
チェックポイント設計 — 「どこまで終わったか」を記録する
AIエージェントが長時間かかるタスクを実行しているとき、途中で障害が起きると最初からやり直しになります。チェックポイントを導入すれば、最後に成功した地点から再開できます。
実装パターン: ステップベースのチェックポイント
// エージェントタスクのチェックポイント管理
// 各ステップの完了状態を永続化し、障害後の再開を可能にする
import { readFile, writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
interface CheckpointData {
taskId: string;
currentStep: number;
totalSteps: number;
completedSteps: Record<string, {
result: unknown;
completedAt: string;
durationMs: number;
}>;
metadata: Record<string, unknown>;
lastUpdated: string;
}
class CheckpointManager {
private checkpointDir: string;
constructor(baseDir: string = '.antigravity/checkpoints') {
this.checkpointDir = baseDir;
}
private getPath(taskId: string): string {
return join(this.checkpointDir, `${taskId}.json`);
}
async load(taskId: string): Promise<CheckpointData | null> {
try {
const raw = await readFile(this.getPath(taskId), 'utf-8');
return JSON.parse(raw) as CheckpointData;
} catch {
return null; // チェックポイントなし → 最初から実行
}
}
async save(data: CheckpointData): Promise<void> {
await mkdir(this.checkpointDir, { recursive: true });
data.lastUpdated = new Date().toISOString();
await writeFile(this.getPath(data.taskId), JSON.stringify(data, null, 2));
}
async markStepComplete(
taskId: string,
stepName: string,
result: unknown,
durationMs: number
): Promise<void> {
const data = await this.load(taskId);
if (\!data) throw new Error(`No checkpoint found for task: ${taskId}`);
data.completedSteps[stepName] = {
result,
completedAt: new Date().toISOString(),
durationMs,
};
data.currentStep++;
await this.save(data);
}
isStepCompleted(checkpoint: CheckpointData, stepName: string): boolean {
return stepName in checkpoint.completedSteps;
}
}
// 使用例: マルチステップのコード分析エージェント
async function runCodeAnalysisAgent(repoPath: string): Promise<void> {
const checkpoints = new CheckpointManager();
const taskId = `code-analysis-${repoPath.replace(/\//g, '-')}`;
// 既存チェックポイントがあれば途中から再開
let checkpoint = await checkpoints.load(taskId);
if (checkpoint) {
console.log(
`Resuming from step ${checkpoint.currentStep}/${checkpoint.totalSteps}`
);
} else {
checkpoint = {
taskId,
currentStep: 0,
totalSteps: 4,
completedSteps: {},
metadata: { repoPath },
lastUpdated: new Date().toISOString(),
};
await checkpoints.save(checkpoint);
}
const steps = [
{ name: 'scan-files', fn: () => scanRepository(repoPath) },
{ name: 'analyze-deps', fn: () => analyzeDependencies(repoPath) },
{ name: 'security-audit', fn: () => runSecurityAudit(repoPath) },
{ name: 'generate-report', fn: () => generateReport(repoPath) },
];
for (const step of steps) {
if (checkpoints.isStepCompleted(checkpoint, step.name)) {
console.log(`Skipping completed step: ${step.name}`);
continue;
}
const start = Date.now();
try {
const result = await step.fn();
await checkpoints.markStepComplete(
taskId, step.name, result, Date.now() - start
);
console.log(`✅ ${step.name} completed in ${Date.now() - start}ms`);
} catch (error) {
console.error(`❌ ${step.name} failed: ${(error as Error).message}`);
console.log('Checkpoint saved. Re-run to resume from this step.');
throw error; // 上位でリトライ判断
}
}
}
// ダミー関数(実際のプロジェクトでは具体的な実装に置き換える)
async function scanRepository(path: string) { return { files: 150 }; }
async function analyzeDependencies(path: string) { return { deps: 42 }; }
async function runSecurityAudit(path: string) { return { issues: 3 }; }
async function generateReport(path: string) { return { reportPath: '/tmp/report.md' }; }
// 期待出力(2回目の実行、step 1まで完了済みの場合):
// Resuming from step 1/4
// Skipping completed step: scan-files
// ✅ analyze-deps completed in 2341ms
// ✅ security-audit completed in 5102ms
// ✅ generate-report completed in 1203ms
チェックポイントの落とし穴
チェックポイントには「陳腐化」の問題があります。外部データが変化した後に古いチェックポイントから再開すると、前半のステップの結果と後半の処理で不整合が起きます。対策として、チェックポイントにTTL(有効期限)を設定し、一定時間を超えたチェックポイントは破棄して最初から実行し直すのが安全です。
グレースフルデグラデーション — 完全停止より「縮退運転」を選ぶ
エージェントが100%の機能を提供できない状況でも、ユーザーに何らかの価値を返す点が肝心です。これがグレースフルデグラデーション(縮退運転)の考え方です。
3段階のデグラデーション戦略
// グレースフルデグラデーション管理
// システムの状態に応じて機能レベルを自動調整する
type ServiceLevel = 'full' | 'degraded' | 'minimal';
interface DegradationPolicy {
level: ServiceLevel;
enabledFeatures: string[];
disabledFeatures: string[];
userMessage: string;
}
class GracefulDegradation {
private policies: Record<ServiceLevel, DegradationPolicy> = {
full: {
level: 'full',
enabledFeatures: ['ai-generation', 'code-analysis', 'multi-agent', 'streaming'],
disabledFeatures: [],
userMessage: '',
},
degraded: {
level: 'degraded',
enabledFeatures: ['ai-generation', 'code-analysis'],
disabledFeatures: ['multi-agent', 'streaming'],
userMessage: '一部機能を制限して運用中です。基本的なAI生成とコード分析は利用可能です。',
},
minimal: {
level: 'minimal',
enabledFeatures: ['code-analysis'],
disabledFeatures: ['ai-generation', 'multi-agent', 'streaming'],
userMessage: 'AI APIに接続できないため、ローカル解析のみ利用可能です。',
},
};
private currentLevel: ServiceLevel = 'full';
evaluate(circuitBreakers: Map<string, CircuitBreaker>): DegradationPolicy {
const openBreakers = Array.from(circuitBreakers.entries())
.filter(([_, cb]) => cb.getState() === 'OPEN')
.map(([name]) => name);
if (openBreakers.length === 0) {
this.currentLevel = 'full';
} else if (openBreakers.includes('gemini-api') && openBreakers.includes('gemma-local')) {
// 全モデルがダウン → minimal
this.currentLevel = 'minimal';
} else {
// 一部モデルがダウン → degraded
this.currentLevel = 'degraded';
}
const policy = this.policies[this.currentLevel];
if (this.currentLevel \!== 'full') {
console.warn(
`[Degradation] Level: ${this.currentLevel} | ` +
`Open breakers: ${openBreakers.join(', ')} | ` +
`Disabled: ${policy.disabledFeatures.join(', ')}`
);
}
return policy;
}
isFeatureEnabled(feature: string): boolean {
return this.policies[this.currentLevel].enabledFeatures.includes(feature);
}
}
// 使用例
const degradation = new GracefulDegradation();
const breakers = new Map<string, CircuitBreaker>([
['gemini-api', geminiBreaker],
['gemma-local', gemmaBreaker],
]);
const policy = degradation.evaluate(breakers);
if (\!degradation.isFeatureEnabled('multi-agent')) {
console.log('マルチエージェントは一時的に無効化されています');
// シングルエージェントモードにフォールバック
}
// 期待出力(geminiBreaker がOPENの場合):
// [Degradation] Level: degraded | Open breakers: gemini-api | Disabled: multi-agent, streaming
// マルチエージェントは一時的に無効化されています
よくある間違いと落とし穴
落とし穴1: リトライ回数を増やせば解決すると思っている
リトライ回数を10回、20回に増やしても、APIが根本的にダウンしていれば無意味です。むしろ、大量のリトライがAPIサーバーの復旧を妨げる。リトライは最大5回程度にとどめ、サーキットブレーカーと組み合わせるのが正解。
落とし穴2: エラーの種類を区別していない
すべてのエラーを同じリトライ戦略で処理するのは危険です。429(レートリミット)は待てば回復するが、401(認証エラー)や400(不正リクエスト)はいくらリトライしても解決しません。エラーコードに応じて「リトライ可能」と「即座に失敗」を分離すること。
落とし穴3: フォールバックモデルの品質差を無視する
Gemini Pro からGemini Flashにフォールバックしたとき、出力品質が大きく変わる可能性があります。フォールバック時はプロンプトを調整する(例: より詳細な指示を追加する、出力フォーマットを厳密に指定する)ことで品質低下を軽減できます。
落とし穴4: チェックポイントのストレージを考慮していない
チェックポイントをファイルシステムに書くなら、ディスク容量とI/O性能を考慮する必要があります。大量のエージェントが同時にチェックポイントを書き込むと、I/Oがボトルネックになります。本番環境ではRedisやKVストアの利用を検討すべきです。
落とし穴5: テスト環境で障害をシミュレートしていない
耐障害性パターンを実装しても、実際に障害が起きたときに正しく動くかテストしなければ意味がありません。Chaos Engineering の考え方を取り入れ、意図的にAPIエラーやタイムアウトを注入するテストを書くこと。
// 障害注入テスト — サーキットブレーカーの動作を検証する
// Antigravity のテストランナーで実行可能
async function testCircuitBreakerTransition(): Promise<void> {
const breaker = new CircuitBreaker('test-breaker', {
failureThreshold: 3,
resetTimeoutMs: 1000,
halfOpenMaxAttempts: 2,
monitorWindowMs: 5000,
});
let callCount = 0;
const failingOperation = async () => {
callCount++;
throw new Error('Simulated API failure');
};
// 3回失敗でOpenに遷移するはず
for (let i = 0; i < 3; i++) {
try {
await breaker.execute(failingOperation);
} catch { /* expected */ }
}
console.assert(
breaker.getState() === 'OPEN',
`Expected OPEN, got ${breaker.getState()}`
);
console.log(`✅ Circuit opened after ${callCount} failures`);
// Open中はフォールバックが呼ばれる
const fallbackResult = await breaker.execute(
failingOperation,
async () => 'fallback-value'
);
console.assert(
fallbackResult === 'fallback-value',
'Fallback should be returned when circuit is OPEN'
);
console.log('✅ Fallback executed during OPEN state');
// 1秒後にHalf-Openに遷移
await new Promise(r => setTimeout(r, 1100));
console.assert(
breaker.getState() === 'OPEN', // まだ execute() を呼ぶまでは OPEN のまま
'Should still be OPEN until next execute()'
);
console.log('✅ All circuit breaker transitions verified');
}
testCircuitBreakerTransition();
// 期待出力:
// ✅ Circuit opened after 3 failures
// ✅ Fallback executed during OPEN state
// ✅ All circuit breaker transitions verified
監視とアラート — 障害を検知してから復旧までの時間を最小化する
耐障害性パターンを導入しても、障害の発生自体を見逃していては改善できません。エージェントの健全性を継続的に監視し、異常を早期に検知する仕組みが必要です。
メトリクス収集のポイント
AIエージェントの監視では、従来のWebサービスとは異なるメトリクスが重要になります。
// AIエージェント監視メトリクス収集
// OpenTelemetry互換のメトリクス記録
interface AgentMetrics {
// リクエストレベル
requestCount: number;
errorCount: number;
retryCount: number;
fallbackCount: number;
// レイテンシ
p50LatencyMs: number;
p95LatencyMs: number;
p99LatencyMs: number;
// トークン消費
inputTokensTotal: number;
outputTokensTotal: number;
estimatedCostUsd: number;
// サーキットブレーカー
circuitOpenEvents: number;
circuitRecoveryEvents: number;
meanTimeToRecoveryMs: number;
// チェックポイント
checkpointSaveCount: number;
checkpointResumeCount: number;
}
class MetricsCollector {
private latencies: number[] = [];
private metrics: AgentMetrics = {
requestCount: 0,
errorCount: 0,
retryCount: 0,
fallbackCount: 0,
p50LatencyMs: 0,
p95LatencyMs: 0,
p99LatencyMs: 0,
inputTokensTotal: 0,
outputTokensTotal: 0,
estimatedCostUsd: 0,
circuitOpenEvents: 0,
circuitRecoveryEvents: 0,
meanTimeToRecoveryMs: 0,
checkpointSaveCount: 0,
checkpointResumeCount: 0,
};
recordRequest(latencyMs: number, success: boolean): void {
this.metrics.requestCount++;
this.latencies.push(latencyMs);
if (\!success) this.metrics.errorCount++;
this.updatePercentiles();
}
recordRetry(): void { this.metrics.retryCount++; }
recordFallback(): void { this.metrics.fallbackCount++; }
recordTokenUsage(input: number, output: number, costPerMillionTokens: number): void {
this.metrics.inputTokensTotal += input;
this.metrics.outputTokensTotal += output;
this.metrics.estimatedCostUsd +=
((input + output) / 1_000_000) * costPerMillionTokens;
}
private updatePercentiles(): void {
const sorted = [...this.latencies].sort((a, b) => a - b);
const len = sorted.length;
if (len === 0) return;
this.metrics.p50LatencyMs = sorted[Math.floor(len * 0.5)] ?? 0;
this.metrics.p95LatencyMs = sorted[Math.floor(len * 0.95)] ?? 0;
this.metrics.p99LatencyMs = sorted[Math.floor(len * 0.99)] ?? 0;
}
getSnapshot(): AgentMetrics {
return { ...this.metrics };
}
// アラート条件の評価
evaluateAlerts(): string[] {
const alerts: string[] = [];
const errorRate = this.metrics.requestCount > 0
? this.metrics.errorCount / this.metrics.requestCount
: 0;
if (errorRate > 0.1) {
alerts.push(`🔴 エラー率が ${(errorRate * 100).toFixed(1)}% に上昇(閾値: 10%)`);
}
if (this.metrics.p95LatencyMs > 30000) {
alerts.push(`🟡 P95レイテンシが ${this.metrics.p95LatencyMs}ms(閾値: 30,000ms)`);
}
if (this.metrics.fallbackCount > this.metrics.requestCount * 0.2) {
alerts.push(`🟠 フォールバック率が20%超 — プライマリモデルの障害を確認`);
}
return alerts;
}
}
// 期待動作: メトリクスを集計し、エラー率10%超・P95レイテンシ30秒超・
// フォールバック率20%超でアラートを発火する
全パターンを統合する — 本番対応エージェントラッパー
ここまで解説した個別パターンを統合し、Antigravityプロジェクトでそのまま使えるエージェントラッパーを構築します。
// 本番対応AIエージェントラッパー
// リトライ・サーキットブレーカー・フォールバック・チェックポイント・監視を統合
class ResilientAgent {
private fallbackChain: ModelFallbackChain;
private checkpoints: CheckpointManager;
private degradation: GracefulDegradation;
private metricsCollector: MetricsCollector;
private circuitBreakers: Map<string, CircuitBreaker>;
constructor(config: {
models: ModelConfig[];
checkpointDir?: string;
}) {
this.fallbackChain = new ModelFallbackChain(config.models);
this.checkpoints = new CheckpointManager(config.checkpointDir);
this.degradation = new GracefulDegradation();
this.metricsCollector = new MetricsCollector();
this.circuitBreakers = new Map(
config.models.map(m => [m.id, m.circuitBreaker])
);
}
async executeTask(taskId: string, steps: Array<{
name: string;
requiresAI: boolean;
execute: (aiGenerate: (prompt: string) => Promise<string>) => Promise<unknown>;
}>): Promise<{ success: boolean; results: Record<string, unknown> }> {
// デグラデーション状態を評価
const policy = this.degradation.evaluate(this.circuitBreakers);
if (policy.level \!== 'full') {
console.warn(`[Agent] Running in ${policy.level} mode: ${policy.userMessage}`);
}
// チェックポイントから再開
let checkpoint = await this.checkpoints.load(taskId);
if (\!checkpoint) {
checkpoint = {
taskId,
currentStep: 0,
totalSteps: steps.length,
completedSteps: {},
metadata: {},
lastUpdated: new Date().toISOString(),
};
await this.checkpoints.save(checkpoint);
}
const results: Record<string, unknown> = {};
for (const step of steps) {
if (this.checkpoints.isStepCompleted(checkpoint, step.name)) {
results[step.name] = checkpoint.completedSteps[step.name]?.result;
continue;
}
// AI必須のステップでAIが利用不可なら飛ばす
if (step.requiresAI && \!this.degradation.isFeatureEnabled('ai-generation')) {
console.warn(`[Agent] Skipping AI step "${step.name}" — AI unavailable`);
continue;
}
const start = Date.now();
try {
const aiGenerate = async (prompt: string): Promise<string> => {
const result = await this.fallbackChain.generate(prompt);
if (result.wasFallback) this.metricsCollector.recordFallback();
this.metricsCollector.recordRequest(Date.now() - start, true);
return result.data;
};
const result = await step.execute(aiGenerate);
const duration = Date.now() - start;
await this.checkpoints.markStepComplete(taskId, step.name, result, duration);
results[step.name] = result;
console.log(`✅ [${step.name}] ${duration}ms`);
} catch (error) {
this.metricsCollector.recordRequest(Date.now() - start, false);
const alerts = this.metricsCollector.evaluateAlerts();
if (alerts.length > 0) {
console.error('[Alerts]', alerts.join('\n'));
}
throw error;
}
}
return { success: true, results };
}
}
// 使用例
const agent = new ResilientAgent({
models: [
/* 前述のModelConfig配列 */
],
checkpointDir: '.antigravity/checkpoints',
});
// このラッパーにより、個別パターンの実装を意識せずに
// 耐障害性のあるエージェントタスクを定義・実行できる
個人開発者の視点から(実体験メモ)
次のアクション
この記事のコードをそのままプロジェクトにコピーして動かすこともできるが、まずは最も効果の高い1つから始めることを勧める。大半の本番障害は「リトライなしでAPIを呼んでいる」ことが原因です。withRetry 関数をプロジェクトに追加し、全てのLLM API呼び出しをラップするところから始めよう。それだけで、深夜のインシデント対応が確実に減る。
サーキットブレーカーとフォールバックは、マルチエージェント構成に進んだタイミングで導入すれば十分です。エージェントが1つだけの段階では複雑さに見合いません。段階的に、必要になった時点で追加していくのが実用的なアプローチです。
AIエージェントの本番運用に関する設計パターンについては、マルチエージェントの落とし穴と解決策や本番環境で動くAIエージェント設計の完全ガイドも参考になるだろう。
耐障害性設計の理論的背景を深く