블로그 목록
AI

Vercel AI SDK로 AI 에이전트 만들기 — tool use로 LLM이 직접 행동하게

기본 챗봇은 텍스트만 생성한다. 사용자가 "오늘 서울 날씨 어때?"라고 물어보면, LLM은 학습 데이터 기반으로 대답할 뿐 실시간 정보를 가져오지 못한다. 이걸 해결하는 게 tool use다. LLM이 직접 함수를 호출하고, 그 결과를 받아서 답변한다.

Vercel AI SDK는 streamTexttools 옵션을 넘기는 것만으로 이 패턴을 구현할 수 있다.

기본 구조

import { anthropic } from '@ai-sdk/anthropic'
import { streamText, tool } from 'ai'
import { z } from 'zod'

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

  const result = streamText({
    model: anthropic('claude-haiku-4-5-20251001'),
    messages,
    tools: {
      getWeather: tool({
        description: '특정 도시의 현재 날씨를 가져옵니다',
        parameters: z.object({
          city: z.string().describe('날씨를 조회할 도시 이름'),
          unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
        }),
        execute: async ({ city, unit }) => {
          // 실제로는 날씨 API를 호출
          const data = await fetch(
            `https://api.weather.example.com?city=${city}&unit=${unit}`
          ).then((r) => r.json())
          return {
            city,
            temperature: data.temp,
            condition: data.condition,
            humidity: data.humidity,
          }
        },
      }),
    },
    maxSteps: 5,
  })

  return result.toDataStreamResponse()
}

tool 함수는 세 가지를 받는다. description은 LLM이 이 도구를 언제 쓸지 판단하는 기준이다. parameters는 Zod 스키마로 입력을 정의한다. execute는 실제로 실행될 비동기 함수다.

maxSteps가 핵심이다

maxSteps: 5가 없으면 LLM이 도구를 호출하고 멈춘다. 결과를 받아서 답변을 생성하지 않는다. maxSteps를 설정하면 LLM이 도구 결과를 받아서 다음 단계를 진행한다. 이게 단순 함수 호출과 에이전트의 차이다.

사용자: "서울이랑 부산 날씨 비교해줘"

1단계: getWeather({ city: "서울" }) 호출
2단계: getWeather({ city: "부산" }) 호출
3단계: 두 결과를 비교해서 텍스트 응답 생성

여러 도구 조합하기

tools: {
  searchProducts: tool({
    description: '상품 데이터베이스를 검색합니다',
    parameters: z.object({
      query: z.string(),
      limit: z.number().default(5),
    }),
    execute: async ({ query, limit }) => {
      return await db.product.findMany({
        where: { name: { contains: query } },
        take: limit,
        select: { id: true, name: true, price: true },
      })
    },
  }),

  calculateDiscount: tool({
    description: '원가와 할인율로 할인가를 계산합니다',
    parameters: z.object({
      originalPrice: z.number(),
      discountPercent: z.number(),
    }),
    execute: async ({ originalPrice, discountPercent }) => ({
      originalPrice,
      discountPercent,
      discountedPrice: Math.round(originalPrice * (1 - discountPercent / 100)),
    }),
  }),
},

"30% 할인된 나이키 운동화 찾아줘"라고 입력하면, LLM이 searchProducts로 상품을 조회하고 calculateDiscount로 각각의 할인가를 계산한 다음 최종 답변을 만든다.

클라이언트 처리

useChat은 tool 결과를 자동으로 처리한다. 별도 설정 없이 maxSteps 응답을 그대로 받는다.

'use client'

import { useChat } from 'ai/react'

export default function AgentChat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({ api: '/api/agent' })

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      <div className="flex-1 overflow-y-auto space-y-3">
        {messages.map((m) => (
          <div key={m.id}>
            {m.role === 'assistant' && m.parts?.map((part, i) => {
              if (part.type === 'tool-invocation') {
                return (
                  <div key={i} className="text-xs text-gray-400 italic">
                    {part.toolInvocation.toolName} 실행 중...
                  </div>
                )
              }
              return (
                <div key={i} className="bg-gray-100 rounded-xl px-4 py-2 text-sm">
                  {part.type === 'text' ? part.text : null}
                </div>
              )
            })}
            {m.role === 'user' && (
              <div className="flex justify-end">
                <div className="bg-blue-500 text-white rounded-xl px-4 py-2 text-sm max-w-xs">
                  {m.content}
                </div>
              </div>
            )}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2 mt-4">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="질문을 입력하세요"
          className="flex-1 border rounded-full px-4 py-2 text-sm"
        />
        <button
          type="submit"
          disabled={isLoading}
          className="px-5 py-2 bg-blue-500 text-white rounded-full text-sm disabled:opacity-50"
        >
          전송
        </button>
      </form>
    </div>
  )
}

도구 실행을 서버에서만 하고 싶을 때

기본적으로 execute가 있으면 서버에서 실행된다. 클라이언트에서 실행해야 하는 경우(브라우저 API 접근 등)는 execute를 빼고 클라이언트에서 onToolCall로 처리할 수 있다.


tool use를 붙이고 나면 LLM이 단순한 텍스트 생성기에서 실제로 뭔가를 할 수 있는 에이전트로 바뀐다. DB 조회, 외부 API 호출, 계산까지 LLM이 판단해서 직접 실행한다. maxSteps 하나가 단순 챗봇과 에이전트의 차이를 만든다.