ドラフト エージェント設計パターン - 評価とモニタリング

aiagentsmonitoringevaluationtypescriptlangchainlanggraphvercel
By sko X opus 4.19/21/202515 min read

本番環境でAIエージェントの包括的な評価とモニタリングシステムを実装する方法を学びます。TypeScript、LangChain、LangGraphを使用して、Vercelプラットフォーム上で観察可能、テスト可能、最適化可能なエージェントシステムを構築します。

メンタルモデル:天文台パターン

エージェントの評価とモニタリングを宇宙望遠鏡天文台の運営に例えて考えてみましょう。異なる波長(動作の側面)を観測する複数の機器(メトリクス)、一時的なイベント(エラー/異常)をキャッチするための継続的な追跡(モニタリング)、精度を確保するための較正システム(評価)が必要です。天文学者が複数の望遠鏡からのデータを組み合わせて天体を理解するように、私たちは複数の評価とモニタリングアプローチを組み合わせて、エージェントの動作を包括的に理解します。

基本例:評価機能を内蔵したエージェント

基本的な評価とモニタリング機能を含むシンプルなカスタマーサポートエージェントから始めましょう。

1. 評価タイプとメトリクスの定義

// app/lib/evaluation/types.ts
import { z } from 'zod';

export const EvaluationMetricSchema = z.object({
  accuracy: z.number().min(0).max(1),
  relevance: z.number().min(0).max(1),
  coherence: z.number().min(0).max(1),
  latency: z.number(),
  tokenUsage: z.object({
    input: z.number(),
    output: z.number(),
    total: z.number(),
  }),
  cost: z.number(),
  timestamp: z.string().datetime(),
});

export type EvaluationMetric = z.infer<typeof EvaluationMetricSchema>;

export const AgentTraceSchema = z.object({
  traceId: z.string(),
  parentId: z.string().optional(),
  agentName: z.string(),
  input: z.any(),
  output: z.any(),
  metrics: EvaluationMetricSchema,
  errors: z.array(z.string()).default([]),
  metadata: z.record(z.any()).default({}),
});

export type AgentTrace = z.infer<typeof AgentTraceSchema>;

型はZodでランタイム検証を行い、評価データの形状を定義します。

2. モニタリングコールバックハンドラの作成

// app/lib/monitoring/callback.ts
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
import { Serialized } from '@langchain/core/load/serializable';
import { ChainValues } from '@langchain/core/utils/types';
import { AgentTrace, EvaluationMetric } from '../evaluation/types';
import { v4 as uuidv4 } from 'uuid';

export class MonitoringCallbackHandler extends BaseCallbackHandler {
  name = 'MonitoringCallbackHandler';
  private traces: Map<string, Partial<AgentTrace>> = new Map();
  private startTimes: Map<string, number> = new Map();

  async handleChainStart(
    chain: Serialized,
    inputs: ChainValues,
    runId: string,
  ): Promise<void> {
    const traceId = uuidv4();
    this.startTimes.set(runId, Date.now());

    this.traces.set(runId, {
      traceId,
      agentName: chain.id?.[chain.id.length - 1] || 'unknown',
      input: inputs,
      timestamp: new Date().toISOString(),
      errors: [],
      metadata: {},
    });
  }

  async handleChainEnd(
    outputs: ChainValues,
    runId: string,
  ): Promise<void> {
    const trace = this.traces.get(runId);
    const startTime = this.startTimes.get(runId);

    if (trace && startTime) {
      const latency = Date.now() - startTime;

      // トークン使用量の計算(簡略化 - 本番環境ではLLMレスポンスから取得)
      const tokenUsage = {
        input: JSON.stringify(trace.input).length / 4, // 概算
        output: JSON.stringify(outputs).length / 4,
        total: 0,
      };
      tokenUsage.total = tokenUsage.input + tokenUsage.output;

      // コストの計算(Gemini Proの価格設定に基づく)
      const cost = (tokenUsage.input * 0.00025 + tokenUsage.output * 0.0005) / 1000;

      const metrics: EvaluationMetric = {
        accuracy: 0, // エバリュエーターによって計算される
        relevance: 0,
        coherence: 0,
        latency,
        tokenUsage,
        cost,
        timestamp: new Date().toISOString(),
      };

      trace.output = outputs;
      trace.metrics = metrics;

      // モニタリングサービスに送信
      await this.sendToMonitoring(trace as AgentTrace);
    }

    this.traces.delete(runId);
    this.startTimes.delete(runId);
  }

  async handleChainError(
    err: Error,
    runId: string,
  ): Promise<void> {
    const trace = this.traces.get(runId);
    if (trace) {
      trace.errors = [...(trace.errors || []), err.message];
      await this.sendToMonitoring(trace as AgentTrace);
    }
  }

  private async sendToMonitoring(trace: AgentTrace): Promise<void> {
    // 本番環境では、モニタリングサービスに送信
    console.log('Trace:', JSON.stringify(trace, null, 2));

    // 例:Langfuse、DataDog、またはカスタムエンドポイントに送信
    if (process.env.MONITORING_ENDPOINT) {
      await fetch(process.env.MONITORING_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(trace),
      });
    }
  }
}

カスタムコールバックハンドラはエージェント実行から詳細なメトリクスをキャプチャします。

3. LLM-as-a-Judgeエバリュエーターの実装

// app/lib/evaluation/evaluator.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { PromptTemplate } from '@langchain/core/prompts';
import { z } from 'zod';
import { StructuredOutputParser } from 'langchain/output_parsers';
import { EvaluationMetric } from './types';
import { memoize } from 'es-toolkit';

const EvaluationResultSchema = z.object({
  accuracy: z.number().min(0).max(1),
  relevance: z.number().min(0).max(1),
  coherence: z.number().min(0).max(1),
  reasoning: z.string(),
});

export class LLMEvaluator {
  private model: ChatGoogleGenerativeAI;
  private parser: StructuredOutputParser<z.infer<typeof EvaluationResultSchema>>;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0,
      apiKey: process.env.GOOGLE_API_KEY,
    });

    this.parser = StructuredOutputParser.fromZodSchema(EvaluationResultSchema);
  }

  // コストを削減するために同一の入力に対する評価をメモ化
  evaluate = memoize(
    async (input: string, output: string, expectedOutput?: string) => {
      const formatInstructions = this.parser.getFormatInstructions();

      const prompt = PromptTemplate.fromTemplate(`
        以下のエージェントレスポンスを評価してください:

        入力: {input}
        エージェント出力: {output}
        {expectedOutput}

        以下の基準で評価してください:
        1. 精度:レスポンスはどの程度事実的に正確ですか?(0-1)
        2. 関連性:入力にどの程度適切に対応していますか?(0-1)
        3. 一貫性:どの程度明確で構造化されていますか?(0-1)

        {formatInstructions}
      `);

      const response = await this.model.invoke(
        await prompt.format({
          input,
          output,
          expectedOutput: expectedOutput
            ? `期待される出力: ${expectedOutput}`
            : '',
          formatInstructions,
        })
      );

      return this.parser.parse(response.content as string);
    },
    {
      // 入力ハッシュに基づくキャッシュキー
      getCacheKey: (args) => JSON.stringify(args),
    }
  );

  async evaluateWithMetrics(
    input: string,
    output: string,
    metrics: Partial<EvaluationMetric>,
    expectedOutput?: string
  ): Promise<EvaluationMetric> {
    const evaluation = await this.evaluate(input, output, expectedOutput);

    return {
      ...metrics,
      accuracy: evaluation.accuracy,
      relevance: evaluation.relevance,
      coherence: evaluation.coherence,
    } as EvaluationMetric;
  }
}

LLMベースのエバリュエーターが自動的にレスポンス品質を評価します。

4. モニタリング対応エージェントの作成

// app/lib/agents/monitored-agent.ts
import { StateGraph, Annotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { MonitoringCallbackHandler } from '../monitoring/callback';
import { LLMEvaluator } from '../evaluation/evaluator';
import { MemorySaver } from '@langchain/langgraph';

const StateAnnotation = Annotation.Root({
  messages: Annotation<(HumanMessage | AIMessage)[]>({
    reducer: (curr, next) => [...curr, ...next],
    default: () => [],
  }),
  evaluationResults: Annotation<any[]>({
    reducer: (curr, next) => [...curr, ...next],
    default: () => [],
  }),
});

export async function createMonitoredAgent() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0.7,
    apiKey: process.env.GOOGLE_API_KEY,
  });

  const monitoringHandler = new MonitoringCallbackHandler();
  const evaluator = new LLMEvaluator();
  const memory = new MemorySaver();

  const workflow = new StateGraph(StateAnnotation)
    .addNode('process', async (state) => {
      const lastMessage = state.messages[state.messages.length - 1];

      const response = await model.invoke(
        state.messages,
        { callbacks: [monitoringHandler] }
      );

      // レスポンスの自動評価
      const evaluation = await evaluator.evaluateWithMetrics(
        lastMessage.content as string,
        response.content as string,
        {
          latency: 0, // コールバックによって設定される
          tokenUsage: { input: 0, output: 0, total: 0 },
          cost: 0,
          timestamp: new Date().toISOString(),
        }
      );

      return {
        messages: [response],
        evaluationResults: [evaluation],
      };
    })
    .addEdge('__start__', 'process')
    .addEdge('process', '__end__');

  return workflow.compile({
    checkpointer: memory,
  });
}

// 使用関数
export async function handleAgentRequest(input: string, sessionId: string) {
  const agent = await createMonitoredAgent();

  const result = await agent.invoke(
    {
      messages: [new HumanMessage(input)],
    },
    {
      configurable: { thread_id: sessionId },
    }
  );

  // 評価結果にアクセス
  const latestEvaluation = result.evaluationResults[result.evaluationResults.length - 1];

  return {
    response: result.messages[result.messages.length - 1].content,
    evaluation: latestEvaluation,
  };
}

統合されたモニタリングと評価機能を持つエージェント。

5. APIエンドポイントの作成

// app/api/agent/monitored/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { handleAgentRequest } from '@/lib/agents/monitored-agent';
import { z } from 'zod';

const RequestSchema = z.object({
  message: z.string().min(1),
  sessionId: z.string().default(() => `session-${Date.now()}`),
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { message, sessionId } = RequestSchema.parse(body);

    const result = await handleAgentRequest(message, sessionId);

    // モニタリング用のメトリクスログ
    console.log('評価メトリクス:', {
      accuracy: result.evaluation.accuracy,
      relevance: result.evaluation.relevance,
      coherence: result.evaluation.coherence,
      cost: result.evaluation.cost,
    });

    return NextResponse.json(result);
  } catch (error) {
    console.error('エージェントエラー:', error);
    return NextResponse.json(
      { error: 'リクエストの処理に失敗しました' },
      { status: 500 }
    );
  }
}

組み込みメトリクスロギング付きAPIエンドポイント。

高度な例:本番環境モニタリングシステム

次に、分散トレーシング、セマンティックキャッシング、リアルタイムダッシュボードを備えた包括的なモニタリングシステムを構築しましょう。

1. 分散トレーシングシステムの実装

// app/lib/tracing/distributed-tracer.ts
import { trace, context, SpanStatusCode, SpanKind } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { v4 as uuidv4 } from 'uuid';
import { memoize, debounce } from 'es-toolkit';

export class DistributedTracer {
  private tracer;
  private provider: NodeTracerProvider;

  constructor(serviceName: string = 'agent-system') {
    // プロバイダーの初期化
    this.provider = new NodeTracerProvider({
      resource: new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
        [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
      }),
    });

    // エクスポーターの設定(本番環境では、OTLPエンドポイントを使用)
    const exporter = new OTLPTraceExporter({
      url: process.env.OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
    });

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

    this.tracer = trace.getTracer('agent-tracer', '1.0.0');
  }

  traceAgent(agentName: string, operation: string) {
    return this.tracer.startSpan(`${agentName}.${operation}`, {
      kind: SpanKind.INTERNAL,
      attributes: {
        'agent.name': agentName,
        'agent.operation': operation,
        'agent.trace_id': uuidv4(),
      },
    });
  }

  traceMultiAgent(
    agents: string[],
    parentSpan?: any
  ) {
    const ctx = parentSpan
      ? trace.setSpan(context.active(), parentSpan)
      : context.active();

    return agents.map(agent =>
      this.tracer.startSpan(`multi-agent.${agent}`, {
        kind: SpanKind.INTERNAL,
        attributes: {
          'agent.name': agent,
          'agent.type': 'multi-agent',
        },
      }, ctx)
    );
  }

  // 高頻度操作のためのデバウンスされたメトリクス集計
  recordMetrics = debounce(
    (span: any, metrics: Record<string, any>) => {
      Object.entries(metrics).forEach(([key, value]) => {
        span.setAttribute(`metric.${key}`, value);
      });
    },
    100 // 100ミリ秒ごとにメトリクスを集計
  );

  endSpan(span: any, status: 'success' | 'error' = 'success', error?: Error) {
    if (status === 'error' && error) {
      span.recordException(error);
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error.message,
      });
    } else {
      span.setStatus({ code: SpanStatusCode.OK });
    }
    span.end();
  }
}

// シングルトンインスタンス
export const tracer = new DistributedTracer();

複雑なマルチエージェントワークフローのための分散トレーシング。

2. セマンティックキャッシングレイヤーの構築

// app/lib/cache/semantic-cache.ts
import { Redis } from '@upstash/redis';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { cosineSimilarity } from 'es-toolkit/compat';
import { LRUCache } from 'lru-cache';
import { z } from 'zod';

const CacheEntrySchema = z.object({
  key: z.string(),
  embedding: z.array(z.number()),
  response: z.string(),
  metadata: z.object({
    timestamp: z.string(),
    hitCount: z.number(),
    cost: z.number(),
  }),
});

type CacheEntry = z.infer<typeof CacheEntrySchema>;

export class SemanticCache {
  private redis: Redis;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private localCache: LRUCache<string, CacheEntry>;
  private similarityThreshold = 0.95;

  constructor() {
    this.redis = new Redis({
      url: process.env.UPSTASH_REDIS_URL!,
      token: process.env.UPSTASH_REDIS_TOKEN!,
    });

    this.embeddings = new GoogleGenerativeAIEmbeddings({
      modelName: 'embedding-001',
      apiKey: process.env.GOOGLE_API_KEY,
    });

    // ホットデータ用のローカルLRUキャッシュ
    this.localCache = new LRUCache<string, CacheEntry>({
      max: 100,
      ttl: 1000 * 60 * 5, // 5分
    });
  }

  async get(query: string): Promise<string | null> {
    // まずローカルキャッシュをチェック
    const localHit = this.checkLocalCache(query);
    if (localHit) {
      console.log('ローカルキャッシュヒット');
      return localHit;
    }

    // クエリのエンベディングを生成
    const queryEmbedding = await this.embeddings.embedQuery(query);

    // Redisで検索
    const cacheKeys = await this.redis.keys('cache:*');

    for (const key of cacheKeys) {
      const entry = await this.redis.get<CacheEntry>(key);
      if (!entry) continue;

      const similarity = cosineSimilarity(queryEmbedding, entry.embedding);

      if (similarity >= this.similarityThreshold) {
        console.log(`セマンティックキャッシュヒット(類似度: ${similarity})`);

        // ヒット数を更新
        entry.metadata.hitCount++;
        await this.redis.set(key, entry);

        // ローカルキャッシュに追加
        this.localCache.set(query, entry);

        return entry.response;
      }
    }

    return null;
  }

  async set(query: string, response: string, cost: number = 0): Promise<void> {
    const embedding = await this.embeddings.embedQuery(query);

    const entry: CacheEntry = {
      key: query,
      embedding,
      response,
      metadata: {
        timestamp: new Date().toISOString(),
        hitCount: 0,
        cost,
      },
    };

    // 有効期限付きでRedisに保存
    const key = `cache:${Date.now()}`;
    await this.redis.set(key, entry, {
      ex: 60 * 60 * 24, // 24時間
    });

    // ローカルキャッシュにも保存
    this.localCache.set(query, entry);
  }

  private checkLocalCache(query: string): string | null {
    const entry = this.localCache.get(query);
    return entry?.response || null;
  }

  async getCacheStats(): Promise<{
    totalEntries: number;
    totalHits: number;
    costSaved: number;
  }> {
    const keys = await this.redis.keys('cache:*');
    let totalHits = 0;
    let costSaved = 0;

    for (const key of keys) {
      const entry = await this.redis.get<CacheEntry>(key);
      if (entry) {
        totalHits += entry.metadata.hitCount;
        costSaved += entry.metadata.cost * entry.metadata.hitCount;
      }
    }

    return {
      totalEntries: keys.length,
      totalHits,
      costSaved,
    };
  }
}

セマンティックキャッシングによりコストとレイテンシを大幅に削減します。

3. リアルタイムモニタリングダッシュボードの作成

// app/lib/monitoring/dashboard.ts
import { EventEmitter } from 'events';
import { groupBy, meanBy, sumBy, maxBy } from 'es-toolkit';
import { AgentTrace } from '../evaluation/types';

interface DashboardMetrics {
  avgLatency: number;
  avgAccuracy: number;
  totalCost: number;
  errorRate: number;
  throughput: number;
  activeAgents: number;
}

export class MonitoringDashboard extends EventEmitter {
  private traces: AgentTrace[] = [];
  private metricsWindow = 60000; // 1分間のウィンドウ
  private updateInterval: NodeJS.Timeout;

  constructor() {
    super();

    // 5秒ごとにメトリクスを更新
    this.updateInterval = setInterval(() => {
      this.calculateMetrics();
    }, 5000);
  }

  addTrace(trace: AgentTrace) {
    this.traces.push(trace);

    // 最近のトレースのみを保持
    const cutoff = Date.now() - this.metricsWindow;
    this.traces = this.traces.filter(t =>
      new Date(t.metrics.timestamp).getTime() > cutoff
    );

    // リアルタイム更新を発行
    this.emit('trace', trace);
  }

  private calculateMetrics() {
    if (this.traces.length === 0) return;

    const metrics: DashboardMetrics = {
      avgLatency: meanBy(this.traces, t => t.metrics.latency),
      avgAccuracy: meanBy(this.traces, t => t.metrics.accuracy),
      totalCost: sumBy(this.traces, t => t.metrics.cost),
      errorRate: this.traces.filter(t => t.errors.length > 0).length / this.traces.length,
      throughput: this.traces.length / (this.metricsWindow / 1000),
      activeAgents: new Set(this.traces.map(t => t.agentName)).size,
    };

    this.emit('metrics', metrics);
  }

  getAggregatedMetrics(groupByField: 'agentName' | 'hour' = 'agentName') {
    if (groupByField === 'hour') {
      const grouped = groupBy(this.traces, t =>
        new Date(t.metrics.timestamp).getHours().toString()
      );

      return Object.entries(grouped).map(([hour, traces]) => ({
        hour,
        avgLatency: meanBy(traces, t => t.metrics.latency),
        totalRequests: traces.length,
        errorRate: traces.filter(t => t.errors.length > 0).length / traces.length,
      }));
    }

    const grouped = groupBy(this.traces, 'agentName');

    return Object.entries(grouped).map(([agent, traces]) => ({
      agent,
      avgLatency: meanBy(traces, t => t.metrics.latency),
      avgAccuracy: meanBy(traces, t => t.metrics.accuracy),
      totalCost: sumBy(traces, t => t.metrics.cost),
      requestCount: traces.length,
    }));
  }

  getAlerts(thresholds: {
    maxLatency?: number;
    minAccuracy?: number;
    maxErrorRate?: number;
  }) {
    const alerts = [];

    const avgLatency = meanBy(this.traces, t => t.metrics.latency);
    if (thresholds.maxLatency && avgLatency > thresholds.maxLatency) {
      alerts.push({
        type: 'latency',
        severity: 'warning',
        message: `平均レイテンシ(${avgLatency}ms)が閾値(${thresholds.maxLatency}ms)を超えています`,
      });
    }

    const avgAccuracy = meanBy(this.traces, t => t.metrics.accuracy);
    if (thresholds.minAccuracy && avgAccuracy < thresholds.minAccuracy) {
      alerts.push({
        type: 'accuracy',
        severity: 'critical',
        message: `平均精度(${avgAccuracy})が閾値(${thresholds.minAccuracy})を下回っています`,
      });
    }

    const errorRate = this.traces.filter(t => t.errors.length > 0).length / this.traces.length;
    if (thresholds.maxErrorRate && errorRate > thresholds.maxErrorRate) {
      alerts.push({
        type: 'errors',
        severity: 'critical',
        message: `エラー率(${errorRate * 100}%)が閾値(${thresholds.maxErrorRate * 100}%)を超えています`,
      });
    }

    return alerts;
  }

  destroy() {
    clearInterval(this.updateInterval);
  }
}

// グローバルダッシュボードインスタンス
export const dashboard = new MonitoringDashboard();

リアルタイムダッシュボードがエージェントメトリクスを集約・監視します。

4. A/Bテストフレームワークの実装

// app/lib/testing/ab-testing.ts
import { StateGraph, Annotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { sample, shuffle } from 'es-toolkit';
import { z } from 'zod';

const ABTestConfigSchema = z.object({
  testId: z.string(),
  variants: z.array(z.object({
    id: z.string(),
    model: z.string().optional(),
    temperature: z.number().optional(),
    systemPrompt: z.string().optional(),
    weight: z.number().default(1),
  })),
  metrics: z.array(z.enum(['accuracy', 'latency', 'cost', 'user_satisfaction'])),
  minSampleSize: z.number().default(100),
});

type ABTestConfig = z.infer<typeof ABTestConfigSchema>;

const TestResultSchema = z.object({
  variantId: z.string(),
  metrics: z.record(z.number()),
  sampleSize: z.number(),
  confidence: z.number(),
});

type TestResult = z.infer<typeof TestResultSchema>;

export class ABTestingFramework {
  private activeTests = new Map<string, ABTestConfig>();
  private results = new Map<string, Map<string, TestResult>>();

  createTest(config: ABTestConfig) {
    this.activeTests.set(config.testId, config);
    this.results.set(config.testId, new Map());

    // 各バリアントの結果を初期化
    config.variants.forEach(variant => {
      this.results.get(config.testId)!.set(variant.id, {
        variantId: variant.id,
        metrics: {},
        sampleSize: 0,
        confidence: 0,
      });
    });
  }

  selectVariant(testId: string): string | null {
    const test = this.activeTests.get(testId);
    if (!test) return null;

    // 重み付きランダム選択
    const weights = test.variants.map(v => v.weight);
    const totalWeight = weights.reduce((a, b) => a + b, 0);

    let random = Math.random() * totalWeight;
    for (let i = 0; i < test.variants.length; i++) {
      random -= weights[i];
      if (random <= 0) {
        return test.variants[i].id;
      }
    }

    return test.variants[0].id;
  }

  async runVariant(testId: string, variantId: string, input: string) {
    const test = this.activeTests.get(testId);
    const variant = test?.variants.find(v => v.id === variantId);

    if (!variant) throw new Error('バリアントが見つかりません');

    // バリアント設定でモデルを作成
    const model = new ChatGoogleGenerativeAI({
      modelName: variant.model || 'gemini-2.5-pro',
      temperature: variant.temperature ?? 0.7,
      apiKey: process.env.GOOGLE_API_KEY,
    });

    const startTime = Date.now();

    const messages = variant.systemPrompt
      ? [
          { role: 'system', content: variant.systemPrompt },
          { role: 'user', content: input },
        ]
      : [{ role: 'user', content: input }];

    const response = await model.invoke(messages);

    const latency = Date.now() - startTime;

    return {
      response: response.content,
      metrics: {
        latency,
        // 他のメトリクスは評価に基づいて計算される
      },
    };
  }

  recordResult(
    testId: string,
    variantId: string,
    metrics: Record<string, number>
  ) {
    const results = this.results.get(testId);
    const variantResult = results?.get(variantId);

    if (!variantResult) return;

    // 移動平均を更新
    const n = variantResult.sampleSize;
    Object.entries(metrics).forEach(([key, value]) => {
      const current = variantResult.metrics[key] || 0;
      variantResult.metrics[key] = (current * n + value) / (n + 1);
    });

    variantResult.sampleSize++;

    // 信頼度を計算(簡略化 - 本番環境では適切な統計を使用)
    variantResult.confidence = Math.min(
      variantResult.sampleSize / 100,
      0.95
    );
  }

  getResults(testId: string): TestResult[] {
    const results = this.results.get(testId);
    if (!results) return [];

    return Array.from(results.values());
  }

  determineWinner(testId: string): string | null {
    const results = this.getResults(testId);
    const test = this.activeTests.get(testId);

    if (!test || results.length === 0) return null;

    // すべてのバリアントが最小サンプルサイズを持っているか確認
    const allHaveMinSamples = results.every(
      r => r.sampleSize >= test.minSampleSize
    );

    if (!allHaveMinSamples) return null;

    // 主要メトリクス(リストの最初)に基づいて勝者を見つける
    const primaryMetric = test.metrics[0];

    const winner = results.reduce((best, current) => {
      const bestScore = best.metrics[primaryMetric] || 0;
      const currentScore = current.metrics[primaryMetric] || 0;

      // レイテンシとコストの場合、小さい方が良い
      if (primaryMetric === 'latency' || primaryMetric === 'cost') {
        return currentScore < bestScore ? current : best;
      }

      return currentScore > bestScore ? current : best;
    });

    // 信頼度が十分高いか確認
    if (winner.confidence >= 0.95) {
      return winner.variantId;
    }

    return null;
  }
}

エージェント構成を比較するためのA/Bテストフレームワーク。

5. 本番環境モニタリングAPIの作成

// app/api/monitoring/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { dashboard } from '@/lib/monitoring/dashboard';
import { SemanticCache } from '@/lib/cache/semantic-cache';
import { ABTestingFramework } from '@/lib/testing/ab-testing';
import { tracer } from '@/lib/tracing/distributed-tracer';

const cache = new SemanticCache();
const abTesting = new ABTestingFramework();

// A/Bテストの初期化
abTesting.createTest({
  testId: 'model-comparison',
  variants: [
    { id: 'gpt4', model: 'gpt-4-turbo-preview', weight: 1 },
    { id: 'gpt35', model: 'gpt-3.5-turbo', weight: 1 },
  ],
  metrics: ['accuracy', 'latency', 'cost'],
  minSampleSize: 50,
});

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const view = searchParams.get('view') || 'metrics';

  switch (view) {
    case 'metrics':
      const metrics = dashboard.getAggregatedMetrics('agentName');
      return NextResponse.json({ metrics });

    case 'alerts':
      const alerts = dashboard.getAlerts({
        maxLatency: 5000,
        minAccuracy: 0.8,
        maxErrorRate: 0.1,
      });
      return NextResponse.json({ alerts });

    case 'cache':
      const cacheStats = await cache.getCacheStats();
      return NextResponse.json({ cacheStats });

    case 'ab-test':
      const results = abTesting.getResults('model-comparison');
      const winner = abTesting.determineWinner('model-comparison');
      return NextResponse.json({ results, winner });

    default:
      return NextResponse.json({ error: '無効なビュー' }, { status: 400 });
  }
}

export async function POST(request: NextRequest) {
  const span = tracer.traceAgent('monitoring-api', 'process-request');

  try {
    const { message, testId } = await request.json();

    // まずキャッシュをチェック
    const cached = await cache.get(message);
    if (cached) {
      tracer.recordMetrics(span, { cache_hit: 1 });
      tracer.endSpan(span, 'success');

      return NextResponse.json({
        response: cached,
        source: 'cache',
      });
    }

    // A/Bテストバリアントを選択
    const variantId = abTesting.selectVariant(testId || 'model-comparison');

    if (variantId) {
      const result = await abTesting.runVariant(
        testId || 'model-comparison',
        variantId,
        message
      );

      // メトリクスを記録
      abTesting.recordResult(
        testId || 'model-comparison',
        variantId,
        result.metrics
      );

      // レスポンスをキャッシュ
      await cache.set(message, result.response as string);

      tracer.recordMetrics(span, {
        variant: variantId,
        latency: result.metrics.latency,
      });
      tracer.endSpan(span, 'success');

      return NextResponse.json({
        response: result.response,
        variant: variantId,
        metrics: result.metrics,
      });
    }

    tracer.endSpan(span, 'error', new Error('バリアントが選択されませんでした'));
    return NextResponse.json(
      { error: 'テスト構成エラー' },
      { status: 500 }
    );

  } catch (error) {
    tracer.endSpan(span, 'error', error as Error);
    return NextResponse.json(
      { error: '処理に失敗しました' },
      { status: 500 }
    );
  }
}

キャッシング、A/Bテスト、分散トレーシングを備えた本番環境API。

6. フロントエンドダッシュボードコンポーネントの作成

// app/components/monitoring-dashboard.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { Line, Bar } from 'recharts';

interface DashboardData {
  metrics?: any[];
  alerts?: any[];
  cacheStats?: any;
  results?: any[];
  winner?: string;
}

export default function MonitoringDashboard() {
  const [view, setView] = useState<'metrics' | 'alerts' | 'cache' | 'ab-test'>('metrics');

  const { data, isLoading } = useQuery<DashboardData>({
    queryKey: ['monitoring', view],
    queryFn: async () => {
      const response = await fetch(`/api/monitoring?view=${view}`);
      return response.json();
    },
    refetchInterval: 5000, // 5秒ごとに更新
  });

  if (isLoading) return <div className="loading loading-spinner" />;

  return (
    <div className="p-6">
      <div className="tabs tabs-boxed mb-4">
        <button
          className={`tab ${view === 'metrics' ? 'tab-active' : ''}`}
          onClick={() => setView('metrics')}
        >
          メトリクス
        </button>
        <button
          className={`tab ${view === 'alerts' ? 'tab-active' : ''}`}
          onClick={() => setView('alerts')}
        >
          アラート
        </button>
        <button
          className={`tab ${view === 'cache' ? 'tab-active' : ''}`}
          onClick={() => setView('cache')}
        >
          キャッシュ
        </button>
        <button
          className={`tab ${view === 'ab-test' ? 'tab-active' : ''}`}
          onClick={() => setView('ab-test')}
        >
          A/Bテスト
        </button>
      </div>

      {view === 'metrics' && data?.metrics && (
        <div className="grid grid-cols-2 gap-4">
          {data.metrics.map((metric: any) => (
            <div key={metric.agent} className="card bg-base-200 p-4">
              <h3 className="text-lg font-bold">{metric.agent}</h3>
              <div className="stats stats-vertical">
                <div className="stat">
                  <div className="stat-title">平均レイテンシ</div>
                  <div className="stat-value text-2xl">{metric.avgLatency.toFixed(0)}ms</div>
                </div>
                <div className="stat">
                  <div className="stat-title">精度</div>
                  <div className="stat-value text-2xl">{(metric.avgAccuracy * 100).toFixed(1)}%</div>
                </div>
                <div className="stat">
                  <div className="stat-title">総コスト</div>
                  <div className="stat-value text-2xl">${metric.totalCost.toFixed(2)}</div>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}

      {view === 'alerts' && data?.alerts && (
        <div className="space-y-2">
          {data.alerts.length === 0 ? (
            <div className="alert alert-success">
              <span>すべてのシステムが正常に動作しています</span>
            </div>
          ) : (
            data.alerts.map((alert: any, idx: number) => (
              <div key={idx} className={`alert alert-${alert.severity === 'critical' ? 'error' : 'warning'}`}>
                <span>{alert.message}</span>
              </div>
            ))
          )}
        </div>
      )}

      {view === 'cache' && data?.cacheStats && (
        <div className="stats shadow">
          <div className="stat">
            <div className="stat-title">キャッシュエントリ</div>
            <div className="stat-value">{data.cacheStats.totalEntries}</div>
          </div>
          <div className="stat">
            <div className="stat-title">総ヒット数</div>
            <div className="stat-value">{data.cacheStats.totalHits}</div>
          </div>
          <div className="stat">
            <div className="stat-title">節約コスト</div>
            <div className="stat-value">${data.cacheStats.costSaved.toFixed(2)}</div>
          </div>
        </div>
      )}

      {view === 'ab-test' && data?.results && (
        <div className="space-y-4">
          <div className="grid grid-cols-2 gap-4">
            {data.results.map((result: any) => (
              <div key={result.variantId} className="card bg-base-200 p-4">
                <h3 className="text-lg font-bold">バリアント: {result.variantId}</h3>
                <div className="stats stats-vertical">
                  <div className="stat">
                    <div className="stat-title">サンプルサイズ</div>
                    <div className="stat-value">{result.sampleSize}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">信頼度</div>
                    <div className="stat-value">{(result.confidence * 100).toFixed(1)}%</div>
                  </div>
                </div>
              </div>
            ))}
          </div>
          {data.winner && (
            <div className="alert alert-success">
              <span>勝者が決定しました: {data.winner}</span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

エージェントパフォーマンスを監視するためのリアルタイムダッシュボードコンポーネント。

まとめ

非決定的なエージェント動作の複雑さを処理する包括的な評価とモニタリングシステムを構築しました。基本例では、LLMベースの評価を使用した基本的なモニタリングを示し、高度な例では、分散トレーシング、セマンティックキャッシング、A/Bテスト、リアルタイムダッシュボードを含む本番環境対応のパターンを示しました。これらのパターンにより、信頼性の高いエージェントシステムを自信を持ってデプロイし、その動作への可視性を維持し、実世界のデータに基づいて継続的にパフォーマンスを最適化できます。