์๋ฆฌ์ฆ: 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 = ์์ด์ ํธ ์์ฑ |
- LLM Tool Calling ํ์ดํ๋ผ์ธ์ ์ฌ์ฉ์์ ์์ฐ์ด ํ ์ค์ด ์ค์ ํจ์ ์คํ์ผ๋ก ๋ฐ๋๋ ์ ์ฒด ๋ด๋ถ ์ฒ๋ฆฌ ๊ณผ์
- LLM์ด ํจ์๋ฅผ ์ง์ ์คํํ์ง ์๊ณ , ํธ์ถ์ ๊ธฐ์ ํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์์ฑํ๋ฉด ๋ณ๋ ํ๋ก๊ทธ๋จ์ด ์คํํ๋ ๊ตฌ์กฐ์ ๋ถ๋ฆฌ
- API JSON ํต์ ์ด๋ฉด์ ์จ์ด์๋ 5๊ฐ ์ฒ๋ฆฌ ๋ ์ด์ด(Chat Template, Tokenization, ๋ชจ๋ธ ์ถ๋ก , Constrained Decoding, ์ถ๋ ฅ ํ์ฑ)์ ์กฐ๊ฐ๋
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- ๊ฐ๋ฐ์๊ฐ MCP server์ tool์ ๋ฑ๋กํ๋ฉด LLM์ด ์์์ ๊ณจ๋ผ ์ด๋ค. ํ์ง๋ง ์ด๋ป๊ฒ ๊ณ ๋ฅด๋์ง๋ ๋ธ๋๋ฐ์ค๋ค
- Claude Code์์ ํ์ผ์ ์ฝ์ ๋ Read, Bash, Grep ์ค ํ๋๋ฅผ ์ ํํ๋ ํ๋จ ๊ณผ์ ์ด ๋ด๋ถ์์ ์ผ์ด๋์ง๋ง ๋ณด์ด์ง ์๋๋ค
- ์ด ๋ธ๋๋ฐ์ค๋ฅผ ์ด์ด์ผ tool ์ ์๋ฅผ ์ ์์ฑํ๊ณ , ๋๋ฒ๊น ํ ์ ์๊ณ , ๋์๊ฐ ์์ฒด ์์ด์ ํธ๋ฅผ ๋ง๋ค ์ ์๋ค
AS-IS
sequenceDiagram autonumber box ์ฌ์ฉ์ ์์ญ participant User as ์ฌ์ฉ์ end box AI ์๋น์ค ์์ญ (Claude, GPT ๋ฑ) participant API as API ์๋ฒ participant LLM as LLM end User->>API: "์์ธ ๋ ์จ ์๋ ค์ค" + tools ์ ์ (JSON) Note over API,LLM: ??? ๋ธ๋๋ฐ์ค ??? LLM-->>API: tool_use: get_weather("Seoul") API-->>User: tool call ๊ฒฐ๊ณผ (JSON)
TO-BE
sequenceDiagram autonumber box ์ฌ์ฉ์ ์์ญ participant User as ์ฌ์ฉ์ end box AI ์๋น์ค ์์ญ (Claude, GPT ๋ฑ) participant API as API ์๋ฒ participant CT as Chat Template participant TK as Tokenizer participant Model as ๋ชจ๋ธ ์ถ๋ก participant Parser as ์ถ๋ ฅ ํ์ end User->>API: "์์ธ ๋ ์จ ์๋ ค์ค" + tools ์ ์ (JSON) API->>CT: JSON โ ํ๋กฌํํธ ํ ์คํธ ์กฐ๋ฆฝ CT->>TK: ํ ์คํธ โ ํ ํฐ ID ์ํ์ค TK->>Model: ํ ํฐ ์ํ์ค ์ ๋ ฅ Model->>Model: ๋ค์ ํ ํฐ ์์ธก ๋ฐ๋ณต Model->>Parser: tool_call ํจํด ํ ํฐ ์์ฑ Parser->>API: ํ ํฐ โ structured JSON ๋ณํ API-->>User: tool_use: get_weather("Seoul")
๊ฐ๋ฐ์๊ฐ ๋งค์ผ ๊ฒฝํํ๋ tool calling
์ฐ๋ฆฌ๋ ์ด๋ฏธ tool calling์ ๋งค์ผ ์ฌ์ฉํ๊ณ ์๋ค.
Claude Code์์ ํ์ผ์ ์ฝ์ ๋:
// Claude Code๊ฐ ๋ด๋ถ์ ์ผ๋ก ํ๋ ์ผ
const response = await anthropic.messages.create({
model: "claude-opus-4-6",
tools: [
{ name: "Read", description: "Read a file", input_schema: { ... } },
{ name: "Bash", description: "Execute a bash command", input_schema: { ... } },
{ name: "Grep", description: "Search file contents", input_schema: { ... } },
],
messages: [{ role: "user", content: "src/index.ts ํ์ผ ๋ด์ฉ ๋ณด์ฌ์ค" }],
});
// response.content โ { type: "tool_use", name: "Read", input: { file_path: "src/index.ts" } }MCP server์์ tool์ ๋ฑ๋กํ ๋:
// MCP server์์ tool ์ ์๋ฅผ ๊ฐ์ ธ์ Claude API์ ์ ๋ฌ
const mcpTools = await mcpClient.listTools();
const claudeTools = mcpTools.tools.map((tool) => ({
name: tool.name,
description: tool.description ?? "",
input_schema: tool.inputSchema, // inputSchema โ input_schema ๋ณํ
}));๋ ๊ฒฝ์ฐ ๋ชจ๋ ๋์ผํ ํ์ดํ๋ผ์ธ์ ํ๋ค. tool์ด ์ด๋์ ์ ์๋์๋ (์ฝ๋ ๋ด์ฅ, MCP server, ์ธ๋ถ API), LLM ๋ด๋ถ์ ์ฒ๋ฆฌ ๊ณผ์ ์ ๊ฐ๋ค.
๋ธ๋๋ฐ์ค ์์ 5๊ฐ ๋ ์ด์ด
API JSON์ด ๋ชจ๋ธ์ ๋ค์ด๊ฐ์ tool call์ด ๋์ค๊ธฐ๊น์ง, ๋ด๋ถ์๋ 5๊ฐ์ ์ฒ๋ฆฌ ๋ ์ด์ด๊ฐ ์กด์ฌํ๋ค.
flowchart LR A[Chat Template] --> B[Tokenization] --> C[๋ชจ๋ธ ์ถ๋ก ] --> D[Constrained Decoding] --> E[์ถ๋ ฅ ํ์ฑ]
| ๋ ์ด์ด | ํ๋ ์ผ | ํธ |
|---|---|---|
| Chat Template | API JSON์ ๋ชจ๋ธ์ด ์ดํดํ ์ ์๋ ํ๋์ ๊ธด ํ ์คํธ๋ก ์กฐ๋ฆฝ | 2ํธ |
| Tokenization | ํ ์คํธ๋ฅผ ์ซ์(ํ ํฐ ID)๋ก ๋ณํ. ๋ชจ๋ธ์ ํ ์คํธ๋ฅผ ์ง์ ์ฝ์ง ๋ชปํ๋ค | 3ํธ |
| ๋ชจ๋ธ ์ถ๋ก | ํ ํฐ ์ํ์ค๋ฅผ ๋ณด๊ณ โtool์ ์ธ๊น ๋ง๊นโ ํ๋จ. ๋ค์ ํ ํฐ ์์ธก์ ๋ฐ๋ณต | 4ํธ |
| Constrained Decoding | tool call์ ์ธ์๊ฐ ํญ์ valid JSON์ด ๋๋๋ก ์์ฑ์ ์ ํ | 4ํธ |
| ์ถ๋ ฅ ํ์ฑ | ๋ชจ๋ธ์ด ์์ฑํ ํ ํฐ์์ tool call ํจํด์ ๊ฐ์งํ๊ณ JSON์ผ๋ก ๋ณํ | 4ํธ |
tool calling์ ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด
Anthropic ๊ณต์ ๋ฌธ์์์ ์ค๋ช ํ๋ tool use์ ๋ผ์ดํ์ฌ์ดํด์ 4๋จ๊ณ๋ค:
- tool ์ ์์ ์ฌ์ฉ์ ํ๋กฌํํธ ์ ๊ณต - tool์ ์ด๋ฆ, ์ค๋ช , input schema๋ฅผ JSON์ผ๋ก ์ ์ํ์ฌ API์ ์ ๋ฌ
- ๋ชจ๋ธ์ด tool ์ฌ์ฉ์ ๊ฒฐ์ - ๋ชจ๋ธ์ด ์ฌ์ฉ์ ์ฟผ๋ฆฌ์ ๋์์ด ๋๋ tool์ด ์๋์ง ํ๊ฐํ๊ณ , ์์ผ๋ฉด tool use ์์ฒญ์ ๊ตฌ์ฑ. ์๋ต์
stop_reason์ดtool_use๋ก ์ค์ ๋๋ค - tool ์คํ ๋ฐ ๊ฒฐ๊ณผ ๋ฐํ - ์๋ต์์ tool ์ด๋ฆ๊ณผ input์ ์ถ์ถํ์ฌ ์ค์ ๋ก ์คํ. ๊ฒฐ๊ณผ๋ฅผ
tool_result๋ก ๋ชจ๋ธ์ ๋ฐํ - ๋ชจ๋ธ์ด ์ต์ข ์๋ต ์์ฑ - tool ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํ์ฌ ์ฌ์ฉ์์๊ฒ ์์ฐ์ด ์๋ต์ ์์ฑ
sequenceDiagram autonumber participant App as ์ ํ๋ฆฌ์ผ์ด์ participant Claude as Claude API App->>Claude: tools ์ ์ + "์์ธ ๋ ์จ ์๋ ค์ค" Claude-->>App: stop_reason: "tool_use"<br/>name: "get_weather"<br/>input: { location: "Seoul" } Note over App: get_weather("Seoul") ์คํ App->>Claude: tool_result: "15ยฐC, ๋ง์" Claude-->>App: "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค"
ํต์ฌ์ LLM์ด tool์ ์ง์ ์คํํ์ง ์๋๋ค๋ ๊ฒ์ด๋ค. LLM์ โ์ด๋ค tool์, ์ด๋ค ์ธ์๋ก ํธ์ถํด์ผ ํ๋์งโ๋ฅผ ๊ธฐ์ ํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค ๋ฟ์ด๊ณ , ์ค์ ์คํ์ ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์์ ํ๋ค.
๋ค์ ํธ: API JSON์ด ๋ชจ๋ธ์ ์ด๋ป๊ฒ ์ ๋ฌ๋๋ ๊ฑฐ์ง?
์ด ๊ธ์์ 5๊ฐ ๋ ์ด์ด์ ์กด์ฌ๋ฅผ ํ์ธํ๋ค. ๊ทธ๋ฐ๋ฐ ์ฒซ ๋ฒ์งธ ๋ ์ด์ด๋ถํฐ ์๋ฌธ์ด ๋ ๋ค.
์ฐ๋ฆฌ๊ฐ API์ ๋ณด๋ด๋ ๊ฒ์ JSON์ด๋ค:
{
"tools": [{ "name": "get_weather", "input_schema": { ... } }],
"messages": [{ "role": "user", "content": "์์ธ ๋ ์จ ์๋ ค์ค" }]
}ํ์ง๋ง LLM์ JSON ํ์๊ฐ ์๋๋ค. ์ด JSON์ด ๋ชจ๋ธ์ ์ง์ ๋ค์ด๊ฐ๋ ๊ฑธ๊น?
์๋๋ค. ๋ค์ ํธ์์ ์ด JSON์ด ์ด๋ค ํํ์ ํ ์คํธ๋ก ๋ณํ๋์ด ๋ชจ๋ธ์ ์ ๋ฌ๋๋์ง, ๊ทธ๋ฆฌ๊ณ ๋ฒค๋๋ง๋ค ๊ทธ ํํ๊ฐ ์ด๋ป๊ฒ ๋ค๋ฅธ์ง๋ฅผ ์ดํด๋ณธ๋ค.
์ฐธ๊ณ ๋ฌธ์
- Anthropic - Tool Use Overview - Claude tool use ๊ณต์ ๋ฌธ์, ๋ผ์ดํ์ฌ์ดํด 4๋จ๊ณ ์ค๋ช
- Martin Fowler - Function Calling using LLMs - LLM์ ํจ์๋ฅผ ์คํํ์ง ์๊ณ ํธ์ถ์ ๊ธฐ์ ํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์์ฑํ๋ค๋ ํต์ฌ ๊ฐ๋
- MCP - Build an MCP Client - MCP tool์ Claude API ํ์์ผ๋ก ๋ณํํ๋ ๋ฐฉ๋ฒ