LLMオブザーバビリティ完全ガイド: プロダクションAIエージェントの監視・トレース・デバッグ方法
AIエージェントが顧客に$2,400の損害を与えた。深夜3時に無限ツールコールループに入り、トークンを燃やしながら意味不明な応答を生成し続けていた。従来のAPMダッシュボード?全部緑。レイテンシ正常、エラーなし、クラッシュなし。だがエージェントは6時間にわたって自信満々に間違った答えを返し続けており、こちらからの可視性はゼロだった。
これがAIプロダクトを静かに殺すオブザーバビリティギャップなんです。従来のモニタリングツールは決定論的ソフトウェアのために作られたもの。リクエストが入り、レスポンスが出て、時間を測る。AIエージェントは根本的に別物だ。推論し、分岐し、ツールを呼び出し、ドキュメントを検索し、同じ入力でも異なる判断を下す。何かがおかしくなったとき、HTTPステータスコードを見るだけじゃ足りない。推論チェーンそのものを追跡する必要がある。全ての決定ポイント、全てのツール呼び出し、消費された全てのトークンを。
このガイドでは、LLMベースシステムのプロダクション品質オブザーバビリティ構築に必要な全てをカバーする。分散トレーシングから自動評価、コスト追跡、ツール選定まで。理論はない。1日数百万リクエストを処理するエージェントを運用しているチームの実戦検証済みパターンだけを詰め込んだ。
従来のモニタリングがLLMアプリで壊れる理由
Datadog、Grafana、New RelicだけでAIエージェントを運用しているなら、目隠しで高速道路を走っているようなもの。なぜかを見ていこう:
決定論の問題
従来のソフトウェアは決定論的だ。同じ入力なら同じ出力。モニタリングもシンプルで、レイテンシ、エラー率、スループットを追跡すればいい。P99レイテンシが跳ねたら調査する、それだけ。
でもLLMは非決定論的なんですよね。同じプロンプトが毎回違う出力を吐く。「成功」のHTTP 200レスポンスに完全に幻覚された回答が入っていることもある。エラー率0%なのに精度40%。従来のAPMツールでは、この障害モードを文字通り検出できない。
マルチステップの問題
シンプルなAPIコールはスパン1つ。リクエスト → レスポンス。AIエージェントは複雑な実行グラフだ:
ユーザークエリ: 「来月NYCから東京への最安フライトを探して」
│
├─ Step 1: 意図分類 (LLM呼び出し, 200ms, 150トークン)
├─ Step 2: パラメータ抽出 (LLM呼び出し, 180ms, 120トークン)
├─ Step 3: ツールコール - フライトAPI (外部API, 2.1秒)
├─ Step 4: 結果パース (LLM呼び出し, 250ms, 800トークン)
├─ Step 5: 価格比較 (LLM呼び出し, 300ms, 1200トークン)
├─ Step 6: レスポンス生成 (LLM呼び出し, 400ms, 500トークン)
│
合計: LLM 5回呼び出し, 3.4秒, 2770トークン, $0.008
このエージェントが間違った結果を返したとき、どのステップで壊れたのか分からない。意図分類を間違えたのか、ツールがおかしなデータを返したのか、LLMが価格比較で幻覚したのか。ステップレベルのトレーシングがないとデバッグは地獄になる。
コストの問題
LLM呼び出しは高い。従来のコンピューティングではCPUサイクルは実質無料だが、トークン1つ1つが直接的なドルコストなんですよね。暴走したエージェントループ1つで、数分のうちに数百ドルが溶ける。エージェント・ユーザー・組織レベルのリアルタイムコスト追跡が必須だが、従来のAPMツールでこれを提供してくれるものはない。
LLMオブザーバビリティスタック
プロダクション品質のLLMオブザーバビリティを実現するには、4つのレイヤーが要る:
┌─────────────────────────────────────────────────────┐
│ Layer 4: ダッシュボード │
│ コスト分析、品質トレンド、SLAトラッキング │
├─────────────────────────────────────────────────────┤
│ Layer 3: 評価 │
│ 自動評価、回帰検出、A/Bテスト │
├─────────────────────────────────────────────────────┤
│ Layer 2: トレーシング │
│ 分散トレース、スパン階層、トークン追跡 │
├─────────────────────────────────────────────────────┤
│ Layer 1: インストルメンテーション │
│ SDK統合、自動キャプチャ、手動アノテーション │
└─────────────────────────────────────────────────────┘
下から順に構築していこう。
Layer 1: インストルメンテーション
インストルメンテーションは全ての基盤になる。アプリのパフォーマンスを壊さずに、全ての決定ポイントでデータをキャプチャしなければならない。
LLM向けOpenTelemetry
業界はインストルメンテーションの標準レイヤーとしてOpenTelemetry(OTel)に収束しつつある。OpenLLMetryプロジェクトがOTelをLLM固有のセマンティック規約で拡張している:
import * as traceloop from '@traceloop/node-server-sdk'; // LLMモジュールのインポート前に初期化 traceloop.initialize({ baseUrl: 'https://your-collector.example.com', appName: 'my-ai-agent', }); // またはOpenTelemetryを直接使うモジュラーアプローチ: import { NodeSDK } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OpenAIInstrumentation } from '@traceloop/instrumentation-openai'; import { AnthropicInstrumentation } from '@traceloop/instrumentation-anthropic'; const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: 'https://your-collector.example.com/v1/traces', }), instrumentations: [ new OpenAIInstrumentation({ captureInputs: true, // プロンプトをログ(本番では注意) captureOutputs: true, // 応答をログ }), new AnthropicInstrumentation(), ], }); sdk.start();
これでOpenAIとAnthropicの全てのAPI呼び出しが自動計測される:
- モデル名とパラメータ(temperature、max_tokens)
- 入力プロンプトと出力応答
- トークン使用量(プロンプトトークン、コンプリーショントークン)
- 呼び出しごとのレイテンシ
- ツール/ファンクション呼び出しの詳細
手動スパンアノテーション
自動計測はLLM呼び出しをキャプチャしてくれるが、ビジネスロジック部分は手動スパンで補う必要がある:
import { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('ai-agent'); async function processUserQuery(query: string, userId: string) { return tracer.startActiveSpan('agent.process_query', async (span) => { span.setAttributes({ 'user.id': userId, 'agent.query': query, 'agent.type': 'flight-search', }); try { // Step 1: 意図を分類 const intent = await tracer.startActiveSpan( 'agent.classify_intent', async (intentSpan) => { const result = await classifyIntent(query); intentSpan.setAttributes({ 'agent.intent': result.intent, 'agent.confidence': result.confidence, }); return result; } ); // Step 2: ツールコールを実行 const toolResults = await tracer.startActiveSpan( 'agent.execute_tools', async (toolSpan) => { toolSpan.setAttribute('agent.tools_count', intent.tools.length); return Promise.all( intent.tools.map((tool) => executeTool(tool)) ); } ); // Step 3: レスポンスを生成 const response = await tracer.startActiveSpan( 'agent.generate_response', async (respSpan) => { const result = await generateResponse(toolResults); respSpan.setAttributes({ 'agent.response_length': result.length, 'agent.tokens_total': result.tokenUsage.total, }); return result; } ); span.setStatus({ code: SpanStatusCode.OK }); return response; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.recordException(error); throw error; } }); }
何をキャプチャし、何をしないか
重要な判断:どのデータをログするか。
| データ | 開発環境 | プロダクション | 理由 |
|---|---|---|---|
| 完全なプロンプト | ✅ はい | ⚠️ サンプリング | PIIリスク、ストレージコスト |
| 完全な応答 | ✅ はい | ⚠️ サンプリング | 同上 |
| トークン数 | ✅ はい | ✅ はい | コスト追跡は必須 |
| モデルパラメータ | ✅ はい | ✅ はい | 回帰のデバッグ |
| ツール呼び出しの入出力 | ✅ はい | ✅ はい | デバッグに必須 |
| ユーザーID | ✅ はい | ✅ はい | ユーザー別コスト追跡 |
| ステップごとのレイテンシ | ✅ はい | ✅ はい | パフォーマンスモニタリング |
| 埋め込みベクトル | ❌ いいえ | ❌ いいえ | 大きすぎ、めったに有用でない |
| 生のAPIレスポンス | ✅ はい | ❌ いいえ | ストレージ爆発 |
プロダクションでは、プロンプト/応答の完全ログにはサンプリングをかけよう。メタデータ(トークン数、レイテンシ、モデル)は100%で取り、テキスト全文は10〜20%に抑える。特定のバグを追う場合は、該当ユーザーやクエリのサンプリング率を一時的に上げればいい。
Layer 2: 分散トレーシング
インストルメンテーションが整ったら、次はLLM固有データを理解できるトレーシングバックエンドが必要になる。ここからが専用ツールの出番だ。
AIエージェントのトレース構造
適切に設計されたAIエージェントのトレースはこうなる:
Trace: agent_run_abc123
│
├─ Span: agent.process_query (ルート)
│ ├─ Attributes: user_id, query, session_id
│ │
│ ├─ Span: agent.classify_intent
│ │ ├─ Span: llm.openai.chat (model: gpt-4.1-mini)
│ │ │ └─ Attributes: tokens_in=150, tokens_out=30, cost=$0.0001
│ │ └─ Result: intent=flight_search, confidence=0.95
│ │
│ ├─ Span: agent.retrieve_context (RAG)
│ │ ├─ Span: vectordb.query (provider: pinecone)
│ │ │ └─ Attributes: top_k=5, similarity_threshold=0.8
│ │ └─ Span: agent.rerank
│ │ └─ Span: llm.anthropic.chat (model: claude-haiku-4.5)
│ │ └─ Attributes: tokens_in=2000, tokens_out=500
│ │
│ ├─ Span: agent.execute_tool
│ │ ├─ Span: tool.flight_api.search
│ │ │ └─ Attributes: duration=2100ms, results_count=15
│ │ └─ Span: tool.flight_api.get_prices
│ │ └─ Attributes: duration=800ms, results_count=15
│ │
│ └─ Span: agent.generate_response
│ └─ Span: llm.openai.chat (model: gpt-4.1)
│ └─ Attributes: tokens_in=3000, tokens_out=800, cost=$0.02
│
└─ 合計: LLM 4回呼び出し, 6800トークン, $0.021, 4.2秒
この構造で以下の質問に答えられる:
- 「なぜこのエージェントは10秒かかった?」→ フライトAPIの呼び出しに8秒かかった。
- 「なぜ2になった?」→ エージェントがツールコールを100回ループした。
- 「なぜ幻覚した?」→ RAG検索が類似度スコアの低い無関係なドキュメントを返した。
トレース伝播の実装
マルチサービスアーキテクチャでは、トレースコンテキストがサービス境界を越えて伝播する必要がある:
// サービスA: エージェントオーケストレーター import { context, propagation } from '@opentelemetry/api'; async function callToolService(toolName: string, params: any) { const headers: Record<string, string> = {}; propagation.inject(context.active(), headers); const response = await fetch(`https://tools.internal/${toolName}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify(params), }); return response.json(); } // サービスB: ツール実行サービス import { context, propagation } from '@opentelemetry/api'; app.post('/flight-search', (req, res) => { const ctx = propagation.extract(context.active(), req.headers); context.with(ctx, async () => { const span = tracer.startSpan('tool.flight_search'); // ... 完全なトレース系譜でのツール実行 span.end(); }); });
Layer 3: 自動評価
トレーシングは何が起きたかを教えてくれる。評価はどれだけうまくいったかを教えてくれる。ほとんどのチームがスキップするレイヤーだが、プロダクションAIの成否を決めるのは実はここなんですよね。
評価パイプライン
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ トレース │ → │ サンプル │ → │ 評価 │ → │ アラート │
│ ストア │ │ 選択 │ │ 実行 │ │ レポート │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
10-20%の LLM-as-Judge Slack/PD
トレース + 決定論的 品質低下時
ルール
LLM-as-Judge評価
最も強力な評価パターンは、別のLLMでエージェントの出力を判定すること:
interface EvalResult { score: number; // 0-1 reasoning: string; // このスコアの理由 dimension: string; // 評価対象 } async function evaluateResponse( query: string, response: string, groundTruth?: string ): Promise<EvalResult[]> { const evaluations: EvalResult[] = []; // 評価1: 事実の正確性 const accuracyEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `あなたは専門の評価者です。AIの応答の事実正確性を 0から1のスケールでスコアリングしてください。 採点基準: - 1.0: 全ての事実が正確で検証可能 - 0.7: おおむね正確だが軽微な不正確さあり - 0.4: 重大な事実誤りを含む - 0.0: 完全に捏造または誤り JSONで回答: { "score": number, "reasoning": string }`, }, { role: 'user', content: `質問: ${query} AI応答: ${response} ${groundTruth ? `正解: ${groundTruth}` : ''}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(accuracyEval.choices[0].message.content), dimension: 'factual_accuracy', }); // 評価2: 関連性 const relevanceEval = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ { role: 'system', content: `ユーザーの質問に対する応答の関連性をスコアリングしてください。 1.0 = 質問に直接回答 0.5 = 部分的に関連 0.0 = 完全にトピック外 JSONで回答: { "score": number, "reasoning": string }`, }, { role: 'user', content: `質問: ${query}\n応答: ${response}`, }, ], response_format: { type: 'json_object' }, }); evaluations.push({ ...JSON.parse(relevanceEval.choices[0].message.content), dimension: 'relevance', }); return evaluations; }
決定論的ガード
全部をLLMジャッジにかける必要はない。既知の障害パターンは決定論的チェックでサクッと捕まえられる:
interface GuardResult { passed: boolean; violation?: string; } function runDeterministicGuards( trace: AgentTrace ): GuardResult[] { const results: GuardResult[] = []; // ガード1: トークン予算超過 const totalTokens = trace.spans .filter((s) => s.name.startsWith('llm.')) .reduce((sum, s) => sum + (s.attributes.tokens_total || 0), 0); results.push({ passed: totalTokens < 50000, violation: totalTokens >= 50000 ? `トークン予算超過: ${totalTokens}トークン` : undefined, }); // ガード2: ツールコールループ検出 const toolCalls = trace.spans .filter((s) => s.name.startsWith('tool.')); const uniqueTools = new Set(toolCalls.map((s) => s.name)); for (const tool of uniqueTools) { const count = toolCalls .filter((s) => s.name === tool).length; results.push({ passed: count <= 10, violation: count > 10 ? `ループの疑い: ${tool}が${count}回呼び出し` : undefined, }); } // ガード3: レイテンシ予算 const totalLatency = trace.duration; results.push({ passed: totalLatency < 30000, violation: totalLatency >= 30000 ? `レイテンシ予算超過: ${totalLatency}ms` : undefined, }); // ガード4: 空もしくは疑わしく短い応答 const finalResponse = trace.output; results.push({ passed: finalResponse && finalResponse.length > 20, violation: !finalResponse || finalResponse.length <= 20 ? '応答が空または疑わしく短い' : undefined, }); return results; }
自動アラート
評価をアラートシステムに接続する:
async function runEvalPipeline(trace: AgentTrace) { // 決定論的ガード(高速、全トレースで実行) const guardResults = runDeterministicGuards(trace); const guardViolations = guardResults .filter((r) => !r.passed); if (guardViolations.length > 0) { await sendAlert({ severity: 'high', title: 'エージェントガード違反', details: guardViolations .map((v) => v.violation) .join('\n'), traceId: trace.traceId, }); } // LLM-as-Judge(高コスト、サンプリングされたトレースのみ) if (shouldSample(trace, 0.1)) { const evalResults = await evaluateResponse( trace.input, trace.output ); const lowScores = evalResults .filter((e) => e.score < 0.5); if (lowScores.length > 0) { await sendAlert({ severity: 'medium', title: 'エージェント品質の低下', details: lowScores .map((e) => `${e.dimension}: ${e.score} - ${e.reasoning}` ) .join('\n'), traceId: trace.traceId, }); } await storeEvalResults(trace.traceId, evalResults); } }
Layer 4: コスト追跡とアナリティクス
トークンコストはAIアプリのクラウド請求書そのもの。きめ細かなコスト追跡がなければ、勘で運用しているのと変わらない。
リアルタイムコスト計算
const MODEL_PRICING: Record<string, { input: number; // 100万トークンあたり output: number; // 100万トークンあたり }> = { 'gpt-4.1': { input: 2.00, output: 8.00 }, 'gpt-4.1-mini': { input: 0.40, output: 1.60 }, 'gpt-4.1-nano': { input: 0.10, output: 0.40 }, 'claude-sonnet-4.6': { input: 3.00, output: 15.00 }, 'claude-haiku-4.5': { input: 1.00, output: 5.00 }, 'gemini-2.5-pro': { input: 1.25, output: 10.00 }, 'gemini-2.5-flash': { input: 0.30, output: 2.50 }, }; function calculateCost( model: string, inputTokens: number, outputTokens: number ): number { const pricing = MODEL_PRICING[model]; if (!pricing) return 0; return ( (inputTokens / 1_000_000) * pricing.input + (outputTokens / 1_000_000) * pricing.output ); } // トレース単位のコスト追跡 function aggregateTraceCost(trace: AgentTrace): CostBreakdown { const llmSpans = trace.spans .filter((s) => s.name.startsWith('llm.')); let totalCost = 0; const breakdown: Record<string, number> = {}; for (const span of llmSpans) { const model = span.attributes.model; const cost = calculateCost( model, span.attributes.tokens_in, span.attributes.tokens_out ); totalCost += cost; breakdown[model] = (breakdown[model] || 0) + cost; } return { totalCost, breakdown, tokenCount: llmSpans.reduce( (sum, s) => sum + s.attributes.tokens_in + s.attributes.tokens_out, 0 ), }; }
コストアラートの閾値
// リクエスト単位のコストガード const MAX_COST_PER_REQUEST = 0.50; // $0.50 // ユーザー単位の時間あたり予算 const MAX_COST_PER_USER_HOUR = 5.00; // $5.00 // 組織単位の日次予算 const MAX_COST_PER_ORG_DAY = 500.00; // $500.00 async function checkCostBudgets( cost: number, userId: string, orgId: string ) { if (cost > MAX_COST_PER_REQUEST) { await sendAlert({ severity: 'high', title: `リクエストコスト超過: $${cost.toFixed(4)}`, }); } const userHourlyCost = await redis.incrbyfloat( `cost:user:${userId}:${getCurrentHour()}`, cost ); await redis.expire( `cost:user:${userId}:${getCurrentHour()}`, 7200 ); if (userHourlyCost > MAX_COST_PER_USER_HOUR) { await sendAlert({ severity: 'critical', title: `ユーザー${userId}の時間あたり予算超過`, }); } }
ツール比較: LangSmith vs Langfuse vs Arize
オブザーバビリティプラットフォームの選定は結構重要な意思決定になる。忖度なしで比較してみよう:
LangSmith
推奨対象: LangChain/LangGraphを既に使っているチーム
import { Client } from 'langsmith'; import { traceable } from 'langsmith/traceable'; const client = new Client({ apiKey: process.env.LANGSMITH_API_KEY, }); const processQuery = traceable( async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); }, { name: 'process_query', tags: ['production'] } );
強み:
- LangChain/LangGraphとの深い統合(ファーストパーティ)
- ビルトインのプロンプトプレイグラウンドとバージョン管理
- プロンプト共有・発見のためのHub
- ヒューマンインザループを含む強力な評価フレームワーク
- エージェント実行グラフの優れた可視化
弱み:
- LangChainエコシステムへのベンダーロックイン
- クローズドソース、ホスティングのみ(セルフホスティング不可)
- トレースボリュームに応じたプライシング(高額になりうる)
- LangChain以外のフレームワークのサポートが限定的
Langfuse
推奨対象: オープンソースでフレームワーク非依存のトレーシングが欲しいチーム
// Langfuse v5+ 統合(推奨: @langfuse/tracing) import { observe } from '@langfuse/tracing'; // デコレータベースのトレーシング(最もシンプル) const processQuery = observe( { name: 'flight-search-agent' }, async (query: string) => { const intent = await classifyIntent(query); const results = await searchFlights(intent); return generateResponse(results); } ); // または細かい制御が必要な場合、クラシックなLangfuseクライアントを使用: import Langfuse from 'langfuse'; const langfuse = new Langfuse({ publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY, }); const trace = langfuse.trace({ name: 'flight-search-agent', userId: 'user-123', metadata: { environment: 'production' }, }); const generation = trace.generation({ name: 'classify-intent', model: 'gpt-4.1-mini', input: [{ role: 'user', content: query }], output: response, usage: { promptTokens: 150, completionTokens: 30, }, });
強み:
- オープンソース(MITライセンス)、セルフホスティング可能
- フレームワーク非依存(どのLLMプロバイダーでも動作)
- ビルトインのコスト追跡とトークンアナリティクス
- プロンプト管理とバージョン管理
- 寛大なフリーティア
弱み:
- LangSmithより小さいコミュニティ
- セルフホスティングにはインフラ管理が必要
- 評価機能がLangSmithほど成熟していない
- UIが若干未完成(急速に改善中)
Arize Phoenix
推奨対象: ML/データサイエンスバックグラウンドのチーム
import { trace as otelTrace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai'; registerInstrumentations({ instrumentations: [new OpenAIInstrumentation()], });
強み:
- OpenTelemetryベース(プロプライエタリロックインなし)
- 埋め込み可視化とドリフト検出が優秀
- RAG分析ツールが秀逸
- ローカルファースト開発体験(Phoenixはローカルで実行)
- 検索品質のデバッグではベストインクラス
弱み:
- 学習曲線がやや急
- エージェントオーケストレーションのトレーシングが弱い
- 直接統合のエコシステムが小さい
- エンタープライズ機能にはArize cloudが必要
比較マトリクス
| 機能 | LangSmith | Langfuse | Arize Phoenix |
|---|---|---|---|
| オープンソース | ❌ | ✅ MIT | ✅ (Phoenix) |
| セルフホスティング | ❌ | ✅ | ✅ (Phoenix) |
| LangChain統合 | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| フレームワーク非依存 | ⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| コスト追跡 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 評価フレームワーク | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| RAG分析 | ⭐⭐ | ⭐ | ⭐⭐⭐ |
| プロンプト管理 | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| 埋め込み分析 | ⭐ | ⭐ | ⭐⭐⭐ |
| 価格(スタートアップ) | $$$ | 無料/$ | 無料/$ |
プロダクションパターンとアンチパターン
パターン1: サーキットブレーカー
暴走エージェントが予算を焼き尽くすのを防ぐパターン:
class AgentCircuitBreaker { private tokenCount = 0; private llmCalls = 0; private toolCalls = 0; private startTime: number; constructor( private limits: { maxTokens: number; maxLLMCalls: number; maxToolCalls: number; maxDurationMs: number; } ) { this.startTime = Date.now(); } check(event: { type: 'llm' | 'tool'; tokens?: number }) { if (event.type === 'llm') { this.llmCalls++; this.tokenCount += event.tokens || 0; } else { this.toolCalls++; } const elapsed = Date.now() - this.startTime; if (this.tokenCount > this.limits.maxTokens) { throw new CircuitBreakerError( `トークン制限超過: ${this.tokenCount}` ); } if (this.llmCalls > this.limits.maxLLMCalls) { throw new CircuitBreakerError( `LLM呼び出し制限超過: ${this.llmCalls}` ); } if (this.toolCalls > this.limits.maxToolCalls) { throw new CircuitBreakerError( `ツールコール制限超過: ${this.toolCalls}` ); } if (elapsed > this.limits.maxDurationMs) { throw new CircuitBreakerError( `実行時間制限超過: ${elapsed}ms` ); } } } // 使用例 const breaker = new AgentCircuitBreaker({ maxTokens: 50000, maxLLMCalls: 20, maxToolCalls: 30, maxDurationMs: 60000, // 1分 }); for (const step of agentSteps) { breaker.check({ type: step.type, tokens: step.tokenUsage, }); await executeStep(step); }
パターン2: トレースベースのデバッグワークフロー
何かが壊れたとき、この手順で追跡する:
1. 検出: 自動評価が品質低下を検知
↓
2. 特定: 低い評価スコアでトレースをフィルタ
↓
3. 比較: 成功したトレースと横並び比較
↓
4. 分離: 分岐ポイントを特定
↓
5. 原因: そのスパンの入出力を確認
↓
6. 修正: プロンプト、コンテキスト、ツール設定を修正
↓
7. 検証: テストデータセットに対して修正のevalを実行
アンチパターン1: 何でもログする
全リクエストの全トークンをログしてはいけない。
// ❌ やってはいけない logger.info('LLM Response', { fullPrompt: systemPrompt + userMessage + context, // 50KB fullResponse: completion, // 10KB metadata: entireTraceObject, // 5KB }); // 結果: リクエストあたり65KB × 日100万リクエスト = 日65GB // ✅ こうする logger.info('LLM Response', { traceId: trace.id, model: 'gpt-4.1-mini', tokensIn: 150, tokensOut: 30, cost: 0.0001, latencyMs: 200, evalScore: 0.95, }); // 結果: リクエストあたり200バイト × 日100万リクエスト = 日200MB
アンチパターン2: LLMエラーをHTTPエラーのように扱う
// ❌ ミスリーディング: HTTP 200だがエージェントの応答はひどい if (response.status === 200) { metrics.increment('agent.success'); } // ✅ 正しい: 実際の品質を測る const evalScore = await quickEval(response.body); if (evalScore > 0.7) { metrics.increment('agent.quality.good'); } else { metrics.increment('agent.quality.poor'); // これが本当の「エラー」。調査を開始 }
アンチパターン3: ベースラインなし
// ❌ アラート: 「Evalスコアは0.72」→ いいの?悪いの? // ✅ まずベースラインを確立する // 第1-2週: アラートなしでevalスコアを収集 // 第3週: P50、P90、P99のベースラインを計算 // 第4週以降: ベースラインからの逸脱でアラート const baseline = { accuracy: { p50: 0.85, p90: 0.92, p99: 0.97 }, relevance: { p50: 0.90, p90: 0.95, p99: 0.99 }, latency: { p50: 2000, p90: 5000, p99: 10000 }, }; function shouldAlert( dimension: string, value: number ): boolean { const b = baseline[dimension]; return value < b.p50 * 0.8; // 中央値の20%以下でアラート }
最小限の実用的オブザーバビリティスタック
ゼロから始めるなら、プロダクション品質オブザーバビリティへの最短ルートを紹介する:
1日目: 基本インストルメンテーション
// 1. Langfuseを導入(最も速く始められる) // npm install langfuse import Langfuse from 'langfuse'; const langfuse = new Langfuse(); // 2. エージェントのメイン関数をラップ async function runAgent(query: string, userId: string) { const trace = langfuse.trace({ name: 'agent-run', userId, input: query, }); // 既存のエージェントコード... trace.update({ output: response }); await langfuse.flushAsync(); }
第1週: コスト追跡を追加
const generation = trace.generation({ name: 'main-llm-call', model: 'gpt-4.1-mini', input: messages, output: completion, usage: { promptTokens: usage.prompt_tokens, completionTokens: usage.completion_tokens, }, // Langfuseがトークン数からコストを自動計算 });
第2週: 決定論的ガードを追加
// パターン1のサーキットブレーカーを適用 // 空応答の検出を追加 // ループ検出を追加 // ガード違反時のSlack/PagerDutyアラートを設定
第4週: LLM-as-Judge評価を追加
// プロダクショントレースの10%で実行 // 2次元で開始: 正確性 + 関連性 // アラートを有効化する前にベースラインを確立
2ヶ月目: フルスタックへ
Langfuse (トレーシング + コスト)
+ カスタム評価パイプライン (品質)
+ Grafana/Datadog (インフラ)
+ PagerDuty (アラート)
LLMオブザーバビリティチェックリスト
AI機能をプロダクションにデプロイする前に毎回:
- 全てのLLM呼び出しにトレースコンテキスト適用済み
- 毎回の呼び出しでトークン数とモデル名をキャプチャ
- ツール呼び出しに入出力ログ設定済み
- リクエスト単位、ユーザー単位のコスト追跡が有効
- サーキットブレーカーの制限を設定(トークン、呼び出し数、実行時間)
- 決定論的ガードが全トレースの100%で実行中
- LLM-as-Judge評価がサンプリングされたトレースで実行中
- 品質メトリクスのベースライン確立済み
- ガード違反と品質低下にアラート設定済み
- プロンプト/応答の完全ログにサンプリング適用(100%ではない)
- プロンプトログ前にPIIスクラビング適用済み
- ダッシュボードでコスト、品質、レイテンシのリアルタイムトレンド表示
- トレース保持ポリシー定義済み(通常30-90日)
AIエージェントは決定論的ソフトウェアじゃない。従来のAPIと同じようにモニタリングしていると、エージェントが静かに暴走するその日まで偽りの安心感を与えるだけだ。このガイドで紹介したオブザーバビリティパターンは、1日数百万のエージェントインタラクションを処理するプロダクションシステムで実戦検証済みのもの。核心はシンプル。推論を追跡できなければ、障害もデバッグできない。全てを計測し、継続的に評価し、エージェントの出力品質を自分の目で測っていないなら、緑色のダッシュボードは絶対に信じるな。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう