์๋ฆฌ์ฆ: 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 Calling Middleware๋ Non-native ๋ชจ๋ธ์ ์๋ tool calling ๋ ์ด์ด(Chat Template, ์ถ๋ ฅ ํ์, ์คํ ๋ฃจํ)๋ฅผ ์ธ๋ถ์์ ๊ตฌํํ๋ ์ํํธ์จ์ด ๊ณ์ธต
- tool ์ ์ โ ์์คํ ํ๋กฌํํธ ํ ์คํธ ๋ณํ, ๋ชจ๋ธ ์ถ๋ ฅ โ tool call ํจํด ๊ฐ์ง ๋ฐ JSON ์ถ์ถ, tool ์คํ โ ๊ฒฐ๊ณผ ํผ๋๋ฐฑ์ 3๊ฐ์ง ์ญํ ์ ๋ด๋นํ๋ ๋ณํ/์คํ ๋ฃจํ
- Hermes, XML ๋ฑ ํ ์คํธ ๊ธฐ๋ฐ ํ๋กํ ์ฝ๋ก ๋ชจ๋ธ๊ณผ ํต์ ํ๋ฉฐ, Native ๋ชจ๋ธ์ control token + constrained decoding ๋์ ์ ๊ท์ ํ์ฑ์ ์์กดํ๋ ๊ตฌ์กฐ
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- 6ํธ์์ Non-native ๋ชจ๋ธ์๋ Chat Template, ์ถ๋ ฅ ํ์, Constrained Decoding ๋ฑ 5๊ฐ ๋ ์ด์ด ์ค ๋๋ถ๋ถ์ด ์๋ค๋ ๊ฒ์ ํ์ธํ๋ค. ์ด ๊ธ์์๋ ๊ทธ ์๋ ๋ ์ด์ด๋ฅผ ์ง์ ์ฝ๋๋ก ๊ตฌํํ๋ค
- โMiddleware๋ฅผ ๋ง๋ ๋คโ๋ ๊ฒ์ด ๊ตฌ์ฒด์ ์ผ๋ก ์ด๋ค ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ธ์ง ํ์ธํด์ผ, 8ํธ์์ ๋ก์ปฌ ๋ชจ๋ธ์ ์ฐ๊ฒฐํ ์ ์๋ค
- Qwen ๊ณต์ ๋ฌธ์์ ํํ๋๋ก, function calling์ ๋ณธ์ง์ ์ผ๋ก prompt engineering์ผ๋ก ๊ตฌํ๋๋ค. ์ด ์ฌ์ค์ ์ฝ๋๋ก ์ง์ ํ์ธํ๋ค
AS-IS
sequenceDiagram autonumber participant User as ์ฌ์ฉ์ participant LLM as ๋ชจ๋ธ ๊ฐ์ค์น User->>LLM: "์์ธ ๋ ์จ ์๋ ค์ค" LLM-->>User: "์์ธ์ ํ์ฌ ๋ ์จ๋ฅผ ํ์ธํ ์ ์์ง๋ง,<br/>์ผ๋ฐ์ ์ผ๋ก 2์์ ์ถฅ์ต๋๋ค..." Note over User,LLM: tool ์ ์๋ฅผ ์ ๋ฌํ ๋ฐฉ๋ฒ์ด ์๋ค<br/>tool call ํ์ ์ถ๋ ฅ๋ ๋์ค์ง ์๋๋ค
TO-BE
sequenceDiagram autonumber participant User as ์ฌ์ฉ์ box ์ ํ๋ฆฌ์ผ์ด์ participant MW as Middleware participant Tool as Tool ํจ์ end participant LLM as ๋ชจ๋ธ ๊ฐ์ค์น User->>MW: "์์ธ ๋ ์จ ์๋ ค์ค" MW->>LLM: tool ์ ์ ํฌํจ ํ ์คํธ ํ๋กฌํํธ LLM-->>MW: "...<tool_call>{...get_weather...}</tool_call>" MW->>MW: tool call ํจํด ๊ฐ์ง + JSON ์ถ์ถ MW->>Tool: get_weather("Seoul") ์คํ Tool-->>MW: {temperature: "15ยฐC", condition: "๋ง์"} MW->>LLM: tool ๊ฒฐ๊ณผ ํฌํจ ํ๋กฌํํธ LLM-->>MW: "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค" MW-->>User: ์ต์ข ์๋ต Note over MW,Tool: tool ์ ์์ ์คํ ํจ์๋ ๊ฐ๋ฐ์๊ฐ ์์ฑ<br/>Middleware๋ ํ๋กฌํํธ ๋ณํ + ํ์ฑ + ์คํ ๋ฃจํ๋ฅผ ๋ด๋น
Middleware์ 3๊ฐ์ง ์ญํ
6ํธ์์ Middleware๊ฐ โ์๋ ๋ ์ด์ด๋ฅผ ์ธ๋ถ์์ ๋ฉ์ด๋คโ๊ณ ํ๋ค. ๊ตฌ์ฒด์ ์ผ๋ก 3๊ฐ์ง ์ญํ ์ด๋ค:
sequenceDiagram autonumber participant User as ์ฌ์ฉ์ box ์ ํ๋ฆฌ์ผ์ด์ participant MW as Middleware participant Tool as Tool ํจ์ end participant LLM as ๋ชจ๋ธ ๊ฐ์ค์น User->>MW: "์์ธ ๋ ์จ ์๋ ค์ค" Note over MW: ์ญํ 1: ํ๋กฌํํธ ์กฐ๋ฆฝ<br/>tool JSON โ ์์คํ ํ๋กฌํํธ ํ ์คํธ MW->>LLM: ํ๋กํ ์ฝ์ ๋ง์ถ ํ ์คํธ ํ๋กฌํํธ LLM-->>MW: "...<tool_call>{...get_weather...}</tool_call>" Note over MW: ์ญํ 2: ์ถ๋ ฅ ํ์ฑ<br/>์ ๊ท์์ผ๋ก tool call ๊ฐ์ง โ JSON ์ถ์ถ MW->>Tool: get_weather("Seoul") ์คํ Tool-->>MW: {temperature: "15ยฐC", condition: "๋ง์"} Note over MW: ์ญํ 3: ์คํ ๋ฃจํ<br/>tool ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋ธ์ ํผ๋๋ฐฑ MW->>LLM: tool ๊ฒฐ๊ณผ ํฌํจ ํ๋กฌํํธ LLM-->>MW: "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค" MW-->>User: ์ต์ข ์๋ต
| ์ญํ | ๋์ฒดํ๋ Native ๋ ์ด์ด | ํ๋ ์ผ | ๋ค์ด์ด๊ทธ๋จ |
|---|---|---|---|
| ํ๋กฌํํธ ์กฐ๋ฆฝ | Chat Template | tool JSON โ ์์คํ ํ๋กฌํํธ ํ ์คํธ ๋ณํ | 2๋ฒ |
| ์ถ๋ ฅ ํ์ฑ | ์ถ๋ ฅ ํ์ + Constrained Decoding | ๋ชจ๋ธ ์ถ๋ ฅ์์ <tool_call> ํจํด ๊ฐ์ง โ JSON ์ถ์ถ | 3๋ฒ |
| ์คํ ๋ฃจํ | SDK์ toolRunner | tool ์คํ โ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋ธ์ ํผ๋๋ฐฑ โ ์ถ๊ฐ tool call ํ์ธ โ ๋ฐ๋ณต | 4~7๋ฒ |
ํ๋กํ ์ฝ - Middleware์ ๋ชจ๋ธ์ด ์ฝ์ํ๋ ํ์ (2,3๋ฒ๊ณผ 6,7๋ฒ ๊ณผ์ )
์ ๋ค์ด์ด๊ทธ๋จ์์ Middleware์ ๋ชจ๋ธ์ด ํต์ ํ๋ ๊ณผ์ (2,3๋ฒ๊ณผ 6,7๋ฒ)์๋ ์ฝ์์ด ํ์ํ๋ค. โtool ์ ์๋ฅผ ์ด๋ค ํ ์คํธ๋ก ์ ๋ฌํ๊ณ , ๋ชจ๋ธ์ tool call์ ์ด๋ค ํ ์คํธ๋ก ํํํ ๊ฒ์ธ๊ฐโ โ ์ด๊ฒ์ด ํ๋กํ ์ฝ์ด๋ค.
Native ๋ชจ๋ธ์์๋ control token์ด ์ด ์ญํ ์ ํ๋ค (3ํธ). Middleware์์๋ control token์ด ์์ผ๋ฏ๋ก, ์ผ๋ฐ ํ ์คํธ ํจํด์ผ๋ก ๋์ฒดํ๋ค.
๋ํ์ ์ธ ํ๋กํ ์ฝ 2๊ฐ์ง:
Hermes ํ๋กํ ์ฝ
NousResearch Hermes ๋ชจ๋ธ์ด ์ ์ํ๊ณ , Qwen3 ๋ฑ tool calling fine-tuning๋ ์คํ์์ค ๋ชจ๋ธ์ด ์ฑํํ ํ์์ด๋ค. <tool_call> XML ํ๊ทธ ์์ JSON์ ๋ฃ๋๋ค:
๋ชจ๋ธ ์ถ๋ ฅ:
<tool_call>
{"name": "get_weather", "arguments": {"location": "Seoul"}}
</tool_call>
์์คํ ํ๋กฌํํธ์์ tool ์ ์๋ JSON์ผ๋ก ์ ๋ฌํ๋ค:
You are provided with function signatures within <tools></tools> XML tags:
<tools>
[{"type": "function", "function": {"name": "get_weather", ...}}]
</tools>
For each function call return a json object with function name and arguments
within <tool_call></tool_call> XML tags:
<tool_call>
{"name": "<function_name>", "arguments": <args_json_object>}
</tool_call>
XML ํ๋กํ ์ฝ
tool ์ด๋ฆ๊ณผ ์ธ์๋ฅผ ๋ชจ๋ XML ํ๊ทธ๋ก ํํํ๋ ํ์์ด๋ค. ai-sdk-tool-call-middleware์ morphXmlToolMiddleware๊ฐ ์ด ๋ฐฉ์์ ์ฌ์ฉํ๋ค:
๋ชจ๋ธ ์ถ๋ ฅ:
<tool_call>
<tool_name>get_weather</tool_name>
<location>Seoul</location>
</tool_call>
ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ด XML ํ๊ทธ ์ด๋ฆ์ด ๋๋ฏ๋ก, JSON ํ์ฑ ์์ด XML ํ์ฑ๋ง์ผ๋ก ์ธ์๋ฅผ ์ถ์ถํ ์ ์๋ค.
์ด๋ค ํ๋กํ ์ฝ์ ์ ํํ๋๊ฐ
| Hermes | XML | |
|---|---|---|
| tool call ์ธ์ | JSON ๊ฐ์ฒด | XML ํ๊ทธ |
| ํ์ฑ ๋์ด๋ | JSON.parse ํ์ | XML ํ๊ทธ ์ถ์ถ๋ง |
| ์ ํฉํ ๋ชจ๋ธ | Hermes format์ผ๋ก fine-tuning๋ ๋ชจ๋ธ (Qwen3, Hermes ๋ฑ) | fine-tuning ์๋ ๋ฒ์ฉ ๋ชจ๋ธ |
| ์ฌ์ฉํ๋ ๊ตฌํ์ฒด | hermesToolMiddleware, qwen3CoderToolMiddleware | morphXmlToolMiddleware |
ํต์ฌ ๊ธฐ์ค: ๋ชจ๋ธ์ด ์ด๋ค ํ์์ผ๋ก ํ์ต๋์๋๊ฐ. 4ํธ์์ ํ์ธํ ๊ฒ์ฒ๋ผ, fine-tuning์ ๋ชจ๋ธ์ด ํน์ ํจํด์ ์์ฑํ ํ๋ฅ ์ ๋์ธ๋ค. Qwen3๊ฐ Hermes ํ์์ผ๋ก fine-tuning๋์๋ค๋ฉด, Hermes ํ๋กํ ์ฝ์ ์ฌ์ฉํด์ผ ํด๋น ํจํด์ ์์ฑ ํ๋ฅ ์ด ๊ฐ์ฅ ๋๋ค.
fine-tuning ์๋ ๋ฒ์ฉ ๋ชจ๋ธ์ด๋ผ๋ฉด? In-context learning์ ์์กดํด์ผ ํ๋ค. ์์คํ ํ๋กฌํํธ์ ํ์์ ์์ธํ ์ง์ํ๊ณ , ๋ชจ๋ธ์ด ๊ทธ ์ง์๋ฅผ ๋ฐ๋ฅด๊ธฐ๋ฅผ ๊ธฐ๋ํ๋ค. ์ด ๊ฒฝ์ฐ XML ํ๋กํ ์ฝ์ด ๋ ๋์ ์ ์๋ค. XML์ ์น ๋ฐ์ดํฐ์์ ์์ฃผ ๋ฑ์ฅํ๋ฏ๋ก, ๋ฒ์ฉ ๋ชจ๋ธ๋ XML ํ๊ทธ ํจํด์ ์ด๋ ์ ๋ ์ต์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์ด ๊ธ์์๋ Hermes ํ๋กํ ์ฝ๋ก ๊ตฌํํ๋ค. Qwen3 ๋ฑ ์ค์ ์ฌ์ฉํ ๋ชจ๋ธ์ด ์ด ํ์์ ์ง์ํ๊ณ , ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ํ์์ด๊ธฐ ๋๋ฌธ์ด๋ค.
Tool ์คํ - ํจ์ ํธ์ถ๊ณผ ๊ฒฐ๊ณผ ๋ฐํ (4,5๋ฒ ๊ณผ์ )
ํ๋กํ ์ฝ์ด Middlewareโ๋ชจ๋ธ ์ฌ์ด์ ํ ์คํธ ํต์ ๊ท์ฝ์ด๋ผ๋ฉด, 4,5๋ฒ์ MiddlewareโTool ์ฌ์ด์ ์ฝ๋ ํต์ ์ด๋ค. ํ๋กํ ์ฝ๊ณผ ๋ฌด๊ดํ๊ฒ, ์ผ๋ฐ์ ์ธ ํจ์ ํธ์ถ์ด๋ค.
- 4๋ฒ (MWโTool): Middleware๊ฐ ์ถ๋ ฅ ํ์ฑ์ผ๋ก ์ถ์ถํ tool ์ด๋ฆ(
"get_weather")๊ณผ ์ธ์({"location": "Seoul"})๋ก, ๋ฑ๋ก๋ ํจ์๋ฅผ ์ฐพ์ ํธ์ถํ๋ค. 5ํธ์์ ๋ค๋ฃฌ โtool ์ด๋ฆ โ ํจ์ ๋งคํ โ ์คํโ๊ณผ ๋์ผํ๋ค. - 5๋ฒ (ToolโMW): Tool ํจ์๊ฐ ์คํ ๊ฒฐ๊ณผ๋ฅผ JSON์ผ๋ก ๋ฐํํ๋ค. ์ด ๊ฒฐ๊ณผ๊ฐ 6๋ฒ์์ ํ๋กํ ์ฝ ํ์์ผ๋ก ๋ชจ๋ธ์ ํผ๋๋ฐฑ๋๋ค.
// 4๋ฒ: tool ์ด๋ฆ์ผ๋ก ํจ์๋ฅผ ์ฐพ์ ํธ์ถ
const tool = tools[parsedToolCall.name]; // "get_weather" โ ํจ์ ๋งคํ
const result = await tool.execute(parsedToolCall.arguments); // ์คํ
// 5๋ฒ: ํจ์๊ฐ JSON์ผ๋ก ๊ฒฐ๊ณผ ๋ฐํ
// result = { temperature: "15ยฐC", condition: "๋ง์" }tool ์ ์์ ์คํ ํจ์๋ ๊ฐ๋ฐ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฑํ๋ค. Middleware๋ ์ด ํจ์๋ฅผ ํธ์ถํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ๋ชจ๋ธ์ ์ ๋ฌํ๋ ์ญํ ๋ง ํ๋ค.
ํ๋กํ ์ฝ๊ณผ Tool ์คํ์ ์ ๋ฆฌํ์ผ๋, ์ด์ 3๊ฐ์ง ์ญํ ์ ์ฝ๋๋ก ๊ตฌํํ๋ค.
์ญํ 1: ํ๋กฌํํธ ์กฐ๋ฆฝ
์ฒซ ๋ฒ์งธ ์ญํ ์ tool ์ ์ JSON์ ์์คํ ํ๋กฌํํธ ํ ์คํธ๋ก ๋ณํํ๋ ๊ฒ์ด๋ค. 2ํธ์์ Claude์ API ์๋ฒ๊ฐ ํ๋ ์ผ์ ์ฐ๋ฆฌ๊ฐ ์ง์ ํ๋ค.
interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, unknown>; // JSON Schema
}
function assembleToolPrompt(tools: ToolDefinition[]): string {
// tool ์ ์๋ฅผ Hermes ํ์์ JSON ๋ฐฐ์ด๋ก ๋ณํ
const toolDefs = tools.map((t) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
return [
"You are a function calling AI model.",
"You are provided with function signatures within <tools></tools> XML tags:",
"<tools>",
JSON.stringify(toolDefs, null, 2),
"</tools>",
"",
"For each function call return a json object with function name and arguments",
"within <tool_call></tool_call> XML tags:",
"<tool_call>",
'{"name": "<function_name>", "arguments": <args_json_object>}',
"</tool_call>",
].join("\n");
}์ด ํจ์๊ฐ ํ๋ ์ผ์ ๋จ์ํ๋ค. tool JSON์ ๋ฐ์์, ๋ชจ๋ธ์ด ์ดํดํ ์ ์๋ ํ ์คํธ๋ก ๊ฐ์ธ๋ ๊ฒ์ด๋ค. ์ถ๋ ฅ ์์:
You are a function calling AI model.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": { "type": "string" }
},
"required": ["location"]
}
}
}
]
</tools>
For each function call return a json object with function name and arguments
within <tool_call></tool_call> XML tags:
<tool_call>
{"name": "<function_name>", "arguments": <args_json_object>}
</tool_call>2ํธ์์ Claude์ ์์คํ
ํ๋กฌํํธ๊ฐ "In this environment you have access to a set of tools..." ๋ก ์์ํ๋ ๊ฒ๊ณผ ๋์ผํ ์ญํ ์ด๋ค. ํ์๋ง ๋ค๋ฅผ ๋ฟ, ํ๋ ์ผ์ ๊ฐ๋ค: tool ์ ์๋ฅผ ํ
์คํธ๋ก ๋ชจ๋ธ์๊ฒ ์๋ ค์ฃผ๋ ๊ฒ.
tool ์คํ ๊ฒฐ๊ณผ๋ฅผ ํผ๋๋ฐฑํ ๋๋ ํ๋กฌํํธ์ ํฌํจํด์ผ ํ๋ค:
function assembleToolResult(
toolName: string,
result: string
): string {
return [
"<tool_response>",
`{"name": "${toolName}", "content": ${result}}`,
"</tool_response>",
].join("\n");
}์ญํ 2: ์ถ๋ ฅ ํ์ฑ
๋ ๋ฒ์งธ ์ญํ ์ ๋ชจ๋ธ์ด ์์ฑํ ํ ์คํธ์์ tool call ํจํด์ ๊ฐ์งํ๊ณ JSON์ผ๋ก ์ถ์ถํ๋ ๊ฒ์ด๋ค. 4ํธ์์ API ์๋ฒ์ ์ถ๋ ฅ ํ์๊ฐ ํ๋ ์ผ์ด๋ค.
๋ชจ๋ธ์ ์ค์ ์ถ๋ ฅ์ ์ด๋ฐ ํ ์คํธ๋ค:
๋ ์จ๋ฅผ ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
<tool_call>
{"name": "get_weather", "arguments": {"location": "Seoul"}}
</tool_call>์ด ํ
์คํธ์์ <tool_call> ํ๊ทธ๋ฅผ ์ฐพ๊ณ , ์์ JSON์ ์ถ์ถํด์ผ ํ๋ค:
interface ParsedToolCall {
name: string;
arguments: Record<string, unknown>;
}
function parseToolCalls(text: string): ParsedToolCall[] {
const pattern = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
const toolCalls: ParsedToolCall[] = [];
let match;
while ((match = pattern.exec(text)) !== null) {
const jsonStr = match[1].trim();
const parsed = JSON.parse(jsonStr);
toolCalls.push({
name: parsed.name,
arguments: parsed.arguments,
});
}
return toolCalls;
}์ ๊ท์ /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g์ด ํต์ฌ์ด๋ค. <tool_call> ๊ณผ </tool_call> ์ฌ์ด์ ๋ด์ฉ์ ์บก์ฒํ๊ณ , [\s\S]*?๋ก ์ค๋ฐ๊ฟ์ ํฌํจํ ๋ชจ๋ ๋ฌธ์๋ฅผ ๋งค์นญํ๋ค.
์ด ๋ถ๋ถ์ด Native ๋ชจ๋ธ๊ณผ ๊ฐ์ฅ ํฐ ์ฐจ์ด๊ฐ ๋ฐ์ํ๋ ์ง์ ์ด๋ค. Native ๋ชจ๋ธ์ control token์ผ๋ก tool call ์์ญ์ ๋ช
ํํ ๊ตฌ๋ถํ๊ณ , constrained decoding์ผ๋ก valid JSON์ ๋ณด์ฅํ๋ค (3ํธ, 4ํธ). Middleware๋ ์ ๊ท์ ๋งค์นญ๊ณผ JSON.parse์ ์์กดํ๋ค. ๋ชจ๋ธ์ด <tool_call> ํ๊ทธ๋ฅผ ์ ํํ ๋ซ์ง ์๊ฑฐ๋, JSON์ด ๊นจ์ ธ ์์ผ๋ฉด ํ์ฑ์ด ์คํจํ๋ค.
์ด ํ๊ณ๋ ์๋ฌ ํธ๋ค๋ง ์น์ ์์ ๋ค๋ฃฌ๋ค.
tool call ์ ๋ฌด๋ก ์๋ต ๋ถ๊ธฐ
ํ์ฑ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์๋ต์ ๋ถ๊ธฐํ๋ค:
function classifyResponse(text: string):
| { type: "tool_calls"; calls: ParsedToolCall[]; textBefore: string }
| { type: "text"; content: string } {
const toolCalls = parseToolCalls(text);
if (toolCalls.length > 0) {
// <tool_call> ์์ ํ
์คํธ ์ถ์ถ (๋ชจ๋ธ์ ์ฌ์ ์ค๋ช
)
const textBefore = text.split("<tool_call>")[0].trim();
return { type: "tool_calls", calls: toolCalls, textBefore };
}
return { type: "text", content: text };
}์ด๊ฒ์ด 5ํธ์ stop_reason: "tool_use" vs stop_reason: "end_turn" ๋ถ๊ธฐ์ ๋์ผํ ์ญํ ์ด๋ค. Native ๋ชจ๋ธ์ API ์๋ฒ๊ฐ stop_reason์ ์ค์ ํด์ฃผ์ง๋ง, Middleware์์๋ ํ
์คํธ์ <tool_call> ํจํด์ด ์๋์ง ์ง์ ํ์ธํด์ผ ํ๋ค.
์ญํ 3: ์คํ ๋ฃจํ
์ธ ๋ฒ์งธ ์ญํ ์ ํ๋กฌํํธ ์กฐ๋ฆฝ โ ๋ชจ๋ธ ํธ์ถ โ ์ถ๋ ฅ ํ์ฑ โ tool ์คํ โ ๊ฒฐ๊ณผ ํผ๋๋ฐฑ์ ๋ฐ๋ณตํ๋ ๊ฒ์ด๋ค. 5ํธ์์ SDK์ toolRunner๊ฐ ํ๋ ์ผ์ด๋ค.
interface Tool {
description: string;
parameters: Record<string, unknown>;
execute: (args: Record<string, unknown>) => Promise<unknown>;
}
type Message = { role: string; content: string };
async function toolCallingLoop(
model: { generate: (opts: { system: string; messages: Message[] }) => Promise<{ text: string }> },
tools: Record<string, Tool>,
prompt: string,
maxSteps: number = 5
): Promise<string> {
// ์ญํ 1: ํ๋กฌํํธ ์กฐ๋ฆฝ
const toolDefs = Object.entries(tools).map(([name, t]) => ({
name,
description: t.description,
parameters: t.parameters,
}));
const systemPrompt = assembleToolPrompt(toolDefs);
const messages: Message[] = [{ role: "user", content: prompt }];
for (let step = 0; step < maxSteps; step++) {
// ๋ชจ๋ธ ํธ์ถ
const response = await model.generate({
system: systemPrompt,
messages,
});
// ์ญํ 2: ์ถ๋ ฅ ํ์ฑ
const classified = classifyResponse(response.text);
if (classified.type === "text") {
return classified.content; // ์ต์ข
ํ
์คํธ ์๋ต โ ๋ฃจํ ์ข
๋ฃ
}
// tool call์ด ์์ผ๋ฉด โ ์คํ ํ ๊ฒฐ๊ณผ ํผ๋๋ฐฑ
messages.push({ role: "assistant", content: response.text });
for (const toolCall of classified.calls) {
const tool = tools[toolCall.name];
if (!tool) {
throw new Error(`Unknown tool: ${toolCall.name}`);
}
// tool ์คํ
const result = await tool.execute(toolCall.arguments);
const resultStr = JSON.stringify(result);
// ๊ฒฐ๊ณผ๋ฅผ ๋ฉ์์ง์ ์ถ๊ฐ โ ๋ค์ ๋ฃจํ์์ ๋ชจ๋ธ์ ์ ๋ฌ
messages.push({
role: "tool",
content: assembleToolResult(toolCall.name, resultStr),
});
}
// ๋ค์ ๋ฃจํ ์์ โ ๋ชจ๋ธ์ด ์ถ๊ฐ tool call ๋๋ ์ต์ข
์๋ต ์์ฑ
}
return "์ต๋ ๋จ๊ณ์ ๋๋ฌํ์ต๋๋ค.";
}5ํธ์ ๋ฉํฐํด tool calling ์ํ์ค ๋ค์ด์ด๊ทธ๋จ์ ์ฝ๋๋ก ๊ตฌํํ ๊ฒ์ด๋ค. ๋ฃจํ์ ์ข ๋ฃ ์กฐ๊ฑด์ ๋ ๊ฐ์ง:
classifyResponse๊ฐ"text"๋ฐํ โ ๋ชจ๋ธ์ด ์ต์ข ์๋ต ์์ฑ (=stop_reason: "end_turn")step >= maxStepsโ ์ต๋ ๋จ๊ณ ๋๋ฌ (= Vercel AI SDK์stopWhen: stepCountIs(5))
์ฌ์ฉ ์์
// ์คํ
const answer = await toolCallingLoop(
ollamaModel,
{
get_weather: {
description: "Get the current weather in a given location",
parameters: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"],
},
execute: async ({ location }) => ({
temperature: "15ยฐC",
condition: "๋ง์",
location,
}),
},
},
"์์ธ ๋ ์จ ์๋ ค์ค"
);
console.log(answer);
// โ "์์ธ์ ํ์ฌ ๋ ์จ๋ 15ยฐC์ด๋ฉฐ ๋ง์ต๋๋ค."์ด๊ฒ์ด Middleware์ ์ ์ฒด ๊ตฌํ์ด๋ค. assembleToolPrompt + parseToolCalls + toolCallingLoop 3๊ฐ ํจ์๋ก Non-native ๋ชจ๋ธ์์ tool calling์ด ๋์ํ๋ค.
์๋ฌ๋ ๋ฐ๋์ ๋ฐ์ํ๋ค
Qwen ๊ณต์ ๋ฌธ์๋ ์ด๋ ๊ฒ ๊ฒฝ๊ณ ํ๋ค:
โIt is not guaranteed that the model generation will always follow the protocol even with proper prompting or templates.โ
Native ๋ชจ๋ธ์์๋ constrained decoding์ด valid JSON์ 100% ๋ณด์ฅํ๋ค (4ํธ). Middleware์๋ ์ด ์์ ์ฅ์น๊ฐ ์๋ค. ๋ชจ๋ธ์ ์์ ๋กญ๊ฒ ์๋ฌด ํ ํฐ์ด๋ ์์ฑํ ์ ์์ผ๋ฏ๋ก, ์๋ฌ๋ ๊ฐ๋ฅ์ฑ์ด ์๋๋ผ ํ์ค์ฑ์ด๋ค.
์ฃผ์ ์๋ฌ ์ ํ๊ณผ ๋์
| ์๋ฌ | ์์ธ | ๋์ |
|---|---|---|
| ํ๊ทธ ๋ฏธ๋ซํ | <tool_call> ์ด๊ณ </tool_call> ์์ด ์ข
๋ฃ | ์ ๊ท์ ๋งค์นญ ์คํจ โ ํ ์คํธ ์๋ต์ผ๋ก ์ฒ๋ฆฌ |
| JSON ํ์ฑ ์คํจ | {"name": "get_weather", "arguments": {location: Seoul}} (๋ฐ์ดํ ๋๋ฝ) | try-catch๋ก ๊ฐ์ธ๊ณ ์ฌ์๋ ๋๋ fallback |
| ์กด์ฌํ์ง ์๋ tool | ๋ชจ๋ธ์ด ์ ์์ ์๋ tool ์ด๋ฆ ์์ฑ | tool ์ด๋ฆ ๊ฒ์ฆ ํ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ชจ๋ธ์ ํผ๋๋ฐฑ |
| ํ์ ๋ถ์ผ์น | "temperature" (string) ๋์ temperature (number) | JSON Schema ๊ธฐ๋ฐ ํ์ ๊ฐ์ ๋ณํ |
์๋ฌ ํธ๋ค๋ง์ ์ถ๊ฐํ ํ์ฑ ํจ์:
function parseToolCallsSafe(text: string): {
toolCalls: ParsedToolCall[];
errors: string[];
} {
const pattern = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
const toolCalls: ParsedToolCall[] = [];
const errors: string[] = [];
let match;
while ((match = pattern.exec(text)) !== null) {
try {
const parsed = JSON.parse(match[1].trim());
if (!parsed.name || typeof parsed.name !== "string") {
errors.push(`Invalid tool name: ${JSON.stringify(parsed.name)}`);
continue;
}
toolCalls.push({
name: parsed.name,
arguments: parsed.arguments ?? {},
});
} catch (e) {
errors.push(`JSON parse failed: ${match[1].trim().slice(0, 100)}`);
}
}
return { toolCalls, errors };
}์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ๋ชจ๋ธ์ ์๋ฌ ๋ฉ์์ง๋ฅผ ํผ๋๋ฐฑํ์ฌ ์ฌ์๋ํ ์ ์๋ค. ์ด๊ฒ์ Vercel AI SDK๊ฐ tool ์คํ ์๋ฌ๋ฅผ tool-error content part๋ก ๋ชจ๋ธ์ ์ ๋ฌํ์ฌ LLM์ด ์์ฒด ๋ณต๊ตฌํ๋๋ก ํ๋ ๊ฒ๊ณผ ๊ฐ์ ์๋ฆฌ๋ค.
Vercel AI SDK Middleware๋ก ์กฐ๋ฆฝํ๊ธฐ
์ง๊ธ๊น์ง ๋ง๋ 3๊ฐ ํจ์(assembleToolPrompt, parseToolCalls, toolCallingLoop)๋ Middleware์ ์๋ฆฌ๋ฅผ ์ดํดํ๊ธฐ ์ํ ๊ฒ์ด์๋ค. ์ค์ ํ๋ก๋์
์์๋ Vercel AI SDK์ middleware ๊ตฌ์กฐ์ ๋ง์ถฐ ํจํค์งํ๋ค.
Vercel AI SDK์ wrapLanguageModel์ 3๊ฐ์ง ํํน ํฌ์ธํธ๋ฅผ ์ ๊ณตํ๋ค:
| ํํน ํฌ์ธํธ | ์ญํ | ์ฐ๋ฆฌ๊ฐ ๋ง๋ ํจ์ |
|---|---|---|
transformParams | ๋ชจ๋ธ์ ์ ๋ฌ๋๊ธฐ ์ ํ๋ผ๋ฏธํฐ ๋ณํ | assembleToolPrompt (ํ๋กฌํํธ ์กฐ๋ฆฝ) |
wrapGenerate | doGenerate ์คํ์ ๊ฐ์ธ๊ธฐ | parseToolCalls (์ถ๋ ฅ ํ์ฑ) |
wrapStream | doStream ์คํ์ ๊ฐ์ธ๊ธฐ | parseToolCalls์ ์คํธ๋ฆฌ๋ฐ ๋ฒ์ |
import { type LanguageModelV3Middleware } from "ai";
const toolCallMiddleware: LanguageModelV3Middleware = {
// ์ญํ 1: ํ๋กฌํํธ ์กฐ๋ฆฝ
transformParams: async ({ params }) => ({
...params,
prompt: {
...params.prompt,
system: assembleToolPrompt(params.tools ?? []),
},
// tool ์ ์๋ ์์คํ
ํ๋กฌํํธ๋ก ์ด๋ํ์ผ๋ฏ๋ก ์ ๊ฑฐ
tools: undefined,
}),
// ์ญํ 2: ์ถ๋ ฅ ํ์ฑ
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate();
const { toolCalls, errors } = parseToolCallsSafe(result.text ?? "");
if (toolCalls.length > 0) {
return {
...result,
toolCalls: toolCalls.map((tc) => ({
toolCallType: "function" as const,
toolCallId: crypto.randomUUID(),
toolName: tc.name,
args: JSON.stringify(tc.arguments),
})),
finishReason: "tool-calls" as const,
};
}
return result;
},
};์ด์ ์ด middleware๋ฅผ ๋ชจ๋ธ์ ๊ฐ์ธ๋ฉด ๋๋ค:
import { wrapLanguageModel, generateText, tool, stepCountIs } from "ai";
import { createOllama } from "ollama-ai-provider";
import { z } from "zod";
const ollama = createOllama();
// middleware๋ก ๋ชจ๋ธ ๊ฐ์ธ๊ธฐ
const wrappedModel = wrapLanguageModel({
model: ollama("qwen3:8b"),
middleware: toolCallMiddleware,
});
// ์ด์ native tool calling์ฒ๋ผ ์ฌ์ฉ
const result = await generateText({
model: wrappedModel,
tools: {
get_weather: tool({
description: "Get the current weather",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => ({
temperature: "15ยฐC",
condition: "๋ง์",
}),
}),
},
stopWhen: stepCountIs(5),
prompt: "์์ธ ๋ ์จ ์๋ ค์ค",
});๊ฐ๋ฐ์ ์ ์ฅ์์๋ native tool calling API์ ๋์ผํ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ค. middleware๊ฐ ๋ด๋ถ์์ ํ๋กฌํํธ ์กฐ๋ฆฝ โ ์ถ๋ ฅ ํ์ฑ โ ์คํ ๋ฃจํ๋ฅผ ์ฒ๋ฆฌํ๋ค.
์ค์ ๊ตฌํ์ฒด: @ai-sdk-tool/parser
์ง์ middleware๋ฅผ ๋ง๋ค ์๋ ์์ง๋ง, ์ด๋ฏธ ๊ฒ์ฆ๋ ๊ตฌํ์ฒด๊ฐ ์๋ค. ai-sdk-tool-call-middleware๋ 4๊ฐ์ง ํ๋กํ ์ฝ์ ์ง์ํ๋ค:
import {
hermesToolMiddleware, // Hermes: JSON in <tool_call> tags
morphXmlToolMiddleware, // XML: ํ๋ผ๋ฏธํฐ๋ฅผ XML ํ๊ทธ๋ก
yamlXmlToolMiddleware, // YAML-XML: XML ํ๊ทธ + YAML body
qwen3CoderToolMiddleware, // Qwen3 ์ ์ฉ
} from "@ai-sdk-tool/parser";
// ๋ชจ๋ธ์ ๋ง๋ ํ๋กํ ์ฝ ์ ํ
const wrappedModel = wrapLanguageModel({
model: ollama("qwen3:8b"),
middleware: hermesToolMiddleware,
});์ปค์คํ ํ๋กํ ์ฝ๋ ๋ง๋ค ์ ์๋ค:
import { createToolMiddleware, hermesProtocol } from "@ai-sdk-tool/parser";
const customMiddleware = createToolMiddleware({
protocol: hermesProtocol,
toolSystemPromptTemplate: (tools) =>
`Use these tools: ${JSON.stringify(tools)}`,
});์คํธ๋ฆฌ๋ฐ ํ๊ฒฝ์์๋ tool-input-start, tool-input-delta, tool-input-end ์ด๋ฒคํธ๋ฅผ ํตํด tool call ํ์ฑ ๊ณผ์ ์ ์ ์ง์ ์ผ๋ก ์ฒ๋ฆฌํ๋ฉฐ, ์๋ฌ ๋ฐ์ ์ providerOptions.toolCallMiddleware.onError ์ฝ๋ฐฑ์ผ๋ก ์ปค์คํ
์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
๋ค์ ํธ: ๋ชจ๋ธ๋ ์ง์ ๋์ธ ์ ์๋?
์ด ๊ธ์์ Middleware์ 3๊ฐ์ง ์ญํ ์ ์ฝ๋๋ก ๊ตฌํํ๋ค. ํ๋กฌํํธ ์กฐ๋ฆฝ, ์ถ๋ ฅ ํ์ฑ, ์คํ ๋ฃจํ โ ์ด 3๊ฐ ํจ์๋ง์ผ๋ก Non-native ๋ชจ๋ธ์์ tool calling์ด ๋์ํ๋ค.
ํ์ง๋ง ์ง๊ธ๊น์ง ollama("qwen3:8b") ๊ฐ์ ๋ชจ๋ธ ํธ์ถ์ ๋น์ฐํ๊ฒ ์ฌ์ฉํ๋ค. ์ด ๋ชจ๋ธ์ ์ด๋์ ์คํ๋๊ณ ์๋ ๊ฑธ๊น? ์คํ์์ค ๋ชจ๋ธ์ ๋ก์ปฌ์์ ์ง์ ๋์ฐ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น?
๋ค์ ํธ์์ Ollama์ vLLM์ผ๋ก ์คํ์์ค ๋ชจ๋ธ์ ๋ก์ปฌ์ ์๋นํ๋ ๊ณผ์ ์ ๋ค๋ฃฌ๋ค. ํนํ, vLLM์ --tool-call-parser hermes ์ต์
์ผ๋ก ์๋น ๋ ๋ฒจ์์ Middleware ์ญํ ์ ์ํํ๋ ๊ฒ๋ ํ์ธํ๋ค.
์ฐธ๊ณ ๋ฌธ์
- NousResearch Hermes 2 Pro - Hermes ํ๋กํ ์ฝ์ ์๋ณธ ์ ์,
<tool_call>+ JSON ํ์ - Qwen3 Function Calling - Hermes-style tool use ์ฑํ, โfunction calling is essentially prompt engineeringโ
- Vercel AI SDK - Language Model Middleware - wrapLanguageModel, transformParams/wrapGenerate/wrapStream ๊ตฌ์กฐ
- ai-sdk-tool-call-middleware - Hermes/XML/YAML-XML ํ๋กํ ์ฝ ๊ธฐ๋ฐ tool call middleware ์ค์ ๊ตฌํ์ฒด
- @ai-sdk-tool/parser npm - hermesToolMiddleware, morphXmlToolMiddleware ํจํค์ง
- Vercel AI SDK - Tool Calling - generateText์ tool ์คํ ๋ฃจํ, stopWhen, tool-error ์ฒ๋ฆฌ