์๋ฆฌ์ฆ: LLM Tool Calling ๋ด๋ถ ์๋ฆฌ๋ถํฐ ์์ด์ ํธ ์ง์ ๊ตฌํ๊น์ง
์ด ์๋ฆฌ์ฆ๋ ์ฌ์ฉ์์ ์์ฐ์ด ํ ์ค์ด tool ์คํ์ผ๋ก ๋ฐ๋๋ ๋ด๋ถ ์ฒ๋ฆฌ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ํด๋ถํ๊ณ , ์ต์ข ์ ์ผ๋ก ์คํ์์ค ๋ชจ๋ธ + ์์ฒด middleware๋ก ๋๋ง์ ์์ด์ ํธ๋ฅผ ์ง์ ๊ตฌํํ๋ ๊ฒ๊น์ง ๋๋ฌํ๋ ๊ณผ์ ์ด๋ค.
| ํธ | ๋ด์ฉ | ํต์ฌ |
|---|---|---|
| 1ํธ | ์ ์ฒด ์กฐ๊ฐ๋ | ์์ฐ์ด โ tool ์คํ๊น์ง 5๊ฐ ๋ ์ด์ด์ ์กด์ฌ๋ฅผ ํ์ธ |
| 2ํธ | Chat Template | JSON์ด ๋ชจ๋ธ์ ์ง์ ๋ค์ด๊ฐ์ง ์๋๋ค |
| 3ํธ | Tokenization | ๋ชจ๋ธ์ ํ ์คํธ๋ฅผ ์ฝ์ง ๋ชปํ๋ค - ํ ํฐ ID์ control token |
| 4ํธ | ๋ชจ๋ธ ์ถ๋ก | โtool์ ์ธ๊น ๋ง๊นโ ํ๋จ๊ณผ constrained decoding |
| 5ํธ (๋ณธ๋ฌธ) | Tool ์คํ | tool_use๋ฅผ ๋ฐ์ ํด๋ผ์ด์ธํธ์ ์คํ ๋ฃจํ |
| 6ํธ | Native vs Non-native | ๊ฐ์ ๊ธฐ๋ฅ, ๋ค๋ฅธ ๊ตฌ์กฐ โ Middleware |
| 7ํธ | Middleware ๋ง๋ค๊ธฐ | ํ๋กฌํํธ ์กฐ๋ฆฝ + ์ถ๋ ฅ ํ์ฑ + ์คํ ๋ฃจํ ์ง์ ๊ตฌํ |
| 8ํธ | ์คํ์์ค ๋ชจ๋ธ ๋ก์ปฌ ๊ตฌ์ถ | Ollama/vLLM์ผ๋ก ๋ก์ปฌ LLM ์๋น |
| 9ํธ | ๋๋ง์ ์์ด์ ํธ | ๋ชจ๋ธ + Middleware = ์์ด์ ํธ ์์ฑ |
- Tool ์คํ์ ๋ชจ๋ธ์ด ๋ฐํํ
tool_use์๋ต์ ํด๋ผ์ด์ธํธ(๊ฐ๋ฐ์์ ์ ํ๋ฆฌ์ผ์ด์ )๊ฐ ๋ฐ์ ์ค์ ํจ์๋ฅผ ์คํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ๋ชจ๋ธ์ ์ ๋ฌํ๋ ๊ณผ์ - LLM์ tool์ ์ง์ ์คํํ์ง ์์ผ๋ฉฐ, โ์ด๋ค tool์, ์ด๋ค ์ธ์๋ก ํธ์ถํด์ผ ํ๋์งโ๋ฅผ ๊ธฐ์ ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ฐํํ ๋ฟ์ธ ๊ตฌ์กฐ์ ๋ถ๋ฆฌ
- ์ด ๊ณผ์ ์ ํ ๋ฒ์ผ๋ก ๋๋์ง ์๊ณ , ๋ชจ๋ธ์ด ์ถ๊ฐ tool call์ ์์ฒญํ๋ฉด ๋ฐ๋ณต๋๋ ์คํ ๋ฃจํ
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- 4ํธ์์ ๋ชจ๋ธ์ด
tool_use: get_weather("Seoul")์ ์์ฑํ๋ ๊ณผ์ ๊น์ง ํ์ธํ๋ค. ํ์ง๋ง ์ด ์๋ต์ด ์ฌ์ฉ์์ PC์ ๋์ฐฉํ ํ ๋๊ฐ, ์ด๋์, ์ด๋ป๊ฒ ์ค์ ๋ก ์คํํ๋์ง๋ ์์ง ๋ค๋ฃจ์ง ์์๋ค - Claude Code์์ ํ์ผ์ ์ฝ๊ฑฐ๋, MCP server์์ tool์ ์คํํ๋ ๊ฒ์ด ๋ชจ๋ ์ด ๋จ๊ณ์์ ์ผ์ด๋๋ค
- ์ด ์คํ ๋ฃจํ๋ฅผ ์ดํดํด์ผ 6ํธ์์ โ์ด ๋ ์ด์ด๊ฐ ์๋ ๋ชจ๋ธโ์ ์๋ฏธ๋ฅผ ์ ํํ ํ์ ํ ์ ์๋ค
AS-IS
sequenceDiagram autonumber box ๊ฐ๋ฐ์ ์์ญ participant App as ์ ํ๋ฆฌ์ผ์ด์ end box AI ์๋น์ค ์์ญ participant LLM as LLM end App->>LLM: "์์ธ ๋ ์จ ์๋ ค์ค" LLM-->>App: tool_use: get_weather("Seoul") Note over App: tool์ ์คํํด์ tool_result๋ฅผ<br/>์ด๋ป๊ฒ ๋ชจ๋ธ์ ๋๋ ค์ฃผ์ง?
TO-BE
sequenceDiagram autonumber box ๊ฐ๋ฐ์ ์์ญ participant App as ์ ํ๋ฆฌ์ผ์ด์ participant Tool as tool ํจ์ end box AI ์๋น์ค ์์ญ participant LLM as LLM end App->>LLM: "์์ธ ๋ ์จ ์๋ ค์ค" LLM-->>App: stop_reason: "tool_use"<br/>name: "get_weather", input: {location: "Seoul"} App->>App: tool ์ด๋ฆ์ผ๋ก ํจ์ ๋งคํ App->>Tool: get_weather("Seoul") ์คํ Tool-->>App: { temperature: "15ยฐC", condition: "๋ง์" } App->>LLM: tool_result ์ ๋ฌ LLM-->>App: "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค"
tool_use ์๋ต์ ๊ตฌ์กฐ
4ํธ์์ ๋ชจ๋ธ์ด ์์ฑํ tool_use ์๋ต์ ์ด๋ฐ ํํ๋ค:
{
"id": "msg_01Aq9w938a90dw8q",
"stop_reason": "tool_use",
"content": [
{
"type": "text",
"text": "์์ธ์ ๋ ์จ๋ฅผ ํ์ธํด๋ณด๊ฒ ์ต๋๋ค."
},
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "Seoul", "unit": "celsius" }
}
]
}ํต์ฌ ํ๋ 3๊ฐ์ง:
name: ์ด๋ค tool์ ํธ์ถํ ์งid: ์ด ํธ์ถ์ ๊ณ ์ ์๋ณ์ (๊ฒฐ๊ณผ ๋ฐํ ์ ๋งค์นญ์ฉ)input: tool์ ์ ๋ฌํ ์ธ์
stop_reason: "tool_use"๊ฐ ๋ฐํ๋์๋ค๋ ๊ฒ์, ๋ชจ๋ธ์ด โ๋๋ ์ฌ๊ธฐ์ ๋ฉ์ถ๊ณ , ์ด tool์ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ฒ ๋คโ๋ ์๋ฏธ๋ค.
๋ชจ๋ tool์ด ํด๋ผ์ด์ธํธ์์ ์คํ๋๋ ๊ฒ์ ์๋๋ค
์ฌ๊ธฐ์ ์ค์ํ ๊ตฌ๋ถ์ด ์๋ค. Claude API์์ tool์ ์คํ ์์น์ ๋ฐ๋ผ ๋ ์ข ๋ฅ๋ก ๋๋๋ค:
| Client Tool | Server Tool | |
|---|---|---|
| ์คํ ์์น | ๊ฐ๋ฐ์์ ์ ํ๋ฆฌ์ผ์ด์ (ํด๋ผ์ด์ธํธ) | Anthropic ์๋ฒ |
| ๋๊ฐ ๋ง๋๋๊ฐ | ๊ฐ๋ฐ์๊ฐ ์ง์ ์ ์ + ์คํ ํจ์ ๊ตฌํ | Anthropic์ด ๋ฏธ๋ฆฌ ๊ตฌํ |
| ์์ | get_weather, Read, Bash, MCP tool | web_search, web_fetch |
| ๋์ ๋ฐฉ์ | stop_reason: "tool_use" ๋ฐํ โ ํด๋ผ์ด์ธํธ๊ฐ ์คํ โ tool_result ๋ฐํ | ๋ชจ๋ธ์ด ํธ์ถํ๋ฉด ์๋ฒ๊ฐ ์๋ ์คํ, ๊ฒฐ๊ณผ๊ฐ ์๋ต์ ํฌํจ |
| ๊ฐ๋ฐ์ ์ญํ | tool ์ ์ + ์คํ ํจ์ ๊ตฌํ | API ์์ฒญ์ tool ํ์ ๋ง ๋ช ์ |
์ฆ, Claude์๊ฒ โ์์ธ ๋ ์จ ๊ฒ์ํด์คโ๋ผ๊ณ ํ๋ฉด:
web_search๋ Anthropic ์๋ฒ์์ ์คํ๋๋ค. ๊ฐ๋ฐ์๊ฐ ๊ฒ์ ํจ์๋ฅผ ๊ตฌํํ ํ์ ์์ด, ์๋ฒ๊ฐ ์์์ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์๋ต์ ํฌํจํ๋ค.get_weather๊ฐ์ ์ปค์คํ tool์ ํด๋ผ์ด์ธํธ์์ ์คํ๋๋ค. ๊ฐ๋ฐ์๊ฐ ์คํ ํจ์๋ฅผ ์ง์ ๊ตฌํํด์ผ ํ๋ค.
Claude Code์์ Read, Bash, Grep ๊ฐ์ tool์? ์ด๊ฒ๋ค์ client tool์ด๋ค. Claude Code ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ด tool๋ค์ ์คํ ํจ์๋ฅผ ๋ฏธ๋ฆฌ ๊ตฌํํด๋ ๊ฒ์ด๋ค. ์ฌ์ฉ์๊ฐ ์ง์ ๋ง๋ ๊ฒ์ ์๋์ง๋ง, ์คํ์ ์ฌ์ฉ์์ PC(Claude Code ํ๋ก์ธ์ค)์์ ์ด๋ฃจ์ด์ง๋ค.
ํด๋ผ์ด์ธํธ์ tool ์คํ ๋ฃจํ (Client Tool)
tool_use ์๋ต์ ๋ฐ์ ํด๋ผ์ด์ธํธ๋ ๋ค์ ๊ณผ์ ์ ์ํํ๋ค:
1๋จ๊ณ: tool ์ด๋ฆ์ผ๋ก ํจ์ ๋งคํ
// tool ์ด๋ฆ โ ์ค์ ํจ์ ๋งคํ
const tools: Record<string, (input: any) => Promise<string>> = {
get_weather: async ({ location, unit }) => {
const res = await fetch(`https://api.weather.com?city=${location}&unit=${unit}`);
return JSON.stringify(await res.json());
},
get_time: async ({ timezone }) => {
return new Date().toLocaleString("ko-KR", { timeZone: timezone });
},
};2๋จ๊ณ: tool ์คํ
// ์๋ต์์ tool_use ๋ธ๋ก ์ถ์ถ
const toolUseBlock = response.content.find((block) => block.type === "tool_use");
// ๋งคํ๋ ํจ์ ์คํ
const toolFunction = tools[toolUseBlock.name];
const result = await toolFunction(toolUseBlock.input);3๋จ๊ณ: tool_result๋ฅผ ๋ชจ๋ธ์ ๋ฐํ
// tool ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ๋ชจ๋ธ์ ์ ๋ฌ
const followUp = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
tools: [/* ๊ฐ์ tool ์ ์ */],
messages: [
// ๊ธฐ์กด ๋ํ ์ด๋ ฅ
{ role: "user", content: "์์ธ ๋ ์จ ์๋ ค์ค" },
// ๋ชจ๋ธ์ tool_use ์๋ต (๊ทธ๋๋ก ํฌํจ)
{ role: "assistant", content: response.content },
// tool ์คํ ๊ฒฐ๊ณผ
{
role: "user",
content: [{
type: "tool_result",
tool_use_id: toolUseBlock.id, // tool_use์ id์ ๋งค์นญ
content: result,
}],
},
],
});
// followUp โ "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค"tool_use_id๋ก ์ด๋ค tool call์ ๋ํ ๊ฒฐ๊ณผ์ธ์ง ๋งค์นญํ๋ค. 3ํธ์์ ๋ณธ Mistral V3์ call_id์ ๋์ผํ ์ญํ ์ด๋ค.
๋ฉํฐํด Tool Calling
tool ์คํ์ ํ ๋ฒ์ผ๋ก ๋๋์ง ์์ ์ ์๋ค. ๋ชจ๋ธ์ด ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ํ ์ถ๊ฐ tool call์ ์์ฒญํ ์ ์๋ค:
sequenceDiagram autonumber box ๊ฐ๋ฐ์ ์์ญ participant App as ์ ํ๋ฆฌ์ผ์ด์ end box AI ์๋น์ค ์์ญ participant LLM as LLM end App->>LLM: "์์ธ๊ณผ ๋์ฟ ๋ ์จ ๋น๊ตํด์ค" LLM-->>App: tool_use: get_weather("Seoul") Note over App: get_weather("Seoul") ์คํ App->>LLM: tool_result: "15ยฐC, ๋ง์" LLM-->>App: tool_use: get_weather("Tokyo") Note over App: get_weather("Tokyo") ์คํ App->>LLM: tool_result: "22ยฐC, ํ๋ฆผ" LLM-->>App: stop_reason: "end_turn"<br/>"์์ธ์ 15ยฐC, ๋์ฟ๋ 22ยฐC. ๋์ฟ๊ฐ 7๋ ๋ ๋ฐ๋ปํฉ๋๋ค."
์ด ๋ฃจํ๊ฐ ๋ฐ๋ณต๋๋ ์กฐ๊ฑด:
stop_reason === "tool_use"โ tool ์คํ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ์ ๋ฌstop_reason === "end_turn"โ ๋ชจ๋ธ์ด ์ต์ข ์๋ต์ ์๋ฃ, ๋ฃจํ ์ข ๋ฃ
SDK๊ฐ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ ์คํ ๋ฃจํ
์์ ์๋ ๋ฃจํ๋ฅผ ๋งค๋ฒ ์์ฑํ๋ ๊ฒ์ ๋ฒ๊ฑฐ๋กญ๋ค. Anthropic SDK์ Vercel AI SDK๋ ์ด ๊ณผ์ ์ ์๋ํํ๋ค.
Anthropic SDK - Tool Runner (beta)
import { Anthropic } from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const anthropic = new Anthropic();
// tool ์ ์ + ์คํ ํจ์๋ฅผ ํจ๊ป ๋ฑ๋ก
const getWeatherTool = betaZodTool({
name: "get_weather",
description: "Get the current weather in a given location",
inputSchema: z.object({
location: z.string().describe("City name"),
unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
run: async (input) => {
// ์ค์ API ํธ์ถ
return JSON.stringify({ temperature: "15ยฐC", condition: "Sunny" });
},
});
// toolRunner๊ฐ ์คํ ๋ฃจํ๋ฅผ ์๋ ์ฒ๋ฆฌ
const runner = anthropic.beta.messages.toolRunner({
model: "claude-opus-4-6",
max_tokens: 1024,
tools: [getWeatherTool],
messages: [{ role: "user", content: "์์ธ ๋ ์จ ์๋ ค์ค" }],
});
for await (const message of runner) {
console.log(message.content[0].text);
}Vercel AI SDK - generateText
import { generateText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { stepCountIs } from "ai";
const result = await generateText({
model: anthropic("claude-opus-4-6"),
tools: {
weather: tool({
description: "Get the weather in a location",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => ({
location,
temperature: 15,
condition: "๋ง์",
}),
}),
},
stopWhen: stepCountIs(5), // ์ต๋ 5๋จ๊ณ๊น์ง ์๋ ๋ฐ๋ณต
prompt: "์์ธ ๋ ์จ ์๋ ค์ค",
});๋ SDK ๋ชจ๋ ํต์ฌ์ ๊ฐ๋ค: tool ์ ์์ ์คํ ํจ์๋ฅผ ํจ๊ป ๋ฑ๋กํ๋ฉด, stop_reason: "tool_use" ๊ฐ์ง โ ํจ์ ์คํ โ ๊ฒฐ๊ณผ ๋ฐํ โ ์ถ๊ฐ ํธ์ถ ํ์ธ์ ๋ฃจํ๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ค.
MCP Server์์์ Tool ์คํ
Claude Code๋ Cursor ๊ฐ์ ํด๋ผ์ด์ธํธ์์ MCP server๋ฅผ ์ฌ์ฉํ๋ฉด, tool ์คํ์ด ํ ๋จ๊ณ ๋ ์์๋๋ค:
sequenceDiagram autonumber box ํด๋ผ์ด์ธํธ ์์ญ participant Client as Claude Code end box MCP Server ์์ญ participant MCP as MCP Server participant Tool as tool ํจ์ end box AI ์๋น์ค ์์ญ participant LLM as LLM end Client->>LLM: tools ์ ์ + ์ฌ์ฉ์ ์ง๋ฌธ LLM-->>Client: tool_use: get_weather("Seoul") Client->>MCP: call_tool("get_weather", {location: "Seoul"}) MCP->>Tool: ์ค์ ํจ์ ์คํ Tool-->>MCP: ๊ฒฐ๊ณผ MCP-->>Client: tool_result Client->>LLM: tool_result ์ ๋ฌ LLM-->>Client: ์ต์ข ์๋ต
ํด๋ผ์ด์ธํธ(Claude Code)๋ tool์ ์ง์ ์คํํ์ง ์๊ณ , MCP server์ ์คํ์ ์์ํ๋ค. ์ด๊ฒ์ด MCP์ ํต์ฌ ๊ฐ์น๋ค: tool ์ ์์ ์คํ์ ์ธ๋ถ ์๋ฒ๋ก ๋ถ๋ฆฌํ์ฌ, ํด๋ผ์ด์ธํธ๋ ํ๋กํ ์ฝ๋ง ์๋ฉด ๋๋ค.
๋ค์ ํธ: ์ด ๋ชจ๋ ๋ ์ด์ด๊ฐ ํญ์ ์๋ ๊ฑด๊ฐ?
1~5ํธ์ ๊ฑธ์ณ ์์ฐ์ด โ Chat Template โ Tokenization โ ๋ชจ๋ธ ์ถ๋ก โ Tool ์คํ์ ์ ์ฒด ํ์ดํ๋ผ์ธ์ ํ์ธํ๋ค. Claude, GPT ๊ฐ์ ์์ฉ ๋ชจ๋ธ API๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ๋ชจ๋ ๋ ์ด์ด๊ฐ ๋ด์ฅ๋์ด ์๋ค.
ํ์ง๋ง ์คํ์์ค ๋ชจ๋ธ(Llama, Gemma ๋ฑ)์ ๋ก์ปฌ์์ ์คํํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น? ์ด ๋ ์ด์ด๋ค์ด ์ ๋ถ ๋๋ ์ผ๋ถ ์กด์ฌํ์ง ์๋๋ค. ๊ทธ๋ฌ๋ฉด tool calling์ ์ด๋ป๊ฒ ๊ตฌํํ ๊น?
๋ค์ ํธ์์ Native tool calling ๋ชจ๋ธ๊ณผ Non-native ๋ชจ๋ธ์ ๊ตฌ์กฐ์ ์ฐจ์ด๋ฅผ ๋น๊ตํ๋ค.
์ฐธ๊ณ ๋ฌธ์
- Anthropic - Tool Use Implementation - tool_result ๋ฐํ, Tool Runner, ๋ฉํฐํด tool calling
- Vercel AI SDK - Tool Calling - generateText์ ์๋ tool ์คํ ๋ฃจํ, stopWhen
- MCP - Build an MCP Client - MCP ํ๋กํ ์ฝ์์์ tool ์คํ ์์