도구
도구(“function calling”이라고도 합니다)를 사용하면 AI 모델이 외부 시스템 및 API와 상호작용하거나 계산을 수행할 수 있습니다. TanStack AI는 서버와 클라이언트 모두에서 작동하는 타입 안전하고 프레임워크에 종속되지 않는 도구 정의를 제공하는 동형 도구 시스템을 제공합니다.
도구를 사용하면 AI 애플리케이션에서 다음을 수행할 수 있습니다.
- API 또는 데이터베이스에서 데이터 가져오기
- 계산 또는 데이터 변환 수행
- 이메일, 캘린더, 결제 시스템과 같은 서비스와 상호작용
- UI 업데이트 또는 로컬 스토리지와 같은 클라이언트 측 작업 실행
- 서버와 클라이언트 컨텍스트 모두에서 실행되는 하이브리드 도구 생성
Anthropic 웹 검색, OpenAI 코드 인터프리터 또는 Gemini URL 컨텍스트와 같은 제공자 네이티브 도구를 찾고 있나요? 제공자 도구를 참조하세요.
프레임워크 지원
TanStack AI는 모든 JavaScript 프레임워크에서 작동합니다.
- TanStack Start, Next.js, Express, Remix, Fastify 등
- React, Vue, Solid, Svelte, vanilla JS 등
TanStack AI는 모든 JavaScript 프레임워크에서 작동합니다.
동형 도구 아키텍처
TanStack AI는 2단계 도구 정의 프로세스를 사용합니다.
toolDefinition()으로 한 번 정의 - 공유 도구 스키마를 생성합니다..server()또는.client()로 구현 - 각 환경의 실행 로직을 추가합니다.
이 접근 방식은 다음을 제공합니다.
- 타입 안전성: Zod 스키마에서 완전한 TypeScript 타입 추론을 제공합니다.
- 코드 재사용: 스키마를 한 번 정의하고 어디서나 사용합니다.
- 유연성: 도구를 서버, 클라이언트 또는 양쪽 모두에서 실행할 수 있습니다.
- 스키마 옵션: Zod 스키마 또는 원시 JSON Schema 객체를 사용합니다.
스키마 옵션
TanStack AI는 도구 스키마를 정의하는 두 가지 방법을 지원합니다.
옵션 1: Zod 스키마(권장)
Zod 스키마는 완전한 TypeScript 타입 추론과 런타임 검증을 제공합니다.
import { z } from "zod";
const inputSchema = z.object({
location: z.string().meta({ description: "City name" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
});
참고: OpenAI 호환 제공자의 경우 생략된
.optional()도구 필드는 도구 실행 시 존재하지 않습니다..nullable()필드는null을 유지합니다.
옵션 2: JSON Schema 객체
이미 JSON Schema 정의가 있거나 Zod를 사용하지 않으려는 경우 원시 JSON Schema 객체를 직접 전달할 수 있습니다.
import type { JSONSchema } from "@tanstack/ai";
const inputSchema: JSONSchema = {
type: "object",
properties: {
location: {
type: "string",
description: "City name",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location"],
};
참고: JSON Schema를 사용하면 TypeScript가 입력/출력 타입을
unknown으로 추론합니다(컴파일 시점에 JSON Schema에서 타입을 도출할 수 없기 때문입니다). 따라서 사용하기 전에args의 타입 범위를 좁히거나 캐스팅해야 합니다. 완전한 타입 안전성을 위해 Zod 스키마를 권장합니다.
팁: Zod 스키마의 타입 안전성은 도구 실행을 넘어 확장됩니다.
chat()이 반환한 스트림을 순회할 때 도구 호출 이벤트의toolName및input필드도 타입이 지정됩니다. 타입 안전한 도구 호출 이벤트를 참조하세요.
도구 정의
도구는 @tanstack/ai의 toolDefinition()을 사용해 정의합니다.
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
// Step 1: Define the tool schema
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().meta({ description: "The city and state, e.g. San Francisco, CA" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
// Step 2: Create a server implementation
const getWeatherServer = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${
unit || "fahrenheit"
}`
);
const data = await response.json();
return {
temperature: data.temperature,
conditions: data.conditions,
location: data.location,
};
});
JSON Schema 사용
JSON Schema를 선호하거나 기존 스키마 정의가 있는 경우 다음과 같이 합니다.
import { toolDefinition } from "@tanstack/ai";
import type { JSONSchema } from "@tanstack/ai";
// Define schemas using JSON Schema
const inputSchema: JSONSchema = {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location"],
};
const outputSchema: JSONSchema = {
type: "object",
properties: {
temperature: { type: "number" },
conditions: { type: "string" },
location: { type: "string" },
},
required: ["temperature", "conditions", "location"],
};
// Create the tool definition
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema,
outputSchema,
});
// With a raw JSON Schema, `args` is `unknown` — narrow it before use
// (prefer a Zod schema for automatic typing).
const getWeatherServer = getWeatherDef.server(async (args) => {
if (typeof args !== "object" || args === null || !("location" in args)) {
throw new Error("Invalid input: expected a location");
}
const location = String(args.location);
const unit = "unit" in args ? String(args.unit) : "fahrenheit";
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${unit}`
);
return await response.json();
});
채팅에서 도구 사용
서버 측
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().meta({ description: "The city and state, e.g. San Francisco, CA" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
export async function POST(request: Request) {
const { messages } = await request.json();
// Create server implementation
const getWeather = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(`https://api.weather.com/v1/current?...`);
return await response.json();
});
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [getWeather], // Pass server tools
});
return toServerSentEventsResponse(stream);
}
타입 안전성을 갖춘 클라이언트 측
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Update the UI with a notification message",
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ success: z.boolean() }),
});
const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save data to storage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
outputSchema: z.object({ saved: z.boolean() }),
});
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// Update UI state
console.log(input.message);
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = [updateUI, saveToStorage];
const textOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof textOptions>;
function ChatComponent() {
const { messages } = useChat(textOptions);
// messages is now fully typed with tool names and outputs!
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.role}</div>
))}
</div>
);
}
하이브리드 도구
도구는 서버와 클라이언트 모두에 구현될 수 있어 유연한 실행 패턴을 지원합니다:
import { toolDefinition, chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { db } from "./db";
// Define once
const addToCartDef = toolDefinition({
name: "add_to_cart",
description: "Add item to shopping cart",
inputSchema: z.object({
itemId: z.string(),
quantity: z.number(),
}),
outputSchema: z.object({
success: z.boolean(),
cartId: z.string(),
}),
needsApproval: true,
});
// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
const cart = await db.carts.create({
data: { itemId: input.itemId, quantity: input.quantity },
});
return { success: true, cartId: cart.id };
});
// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist.push(input.itemId);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
return { success: true, cartId: "local" };
});
서버에서는 정의(클라이언트 실행용) 또는 서버 구현을 각각 별도의 chat() 호출로 전달합니다.
const messages = [{ role: 'user' as const, content: 'Add item abc to my cart' }]
// Pass the definition: the client will execute the tool
chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [addToCartDef],
});
// Or pass the server implementation: the server will execute the tool
chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [addToCartServer],
});
타입 안전성의 이점
동형 아키텍처는 완전한 타입 안전성을 제공합니다.
import { useChat } from "@tanstack/ai-react";
import { fetchServerSentEvents } from "@tanstack/ai-client";
function CartChat() {
const { messages: uiMessages } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
// In your React component
uiMessages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === 'tool-call' && part.name === 'add_to_cart') {
// ✅ TypeScript knows part.name is literally 'add_to_cart'
// ✅ part.input is typed as { itemId: string, quantity: number }
// ✅ part.output is typed as { success: boolean, cartId: string } | undefined
if (part.output) {
console.log(part.output.cartId); // ✅ Fully typed!
}
}
});
});
return null;
}
도구 실행 흐름
- 모델이 도구 호출을 결정합니다 - 사용자 입력과 도구 설명을 기반으로 결정합니다.
- 도구가 식별됩니다 - 서버 또는 클라이언트 구현을 확인합니다.
- 도구가 실행됩니다 - 서버 또는 클라이언트에서 자동으로 실행됩니다.
- 결과가 반환됩니다 - 도구 결과 메시지로 모델에 반환됩니다.
- 모델이 계속 진행합니다 - 결과를 사용해 응답을 생성합니다.
진행 이벤트와 런타임 컨텍스트
서버 도구의 .server() 구현은 두 번째 인자로 ToolExecutionContext인 { context, toolCallId, emitCustomEvent }를 받습니다. 도구가 실행되는 동안 타입이 지정된 진행 상황을 클라이언트로 스트리밍하려면 emitCustomEvent를 사용하고, 요청 범위의 의존성(auth, DB 클라이언트 등)을 읽으려면 context를 사용합니다.
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
type ImportContext = {
db: {
read(source: string): Promise<unknown[]>;
write(rows: unknown[]): Promise<void>;
};
};
const importDataDef = toolDefinition({
name: "import_data",
description: "Import data from a source",
inputSchema: z.object({ source: z.string() }),
outputSchema: z.object({ imported: z.number() }),
});
const importData = importDataDef.server<ImportContext>(async (input, { context, emitCustomEvent }) => {
emitCustomEvent("progress", { step: 1, total: 3 });
const rows = await context.db.read(input.source);
emitCustomEvent("progress", { step: 2, total: 3 });
await context.db.write(rows);
emitCustomEvent("progress", { step: 3, total: 3 });
return { imported: rows.length };
});
서버 도구에 대한 전체 런타임 컨텍스트 패턴을 참조하세요.
도구 상태
도구는 실행 중 여러 상태를 거칩니다.
awaiting-input- 도구 호출을 받고 인자를 기다리는 중입니다.input-streaming- 일부 인자가 스트리밍되는 중입니다.input-complete- 모든 인자를 받았습니다.approval-requested- 도구에 사용자 승인이 필요합니다(needsApproval: true인 경우).approval-responded- 사용자가 승인하거나 거부했습니다.
인자와 필요한 경우 승인을 받으면 결과가 도구 호출 부분의 part.output으로 나타나며, state가 complete 또는 error인 별도의 형제 tool-result 부분으로도 나타납니다. 전체 상태 모델은 도구 아키텍처를 참조하세요.
팁: 사용 사례에 필터링, 집계, 병렬 호출과 같은 복잡한 로직으로 여러 도구를 호출하는 작업이 포함된다면 Code Mode를 고려하세요. 한 번에 하나의 도구를 호출하는 대신 LLM이 단일 실행에서 도구를 오케스트레이션하는 TypeScript 프로그램을 작성할 수 있습니다.