초안 에이전트 설계 패턴 - 도구 사용

aiagentslangchainlanggraphtool-usefunction-callingtypescript
By sko X opus 4.19/21/20259 min read

도구 사용(함수 호출)은 AI 에이전트를 수동적 응답자에서 능동적 실행자로 변환하여 외부 시스템, API 및 데이터베이스와 상호작용할 수 있게 합니다. 이 가이드는 Vercel의 서버리스 플랫폼에서 LangChain, LangGraph, Next.js 15를 사용하여 프로덕션 준비 도구 사용 패턴을 구현하는 방법을 보여줍니다.

멘탈 모델: 운영 체제로서의 AI

도구 사용을 운영 체제의 시스템 콜처럼 생각해보세요. 프로그램이 시스템 콜을 사용하여 하드웨어 및 리소스와 상호작용하는 것처럼, AI 에이전트는 도구를 사용하여 외부 시스템과 상호작용합니다. LLM은 사용자의 의도에 따라 어떤 도구를 호출할지 결정하는 "커널" 역할을 합니다. LangChain은 도구 인터페이스를 표준화하는 "드라이버 계층"을 제공하고, LangGraph는 복잡한 다중 도구 워크플로우를 위한 "프로세스 스케줄링"을 관리합니다. 이 아키텍처를 통해 에이전트는 데이터베이스 쿼리, API 호출, 계산 등의 작업을 수행할 수 있습니다 - OS가 파일 I/O, 네트워크 요청, CPU 작업을 조정하는 것과 유사합니다.

기본 도구 구현

1. Zod 스키마로 도구 정의

// lib/tools/weather-tool.ts
import { z } from 'zod';
import { tool } from '@langchain/core/tools';
import { get } from 'es-toolkit/object';
import { isString } from 'es-toolkit/predicate';

export const weatherTool = tool(
  async ({ location, unit = 'celsius' }) => {
    // API 호출 시뮬레이션 - 실제 날씨 API로 교체
    const response = await fetch(
      `https://api.weather.example/current?q=${location}&units=${unit}`
    );

    const data = await response.json();

    // es-toolkit을 사용한 안전한 데이터 접근
    const temperature = get(data, 'main.temp');
    const description = get(data, 'weather[0].description');

    if (!isString(description)) {
      throw new Error('유효하지 않은 날씨 데이터 수신');
    }

    return `${location}의 현재 날씨: ${temperature}°${
      unit === 'celsius' ? 'C' : 'F'
    }, ${description}`;
  },
  {
    name: 'get_weather',
    description: '위치의 현재 날씨 가져오기',
    schema: z.object({
      location: z.string().describe('도시 이름 또는 위치'),
      unit: z.enum(['celsius', 'fahrenheit']).optional(),
    }),
  }
);

Zod 스키마 검증으로 날씨 도구를 정의하여 입력 매개변수와 TypeScript 통합 모두에 대한 타입 안정성을 보장합니다.

2. 계산기 도구 생성

// lib/tools/calculator-tool.ts
import { z } from 'zod';
import { tool } from '@langchain/core/tools';
import { evaluate } from 'es-toolkit/math';
import { attempt } from 'es-toolkit/function';

export const calculatorTool = tool(
  async ({ expression }) => {
    // 안전한 실행을 위해 es-toolkit의 attempt 사용
    const result = attempt(() => {
      // 프로덕션에서는 mathjs 같은 적절한 수학 파서 사용
      return evaluate(expression);
    });

    if (result instanceof Error) {
      return `오류: 잘못된 표현식 "${expression}"`;
    }

    return `결과: ${result}`;
  },
  {
    name: 'calculator',
    description: '수학적 계산 수행',
    schema: z.object({
      expression: z.string().describe('평가할 수학 표현식'),
    }),
  }
);

안전한 실행을 위해 es-toolkit의 attempt 패턴을 사용한 오류 처리로 계산기 도구를 구현합니다.

3. 데이터베이스 쿼리 도구

// lib/tools/database-tool.ts
import { z } from 'zod';
import { tool } from '@langchain/core/tools';
import { sql } from '@vercel/postgres';
import { mapValues, pick } from 'es-toolkit/object';
import { chunk } from 'es-toolkit/array';

export const databaseTool = tool(
  async ({ query, parameters = {} }) => {
    try {
      // 쿼리 검증 및 살균
      if (query.toLowerCase().includes('drop') ||
          query.toLowerCase().includes('delete')) {
        return '오류: 파괴적인 작업은 허용되지 않습니다';
      }

      // 매개변수로 쿼리 실행
      const result = await sql.query(query, Object.values(parameters));

      // 대용량 데이터셋을 위한 청크 처리
      const rows = result.rows;
      const chunks = chunk(rows, 100);

      // 서버리스 메모리 제한을 위해 첫 번째 청크 반환
      return JSON.stringify({
        rowCount: result.rowCount,
        data: chunks[0] || [],
        hasMore: chunks.length > 1,
      });
    } catch (error) {
      return `데이터베이스 오류: ${error.message}`;
    }
  },
  {
    name: 'query_database',
    description: '데이터베이스에서 안전한 SELECT 쿼리 실행',
    schema: z.object({
      query: z.string().describe('SQL SELECT 쿼리'),
      parameters: z.record(z.any()).optional().describe('쿼리 매개변수'),
    }),
  }
);

서버리스 메모리 제약에 최적화된 안전 검사 및 청크 결과 처리를 포함한 데이터베이스 도구를 생성합니다.

4. LLM에 도구 바인딩

// lib/agents/tool-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { weatherTool } from '@/lib/tools/weather-tool';
import { calculatorTool } from '@/lib/tools/calculator-tool';
import { databaseTool } from '@/lib/tools/database-tool';
import { pipe } from 'es-toolkit/function';

export function createToolAgent() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0,
    streaming: true,
  });

  const tools = [weatherTool, calculatorTool, databaseTool];

  // 모델에 도구 바인딩
  const modelWithTools = model.bindTools(tools);

  return {
    model: modelWithTools,
    tools,
    // 도구 호출 형식화 헬퍼
    formatToolCall: pipe(
      (call: any) => call.args,
      JSON.stringify
    ),
  };
}

사용자 쿼리를 기반으로 사용할 도구를 결정할 수 있도록 여러 도구를 LLM에 바인딩합니다.

5. 도구 실행이 포함된 API 라우트

// app/api/tools/route.ts
import { NextResponse } from 'next/server';
import { createToolAgent } from '@/lib/agents/tool-agent';
import { HumanMessage } from '@langchain/core/messages';
import { debounce } from 'es-toolkit/function';

export const runtime = 'nodejs';
export const maxDuration = 777; // 800초 제한에 대한 안전 버퍼

// 동시 요청 디바운스
const processRequest = debounce(async (message: string) => {
  const { model, tools } = createToolAgent();

  // 도구 호출이 포함된 LLM 응답 가져오기
  const response = await model.invoke([
    new HumanMessage(message)
  ]);

  // 도구 호출이 있으면 실행
  if (response.tool_calls && response.tool_calls.length > 0) {
    const toolResults = await Promise.all(
      response.tool_calls.map(async (toolCall) => {
        const tool = tools.find(t => t.name === toolCall.name);
        if (!tool) return null;

        const result = await tool.invoke(toolCall.args);
        return {
          tool: toolCall.name,
          result,
        };
      })
    );

    return { toolResults, message: response.content };
  }

  return { message: response.content };
}, 100);

export async function POST(req: Request) {
  try {
    const { message } = await req.json();
    const result = await processRequest(message);

    return NextResponse.json(result);
  } catch (error) {
    return NextResponse.json(
      { error: '도구 실행 실패' },
      { status: 500 }
    );
  }
}

메시지를 처리하고, 도구 호출을 실행하고, 속도 제한을 위한 디바운싱으로 결과를 반환하는 서버리스 API 라우트를 구현합니다.

고급 도구 오케스트레이션

1. 병렬 도구 실행

// lib/orchestration/parallel-tools.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableParallel } from '@langchain/core/runnables';
import { groupBy } from 'es-toolkit/array';
import { mapValues } from 'es-toolkit/object';

export class ParallelToolOrchestrator {
  private model: ChatGoogleGenerativeAI;
  private tools: Map<string, any>;

  constructor(tools: any[]) {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0,
    });

    this.tools = new Map(tools.map(t => [t.name, t]));
    this.model = this.model.bindTools(tools);
  }

  async executeParallel(message: string) {
    // LLM에서 도구 호출 가져오기
    const response = await this.model.invoke([
      { role: 'user', content: message }
    ]);

    if (!response.tool_calls?.length) {
      return { message: response.content };
    }

    // 종속성별로 도구 그룹화 (독립 도구는 병렬 실행 가능)
    const toolGroups = this.groupIndependentTools(response.tool_calls);

    // 각 그룹을 병렬로 실행
    const results = [];
    for (const group of toolGroups) {
      const groupResults = await Promise.all(
        group.map(async (call) => {
          const tool = this.tools.get(call.name);
          if (!tool) return null;

          const startTime = Date.now();
          const result = await tool.invoke(call.args);
          const duration = Date.now() - startTime;

          return {
            tool: call.name,
            result,
            duration,
            timestamp: new Date().toISOString(),
          };
        })
      );
      results.push(...groupResults.filter(Boolean));
    }

    return {
      message: response.content,
      toolResults: results,
      parallelGroups: toolGroups.length,
      totalDuration: Math.max(...results.map(r => r.duration)),
    };
  }

  private groupIndependentTools(toolCalls: any[]) {
    // 간단한 휴리스틱: 공유 매개변수가 없는 도구는 병렬 실행 가능
    const groups: any[][] = [];
    const used = new Set<number>();

    toolCalls.forEach((call, i) => {
      if (used.has(i)) return;

      const group = [call];
      used.add(i);

      // 다른 독립 도구 찾기
      toolCalls.forEach((other, j) => {
        if (i !== j && !used.has(j) && this.areIndependent(call, other)) {
          group.push(other);
          used.add(j);
        }
      });

      groups.push(group);
    });

    return groups;
  }

  private areIndependent(tool1: any, tool2: any): boolean {
    // 데이터 종속성을 공유하지 않으면 도구는 독립적
    const args1 = JSON.stringify(tool1.args);
    const args2 = JSON.stringify(tool2.args);

    // 간단한 검사: 공유 값 없음
    return !Object.values(tool1.args).some(v =>
      args2.includes(String(v))
    );
  }
}

서버리스 환경에서 처리량을 최대화하기 위한 종속성 분석이 포함된 병렬 도구 실행을 구현합니다.

2. 오류 복구를 포함한 도구 체인

// lib/orchestration/tool-chain.ts
import { StateGraph, END } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';
import { retry, delay } from 'es-toolkit/promise';
import { pipe } from 'es-toolkit/function';

interface ChainState {
  messages: BaseMessage[];
  toolResults: any[];
  errors: any[];
  retryCount: number;
}

export function createToolChain(tools: any[]) {
  const graph = new StateGraph<ChainState>({
    channels: {
      messages: {
        value: (x, y) => [...x, ...y],
        default: () => [],
      },
      toolResults: {
        value: (x, y) => [...x, ...y],
        default: () => [],
      },
      errors: {
        value: (x, y) => [...x, ...y],
        default: () => [],
      },
      retryCount: {
        value: (x, y) => y,
        default: () => 0,
      },
    },
  });

  // 재시도 로직으로 도구 실행
  graph.addNode('execute_tool', async (state) => {
    const currentTool = tools[state.toolResults.length];
    if (!currentTool) {
      return { ...state };
    }

    try {
      const result = await retry(
        async () => {
          const res = await currentTool.invoke(
            state.messages[state.messages.length - 1]
          );
          return res;
        },
        {
          times: 3,
          delay: 1000,
          onRetry: (error, attempt) => {
            console.log(`${currentTool.name} 재시도 ${attempt}:`, error);
          },
        }
      );

      return {
        toolResults: [{
          tool: currentTool.name,
          result,
          success: true
        }],
      };
    } catch (error) {
      return {
        errors: [{
          tool: currentTool.name,
          error: error.message
        }],
        retryCount: state.retryCount + 1,
      };
    }
  });

  // 오류용 대체 노드
  graph.addNode('handle_error', async (state) => {
    const lastError = state.errors[state.errors.length - 1];

    // 대체 도구 또는 우아한 저하 시도
    const fallbackResult = {
      tool: 'fallback',
      result: `${lastError.tool} 실행 실패. 캐시된 결과 사용.`,
      fallback: true,
    };

    return {
      toolResults: [fallbackResult],
    };
  });

  // 결정 노드
  graph.addNode('check_status', async (state) => {
    if (state.errors.length > 0 && state.retryCount < 3) {
      return 'handle_error';
    }
    if (state.toolResults.length < tools.length) {
      return 'execute_tool';
    }
    return END;
  });

  // 엣지 정의
  graph.setEntryPoint('execute_tool');
  graph.addEdge('execute_tool', 'check_status');
  graph.addEdge('handle_error', 'execute_tool');
  graph.addConditionalEdges('check_status', async (state) => {
    if (state.errors.length > 0 && state.retryCount < 3) {
      return 'handle_error';
    }
    if (state.toolResults.length < tools.length) {
      return 'execute_tool';
    }
    return END;
  });

  return graph.compile();
}

프로덕션 신뢰성을 위한 자동 재시도 로직과 대체 메커니즘을 갖춘 탄력적인 도구 체인을 생성합니다.

3. 동적 도구 선택

// lib/orchestration/dynamic-tools.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { z } from 'zod';
import { tool } from '@langchain/core/tools';
import { maxBy, minBy } from 'es-toolkit/array';
import { memoize } from 'es-toolkit/function';

export class DynamicToolSelector {
  private model: ChatGoogleGenerativeAI;
  private toolRegistry: Map<string, any>;
  private performanceMetrics: Map<string, any>;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-flash',
      temperature: 0,
    });
    this.toolRegistry = new Map();
    this.performanceMetrics = new Map();
  }

  // 메타데이터와 함께 도구 등록
  registerTool(toolDef: any, metadata: {
    cost: number;
    latency: number;
    reliability: number;
    capabilities: string[];
  }) {
    this.toolRegistry.set(toolDef.name, {
      tool: toolDef,
      metadata,
    });
  }

  // 성능을 위한 메모이제이션된 도구 선택
  private selectOptimalTool = memoize(
    async (requirement: string, constraints: any) => {
      const availableTools = Array.from(this.toolRegistry.values());

      // 요구사항에 따라 각 도구 점수 매기기
      const scoredTools = await Promise.all(
        availableTools.map(async (entry) => {
          const score = this.calculateToolScore(
            entry,
            requirement,
            constraints
          );
          return { ...entry, score };
        })
      );

      // 최적 도구 선택
      const bestTool = maxBy(scoredTools, t => t.score);
      return bestTool?.tool;
    },
    {
      getCacheKey: (req, cons) => `${req}-${JSON.stringify(cons)}`,
    }
  );

  private calculateToolScore(
    entry: any,
    requirement: string,
    constraints: any
  ): number {
    const { metadata } = entry;
    let score = 0;

    // 기능 매칭
    const capabilityMatch = metadata.capabilities.some((cap: string) =>
      requirement.toLowerCase().includes(cap.toLowerCase())
    );
    if (capabilityMatch) score += 40;

    // 제약사항 고려
    if (constraints.maxLatency && metadata.latency <= constraints.maxLatency) {
      score += 20;
    }
    if (constraints.maxCost && metadata.cost <= constraints.maxCost) {
      score += 20;
    }

    // 신뢰성 보너스
    score += metadata.reliability * 20;

    // 과거 성능
    const metrics = this.performanceMetrics.get(entry.tool.name);
    if (metrics?.successRate) {
      score += metrics.successRate * 10;
    }

    return score;
  }

  async executeDynamic(
    requirement: string,
    constraints: {
      maxLatency?: number;
      maxCost?: number;
      preferredTools?: string[];
    } = {}
  ) {
    // 최적 도구 선택
    const selectedTool = await this.selectOptimalTool(
      requirement,
      constraints
    );

    if (!selectedTool) {
      throw new Error('요구사항에 적합한 도구를 찾을 수 없습니다');
    }

    const startTime = Date.now();
    try {
      // 선택된 도구 실행
      const result = await selectedTool.invoke({ query: requirement });

      // 성능 메트릭 업데이트
      this.updateMetrics(selectedTool.name, {
        success: true,
        latency: Date.now() - startTime,
      });

      return {
        tool: selectedTool.name,
        result,
        metrics: {
          latency: Date.now() - startTime,
          cost: this.toolRegistry.get(selectedTool.name)?.metadata.cost,
        },
      };
    } catch (error) {
      // 실패 메트릭 업데이트
      this.updateMetrics(selectedTool.name, {
        success: false,
        error: error.message,
      });

      throw error;
    }
  }

  private updateMetrics(toolName: string, metrics: any) {
    const existing = this.performanceMetrics.get(toolName) || {
      totalCalls: 0,
      successfulCalls: 0,
      totalLatency: 0,
    };

    existing.totalCalls++;
    if (metrics.success) {
      existing.successfulCalls++;
      existing.totalLatency += metrics.latency;
    }

    existing.successRate = existing.successfulCalls / existing.totalCalls;
    existing.avgLatency = existing.totalLatency / existing.successfulCalls;

    this.performanceMetrics.set(toolName, existing);
  }
}

요구사항, 제약사항 및 과거 성능 메트릭을 기반으로 동적 도구 선택을 구현합니다.

4. 도구 결과 스트리밍

// app/api/stream-tools/route.ts
import { createToolAgent } from '@/lib/agents/tool-agent';
import { ParallelToolOrchestrator } from '@/lib/orchestration/parallel-tools';

export const runtime = 'nodejs';
export const maxDuration = 777;

export async function POST(req: Request) {
  const { message } = await req.json();

  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();

  const { model, tools } = createToolAgent();
  const orchestrator = new ParallelToolOrchestrator(tools);

  // 백그라운드에서 처리
  (async () => {
    try {
      // 초기 사고 스트리밍
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({
          type: 'thinking',
          content: '요청을 분석하고 도구를 선택 중...'
        })}\n\n`)
      );

      // LLM에서 도구 호출 가져오기
      const response = await model.invoke([
        { role: 'user', content: message }
      ]);

      if (response.tool_calls) {
        // 도구 실행 업데이트 스트리밍
        for (const call of response.tool_calls) {
          await writer.write(
            encoder.encode(`data: ${JSON.stringify({
              type: 'tool_start',
              tool: call.name,
              args: call.args
            })}\n\n`)
          );

          const tool = tools.find(t => t.name === call.name);
          if (tool) {
            const result = await tool.invoke(call.args);

            await writer.write(
              encoder.encode(`data: ${JSON.stringify({
                type: 'tool_complete',
                tool: call.name,
                result
              })}\n\n`)
            );
          }
        }
      }

      // 최종 응답 스트리밍
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({
          type: 'complete',
          content: response.content
        })}\n\n`)
      );

    } catch (error) {
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({
          type: 'error',
          error: error.message
        })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

실시간으로 도구 실행 진행 상황을 스트리밍하기 위한 서버 전송 이벤트를 구현합니다.

5. TanStack Query를 사용한 프론트엔드 통합

// components/ToolInterface.tsx
'use client';

import { useMutation } from '@tanstack/react-query';
import { useState, useCallback } from 'react';
import { debounce } from 'es-toolkit/function';

interface ToolResult {
  tool: string;
  result: any;
  duration?: number;
}

export default function ToolInterface() {
  const [input, setInput] = useState('');
  const [streamedResults, setStreamedResults] = useState<ToolResult[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const streamTools = useCallback(
    debounce(async (message: string) => {
      setIsStreaming(true);
      setStreamedResults([]);

      const eventSource = new EventSource(
        `/api/stream-tools?message=${encodeURIComponent(message)}`
      );

      eventSource.onmessage = (event) => {
        const data = JSON.parse(event.data);

        switch (data.type) {
          case 'tool_start':
            setStreamedResults(prev => [...prev, {
              tool: data.tool,
              result: '실행 중...',
            }]);
            break;

          case 'tool_complete':
            setStreamedResults(prev =>
              prev.map(r =>
                r.tool === data.tool
                  ? { ...r, result: data.result }
                  : r
              )
            );
            break;

          case 'complete':
            setIsStreaming(false);
            eventSource.close();
            break;

          case 'error':
            console.error('스트림 오류:', data.error);
            setIsStreaming(false);
            eventSource.close();
            break;
        }
      };

      return () => eventSource.close();
    }, 300),
    []
  );

  const executeTool = useMutation({
    mutationFn: async (message: string) => {
      const response = await fetch('/api/tools', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message }),
      });
      return response.json();
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim()) {
      streamTools(input);
    }
  };

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">AI 도구 실행기</h2>

        <form onSubmit={handleSubmit}>
          <div className="form-control">
            <label className="label">
              <span className="label-text">무엇을 도와드릴까요?</span>
            </label>
            <textarea
              className="textarea textarea-bordered h-24"
              placeholder="144의 제곱근을 계산하고 도쿄의 날씨 확인하기"
              value={input}
              onChange={(e) => setInput(e.target.value)}
            />
          </div>

          <div className="form-control mt-4">
            <button
              type="submit"
              className="btn btn-primary"
              disabled={isStreaming || !input.trim()}
            >
              {isStreaming ? (
                <>
                  <span className="loading loading-spinner"></span>
                  도구 실행 중...
                </>
              ) : '실행'}
            </button>
          </div>
        </form>

        {streamedResults.length > 0 && (
          <div className="mt-6">
            <h3 className="text-lg font-semibold mb-3">도구 결과:</h3>
            <div className="space-y-3">
              {streamedResults.map((result, idx) => (
                <div key={idx} className="alert alert-info">
                  <div>
                    <span className="font-bold">{result.tool}:</span>
                    <pre className="mt-2 text-sm">{
                      typeof result.result === 'object'
                        ? JSON.stringify(result.result, null, 2)
                        : result.result
                    }</pre>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

서버 전송 이벤트를 사용하여 도구 실행 결과를 실시간으로 스트리밍하는 React 컴포넌트입니다.

6. 도구 모니터링 대시보드

// app/tools/dashboard/page.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import { groupBy } from 'es-toolkit/array';
import { useState, useEffect } from 'react';

interface ToolMetrics {
  name: string;
  calls: number;
  avgLatency: number;
  successRate: number;
  lastUsed: string;
}

export default function ToolDashboard() {
  const [metrics, setMetrics] = useState<ToolMetrics[]>([]);

  const { data: liveMetrics } = useQuery({
    queryKey: ['tool-metrics'],
    queryFn: async () => {
      const res = await fetch('/api/tools/metrics');
      return res.json();
    },
    refetchInterval: 5000, // 5초마다 폴링
  });

  useEffect(() => {
    if (liveMetrics) {
      setMetrics(liveMetrics);
    }
  }, [liveMetrics]);

  const totalCalls = metrics.reduce((sum, m) => sum + m.calls, 0);
  const avgSuccessRate = metrics.length > 0
    ? metrics.reduce((sum, m) => sum + m.successRate, 0) / metrics.length
    : 0;

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-4xl font-bold mb-8">도구 모니터링 대시보드</h1>

      <div className="stats shadow mb-8">
        <div className="stat">
          <div className="stat-title">전체 도구 호출</div>
          <div className="stat-value">{totalCalls}</div>
          <div className="stat-desc">모든 도구 전체</div>
        </div>

        <div className="stat">
          <div className="stat-title">평균 성공률</div>
          <div className="stat-value">{(avgSuccessRate * 100).toFixed(1)}%</div>
          <div className="stat-desc">시스템 신뢰성</div>
        </div>

        <div className="stat">
          <div className="stat-title">활성 도구</div>
          <div className="stat-value">{metrics.length}</div>
          <div className="stat-desc">현재 등록됨</div>
        </div>
      </div>

      <div className="overflow-x-auto">
        <table className="table table-zebra w-full">
          <thead>
            <tr>
              <th>도구 이름</th>
              <th>호출</th>
              <th>평균 지연시간</th>
              <th>성공률</th>
              <th>마지막 사용</th>
              <th>상태</th>
            </tr>
          </thead>
          <tbody>
            {metrics.map((metric) => (
              <tr key={metric.name}>
                <td className="font-medium">{metric.name}</td>
                <td>{metric.calls}</td>
                <td>{metric.avgLatency.toFixed(0)}ms</td>
                <td>
                  <div className="flex items-center gap-2">
                    <progress
                      className="progress progress-success w-20"
                      value={metric.successRate * 100}
                      max="100"
                    />
                    <span>{(metric.successRate * 100).toFixed(1)}%</span>
                  </div>
                </td>
                <td>{new Date(metric.lastUsed).toLocaleString()}</td>
                <td>
                  <div className={`badge ${
                    metric.successRate > 0.95 ? 'badge-success' :
                    metric.successRate > 0.8 ? 'badge-warning' :
                    'badge-error'
                  }`}>
                    {metric.successRate > 0.95 ? '정상' :
                     metric.successRate > 0.8 ? '저하' :
                     '위험'}
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

도구 성능, 성공률 및 시스템 상태를 추적하기 위한 실시간 모니터링 대시보드입니다.

결론

도구 사용 패턴은 AI 에이전트를 수동적 응답자에서 외부 시스템과 상호작용할 수 있는 능동적 실행자로 변환합니다. 이 구현은 병렬 실행, 오류 복구, 동적 선택 및 실시간 스트리밍을 포함한 프로덕션 준비 패턴을 보여줍니다 - 모두 777초 안전 버퍼로 Vercel의 서버리스 플랫폼에 최적화되었습니다. LangChain의 표준화된 도구 인터페이스, LangGraph의 오케스트레이션 기능, es-toolkit의 유틸리티 함수를 결합하면 프로덕션 환경에서 복잡한 다단계 워크플로우를 안정적으로 실행할 수 있는 정교한 AI 에이전트를 구축하기 위한 견고한 기반이 만들어집니다.