์‹œ๋ฆฌ์ฆˆ: LLM Tool Calling ๋‚ด๋ถ€ ์›๋ฆฌ๋ถ€ํ„ฐ ์—์ด์ „ํŠธ ์ง์ ‘ ๊ตฌํ˜„๊นŒ์ง€

์ด ์‹œ๋ฆฌ์ฆˆ๋Š” ์‚ฌ์šฉ์ž์˜ ์ž์—ฐ์–ด ํ•œ ์ค„์ด tool ์‹คํ–‰์œผ๋กœ ๋ฐ”๋€Œ๋Š” ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ํ•ด๋ถ€ํ•˜๊ณ , ์ตœ์ข…์ ์œผ๋กœ ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ + ์ž์ฒด middleware๋กœ ๋‚˜๋งŒ์˜ ์—์ด์ „ํŠธ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๊นŒ์ง€ ๋„๋‹ฌํ•˜๋Š” ๊ณผ์ •์ด๋‹ค.

ํŽธ๋‚ด์šฉํ•ต์‹ฌ
1ํŽธ (๋ณธ๋ฌธ)์ „์ฒด ์กฐ๊ฐ๋„์ž์—ฐ์–ด โ†’ tool ์‹คํ–‰๊นŒ์ง€ 5๊ฐœ ๋ ˆ์ด์–ด์˜ ์กด์žฌ๋ฅผ ํ™•์ธ
2ํŽธChat TemplateJSON์ด ๋ชจ๋ธ์— ์ง์ ‘ ๋“ค์–ด๊ฐ€์ง€ ์•Š๋Š”๋‹ค
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 TemplateAPI JSON์„ ๋ชจ๋ธ์ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ํ•˜๋‚˜์˜ ๊ธด ํ…์ŠคํŠธ๋กœ ์กฐ๋ฆฝ2ํŽธ
Tokenizationํ…์ŠคํŠธ๋ฅผ ์ˆซ์ž(ํ† ํฐ ID)๋กœ ๋ณ€ํ™˜. ๋ชจ๋ธ์€ ํ…์ŠคํŠธ๋ฅผ ์ง์ ‘ ์ฝ์ง€ ๋ชปํ•œ๋‹ค3ํŽธ
๋ชจ๋ธ ์ถ”๋ก ํ† ํฐ ์‹œํ€€์Šค๋ฅผ ๋ณด๊ณ  โ€œtool์„ ์“ธ๊นŒ ๋ง๊นŒโ€ ํŒ๋‹จ. ๋‹ค์Œ ํ† ํฐ ์˜ˆ์ธก์˜ ๋ฐ˜๋ณต4ํŽธ
Constrained Decodingtool call์˜ ์ธ์ž๊ฐ€ ํ•ญ์ƒ valid JSON์ด ๋˜๋„๋ก ์ƒ์„ฑ์„ ์ œํ•œ4ํŽธ
์ถœ๋ ฅ ํŒŒ์‹ฑ๋ชจ๋ธ์ด ์ƒ์„ฑํ•œ ํ† ํฐ์—์„œ tool call ํŒจํ„ด์„ ๊ฐ์ง€ํ•˜๊ณ  JSON์œผ๋กœ ๋ณ€ํ™˜4ํŽธ

tool calling์˜ ์ „์ฒด ๋ผ์ดํ”„์‚ฌ์ดํด

Anthropic ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์„ค๋ช…ํ•˜๋Š” tool use์˜ ๋ผ์ดํ”„์‚ฌ์ดํด์€ 4๋‹จ๊ณ„๋‹ค:

  1. tool ์ •์˜์™€ ์‚ฌ์šฉ์ž ํ”„๋กฌํ”„ํŠธ ์ œ๊ณต - tool์˜ ์ด๋ฆ„, ์„ค๋ช…, input schema๋ฅผ JSON์œผ๋กœ ์ •์˜ํ•˜์—ฌ API์— ์ „๋‹ฌ
  2. ๋ชจ๋ธ์ด tool ์‚ฌ์šฉ์„ ๊ฒฐ์ • - ๋ชจ๋ธ์ด ์‚ฌ์šฉ์ž ์ฟผ๋ฆฌ์— ๋„์›€์ด ๋˜๋Š” tool์ด ์žˆ๋Š”์ง€ ํ‰๊ฐ€ํ•˜๊ณ , ์žˆ์œผ๋ฉด tool use ์š”์ฒญ์„ ๊ตฌ์„ฑ. ์‘๋‹ต์˜ stop_reason์ด tool_use๋กœ ์„ค์ •๋œ๋‹ค
  3. tool ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ - ์‘๋‹ต์—์„œ tool ์ด๋ฆ„๊ณผ input์„ ์ถ”์ถœํ•˜์—ฌ ์‹ค์ œ๋กœ ์‹คํ–‰. ๊ฒฐ๊ณผ๋ฅผ tool_result๋กœ ๋ชจ๋ธ์— ๋ฐ˜ํ™˜
  4. ๋ชจ๋ธ์ด ์ตœ์ข… ์‘๋‹ต ์ƒ์„ฑ - 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 ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•