T-CREATOR

LlamaIndex の可観測性運用:Tracing/Telemetry/失敗ケースの可視化

LlamaIndex の可観測性運用:Tracing/Telemetry/失敗ケースの可視化

LLM アプリケーションを本番環境で安定稼働させるためには、システムの振る舞いを可視化し、問題を素早く特定できる体制が不可欠です。LlamaIndex では、Tracing や Telemetry といった可観測性機能を標準で提供しており、RAG パイプラインの各ステップを詳細に追跡できます。本記事では、LlamaIndex の可観測性機能を活用した運用方法について、実装例を交えながら詳しく解説していきます。

背景

LLM アプリケーションにおける可観測性の必要性

LLM を活用したアプリケーションは、従来のシステムと比較して以下のような特徴があります。

まず、処理フローが複雑化しやすい点が挙げられます。RAG(Retrieval-Augmented Generation)では、ドキュメントの取得、埋め込みベクトルの生成、類似度検索、プロンプト生成、LLM への問い合わせなど、多段階の処理が連鎖的に実行されるため、どの段階で問題が発生したのかを特定することが困難です。

次に、非決定的な挙動があります。LLM の出力は確率的であり、同じ入力でも異なる結果が返ることがあるため、再現性の確保やデバッグが従来のシステムよりも難しくなります。

また、外部 API への依存度が高いことも特徴です。OpenAI や Anthropic などの外部 API を利用する場合、レイテンシーやエラー率、コストなどを継続的に監視する必要があります。

LlamaIndex が提供する可観測性機能

LlamaIndex は、これらの課題に対応するため、充実した可観測性機能を提供しています。

#機能説明
1Tracingクエリ実行の各ステップを記録し、処理フローを可視化
2Telemetryメトリクスやログを収集し、システムの健全性を監視
3Callback Systemイベント駆動でカスタム処理を挿入可能
4IntegrationArize Phoenix、LangSmith、OpenTelemetry など外部ツールとの連携

以下の図は、LlamaIndex アプリケーションにおける可観測性のデータフローを示しています。

mermaidflowchart TB
    user["ユーザークエリ"] -->|入力| app["LlamaIndex<br/>アプリケーション"]
    app -->|1.取得| retriever["Retriever"]
    app -->|2.生成| llm["LLM"]

    retriever -->|Trace| tracer["Tracing<br/>システム"]
    llm -->|Trace| tracer

    tracer -->|メトリクス| collector["Telemetry<br/>Collector"]
    collector -->|可視化| dashboard["監視<br/>ダッシュボード"]

    app -->|失敗| error["エラー<br/>ハンドラ"]
    error -->|記録| tracer

この図から、各コンポーネントがどのように連携して可観測性を実現しているかがわかります。Retriever や LLM の処理は Tracing システムに記録され、Telemetry Collector を経由してダッシュボードで可視化されます。

課題

LLM アプリケーションの運用における課題

本番環境で LLM アプリケーションを運用する際、以下のような課題に直面することが多いです。

ブラックボックス化による問題特定の困難さ

ユーザーからの問い合わせに対して不適切な回答が返された場合、どの段階で問題が発生したのかを特定するのが困難です。検索結果が不適切だったのか、プロンプトの構成に問題があったのか、LLM の出力そのものに問題があったのか、これらを切り分けるには詳細なログが必要となります。

パフォーマンスのボトルネック特定

RAG パイプラインのどの部分が処理時間を占めているのかが不明確だと、最適化の方向性を決められません。ベクトル検索が遅いのか、LLM への API 呼び出しがボトルネックなのか、データに基づいた判断が求められます。

コストの可視化と最適化

LLM API の利用料金は、トークン数に応じて課金されることが一般的です。どのクエリがコストを押し上げているのか、プロンプトの長さは適切かなどを把握する必要があります。

失敗ケースの検出と分析

以下のような失敗ケースを早期に検出し、対応することが重要です。

#失敗ケース影響
1LLM API のタイムアウトユーザー体験の悪化
2検索結果が 0 件不適切な回答生成
3プロンプトが長すぎてトークン制限超過API エラー
4埋め込みモデルのエラーシステム全体の停止

以下の図は、失敗ケースが発生した際の検出フローを示しています。

mermaidflowchart TD
    query["クエリ実行"] --> check1{"検索結果<br/>あり?"}
    check1 -->|No| fail1["失敗: 検索結果0件"]
    check1 -->|Yes| check2{"トークン数<br/>制限内?"}

    check2 -->|No| fail2["失敗: トークン制限超過"]
    check2 -->|Yes| api["LLM API<br/>呼び出し"]

    api --> check3{"API<br/>成功?"}
    check3 -->|No| fail3["失敗: API エラー"]
    check3 -->|Yes| success["成功"]

    fail1 --> log["エラーログ<br/>記録"]
    fail2 --> log
    fail3 --> log
    log --> alert["アラート<br/>通知"]

図解の要点:

  • 検索結果の有無、トークン数、API 呼び出しの成功可否という 3 つのチェックポイントで失敗を検出
  • 各失敗ケースは統一的にログに記録され、アラートとして通知される
  • 段階的なチェックにより、問題の発生箇所を明確に特定可能

これらの課題を解決するには、適切な可観測性の仕組みが不可欠です。

解決策

LlamaIndex の Tracing 機能の活用

LlamaIndex では、Tracing を有効にすることで、クエリ実行の全ステップを記録できます。この機能により、処理の流れを時系列で追跡し、問題の発生箇所を特定できるようになります。

基本的な Tracing の設定

まず、LlamaIndex で Tracing を有効にする基本的な方法を見ていきましょう。

typescriptimport { Settings } from 'llamaindex';

上記のコードでは、LlamaIndex の Settings モジュールをインポートしています。このモジュールを使って、グローバルな設定を行います。

typescript// Tracing を有効化
Settings.callbackManager.setGlobalHandler('simple');

setGlobalHandler メソッドに 'simple' を指定することで、シンプルなコンソール出力ベースの Tracing が有効になります。これにより、各処理ステップがコンソールに出力されるようになります。

Arize Phoenix との連携

より高度な可視化を実現するには、Arize Phoenix などの専用ツールとの連携が効果的です。

typescriptimport { ArizePhoenixHandler } from 'llamaindex';

Arize Phoenix 用のハンドラーをインポートします。

typescript// Phoenix の設定
const phoenixHandler = new ArizePhoenixHandler({
  endpoint: 'http://localhost:6006',
  projectName: 'my-rag-app',
});

// グローバルハンドラーとして設定
Settings.callbackManager.setGlobalHandler(phoenixHandler);

このコードでは、Phoenix のエンドポイントとプロジェクト名を指定して、ハンドラーを初期化しています。プロジェクト名を指定することで、複数のアプリケーションを区別して管理できます。

カスタム Callback の実装

特定のイベントに対してカスタム処理を実行したい場合は、Callback を自作できます。

typescriptimport {
  BaseCallbackHandler,
  CallbackManager,
} from 'llamaindex';

ベースとなる BaseCallbackHandler クラスと CallbackManager をインポートします。

typescriptclass CustomTracingHandler extends BaseCallbackHandler {
  // LLM の開始イベント
  onLLMStart(event: any) {
    console.log('LLM Start:', {
      timestamp: new Date().toISOString(),
      prompt: event.prompt,
    });
  }

  // LLM の終了イベント
  onLLMEnd(event: any) {
    console.log('LLM End:', {
      timestamp: new Date().toISOString(),
      response: event.response,
      tokenUsage: event.tokenUsage,
    });
  }
}

BaseCallbackHandler を継承したクラスを定義し、onLLMStartonLLMEnd メソッドをオーバーライドします。これにより、LLM の呼び出し開始と終了時に、タイムスタンプやトークン使用量などの情報をログに記録できます。

typescript// カスタムハンドラーの登録
const customHandler = new CustomTracingHandler();
Settings.callbackManager.addHandler(customHandler);

作成したカスタムハンドラーを CallbackManager に追加することで、イベント発生時に自動的に呼び出されるようになります。

Telemetry によるメトリクス収集

Tracing がイベント単位の記録であるのに対し、Telemetry はシステム全体のメトリクスを継続的に収集する仕組みです。

OpenTelemetry との統合

OpenTelemetry は、クラウドネイティブなアプリケーションの可観測性を実現する標準的なフレームワークです。LlamaIndex は OpenTelemetry と連携できます。

typescriptimport { trace } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

OpenTelemetry の必要なモジュールをインポートします。NodeTracerProvider はトレースの提供者、BatchSpanProcessor はスパン(処理単位)をバッチ処理するためのプロセッサーです。

typescript// Tracer Provider の初期化
const provider = new NodeTracerProvider();
provider.register();

// Span Processor の設定
const spanProcessor = new BatchSpanProcessor(
  new ConsoleSpanExporter() // 本番環境では OTLP Exporter を使用
);
provider.addSpanProcessor(spanProcessor);

NodeTracerProvider を初期化し、register() で登録します。Span Processor には、開発環境では ConsoleSpanExporter を使い、本番環境では OTLP(OpenTelemetry Protocol)Exporter を使うのが一般的です。

メトリクスの記録

LlamaIndex のクエリ実行時に、カスタムメトリクスを記録する方法を見ていきましょう。

typescriptimport { metrics } from '@opentelemetry/api';

OpenTelemetry の metrics API をインポートします。

typescript// メーター(計測器)の取得
const meter = metrics.getMeter('llamaindex-app');

// カウンター(累積値)の作成
const queryCounter = meter.createCounter('query_count', {
  description: 'クエリ実行回数',
});

// ヒストグラム(分布)の作成
const latencyHistogram = meter.createHistogram(
  'query_latency',
  {
    description: 'クエリレイテンシー(ミリ秒)',
    unit: 'ms',
  }
);

getMeter でメーターを取得し、createCounter でクエリ実行回数を記録するカウンターを、createHistogram でレイテンシーの分布を記録するヒストグラムを作成します。

typescript// クエリ実行時にメトリクスを記録
async function executeQueryWithMetrics(query: string) {
  const startTime = Date.now();

  try {
    // クエリ実行
    const response = await queryEngine.query(query);

    // メトリクスの記録
    const duration = Date.now() - startTime;
    queryCounter.add(1, { status: 'success' });
    latencyHistogram.record(duration, {
      status: 'success',
    });

    return response;
  } catch (error) {
    const duration = Date.now() - startTime;
    queryCounter.add(1, { status: 'error' });
    latencyHistogram.record(duration, { status: 'error' });
    throw error;
  }
}

クエリ実行の前後で時間を計測し、成功時と失敗時の両方でメトリクスを記録します。status ラベルを付与することで、成功と失敗を区別して集計できます。

失敗ケースの可視化

失敗ケースを効果的に可視化するには、エラーの種類ごとに適切な情報を記録し、集約する仕組みが必要です。

エラー分類とロギング

まず、エラーを分類して記録する仕組みを実装しましょう。

typescript// エラータイプの定義
enum ErrorType {
  RETRIEVAL_FAILED = 'retrieval_failed',
  NO_RESULTS = 'no_results',
  TOKEN_LIMIT_EXCEEDED = 'token_limit_exceeded',
  LLM_API_ERROR = 'llm_api_error',
  EMBEDDING_ERROR = 'embedding_error',
}

主要なエラータイプを列挙型で定義します。これにより、エラーの種類を統一的に扱えます。

typescript// エラー情報の型定義
interface ErrorLog {
  type: ErrorType;
  message: string;
  query: string;
  timestamp: string;
  metadata?: Record<string, any>;
}

エラーログの構造を型定義します。エラータイプ、メッセージ、クエリ内容、タイムスタンプを必須項目とし、追加情報を metadata に格納できるようにします。

typescript// エラーロガーの実装
class ErrorLogger {
  private logs: ErrorLog[] = [];

  log(
    type: ErrorType,
    message: string,
    query: string,
    metadata?: any
  ) {
    const errorLog: ErrorLog = {
      type,
      message,
      query,
      timestamp: new Date().toISOString(),
      metadata,
    };

    this.logs.push(errorLog);

    // コンソールにも出力
    console.error('[Error]', errorLog);

    // 外部サービスへの送信(例: Sentry, Datadog)
    this.sendToExternalService(errorLog);
  }

  private sendToExternalService(log: ErrorLog) {
    // 実装例: エラートラッキングサービスへの送信
  }
}

ErrorLogger クラスを実装し、エラーログを配列に保存するとともに、コンソールへの出力と外部サービスへの送信を行います。

実際の失敗検出の実装

RAG パイプラインの各段階で失敗を検出し、適切にログに記録する実装例を見ていきます。

typescriptconst errorLogger = new ErrorLogger();

エラーロガーのインスタンスを作成します。

typescriptasync function executeRAGWithErrorHandling(query: string) {
  try {
    // ステップ 1: ドキュメント検索
    const retrievedDocs = await retriever.retrieve(query);

    if (retrievedDocs.length === 0) {
      errorLogger.log(
        ErrorType.NO_RESULTS,
        '検索結果が0件でした',
        query
      );
      return { error: '関連する情報が見つかりませんでした' };
    }

ドキュメント検索を実行し、結果が 0 件の場合はエラーログに記録します。この情報は後でダッシュボードで可視化できます。

typescript// ステップ 2: トークン数チェック
const promptTokens = estimateTokenCount(
  query,
  retrievedDocs
);

if (promptTokens > 4000) {
  errorLogger.log(
    ErrorType.TOKEN_LIMIT_EXCEEDED,
    `プロンプトトークン数が制限を超過: ${promptTokens}`,
    query,
    { tokenCount: promptTokens }
  );
  // ドキュメント数を削減して再試行
  retrievedDocs.splice(3); // 上位3件のみ使用
}

プロンプトのトークン数を推定し、制限を超える場合はエラーログに記録します。同時に、ドキュメント数を削減して処理を継続する自動修復も実装しています。

typescript    // ステップ 3: LLM API 呼び出し
    const response = await llm.complete({
      prompt: buildPrompt(query, retrievedDocs),
    });

    return { response: response.text };

  } catch (error: any) {
    // API エラーの分類
    if (error.code === 'ETIMEDOUT') {
      errorLogger.log(
        ErrorType.LLM_API_ERROR,
        'LLM API がタイムアウトしました',
        query,
        { error: error.message }
      );
    } else if (error.status === 429) {
      errorLogger.log(
        ErrorType.LLM_API_ERROR,
        'レート制限に達しました',
        query,
        { error: error.message }
      );
    } else {
      errorLogger.log(
        ErrorType.LLM_API_ERROR,
        '予期しないエラーが発生しました',
        query,
        { error: error.message, stack: error.stack }
      );
    }

    throw error;
  }
}

LLM API 呼び出し時のエラーを catch し、エラーコードに応じて適切に分類します。タイムアウト(ETIMEDOUT)やレート制限(HTTP 429)など、具体的なエラーコードを使って分類することで、後で統計を取りやすくなります。

ダッシュボードでの可視化

収集したエラー情報をダッシュボードで可視化する際の設計例を紹介します。

typescript// エラー統計の集計
interface ErrorStats {
  totalErrors: number;
  errorsByType: Record<ErrorType, number>;
  recentErrors: ErrorLog[];
}

エラー統計の型を定義します。総エラー数、タイプ別のエラー数、直近のエラーログを含みます。

typescriptfunction calculateErrorStats(logs: ErrorLog[]): ErrorStats {
  const errorsByType = logs.reduce((acc, log) => {
    acc[log.type] = (acc[log.type] || 0) + 1;
    return acc;
  }, {} as Record<ErrorType, number>);

  const recentErrors = logs
    .sort(
      (a, b) =>
        new Date(b.timestamp).getTime() -
        new Date(a.timestamp).getTime()
    )
    .slice(0, 10); // 直近10件

  return {
    totalErrors: logs.length,
    errorsByType,
    recentErrors,
  };
}

エラーログの配列から統計情報を計算します。reduce を使ってタイプ別にカウントし、タイムスタンプでソートして直近 10 件を抽出します。

以下の図は、エラー統計情報がどのようにダッシュボードに表示されるかを示しています。

mermaidflowchart LR
    subgraph sources["データソース"]
        logs["エラーログ<br/>配列"]
    end

    subgraph processing["処理層"]
        calc["統計計算<br/>関数"]
    end

    subgraph output["出力層"]
        total["総エラー数<br/>表示"]
        bytype["タイプ別<br/>円グラフ"]
        recent["直近エラー<br/>テーブル"]
    end

    logs --> calc
    calc --> total
    calc --> bytype
    calc --> recent

この図から、エラーログが統計計算を経て、総数、タイプ別グラフ、直近のエラーテーブルという 3 つの形式で可視化されることがわかります。

具体例

RAG アプリケーションの完全な可観測性実装

ここでは、TypeScript を使った RAG アプリケーションの完全な実装例を、可観測性機能を含めて紹介します。

プロジェクトのセットアップ

まず、必要なパッケージをインストールします。

bashyarn add llamaindex
yarn add @opentelemetry/api @opentelemetry/sdk-trace-node
yarn add @opentelemetry/exporter-trace-otlp-http

これらのコマンドで、LlamaIndex と OpenTelemetry の関連パッケージをインストールします。

設定ファイルの作成

可観測性の設定をまとめた設定ファイルを作成しましょう。

typescript// observability-config.ts

import { Settings } from 'llamaindex';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

必要なモジュールをインポートします。OTLPTraceExporter は、トレースデータを OpenTelemetry Collector に送信するためのエクスポーターです。

typescriptexport function setupObservability() {
  // OpenTelemetry の初期化
  const provider = new NodeTracerProvider();

  const exporter = new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
  });

  provider.addSpanProcessor(new BatchSpanProcessor(exporter));
  provider.register();

環境変数から OpenTelemetry Collector のエンドポイントを取得し、トレースエクスポーターを設定します。本番環境では、Jaeger や Zipkin などの分散トレーシングツールのエンドポイントを指定します。

typescript  // LlamaIndex の Tracing 設定
  Settings.callbackManager.setGlobalHandler('simple');

  console.log('可観測性の設定が完了しました');
}

LlamaIndex のグローバルハンドラーを設定し、トレーシングを有効化します。

メイン処理の実装

可観測性機能を組み込んだメイン処理を実装します。

typescript// main.ts

import {
  VectorStoreIndex,
  SimpleDirectoryReader,
  OpenAI,
} from 'llamaindex';
import { setupObservability } from './observability-config';

LlamaIndex の主要なクラスと、先ほど作成した設定関数をインポートします。

typescript// 可観測性の初期化
setupObservability();

const errorLogger = new ErrorLogger();

アプリケーション起動時に可観測性の設定を実行し、エラーロガーを初期化します。

typescriptasync function main() {
  try {
    // ドキュメントの読み込み
    const reader = new SimpleDirectoryReader();
    const documents = await reader.loadData('./data');

    console.log(`${documents.length} 件のドキュメントを読み込みました`);

指定したディレクトリからドキュメントを読み込みます。読み込んだドキュメント数をログに出力することで、処理の進捗を把握できます。

typescript// インデックスの作成
const index = await VectorStoreIndex.fromDocuments(
  documents
);

console.log('インデックスの作成が完了しました');

読み込んだドキュメントからベクトルインデックスを作成します。この処理は時間がかかるため、完了メッセージを出力します。

typescript// クエリエンジンの作成
const queryEngine = index.asQueryEngine({
  llm: new OpenAI({
    model: 'gpt-4',
    temperature: 0,
  }),
});

作成したインデックスからクエリエンジンを生成します。temperature: 0 に設定することで、より決定的な出力が得られます。

typescript// クエリの実行(可観測性付き)
const query = 'LlamaIndex の主な機能は何ですか?';

const startTime = Date.now();
const response = await executeRAGWithErrorHandling(
  query,
  queryEngine
);
const duration = Date.now() - startTime;

console.log(`\nクエリ: ${query}`);
console.log(`回答: ${response.response}`);
console.log(`処理時間: ${duration}ms`);

クエリを実行し、処理時間を計測してログに出力します。これにより、パフォーマンスの問題を早期に発見できます。

typescript  } catch (error) {
    console.error('アプリケーションエラー:', error);
    throw error;
  }
}

main().catch(console.error);

メイン関数を実行し、未処理のエラーをキャッチしてログに出力します。

高度な監視機能の実装

さらに高度な監視機能として、リトライロジックとサーキットブレーカーを実装してみましょう。

typescript// retry-logic.ts

interface RetryConfig {
  maxRetries: number;
  initialDelayMs: number;
  maxDelayMs: number;
}

リトライの設定を定義する型です。最大リトライ回数、初期遅延時間、最大遅延時間を指定します。

typescriptasync function executeWithRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig,
  errorLogger: ErrorLogger
): Promise<T> {
  let lastError: any;
  let delayMs = config.initialDelayMs;

  for (let attempt = 0; attempt < config.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: any) {
      lastError = error;

      // リトライ可能なエラーか判定
      if (!isRetryableError(error)) {
        throw error;
      }

指定された関数を実行し、失敗した場合はリトライします。リトライ可能なエラーかどうかを判定し、そうでなければ即座に例外をスローします。

typescript      // エラーログの記録
      errorLogger.log(
        ErrorType.LLM_API_ERROR,
        `リトライ ${attempt + 1}/${config.maxRetries}`,
        '',
        { error: error.message, attempt }
      );

      // 指数バックオフで待機
      await sleep(delayMs);
      delayMs = Math.min(delayMs * 2, config.maxDelayMs);
    }
  }

  throw lastError;
}

リトライごとにエラーログを記録し、指数バックオフ(待機時間を 2 倍ずつ増やす)で再試行します。最大遅延時間に達した後は、それ以上増やしません。

typescriptfunction isRetryableError(error: any): boolean {
  // タイムアウトやレート制限はリトライ可能
  return (
    error.code === 'ETIMEDOUT' ||
    error.status === 429 ||
    error.status === 503
  );
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

エラーコードに基づいてリトライ可能かどうかを判定します。タイムアウト、レート制限(429)、サービス利用不可(503)の場合はリトライします。

サーキットブレーカーパターン

連続してエラーが発生する場合に、一時的にリクエストを遮断するサーキットブレーカーパターンを実装します。

typescript// circuit-breaker.ts

enum CircuitState {
  CLOSED = 'closed', // 正常動作
  OPEN = 'open', // 遮断中
  HALF_OPEN = 'half_open', // 回復テスト中
}

サーキットの状態を表す列挙型です。CLOSED は正常、OPEN は遮断中、HALF_OPEN は回復テスト中を表します。

typescriptclass CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount: number = 0;
  private lastFailureTime: number = 0;

  constructor(
    private threshold: number,        // 連続失敗の閾値
    private resetTimeoutMs: number    // リセットまでの時間
  ) {}

サーキットブレーカーのコンストラクタです。連続失敗の閾値と、リセットまでの時間を指定します。

typescript  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      // 一定時間経過後、HALF_OPEN に移行
      if (Date.now() - this.lastFailureTime > this.resetTimeoutMs) {
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new Error('Circuit breaker is OPEN: サービスが一時的に利用できません');
      }
    }

サーキットが OPEN(遮断中)の場合、リセット時間が経過していれば HALF_OPEN に移行します。そうでなければ、エラーをスローしてリクエストを遮断します。

typescript    try {
      const result = await fn();

      // 成功したらリセット
      this.failureCount = 0;
      if (this.state === CircuitState.HALF_OPEN) {
        this.state = CircuitState.CLOSED;
        console.log('Circuit breaker: HALF_OPEN → CLOSED');
      }

      return result;
    } catch (error) {
      this.failureCount++;
      this.lastFailureTime = Date.now();

      if (this.failureCount >= this.threshold) {
        this.state = CircuitState.OPEN;
        console.error(`Circuit breaker: OPEN (失敗回数: ${this.failureCount})`);
      }

      throw error;
    }
  }
}

関数の実行に成功した場合は失敗カウントをリセットし、HALF_OPEN から CLOSED に移行します。失敗した場合は失敗カウントを増やし、閾値に達したら OPEN に移行します。

以下の図は、サーキットブレーカーの状態遷移を示しています。

mermaidstateDiagram-v2
    [*] --> CLOSED: 初期状態

    CLOSED --> OPEN: 失敗が閾値に到達
    OPEN --> HALF_OPEN: タイムアウト経過
    HALF_OPEN --> CLOSED: リクエスト成功
    HALF_OPEN --> OPEN: リクエスト失敗

    CLOSED --> CLOSED: リクエスト成功
    OPEN --> OPEN: タイムアウト未経過

図解の要点:

  • CLOSED 状態では通常通りリクエストを処理
  • 連続失敗が閾値に達すると OPEN に移行し、リクエストを遮断
  • 一定時間経過後、HALF_OPEN で回復をテスト
  • 成功すれば CLOSED に戻り、失敗すれば OPEN に戻る

統合した実装例

これまでの機能を統合した最終的な実装例です。

typescript// integrated-app.ts

const retryConfig: RetryConfig = {
  maxRetries: 3,
  initialDelayMs: 1000,
  maxDelayMs: 5000,
};

const circuitBreaker = new CircuitBreaker(5, 30000); // 5回失敗で30秒遮断

リトライ設定とサーキットブレーカーを初期化します。

typescriptasync function queryWithFullObservability(
  query: string,
  queryEngine: any
): Promise<any> {
  return await circuitBreaker.execute(async () => {
    return await executeWithRetry(
      async () => {
        return await executeRAGWithErrorHandling(
          query,
          queryEngine
        );
      },
      retryConfig,
      errorLogger
    );
  });
}

サーキットブレーカー、リトライ、エラーハンドリングを組み合わせた関数です。この 3 層の防御により、高い可用性と障害への耐性を実現します。

実行結果の例

実際にアプリケーションを実行した際のログ出力例を見てみましょう。

css可観測性の設定が完了しました
5 件のドキュメントを読み込みました
インデックスの作成が完了しました

[Trace] Retriever Start: { timestamp: '2025-01-15T10:30:00.123Z', query: 'LlamaIndex の主な機能は何ですか?' }
[Trace] Retriever End: { timestamp: '2025-01-15T10:30:00.456Z', resultCount: 3, duration: 333 }

[Trace] LLM Start: { timestamp: '2025-01-15T10:30:00.457Z', tokens: 850 }
[Trace] LLM End: { timestamp: '2025-01-15T10:30:02.123Z', tokens: 1200, duration: 1666 }

クエリ: LlamaIndex の主な機能は何ですか?
回答: LlamaIndex の主な機能は、RAG パイプラインの構築、ベクトル検索、LLM との統合、可観測性機能などです。
処理時間: 2000ms

このログから、Retriever の処理に 333ms、LLM の処理に 1666ms かかったことがわかります。ボトルネックは LLM の呼び出しであることが明確です。

エラー発生時のログ

エラーが発生した場合のログ出力例です。

css[Error] {
  type: 'no_results',
  message: '検索結果が0件でした',
  query: '存在しない情報についての質問',
  timestamp: '2025-01-15T10:35:00.000Z'
}

[Error] {
  type: 'llm_api_error',
  message: 'LLM API がタイムアウトしました',
  query: '複雑な質問',
  timestamp: '2025-01-15T10:36:00.000Z',
  metadata: { error: 'ETIMEDOUT' }
}

リトライ 1/3
Circuit breaker: OPEN (失敗回数: 5)
Error: Circuit breaker is OPEN: サービスが一時的に利用できません

このログから、検索結果が 0 件のエラーや、API タイムアウト、そしてサーキットブレーカーが作動したことがわかります。

まとめ

本記事では、LlamaIndex の可観測性運用について、Tracing、Telemetry、失敗ケースの可視化という 3 つの観点から詳しく解説しました。

LLM アプリケーションの本番運用では、ブラックボックス化しがちな処理フローを可視化し、問題の早期発見と迅速な対応が求められます。LlamaIndex は標準で Tracing 機能を提供しており、Arize Phoenix や OpenTelemetry といった外部ツールとの連携も容易です。

Telemetry によるメトリクス収集では、クエリ実行回数、レイテンシー、トークン使用量などを継続的に監視することで、パフォーマンスの劣化やコストの増加を早期に検出できます。OpenTelemetry を活用することで、業界標準のフォーマットでデータを収集し、様々なモニタリングツールと連携できるようになります。

失敗ケースの可視化では、エラーを適切に分類し、発生箇所と原因を明確にすることが重要です。リトライロジックやサーキットブレーカーパターンを実装することで、一時的な障害に対する耐性を高め、システム全体の可用性を向上させることができます。

可観測性は、アプリケーション開発の初期段階から組み込むことが理想的です。本記事で紹介した実装パターンを参考に、皆様の LlamaIndex アプリケーションに適した可観測性の仕組みを構築していただければ幸いです。

継続的な監視と改善により、より安定した RAG アプリケーションの運用が実現できるでしょう。

関連リンク