블로그 목록
ai

Vercel AI SDK로 툴 쓰면서 구조화 응답도 받기 — experimental_output

구조화된 JSON이 필요하면 generateObject를 쓰면 된다. Zod 스키마 넘기면 타입 딱 맞는 객체가 돌아오니까 편하다. 그런데 최근에 벽에 부딪혔다. 모델이 툴을 호출하면서 동시에 구조화된 결과를 뱉게 하고 싶었는데, generateObject는 툴을 못 쓴다.

내가 만들던 건 날씨 기반 옷차림 추천이었다. 모델이 날씨 API를 호출(툴)하고, 그 결과를 바탕으로 { summary, items[] } 같은 정해진 형태로 답을 줘야 했다. generateText로 툴은 돌아가는데 응답이 자유 텍스트라 파싱이 지저분했고, generateObject로 바꾸면 이번엔 툴을 못 붙였다. 둘 다 반쪽이었다.

experimental_output이 그 틈을 메운다

generateText에 experimental_output을 넘기면, 툴 호출 루프는 그대로 돌면서 마지막 응답만 스키마에 맞춰 받을 수 있다. 툴과 구조화 출력을 한 호출에서 같이 쓰는 유일한 방법이다.

import { generateText, tool, Output, stepCountIs } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';

const { experimental_output } = await generateText({
  model: anthropic('claude-sonnet-5'),
  stopWhen: stepCountIs(5),
  tools: {
    getWeather: tool({
      description: '도시의 현재 날씨를 가져온다',
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => fetchWeather(city),
    }),
  },
  experimental_output: Output.object({
    schema: z.object({
      summary: z.string(),
      items: z.array(z.string()),
    }),
  }),
  prompt: '서울 날씨 보고 오늘 입을 옷 추천해줘.',
});

console.log(experimental_output.items); // string[] — 타입까지 맞는다

반환값에서 text 대신 experimental_output을 꺼내면 끝이다. 모델이 getWeather를 알아서 호출하고, 그 결과를 재료 삼아 마지막에 스키마대로 정리해준다. 중간 과정은 stopWhen으로 단계 수만 막아두면 된다.

걸렸던 점

처음엔 툴 execute가 돌기 전에 모델이 구조화 응답부터 내놓을까 걱정했는데, 그런 일은 없었다. 구조화 출력은 항상 루프 맨 끝에서만 생성된다. 툴을 다 쓴 뒤에 최종 정리를 하는 구조라, 순서 꼬일 걱정은 안 해도 됐다.

이름에 experimental_이 붙어 있어서 API가 바뀔 여지는 있다. 핵심 로직은 호출부에 묶지 말고 함수로 빼두는 게 안전하다.

정리하면 — 툴이 필요 없으면 그냥 generateObject, 툴을 돌리면서 결과를 정해진 형태로 받아야 하면 generateText + experimental_output. 이 기준 하나로 그동안의 어정쩡함이 깔끔하게 정리됐다.