Next.js 라우트 핸들러에서 도구 호출(Tool Calling)을 사용하여 AI 에이전트 구축하기 (Claude API)
요약
본 가이드는 Next.js 라우트 핸들러와 Anthropic Claude API를 사용하여 실제 함수 호출 및 상태 유지가 가능한 대화형 AI 에이전트를 구축하는 방법을 단계별로 안내합니다. 단순 챗봇을 넘어, 사용자의 요청에 따라 외부 도구를 실행하고 그 결과를 바탕으로 다음 행동을 결정하는 프로덕션급 패턴을 구현할 수 있습니다.
핵심 포인트
- 도구(Tool)는 모델 호출 시 JSON Schema 설명 형태로 제공되며, 실제 함수 실행은 사용자 코드 영역에서 이루어집니다.
- Claude API는 상태 비저장(stateless)이므로, 전체 대화 기록(transcript)을 매번 서버로 전송하여 상태를 유지해야 합니다.
- 에이전트 루프 패턴은 모델 호출 -> 도구 사용 요청 감지 -> 도구 실행 및 결과 추가 -> 재호출의 순서로 진행됩니다.
- API 히스토리 관리 시, 전체 콘텐츠 배열을 누락 없이 추가하는 것이 중요합니다.
AutoValue의 리스팅 에이전트를 구현하는 패턴에 대한 단계별 가이드: 실제 함수를 호출하고, 턴(turn) 간 상태를 유지하며, 단일 Next.js 라우트 내에서 완전히 작동하는 대화형 AI.
대부분의 '챗봇 만들기' 튜토리얼은 텍스트 입력에 텍스트 출력이 끝입니다. 하지만 프로덕션 에이전트는 다릅니다. 이들은 사용자의 함수를 호출하고, 실제 데이터를 기다리며, 그 결과에 따라 다음에 무엇을 할지 결정합니다. 저는 AutoValue에서 실제로 작동하는 에이전트를 운영하고 있는데, 이 에이전트는 나이지리아의 자동차 판매자에게 전체 리스팅 흐름(listing flow)을 안내합니다: 사진으로 차량 식별하기, 주행거리계 읽기, 실시간 시장 가격 가져오기, 그리고 완성된 리스팅을 마켓플레이스에 게시하는 것입니다. 본 튜토리얼은 이 패턴의 핵심인 도구 호출 에이전트를 단일 Next.js 라우트 핸들러 내에서 처음부터 구축합니다.
Node 18 이상, Next.js App Router 프로젝트, 그리고 Anthropic API 키가 필요합니다.
단계 1: SDK 설치 및 키 설정
npm install @anthropic-ai/sdk
키를 .env.local에 넣어 Next.js에서 서버 측으로 유지되게 합니다:
ANTHROPIC_API_KEY=sk-ant-...
SDK가 이 변수를 자동으로 읽기 때문에 클라이언트 측에서는 인수가 필요 없습니다.
단계 2: 도구 정의하기
도구(tool)란 모델이 호출할 수 있는 함수에 대한 JSON Schema 설명입니다. 모델 자체는 아무것도 실행하지 못하며, 구조화된 요청을 반환하고 사용자의 코드가 그 함수를 실행합니다. 여기 AutoValue의 가격 책정 도구를 단순화한 버전이 있습니다:
import Anthropic from "@anthropic-ai/sdk";
const tools: Anthropic.Tool[] = [
...
이 설명에는 프로덕션에서 얻은 두 가지 교훈이 숨어 있습니다. 첫째, 단순히 무엇을 하는지뿐만 아니라 언제 도구를 호출해야 하는지를 명시하세요. 모델은 이 설명을 바탕으로 판단하며,
3단계: 에이전트 루프를 가진 라우트 핸들러
Claude API는 상태 비저장(stateless)입니다. 즉, 매번 전체 대화 내용을 전송해야 합니다. 에이전트 루프는 형태가 간단합니다. 모델을 호출하고, 만약 모델이 도구를 사용하길 원해서 멈춘 경우(stop_reason: "tool_use"), 해당 도구를 실행한 다음, 결과를 추가하고 다시 호출하는 것입니다. 다른 이유로 멈추면 답변을 얻게 됩니다.
app/api/agent/route.ts:
import Anthropic from "@anthropic-ai/sdk";
import { NextResponse } from "next/server";
...
프론트엔드는 단순히 { messages }를 POST하고, 반환된 messages 배열을 다음 턴에 저장합니다. 이것이 전체 상태 모델입니다. 클라이언트가 대화 기록(transcript)을 가지고 있고, 서버는 아무것도 가지지 않습니다.
4단계: 여러분이 마주칠 네 가지 버그 (제가 겪었기 때문에)
1. 텍스트만 추가하는 경우. 전체 response.content 대신 { role: "assistant", content: replyText }를 푸시하면, tool_use 블록을 누락하게 되고, API는 기록(history)에 더 이상 존재하지 않는 ID를 참조하기 때문에 다음 요청을 거부합니다. 항상 전체 콘텐츠 배열을 추가해야 합니다.
2. 병렬 도구 결과를 분할하는 경우. 모델은 한 턴에 세 개의 도구를 요청할 수 있습니다. 만약 각 결과를 별도의 사용자 메시지로 보내면, 역할 교대(role alternation)에서 요청이 실패할 수 있으며, 더 나쁜 것은 모델에게 병렬 처리(parallelizing)를 중단하도록 조용히 학습시키는 것입니다. 모든 결과는 하나의 사용자 메시지에 포함되어야 합니다.
3. 도구 오류를 무시하는 경우. 도구가 예외를 발생시키고 그 결과를 건너뛰면, API는 끊어진 tool_use 때문에 오류가 발생합니다. 대신 is_error: true를 가진 tool_result를 반환하세요. 그러면 모델이 오류를 읽고 적응하며, 종종 사용자에게 명확히 하는 질문을 함으로써, 이것이 바로 여러분이 원하는 바입니다.
4. 모델이 상태를 유지하도록 신뢰하기. AutoValue의 6단계 흐름에서 우리는 모델이 첫 번째 단계(예: 체형)에서 수집한 데이터가, 모델에게 도구 인수로 다시 방출되기를 기대할 때 세 번째 단계의 도구 호출까지 안정적으로 살아남지 못한다는 것을 배웠습니다. 권위 있는 상태는 자체 코드에 누적하고 이를 직접 도구 실행에 주입하세요. 모델이 대화를 이끌고; 여러분의 코드가 사실(fact)을 소유합니다.
다음으로 나아갈 방향
- SDK가 루프를 대신 실행할 수 있습니다.
client.beta.messages.toolRunner()와betaZodTool은 호출-실행-추가 사이클을 자동으로 처리합니다. 제가 수동 루프를 보여드린 이유는, 이 기능을 사용하기 전에 러너(runner)가 무엇을 하는지 이해해야 하기 때문입니다. - 스트리밍:
create대신client.messages.stream()으로 교체하고 텍스트 델타(text deltas)를 클라이언트로 전달하여 타이핑 효과를 구현할 수 있습니다.stream.finalMessage()는 루프가 필요로 하는 것과 동일한 객체를 제공합니다. - 비용: 트래픽이 많은 경로의 경우,
claude-haiku-4-5와 같은 더 작은 모델이 좁고 잘 프롬프트된 도구 흐름을 훨씬 적은 비용으로 처리할 수 있습니다. AutoValue는 작업마다 다른 모델을 사용합니다: 대화형 에이전트에는 큰 모델을, 가격 계산기에는 작은 모델을 사용합니다.
이 패턴의 전체 프로덕션 버전(비전 기반 차량 식별, 주행거리계 OCR, 사진 단계 관리, 게시 흐름)은 autovalue.tech에서 실행됩니다. 저는 get_market_price 도구 뒤에 있는 가격 책정 엔진과 거의 그것을 무너뜨릴 뻔했던 통화 버그에 대해 이전 기사에 작성했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기