초안 에이전트 설계 패턴 - 평가와 모니터링

aiagentsmonitoringevaluationtypescriptlangchainlanggraphvercel
By sko X opus 4.19/21/202513 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 // 100ms마다 메트릭 집계
  );

  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 테스팅 및 실시간 대시보드를 포함한 프로덕션 준비 패턴을 보여줍니다. 이러한 패턴을 통해 신뢰할 수 있는 에이전트 시스템을 자신 있게 배포하고, 동작에 대한 가시성을 유지하며, 실제 데이터를 기반으로 지속적으로 성능을 최적화할 수 있습니다.