T-CREATOR

LangChain コスト監視ダッシュボード:使用量・失敗率・応答品質を KPI 化

LangChain コスト監視ダッシュボード:使用量・失敗率・応答品質を KPI 化

LangChain を使った AI アプリケーションを本番運用する際、どのくらいコストがかかっているのか、どのくらいの頻度でエラーが発生しているのか、ユーザーの満足度はどうなのかといった指標を可視化することは非常に重要です。本記事では、LangChain のコスト監視ダッシュボードを構築し、使用量・失敗率・応答品質を KPI として管理する方法を詳しく解説します。これにより、運用コストの最適化や品質改善に向けたアクションを迅速に取れるようになるでしょう。

背景

LangChain アプリケーションの運用課題

LangChain を使った AI アプリケーションは、OpenAI や Anthropic などの LLM API を呼び出してユーザーに価値を提供します。しかし、本番環境で運用を始めると、以下のような課題に直面することが多いです。

  • コストの可視化が難しい: API 呼び出しごとに課金されるため、どの機能やユーザーがどのくらいのコストを発生させているのかが見えにくい
  • エラーの検知が遅れる: API のレート制限やネットワークエラーなどが発生しても、ログを手動で確認しなければ気づけない
  • 品質の定量評価ができない: ユーザーがどのくらい満足しているのか、応答の品質がどうなのかを数値で把握できない

これらの課題を放置すると、予算オーバーやユーザー体験の低下につながります。

ダッシュボードによる可視化の重要性

運用の透明性を高めるには、重要な指標をリアルタイムで可視化するダッシュボードが不可欠です。ダッシュボードがあれば、以下のようなメリットが得られます。

  • コストの急増をすぐに検知し、予算超過を防げる
  • エラー発生時にアラートを受け取り、迅速に対応できる
  • 応答品質のトレンドを追跡し、改善施策の効果を測定できる

本記事では、これらの指標を KPI(Key Performance Indicator) として設定し、監視する仕組みを構築していきます。

以下の図は、LangChain アプリケーションにおける監視対象の全体像を示します。

mermaidflowchart TB
  user["ユーザー"] -->|リクエスト| app["LangChain<br/>アプリケーション"]
  app -->|API 呼び出し| llm["LLM API<br/>(OpenAI, Anthropic)"]
  llm -->|応答| app
  app -->|結果| user

  app -->|ログ記録| logger["ロギング<br/>システム"]
  logger -->|集計| metrics["メトリクス<br/>収集"]
  metrics -->|可視化| dashboard["監視<br/>ダッシュボード"]

  dashboard -->|KPI 確認| operator["運用者"]

この図からわかるように、アプリケーションから出力されるログを収集・集計し、ダッシュボードで可視化することで、運用者がリアルタイムに状況を把握できます。

課題

コスト管理の難しさ

LangChain アプリケーションでは、複数の LLM を組み合わせたり、ベクトルストアや外部ツールを呼び出したりするため、コスト構造が複雑になりがちです。

  • トークン数の把握: 入力・出力それぞれのトークン数を正確に記録しないと、正確なコストが計算できない
  • モデルごとの単価: GPT-4、GPT-3.5、Claude など、モデルごとに料金が異なるため、どのモデルがどのくらい使われているかを把握する必要がある
  • ユーザーごとの利用量: 特定のユーザーやテナントが過剰に利用している場合、それを検知してアクションを取る必要がある

エラー・失敗の検知

API 呼び出しは常に成功するとは限りません。以下のようなエラーが発生する可能性があります。

  • レート制限エラー: RateLimitError など、API の呼び出し制限に達した場合
  • タイムアウト: ネットワーク遅延や処理時間超過による失敗
  • バリデーションエラー: 不正な入力や出力形式による失敗

これらのエラーを迅速に検知し、失敗率として可視化することで、問題の早期発見につながります。

応答品質の評価

ユーザー満足度を測るには、応答の品質を定量的に評価する必要があります。しかし、自然言語の品質評価は難しく、以下の課題があります。

  • 主観的な評価: 品質は人によって感じ方が異なるため、客観的な指標が必要
  • 自動評価の精度: LLM による自己評価やルールベースの評価は、必ずしも人間の評価と一致しない
  • 評価コスト: すべての応答を人手で評価するのは現実的ではない

本記事では、自動評価とサンプリングを組み合わせた現実的なアプローチを紹介します。

以下の図は、監視すべき 3 つの主要な課題を整理したものです。

mermaidflowchart TB
  challenge["監視の課題"]

  challenge --> cost["コスト管理"]
  challenge --> error["エラー検知"]
  challenge --> quality["品質評価"]

  cost --> cost1["トークン数の把握"]
  cost --> cost2["モデル別単価"]
  cost --> cost3["ユーザー別利用量"]

  error --> error1["レート制限エラー"]
  error --> error2["タイムアウト"]
  error --> error3["バリデーションエラー"]

  quality --> quality1["主観的評価"]
  quality --> quality2["自動評価の精度"]
  quality --> quality3["評価コスト"]

これらの課題を解決するために、次のセクションで具体的な解決策を見ていきましょう。

解決策

アーキテクチャの全体像

LangChain のコスト監視ダッシュボードを構築するには、以下の要素を組み合わせます。

  • LangSmith: LangChain 公式のトレーシング・監視ツール
  • カスタムコールバック: LangChain のコールバック機能を使って、独自のメトリクスを記録
  • データストア: PostgreSQL や MongoDB などのデータベースにログを保存
  • 可視化ツール: Grafana や Metabase などで KPI を可視化

これらを組み合わせることで、柔軟かつ強力な監視基盤を構築できます。

以下の図は、監視ダッシュボードのアーキテクチャ全体を示しています。

mermaidflowchart LR
  app["LangChain<br/>アプリ"]

  app -->|トレース| langsmith["LangSmith"]
  app -->|カスタム<br/>コールバック| callback["コールバック<br/>ハンドラー"]

  callback -->|メトリクス記録| db[("PostgreSQL<br/>or MongoDB")]

  db -->|データ集計| analytics["分析<br/>スクリプト"]
  analytics -->|可視化| grafana["Grafana<br/>ダッシュボード"]

  langsmith -->|API 連携| grafana

LangSmith は基本的なトレーシングと可視化を提供し、カスタムコールバックで独自の KPI を記録します。データベースに保存したメトリクスを Grafana で可視化することで、リアルタイムな監視が可能になります。

LangSmith による基本的なトレーシング

LangSmith は LangChain 公式の監視ツールで、API キーを設定するだけで自動的にトレースを記録できます。

環境変数の設定

LangSmith を有効にするには、以下の環境変数を設定します。

bashexport LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="your-langsmith-api-key"
export LANGCHAIN_PROJECT="your-project-name"

これにより、LangChain の実行がすべて LangSmith に記録されます。

LangSmith の主な機能

LangSmith を使うと、以下の情報が自動的に記録されます。

  • トレース情報: Chain や Agent の実行フロー全体
  • トークン数: 入力・出力のトークン数とコスト
  • 実行時間: 各ステップの処理時間
  • エラー情報: 例外やエラーメッセージ

LangSmith の Web UI で、これらの情報を時系列グラフや表形式で確認できます。

カスタムコールバックによるメトリクス記録

LangSmith だけではカバーできない独自の KPI を記録するには、LangChain のコールバック機能を使います。

コールバックハンドラーの実装

カスタムコールバックハンドラーを実装することで、各イベント(LLM 呼び出し開始、終了など)をフックして、独自のロジックを実行できます。

typescriptimport { BaseCallbackHandler } from '@langchain/core/callbacks/base';
import { LLMResult } from '@langchain/core/outputs';

上記のインポートで、LangChain のコールバック基底クラスと出力型をインポートしています。

typescript// データベース接続(例:PostgreSQL)
import { Pool } from 'pg';

const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});

PostgreSQL のコネクションプールを初期化します。環境変数から接続情報を読み込むことで、本番環境とローカル環境で設定を切り替えられます。

typescript// カスタムコールバックハンドラー
class MetricsCallbackHandler extends BaseCallbackHandler {
  name = "MetricsCallbackHandler";

  // LLM 呼び出し開始時
  async handleLLMStart(
    llm: { name: string },
    prompts: string[],
    runId: string
  ) {
    console.log(`[LLM Start] runId: ${runId}, model: ${llm.name}`);

    // データベースに記録
    await pool.query(
      `INSERT INTO llm_calls (run_id, model_name, prompt, started_at)
       VALUES ($1, $2, $3, NOW())`,
      [runId, llm.name, prompts[0]]
    );
  }

LLM 呼び出しが開始されたタイミングで、実行 ID やモデル名、プロンプトをデータベースに記録します。これにより、後からコストや実行時間を分析できます。

typescript  // LLM 呼び出し終了時
  async handleLLMEnd(output: LLMResult, runId: string) {
    const generation = output.generations[0][0];
    const text = generation.text;

    // トークン数を取得
    const tokenUsage = output.llmOutput?.tokenUsage;
    const promptTokens = tokenUsage?.promptTokens || 0;
    const completionTokens = tokenUsage?.completionTokens || 0;
    const totalTokens = tokenUsage?.totalTokens || 0;

    console.log(`[LLM End] runId: ${runId}, tokens: ${totalTokens}`);

    // データベースに記録
    await pool.query(
      `UPDATE llm_calls
       SET response = $1,
           prompt_tokens = $2,
           completion_tokens = $3,
           total_tokens = $4,
           ended_at = NOW()
       WHERE run_id = $5`,
      [text, promptTokens, completionTokens, totalTokens, runId]
    );
  }

LLM の応答が返ってきたタイミングで、トークン数と応答テキストを記録します。tokenUsage には各トークンカウントが含まれており、これを使ってコストを計算できます。

typescript  // エラー発生時
  async handleLLMError(error: Error, runId: string) {
    console.error(`[LLM Error] runId: ${runId}, error: ${error.message}`);

    // データベースに記録
    await pool.query(
      `UPDATE llm_calls
       SET error_message = $1,
           error_type = $2,
           ended_at = NOW()
       WHERE run_id = $3`,
      [error.message, error.constructor.name, runId]
    );
  }
}

エラーが発生した場合も記録することで、失敗率を計算できます。エラータイプを保存しておくことで、どのような種類のエラーが多いのかを分析できます。

コールバックハンドラーの使用

作成したコールバックハンドラーを LangChain の実行に組み込みます。

typescriptimport { ChatOpenAI } from '@langchain/openai';

const llm = new ChatOpenAI({
  modelName: 'gpt-4',
  temperature: 0.7,
  callbacks: [new MetricsCallbackHandler()],
});

callbacks オプションにカスタムハンドラーを渡すことで、すべての LLM 呼び出しで自動的にメトリクスが記録されます。

typescript// 実行例
const response = await llm.invoke(
  'TypeScript の型安全性について教えてください'
);
console.log(response.content);

この実行が行われると、handleLLMStarthandleLLMEnd(またはエラー時は handleLLMError)が自動的に呼び出され、データベースに記録されます。

コスト計算の実装

トークン数からコストを計算するには、各モデルの料金表を参照します。

モデルごとの料金テーブル

typescript// モデルごとの料金(USD / 1M tokens)
const PRICING = {
  'gpt-4': {
    prompt: 30.0, // $30 / 1M tokens
    completion: 60.0, // $60 / 1M tokens
  },
  'gpt-3.5-turbo': {
    prompt: 0.5, // $0.50 / 1M tokens
    completion: 1.5, // $1.50 / 1M tokens
  },
  'claude-3-opus-20240229': {
    prompt: 15.0, // $15 / 1M tokens
    completion: 75.0, // $75 / 1M tokens
  },
};

料金表は公式ドキュメントから取得し、定期的に更新する必要があります。OpenAI や Anthropic の料金は変更される可能性があるため、注意が必要です。

コスト計算関数

typescript/**
 * トークン数からコストを計算する関数
 * @param modelName モデル名(例: "gpt-4")
 * @param promptTokens プロンプトトークン数
 * @param completionTokens 補完トークン数
 * @returns コスト(USD)
 */
function calculateCost(
  modelName: string,
  promptTokens: number,
  completionTokens: number
): number {
  const pricing = PRICING[modelName];

  if (!pricing) {
    console.warn(`Unknown model: ${modelName}`);
    return 0;
  }

  // 1M tokens あたりの料金なので、1,000,000 で割る
  const promptCost =
    (promptTokens / 1_000_000) * pricing.prompt;
  const completionCost =
    (completionTokens / 1_000_000) * pricing.completion;

  return promptCost + completionCost;
}

この関数を使うことで、トークン数から正確なコストを計算できます。

データベーステーブル定義

コストを記録するためのテーブルを定義します。

sql-- LLM 呼び出しログテーブル
CREATE TABLE llm_calls (
  id SERIAL PRIMARY KEY,
  run_id VARCHAR(255) UNIQUE NOT NULL,
  model_name VARCHAR(100) NOT NULL,
  prompt TEXT,
  response TEXT,
  prompt_tokens INTEGER,
  completion_tokens INTEGER,
  total_tokens INTEGER,
  cost_usd DECIMAL(10, 6),  -- コスト(USD)
  error_message TEXT,
  error_type VARCHAR(100),
  started_at TIMESTAMP,
  ended_at TIMESTAMP,
  duration_ms INTEGER,  -- 実行時間(ミリ秒)
  user_id VARCHAR(255),  -- ユーザー識別子
  session_id VARCHAR(255),  -- セッション識別子
  created_at TIMESTAMP DEFAULT NOW()
);

このテーブルには、コスト計算に必要なすべての情報が含まれています。user_idsession_id を記録することで、ユーザーごとやセッションごとの分析も可能になります。

sql-- インデックス作成(集計クエリの高速化)
CREATE INDEX idx_llm_calls_started_at ON llm_calls(started_at);
CREATE INDEX idx_llm_calls_model_name ON llm_calls(model_name);
CREATE INDEX idx_llm_calls_user_id ON llm_calls(user_id);

集計クエリを高速化するために、よく使うカラムにインデックスを作成します。

コスト計算の自動化

コールバックハンドラーにコスト計算を組み込みます。

typescriptasync handleLLMEnd(output: LLMResult, runId: string) {
  const generation = output.generations[0][0];
  const text = generation.text;
  const tokenUsage = output.llmOutput?.tokenUsage;

  const promptTokens = tokenUsage?.promptTokens || 0;
  const completionTokens = tokenUsage?.completionTokens || 0;
  const totalTokens = tokenUsage?.totalTokens || 0;

  // モデル名を取得
  const result = await pool.query(
    `SELECT model_name, started_at FROM llm_calls WHERE run_id = $1`,
    [runId]
  );
  const { model_name, started_at } = result.rows[0];

  // コストを計算
  const cost = calculateCost(model_name, promptTokens, completionTokens);

  // 実行時間を計算
  const durationMs = Date.now() - new Date(started_at).getTime();

  // データベースに記録
  await pool.query(
    `UPDATE llm_calls
     SET response = $1,
         prompt_tokens = $2,
         completion_tokens = $3,
         total_tokens = $4,
         cost_usd = $5,
         duration_ms = $6,
         ended_at = NOW()
     WHERE run_id = $7`,
    [text, promptTokens, completionTokens, totalTokens, cost, durationMs, runId]
  );
}

handleLLMEnd 内でコストと実行時間を計算し、データベースに保存します。これにより、後から集計クエリで簡単にコストを分析できます。

失敗率の計算

エラーが発生した呼び出しを記録することで、失敗率を計算できます。

失敗率を取得する SQL クエリ

sql-- 過去 24 時間の失敗率
SELECT
  COUNT(*) FILTER (WHERE error_message IS NOT NULL) AS failed_count,
  COUNT(*) AS total_count,
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE error_message IS NOT NULL) / COUNT(*),
    2
  ) AS failure_rate_percent
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours';

このクエリは、過去 24 時間の総呼び出し数と失敗数、失敗率をパーセントで返します。

エラータイプごとの集計

sql-- エラータイプごとの集計
SELECT
  error_type,
  COUNT(*) AS error_count,
  AVG(duration_ms) AS avg_duration_ms
FROM llm_calls
WHERE error_message IS NOT NULL
  AND started_at > NOW() - INTERVAL '7 days'
GROUP BY error_type
ORDER BY error_count DESC;

エラータイプごとに集計することで、どのようなエラーが多発しているのかを把握できます。たとえば、RateLimitError が多い場合は、リトライロジックやレート制限の調整が必要です。

応答品質の評価

応答品質を自動的に評価するには、以下のようなアプローチがあります。

LLM による自己評価

生成された応答を別の LLM に評価させることで、品質スコアを付けられます。

typescriptimport { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';

評価用の LLM とプロンプトテンプレートをインポートします。

typescript// 評価用の LLM
const evaluatorLLM = new ChatOpenAI({
  modelName: 'gpt-3.5-turbo', // コスト削減のため軽量モデルを使用
  temperature: 0, // 評価の一貫性を保つため temperature は 0
});

評価には軽量なモデルを使うことで、評価自体のコストを抑えられます。

typescript// 評価用プロンプト
const evaluationPrompt = PromptTemplate.fromTemplate(`
あなたは AI 応答の品質を評価する専門家です。以下の質問と応答を評価してください。

質問: {question}
応答: {response}

以下の基準で 1-5 のスコアを付けてください:
- 正確性: 応答が質問に対して正確か
- 完全性: 必要な情報がすべて含まれているか
- 明瞭性: わかりやすく説明されているか
- 有用性: ユーザーにとって役立つ情報か

JSON 形式で回答してください:
{{"accuracy": <1-5>, "completeness": <1-5>, "clarity": <1-5>, "usefulness": <1-5>, "overall": <1-5>}}
`);

評価基準を明確に定義することで、一貫性のある評価が得られます。

typescript/**
 * 応答品質を評価する関数
 * @param question 質問
 * @param response 応答
 * @returns 品質スコア
 */
async function evaluateResponse(
  question: string,
  response: string
): Promise<{
  accuracy: number;
  completeness: number;
  clarity: number;
  usefulness: number;
  overall: number;
}> {
  const prompt = await evaluationPrompt.format({
    question,
    response,
  });
  const result = await evaluatorLLM.invoke(prompt);

  try {
    const scores = JSON.parse(result.content as string);
    return scores;
  } catch (error) {
    console.error(
      'Failed to parse evaluation result:',
      error
    );
    return {
      accuracy: 0,
      completeness: 0,
      clarity: 0,
      usefulness: 0,
      overall: 0,
    };
  }
}

評価結果を JSON としてパースし、各指標のスコアを返します。パースに失敗した場合は、すべて 0 として扱います。

評価結果の記録

typescript// コールバックハンドラーに評価ロジックを追加
async handleLLMEnd(output: LLMResult, runId: string) {
  // ... 既存のコード ...

  // 応答品質を評価(サンプリング:10% の確率で評価)
  if (Math.random() < 0.1) {
    const result = await pool.query(
      `SELECT prompt, response FROM llm_calls WHERE run_id = $1`,
      [runId]
    );
    const { prompt, response } = result.rows[0];

    const scores = await evaluateResponse(prompt, response);

    // 評価スコアをデータベースに記録
    await pool.query(
      `UPDATE llm_calls
       SET quality_accuracy = $1,
           quality_completeness = $2,
           quality_clarity = $3,
           quality_usefulness = $4,
           quality_overall = $5
       WHERE run_id = $6`,
      [
        scores.accuracy,
        scores.completeness,
        scores.clarity,
        scores.usefulness,
        scores.overall,
        runId
      ]
    );
  }
}

すべての応答を評価するとコストが高くなるため、ランダムサンプリング(ここでは 10%)で評価します。サンプリング率は、予算や必要性に応じて調整してください。

品質スコアの集計

sql-- 過去 7 日間の平均品質スコア
SELECT
  AVG(quality_accuracy) AS avg_accuracy,
  AVG(quality_completeness) AS avg_completeness,
  AVG(quality_clarity) AS avg_clarity,
  AVG(quality_usefulness) AS avg_usefulness,
  AVG(quality_overall) AS avg_overall
FROM llm_calls
WHERE quality_overall IS NOT NULL
  AND started_at > NOW() - INTERVAL '7 days';

サンプリングされた応答の品質スコアを集計することで、全体的な品質トレンドを把握できます。

Grafana によるダッシュボード構築

データベースに記録されたメトリクスを、Grafana で可視化します。

Grafana のセットアップ

Grafana を Docker で起動します。

bashdocker run -d \
  -p 3000:3000 \
  --name=grafana \
  -e "GF_SECURITY_ADMIN_PASSWORD=admin" \
  grafana/grafana:latest

ブラウザで http:​/​​/​localhost:3000 にアクセスし、admin / admin でログインします。

データソースの追加

Grafana の設定画面で、PostgreSQL をデータソースとして追加します。

  • Host: host.docker.internal:5432(Docker の場合)
  • Database: データベース名
  • User: ユーザー名
  • Password: パスワード
  • SSL Mode: disable(ローカル環境の場合)

ダッシュボードパネルの作成

以下のような KPI パネルを作成します。

1. 総コスト(過去 24 時間)

sqlSELECT
  SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours';

このクエリの結果を「Stat」パネルで表示し、大きな数字として可視化します。

2. モデル別コスト(過去 7 日間)

sqlSELECT
  model_name,
  SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY model_name
ORDER BY total_cost DESC;

このクエリの結果を「Bar Chart」パネルで表示し、どのモデルがコストを占めているかを可視化します。

3. 失敗率の推移(過去 7 日間、1 時間ごと)

sqlSELECT
  DATE_TRUNC('hour', started_at) AS time,
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE error_message IS NOT NULL) / COUNT(*),
    2
  ) AS failure_rate
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY time
ORDER BY time;

このクエリの結果を「Time Series」パネルで表示し、失敗率の推移を折れ線グラフで可視化します。

4. 平均応答時間(過去 24 時間、10 分ごと)

sqlSELECT
  DATE_TRUNC('minute', started_at) AS time,
  AVG(duration_ms) AS avg_duration
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours'
  AND ended_at IS NOT NULL
GROUP BY time
ORDER BY time;

応答時間の推移を可視化することで、パフォーマンスの問題を検知できます。

5. 品質スコアの推移(過去 7 日間)

sqlSELECT
  DATE_TRUNC('day', started_at) AS time,
  AVG(quality_overall) AS avg_quality
FROM llm_calls
WHERE quality_overall IS NOT NULL
  AND started_at > NOW() - INTERVAL '7 days'
GROUP BY time
ORDER BY time;

品質スコアの推移を可視化することで、改善施策の効果を測定できます。

以下の図は、Grafana ダッシュボードで可視化される KPI の全体像を示しています。

mermaidflowchart TB
  db[("PostgreSQL<br/>メトリクス DB")]

  db -->|集計クエリ| panel1["総コスト<br/>パネル"]
  db -->|集計クエリ| panel2["モデル別<br/>コスト"]
  db -->|集計クエリ| panel3["失敗率<br/>推移"]
  db -->|集計クエリ| panel4["応答時間<br/>推移"]
  db -->|集計クエリ| panel5["品質スコア<br/>推移"]

  panel1 --> dashboard["Grafana<br/>ダッシュボード"]
  panel2 --> dashboard
  panel3 --> dashboard
  panel4 --> dashboard
  panel5 --> dashboard

  dashboard -->|KPI 確認| operator["運用者"]

各パネルがそれぞれの KPI を可視化し、運用者がリアルタイムに状況を把握できます。

アラート設定

Grafana では、特定の条件を満たしたときにアラートを送信できます。

コストアラートの設定

1 日のコストが一定額を超えた場合にアラートを送信します。

sqlSELECT
  SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours';

このクエリの結果が 100(USD)を超えた場合にアラートを発火させ、Slack や Email に通知します。

失敗率アラートの設定

失敗率が一定の閾値を超えた場合にアラートを送信します。

sqlSELECT
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE error_message IS NOT NULL) / COUNT(*),
    2
  ) AS failure_rate
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '1 hour';

過去 1 時間の失敗率が 10% を超えた場合にアラートを発火させます。

具体例

サンプルアプリケーションの実装

実際に LangChain アプリケーションを作成し、監視機能を組み込んでみましょう。

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

bashmkdir langchain-monitoring-demo
cd langchain-monitoring-demo
yarn init -y

新しいプロジェクトを作成します。

bashyarn add @langchain/core @langchain/openai langchain pg dotenv
yarn add -D typescript @types/node @types/pg ts-node

必要なパッケージをインストールします。pg は PostgreSQL クライアント、dotenv は環境変数の読み込みに使います。

TypeScript 設定

json{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

tsconfig.json を作成し、TypeScript のコンパイラオプションを設定します。

環境変数の設定

bash# .env ファイル
OPENAI_API_KEY=sk-...
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=ls__...
LANGCHAIN_PROJECT=monitoring-demo

DB_HOST=localhost
DB_NAME=langchain_metrics
DB_USER=postgres
DB_PASSWORD=password

.env ファイルに、API キーやデータベース接続情報を記述します。

データベースのセットアップ

sql-- データベース作成
CREATE DATABASE langchain_metrics;

-- テーブル作成(前述の定義を使用)
\c langchain_metrics

CREATE TABLE llm_calls (
  id SERIAL PRIMARY KEY,
  run_id VARCHAR(255) UNIQUE NOT NULL,
  model_name VARCHAR(100) NOT NULL,
  prompt TEXT,
  response TEXT,
  prompt_tokens INTEGER,
  completion_tokens INTEGER,
  total_tokens INTEGER,
  cost_usd DECIMAL(10, 6),
  error_message TEXT,
  error_type VARCHAR(100),
  started_at TIMESTAMP,
  ended_at TIMESTAMP,
  duration_ms INTEGER,
  user_id VARCHAR(255),
  session_id VARCHAR(255),
  quality_accuracy DECIMAL(3, 2),
  quality_completeness DECIMAL(3, 2),
  quality_clarity DECIMAL(3, 2),
  quality_usefulness DECIMAL(3, 2),
  quality_overall DECIMAL(3, 2),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_llm_calls_started_at ON llm_calls(started_at);
CREATE INDEX idx_llm_calls_model_name ON llm_calls(model_name);
CREATE INDEX idx_llm_calls_user_id ON llm_calls(user_id);

PostgreSQL でデータベースとテーブルを作成します。

カスタムコールバックハンドラーの実装

src​/​callbacks​/​metrics-callback.ts を作成します。

typescriptimport { BaseCallbackHandler } from '@langchain/core/callbacks/base';
import { LLMResult } from '@langchain/core/outputs';
import { Pool } from 'pg';

// データベース接続
const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});

データベース接続を初期化します。

typescript// モデルごとの料金(USD / 1M tokens)
const PRICING: Record<
  string,
  { prompt: number; completion: number }
> = {
  'gpt-4': { prompt: 30.0, completion: 60.0 },
  'gpt-3.5-turbo': { prompt: 0.5, completion: 1.5 },
  'gpt-4-turbo-preview': { prompt: 10.0, completion: 30.0 },
};

// コスト計算関数
function calculateCost(
  modelName: string,
  promptTokens: number,
  completionTokens: number
): number {
  const pricing = PRICING[modelName];
  if (!pricing) return 0;

  const promptCost =
    (promptTokens / 1_000_000) * pricing.prompt;
  const completionCost =
    (completionTokens / 1_000_000) * pricing.completion;
  return promptCost + completionCost;
}

料金表とコスト計算関数を定義します。

typescript// メトリクスコールバックハンドラー
export class MetricsCallbackHandler extends BaseCallbackHandler {
  name = "MetricsCallbackHandler";

  private userId?: string;
  private sessionId?: string;

  constructor(userId?: string, sessionId?: string) {
    super();
    this.userId = userId;
    this.sessionId = sessionId;
  }

コンストラクタで userIdsessionId を受け取ることで、ユーザーごとやセッションごとの分析が可能になります。

typescript  async handleLLMStart(
    llm: { name: string },
    prompts: string[],
    runId: string
  ) {
    await pool.query(
      `INSERT INTO llm_calls (run_id, model_name, prompt, user_id, session_id, started_at)
       VALUES ($1, $2, $3, $4, $5, NOW())`,
      [runId, llm.name, prompts[0], this.userId, this.sessionId]
    );
  }

LLM 呼び出し開始時に、基本情報をデータベースに記録します。

typescript  async handleLLMEnd(output: LLMResult, runId: string) {
    const generation = output.generations[0][0];
    const text = generation.text;
    const tokenUsage = output.llmOutput?.tokenUsage;

    const promptTokens = tokenUsage?.promptTokens || 0;
    const completionTokens = tokenUsage?.completionTokens || 0;
    const totalTokens = tokenUsage?.totalTokens || 0;

    // モデル名と開始時刻を取得
    const result = await pool.query(
      `SELECT model_name, started_at FROM llm_calls WHERE run_id = $1`,
      [runId]
    );
    const { model_name, started_at } = result.rows[0];

    // コストと実行時間を計算
    const cost = calculateCost(model_name, promptTokens, completionTokens);
    const durationMs = Date.now() - new Date(started_at).getTime();

    // データベースに記録
    await pool.query(
      `UPDATE llm_calls
       SET response = $1,
           prompt_tokens = $2,
           completion_tokens = $3,
           total_tokens = $4,
           cost_usd = $5,
           duration_ms = $6,
           ended_at = NOW()
       WHERE run_id = $7`,
      [text, promptTokens, completionTokens, totalTokens, cost, durationMs, runId]
    );
  }

LLM 呼び出し終了時に、トークン数、コスト、実行時間を記録します。

typescript  async handleLLMError(error: Error, runId: string) {
    await pool.query(
      `UPDATE llm_calls
       SET error_message = $1,
           error_type = $2,
           ended_at = NOW()
       WHERE run_id = $3`,
      [error.message, error.constructor.name, runId]
    );
  }
}

エラー発生時には、エラーメッセージとタイプを記録します。

サンプルアプリケーションのメインコード

src​/​index.ts を作成します。

typescriptimport "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { MetricsCallbackHandler } from "./callbacks/metrics-callback";

async function main() {
  // ユーザー ID とセッション ID を設定
  const userId = "user-123";
  const sessionId = "session-456";

  // LLM を初期化(カスタムコールバックを追加)
  const llm = new ChatOpenAI({
    modelName: "gpt-4",
    temperature: 0.7,
    callbacks: [new MetricsCallbackHandler(userId, sessionId)],
  });

MetricsCallbackHandlercallbacks オプションに渡すことで、自動的にメトリクスが記録されます。

typescript  // テスト用の質問
  const questions = [
    "TypeScript の型安全性について簡潔に教えてください",
    "Next.js と React の違いは何ですか?",
    "Docker のメリットとデメリットを教えてください",
  ];

  // 各質問に対して LLM を呼び出し
  for (const question of questions) {
    console.log(`\n質問: ${question}`);

    try {
      const response = await llm.invoke(question);
      console.log(`応答: ${response.content}\n`);
    } catch (error) {
      console.error(`エラー: ${error.message}\n`);
    }

    // レート制限を避けるため少し待機
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

main();

複数の質問を順番に処理し、それぞれのメトリクスが自動的に記録されます。

実行

bashyarn ts-node src/index.ts

このコマンドでアプリケーションを実行すると、各 LLM 呼び出しのメトリクスがデータベースに記録されます。

データベースの確認

sql-- 記録されたメトリクスを確認
SELECT
  id,
  model_name,
  prompt_tokens,
  completion_tokens,
  cost_usd,
  duration_ms,
  error_message
FROM llm_calls
ORDER BY created_at DESC
LIMIT 10;

このクエリで、最新 10 件の呼び出しログを確認できます。

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

Grafana でダッシュボードを作成し、実際に可視化してみましょう。

ダッシュボードの作成手順

  1. Grafana にログイン(http:​/​​/​localhost:3000
  2. 「Dashboards」→「New Dashboard」→「Add visualization」
  3. データソースに PostgreSQL を選択
  4. 以下のパネルを追加していく

パネル 1: 総コスト

  • パネルタイプ: Stat
  • クエリ:
sqlSELECT SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours';
  • 表示設定: 単位を「USD」に設定

パネル 2: モデル別コスト

  • パネルタイプ: Bar Chart
  • クエリ:
sqlSELECT model_name, SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY model_name
ORDER BY total_cost DESC;
  • 表示設定: X 軸を model_name、Y 軸を total_cost に設定

パネル 3: 失敗率の推移

  • パネルタイプ: Time Series
  • クエリ:
sqlSELECT
  DATE_TRUNC('hour', started_at) AS time,
  ROUND(100.0 * COUNT(*) FILTER (WHERE error_message IS NOT NULL) / COUNT(*), 2) AS failure_rate
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY time
ORDER BY time;
  • 表示設定: 単位を「%」に設定、Y 軸の最大値を 100 に固定

パネル 4: 平均応答時間

  • パネルタイプ: Time Series
  • クエリ:
sqlSELECT
  DATE_TRUNC('minute', started_at) AS time,
  AVG(duration_ms) AS avg_duration
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '24 hours' AND ended_at IS NOT NULL
GROUP BY time
ORDER BY time;
  • 表示設定: 単位を「ms」に設定

パネル 5: ユーザー別コスト Top 10

  • パネルタイプ: Table
  • クエリ:
sqlSELECT
  user_id,
  COUNT(*) AS call_count,
  SUM(cost_usd) AS total_cost
FROM llm_calls
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY user_id
ORDER BY total_cost DESC
LIMIT 10;
  • 表示設定: total_cost でソート

以下の図は、ダッシュボードにおける各 KPI パネルの配置イメージを示します。

mermaidflowchart TB
  dashboard["Grafana ダッシュボード"]

  subgraph row1["1 行目"]
    stat1["総コスト<br/>(24h)"]
    stat2["総呼び出し数<br/>(24h)"]
    stat3["失敗率<br/>(1h)"]
  end

  subgraph row2["2 行目"]
    chart1["モデル別コスト<br/>(Bar Chart)"]
    chart2["失敗率推移<br/>(Time Series)"]
  end

  subgraph row3["3 行目"]
    chart3["応答時間推移<br/>(Time Series)"]
    chart4["品質スコア推移<br/>(Time Series)"]
  end

  subgraph row4["4 行目"]
    table1["ユーザー別コスト<br/>(Table)"]
  end

  dashboard --> row1
  dashboard --> row2
  dashboard --> row3
  dashboard --> row4

各行に異なるタイプの KPI パネルを配置することで、運用状況を一目で把握できます。

アラート設定の実例

Slack 通知の設定

Grafana の「Alerting」→「Contact points」で Slack Webhook を設定します。

  1. Slack で Incoming Webhook を作成(https://api.slack.com/messaging/webhooks)
  2. Webhook URL をコピー
  3. Grafana で「New contact point」を作成し、Webhook URL を入力

コストアラートの作成

「Dashboards」→ 総コストパネル → 「Edit」→「Alert」タブで以下を設定します。

  • 条件: total_cost > 100(USD)
  • 評価間隔: 5 分ごと
  • 通知先: Slack
  • メッセージ: "⚠️ 24 時間のコストが $100 を超えました"

失敗率アラートの作成

失敗率パネルに対して以下のアラートを設定します。

  • 条件: failure_rate > 10(%)
  • 評価間隔: 5 分ごと
  • 通知先: Slack
  • メッセージ: "⚠️ 過去 1 時間の失敗率が 10% を超えました"

これらのアラート設定により、問題が発生した際に即座に通知を受け取れます。

実際の運用での注意点

データベースのパフォーマンス

メトリクスが増えると、データベースのパフォーマンスが低下する可能性があります。以下の対策を検討してください。

  • インデックスの最適化: 集計クエリで使うカラムにインデックスを作成
  • パーティショニング: 日付ごとにテーブルを分割
  • 古いデータの削除: 一定期間経過したログを定期的にアーカイブ

コスト最適化

評価用の LLM 呼び出しもコストがかかるため、以下の工夫が必要です。

  • サンプリング率の調整: 必要最小限のサンプリング率に抑える
  • 軽量モデルの使用: 評価には GPT-3.5 など軽量なモデルを使う
  • バッチ評価: リアルタイムではなく、バッチで定期的に評価する

セキュリティとプライバシー

ログにはユーザーの入力内容が含まれるため、以下の点に注意してください。

  • 個人情報の除外: ログに記録する前に、個人情報をマスキング
  • アクセス制限: データベースやダッシュボードへのアクセスを制限
  • 保存期間の設定: 必要以上に長期間ログを保存しない

まとめ

本記事では、LangChain のコスト監視ダッシュボードを構築し、使用量・失敗率・応答品質を KPI として管理する方法を解説しました。

重要なポイント

  • カスタムコールバック: LangChain のコールバック機能を使うことで、独自のメトリクスを記録できる
  • データベース設計: トークン数、コスト、エラー情報を構造化して保存することで、柔軟な集計が可能になる
  • 可視化ツール: Grafana などのツールを使うことで、リアルタイムに KPI を監視できる
  • アラート設定: コストや失敗率が閾値を超えた際に通知を受け取ることで、迅速に対応できる
  • 品質評価: LLM による自己評価とサンプリングを組み合わせることで、現実的なコストで品質を測定できる

次のステップ

本記事で紹介した基本的な監視基盤を構築できたら、以下のような拡張を検討してみてください。

  • A/B テスト: 異なるプロンプトやモデルの効果を比較する
  • 異常検知: 機械学習を使って、通常と異なるパターンを自動検出する
  • コスト予測: 過去のトレンドから将来のコストを予測する
  • 自動スケーリング: 負荷に応じてリソースを自動調整する

LangChain アプリケーションの運用において、監視とメトリクスの可視化は非常に重要です。本記事の内容を参考に、あなたのプロジェクトに適した監視基盤を構築してみてください。

関連リンク