시리즈: 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 = 에이전트 완성 |
- 오픈소스 모델 로컬 구축은 HuggingFace에서 다운로드한 모델 가중치를 Ollama 또는 vLLM 같은 서빙 프레임워크로 HTTP API 서버로 띄우는 과정
- 서빙 프레임워크가 2~4편의 레이어(Chat Template, Tokenization, 출력 파싱)를 자동 처리하여, Non-native 모델을 Native처럼 사용할 수 있게 해주는 구조
- tool calling 관점에서 모델 선택 시 fine-tuning 여부가 결정적이며, 서빙 프레임워크의 tool parser가 7편 Middleware의 역할을 서빙 레벨에서 대체할 수 있는 관계
해당 개념이 필요한 이유
- 7편에서 Middleware를 만들었다. 하지만
model.generate()를 호출하는 코드만 있고, 그 모델이 실제로 어디서 실행되는지는 다루지 않았다 ollama("qwen3:8b")라고 쓰면 동작하지만, 이 모델을 직접 띄우려면 어떤 프레임워크를 쓰고 어떤 옵션을 줘야 하는지 알아야 한다- 특히 vLLM의
--tool-call-parser옵션은 7편에서 직접 구현한 Middleware 역할을 서빙 레벨에서 대신한다. 이 관계를 이해해야 9편에서 어떤 조합을 선택할지 결정할 수 있다
AS-IS
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 participant MW as Middleware (7편) end participant Q as ??? User->>MW: "서울 날씨 알려줘" MW->>Q: model.generate(프롬프트) Note over Q: 모델은 어디서 실행되지?<br/>GPU는? API 서버는? Q-->>MW: ???
TO-BE
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 participant MW as Middleware participant Tool as Tool 함수 end box 로컬 서빙 (Ollama / vLLM) participant API as OpenAI 호환 API participant LLM as 모델 가중치 end User->>MW: "서울 날씨 알려줘" MW->>API: POST /v1/chat/completions API->>LLM: Chat Template → Tokenization → 추론 LLM-->>API: 토큰 → 출력 파싱 API-->>MW: tool_calls: [{name: "get_weather", ...}] MW->>Tool: get_weather("Seoul") 실행 Tool-->>MW: {temperature: "15°C"} MW->>API: tool 결과 포함 요청 API->>LLM: 추론 LLM-->>API: 최종 응답 API-->>MW: "서울의 현재 날씨는 15°C입니다" MW-->>User: 최종 응답
서빙 프레임워크 - 모델을 실행하는 도구
혼동하기 쉬운 점부터 짚자: Ollama는 모델이 아니다. Qwen3가 학습된 모델이고, Ollama는 그 모델을 실행하는 도구다.
Ollama / vLLM = 모델을 실행하는 도구 (서빙 프레임워크)
Qwen3 / Llama = 학습된 모델 (가중치 파일)
비유:
Ollama = 동영상 플레이어 (VLC, 곰플레이어)
Qwen3 = 동영상 파일 (영화.mp4)
ollama pull qwen3:8b는 “Qwen3 모델 파일을 다운로드한다”, ollama serve는 “그 모델을 실행할 수 있는 API 서버를 띄운다”는 뜻이다.
Ollama와 vLLM은 같은 레벨이다. 둘 다 서빙 프레임워크로, 하는 일이 같다:
모델 가중치 파일 로드 → API 서버 띄우기 → 요청 받으면 모델 실행 → 응답 반환
차이는 성능과 기능 수준뿐:
- Ollama = 간편함 (brew install, 바로 사용)
- vLLM = 프로덕션급 (GPU 최적화, guided decoding, 세밀한 tool parser 설정)
OpenAI 호환 API - baseURL만 바꾸면 된다
Ollama와 vLLM 모두 OpenAI 호환 API를 제공한다. 이것은 OpenAI가 정의한 API 형식(POST /v1/chat/completions, tools 필드, tool_calls 응답)을 그대로 사용한다는 뜻이다.
이점: Claude/GPT용으로 작성한 코드를 코드 변경 없이 로컬 모델에 연결할 수 있다. baseURL만 바꾸면 된다:
// Claude API (외부 서버)
const client = new OpenAI({ baseURL: "https://api.anthropic.com/v1" });
// vLLM (로컬 서버)
const client = new OpenAI({ baseURL: "http://localhost:8000/v1" });
// Ollama (로컬 서버)
const client = new OpenAI({ baseURL: "http://localhost:11434/v1" });
// 이 아래 코드는 세 경우 모두 동일
const response = await client.chat.completions.create({
model: "qwen3:8b",
tools: [{ /* tool 정의 */ }],
messages: [{ role: "user", content: "서울 날씨 알려줘" }],
});7편에서 만든 Middleware, 5편의 Vercel AI SDK 코드, MCP 클라이언트 — 모두 이 API 형식을 사용하므로 baseURL만 변경하면 로컬 모델로 전환할 수 있다.
모델 선택 기준 - tool calling 관점
오픈소스 모델을 선택할 때, tool calling 용도라면 가장 중요한 기준은 tool calling fine-tuning 여부다. 4편에서 확인한 것처럼, fine-tuning은 모델이 tool call 패턴을 생성할 확률을 결정적으로 높인다.
| 모델 | 크기 | tool calling fine-tuning | 형식 | 라이선스 |
|---|---|---|---|---|
| Qwen3 | 0.6B ~ 235B | 완료 | Hermes | Apache 2.0 |
| Hermes 2 Pro | 8B | 완료 (90% 정확도) | Hermes | Llama 3 Community |
| Llama 3.1 | 8B, 70B, 405B | 완료 | JSON | Llama 3.1 Community |
| Mistral Nemo | 12B | 완료 | Mistral format | Apache 2.0 |
| DeepSeek V3 | 671B (MoE) | 완료 | JSON | MIT |
선택 기준 우선순위:
- fine-tuning 여부: tool calling fine-tuning이 안 된 모델은 in-context learning에만 의존하므로 정확도가 낮다. Hermes 2 Pro의 90% vs 일반 모델의 불확실한 성공률
- 형식 호환성: 7편에서 선택한 Hermes 프로토콜과 맞는 모델이면 가장 좋다. Qwen3, Hermes 2 Pro가 해당
- 크기 vs 하드웨어: 8B 모델은 ~5GB VRAM (4-bit 양자화), 70B 모델은 ~40GB VRAM. 로컬 개발이면 8B가 현실적
- 라이선스: 상업적 사용이 필요하면 Apache 2.0 (Qwen3, Mistral)이 안전
Ollama - 개발 환경에서 모델 띄우기
Ollama는 로컬 모델 서빙을 가장 간단하게 시작할 수 있는 도구다. 설치부터 API 서빙까지 3줄이면 된다:
# 1. 설치 (macOS)
brew install ollama
# 2. 모델 다운로드
ollama pull qwen3:8b
# 3. 대화 테스트
ollama run qwen3:8b "서울 날씨 알려줘"ollama pull을 실행하면 모델 가중치가 로컬에 다운로드되고, ollama serve로 HTTP API 서버가 localhost:11434에 뜬다 (설치 시 자동 시작).
Ollama의 내장 tool calling
Ollama는 지원 모델(Qwen3, Llama 3.1, Mistral Nemo 등)에서 tool calling을 자동 처리한다. 7편의 Middleware 없이도 tool calling이 동작한다:
import ollama from "ollama";
const response = await ollama.chat({
model: "qwen3:8b",
messages: [{ role: "user", content: "서울 날씨 알려줘" }],
tools: [{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather",
parameters: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"],
},
},
}],
});
// Ollama가 tool_calls를 파싱하여 반환
console.log(response.message.tool_calls);
// → [{ function: { name: "get_weather", arguments: { location: "Seoul" } } }]Ollama가 내부적으로 하는 일: 모델의 chat template으로 프롬프트 조립 → 모델 추론 → 출력에서 tool call 패턴 파싱. 7편에서 우리가 직접 구현한 것과 동일한 역할을 Ollama가 서빙 레벨에서 자동 처리한다.
vLLM - 프로덕션 서빙과 tool parser
vLLM은 프로덕션 환경을 위한 서빙 프레임워크다. Ollama보다 설정이 복잡하지만, 성능 최적화와 세밀한 tool calling 제어가 가능하다:
# 설치
pip install vllm
# 모델 서빙 (tool calling 활성화)
vllm serve Qwen/Qwen3-8B \
--enable-auto-tool-choice \
--tool-call-parser hermes이 명령 하나로 localhost:8000/v1에 OpenAI 호환 API 서버가 뜨고, tool calling이 활성화된다.
—tool-call-parser의 의미
--tool-call-parser hermes는 vLLM에게 “모델 출력에서 Hermes 형식의 tool call 패턴을 파싱하라”고 지시한다. vLLM이 지원하는 파서 목록:
| 파서 | 대상 모델 | 형식 |
|---|---|---|
hermes | Qwen3, Hermes 2 Pro | <tool_call> + JSON |
mistral | Mistral, Mixtral | [TOOL_CALLS] + JSON |
llama3_json | Llama 3.1 | JSON 직접 출력 |
deepseek_v3 | DeepSeek V3/R1 | DeepSeek 고유 형식 |
internlm | InternLM | InternLM 형식 |
vLLM의 tool parser = 서빙 레벨 Middleware
vLLM이 --tool-call-parser hermes로 실행되면, 내부적으로 7편 Middleware의 역할을 서빙 레벨에서 대신 처리한다:
| vLLM이 하는 일 | 대응하는 레이어 | 7편 Middleware 역할 |
|---|---|---|
tokenizer_config.json에서 chat template 로드 → 프롬프트 조립 | Chat Template (2편) | 역할 1: 프롬프트 조립 |
| guided decoding으로 JSON 형식 강제 | Constrained Decoding (4편) | Native에만 있던 기능 |
hermes 파서로 <tool_call> 패턴 감지 → JSON 추출 | 출력 파서 (4편) | 역할 2: 출력 파싱 |
결과: vLLM의 API 응답에 tool_calls 필드가 포함된다. Claude/GPT API와 동일한 형태다:
{
"choices": [{
"message": {
"role": "assistant",
"tool_calls": [{
"id": "chatcmpl-tool-abc123",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"Seoul\"}"
}
}]
},
"finish_reason": "tool_calls"
}]
}이것은 6편의 Non-native 모델이 서빙 프레임워크의 도움으로 Native처럼 동작하는 것이다.
Ollama vs vLLM 비교
| Ollama | vLLM | |
|---|---|---|
| 목적 | 개발/프로토타입 | 프로덕션 배포 |
| 설치 | brew install ollama | pip install vllm + GPU 드라이버 |
| 모델 다운로드 | ollama pull qwen3:8b | HuggingFace에서 자동 다운로드 |
| API | localhost:11434/v1 | localhost:8000/v1 |
| tool calling | 지원 모델에서 자동 | --tool-call-parser로 세밀한 제어 |
| 성능 최적화 | 기본 | Continuous Batching, PagedAttention, Tensor Parallelism |
| guided decoding | 미지원 | 지원 (JSON schema 강제) |
| 적합한 환경 | 로컬 개발, 소규모 서비스 | B2B 프로덕션, 대규모 서빙 |
개발 단계에서는 Ollama로 빠르게 시작하고, 프로덕션으로 전환할 때 vLLM으로 옮기는 것이 일반적인 경로다.
레이어별 비교 - 누가 어디까지 담당하는가
1~7편에 걸쳐 확인한 레이어들을 누가 담당하는가 관점에서 조합별로 비교한다. 위에서 아래로 처리 순서다:
| 레이어 | Claude/GPT API | Qwen3 + vLLM (parser O) | Qwen3 + Ollama | 범용 모델 + 7편 Middleware | + Vercel AI SDK 추가 시 |
|---|---|---|---|---|---|
| 프롬프트 조립 (2편) | API 서버 | vLLM | Ollama | Middleware | 변화 없음 (SDK가 할 수 없음) |
| Tokenization (3편) | API 서버 | vLLM | Ollama | 서빙 프레임워크 | 변화 없음 |
| Constrained Decoding (4편) | API 서버 | vLLM (guided decoding) | 없음 | 없음 | 변화 없음 |
| 출력 파싱 (4편) | API 서버 | vLLM (hermes parser) | Ollama | Middleware | 변화 없음 (SDK가 할 수 없음) |
| Tool 실행 루프 (5편) | 애플리케이션 (또는 MCP 서버) | 애플리케이션 (또는 MCP 서버) | 애플리케이션 (또는 MCP 서버) | 애플리케이션 (또는 MCP 서버) | Vercel AI SDK가 자동화 |
| 모델 추론 (4편) | 모델 (fine-tuned) | 모델 (fine-tuned) | 모델 (fine-tuned) | 모델 + Middleware (in-context learning) | 변화 없음 |
핵심 인사이트: Qwen3 + vLLM/Ollama 조합에서는 서빙 프레임워크가 대부분의 레이어를 처리하므로, 7편 Middleware가 불필요하다.
마지막 열 ”+ Vercel AI SDK 추가 시”는 어떤 조합에든 적용된다. Vercel AI SDK가 바꾸는 것은 “Tool 실행 루프” 한 줄뿐이다. 나머지 레이어는 SDK가 처리할 수 없으므로 변화가 없다. SDK 유무를 포함한 전체 6가지 조합은 아래 “모든 조합 한눈에 보기”에서 종합 정리한다.
모델 추론 - “다음 토큰 예측”
표의 마지막 행 “모델 추론”이 무엇을 하는지 명확히 하자. 4편에서 다뤘듯이, 모델이 하는 일은 딱 하나: 다음 토큰 예측의 반복이다.
모델은 “자연어 답변”과 “tool call”을 구분하지 않는다. 확률이 가장 높은 토큰을 계속 생성할 뿐이다. fine-tuned 모델은 tool call 패턴(<tool_call>{...}</tool_call>)을 생성할 확률이 높고, 미학습 모델은 일반 텍스트를 생성할 확률이 높다.
표에서 “모델 + Middleware (in-context learning)“라고 쓴 이유:
- Fine-tuned 모델: tool call 패턴이 모델 가중치에 이미 학습 → 모델 자체의 역할
- 범용 모델 + Middleware: 시스템 프롬프트에 tool 정의를 삽입하여 모델이 따르도록 유도 → **Middleware(프롬프트 조립)**가 활성화하는 역할
이 차이는 4편의 Fine-tuning vs In-context learning 비교에서 상세히 다뤘다.
Vercel AI SDK의 위치 - 서빙 프레임워크와 애플리케이션 사이의 편의 도구
Vercel AI SDK는 모델을 실행하는 도구가 아니다. vLLM/Ollama처럼 모델 가중치를 로드하거나, 프롬프트를 조립하거나, 출력을 파싱하는 기능이 없다. Vercel AI SDK 혼자서는 Qwen3를 실행할 수 없고, 반드시 서빙 프레임워크(vLLM/Ollama) 또는 API 서버(Claude/GPT)가 필요하다.
flowchart TB subgraph 서빙["서빙 프레임워크 (Ollama / vLLM)"] direction LR S1[모델 실행] --- S2[프롬프트 조립] --- S3[출력 파싱] end subgraph SDK["Vercel AI SDK (npm 패키지)"] direction LR V1[API 호출] --- V2[Tool 실행 루프 자동화] end subgraph App["애플리케이션 코드 (개발자 작성)"] direction LR A1[Tool 함수 정의] --- A2[사용자 인터페이스] end 서빙 <-->|OpenAI 호환 HTTP API| SDK SDK <--> App
그러면 무엇을 하는가? Tool 실행 루프의 자동화와 함께, 개발 편의 기능을 제공하는 클라이언트 라이브러리다:
| Vercel AI SDK 기능 | 하는 일 |
|---|---|
| Tool 실행 루프 자동화 | stopWhen: stepCountIs(5)로 tool_calls 감지 → 실행 → 피드백 반복을 자동 처리 |
| Provider 통합 | ollama("qwen3:8b") → anthropic("claude-opus-4-6") 모델 교체 시 코드 2줄만 변경 |
| 스트리밍 | streamText로 토큰 단위 실시간 응답 |
| 구조화된 출력 | generateObject로 JSON Schema 준수 데이터 생성 |
| Middleware 적용 | wrapLanguageModel로 7편의 middleware 패키징 |
| UI 훅 | useChat으로 채팅 인터페이스 자동 구성 |
Vercel AI SDK 없이 (직접 HTTP 요청):
// 1. Ollama API에 직접 요청
const response = await fetch("http://localhost:11434/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "qwen3:8b",
messages: [{ role: "user", content: "서울 날씨 알려줘" }],
tools: [{ type: "function", function: { name: "get_weather", parameters: { /*...*/ } } }],
}),
});
const data = await response.json();
// 2. tool_calls가 있으면 tool 실행 → 결과를 다시 요청 → 반복
// 이 반복 로직을 매번 직접 작성해야 함
if (data.choices[0].message.tool_calls) {
const toolCall = data.choices[0].message.tool_calls[0];
const result = await getWeather(toolCall.function.arguments);
// 다시 fetch... 또 tool_calls 확인... 또 실행... 반복
}Vercel AI SDK 사용 시:
import { generateText, tool, stepCountIs } from "ai";
import { createOllama } from "ollama-ai-provider";
const ollama = createOllama();
// 위의 반복 로직이 stopWhen 한 줄로 자동화
const result = await generateText({
model: ollama("qwen3:8b"),
tools: {
get_weather: tool({
description: "Get the current weather",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => ({ temperature: "15°C" }),
}),
},
stopWhen: stepCountIs(5),
prompt: "서울 날씨 알려줘",
});핵심: Vercel AI SDK는 vLLM/Ollama의 대안이 아니라, 위에 얹는 편의 도구다. 없어도 동작하지만, 있으면 Tool 실행 루프와 모델 교체가 편해진다.
각 구성요소의 역할 정리
이 시리즈에서 등장한 구성요소들이 많다. 각각의 역할을 비유와 함께 정리한다:
| 구성요소 | 비유 | 역할 |
|---|---|---|
| 서빙 프레임워크 (Ollama/vLLM) | 엔진 (+ 통역사 겸임 가능) | 모델을 로드하고 실행하는 환경. tool parser가 있으면 프롬프트 조립 + 출력 파싱까지 처리 (통역사 겸임) |
| Middleware (7편) | 통역사 | LLM이 이해할 수 있도록 프롬프트를 조립하고, LLM 출력에서 tool call 요청을 추출. 서빙 프레임워크에 통역 기능이 없을 때 필요 |
| Vercel AI SDK | 진행자 (없어도 동작) | tool 실행 루프를 자동화. 없으면 직접 구현(7편의 toolCallingLoop). 있으면 모델 교체, 스트리밍 등이 편해짐 |
| Tool 함수 / MCP 서버 | 실무자 | 실제 일을 하는 주체. 날씨 API 호출, 파일 읽기 등 구체적 작업 수행 |
| 모델 가중치 (Qwen3 등) | 두뇌 | 다음 토큰을 예측하는 연산. fine-tuning 여부가 tool call 정확도를 결정 |
핵심 관계:
- 서빙 프레임워크(tool parser O) = 엔진 + 통역사 → Middleware 불필요
- 서빙 프레임워크(tool parser X) = 엔진만 → Middleware(통역사)가 별도로 필요
- 진행자(SDK) → 없어도 동작하지만, 있으면 실행 루프가 편해짐
이 구성요소들이 협력하는 흐름을 두 가지 경우로 나눠본다:
서빙 프레임워크에 통역 기능이 없는 경우 (Middleware 필요):
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 participant SDK as Vercel AI SDK<br/>(진행자) participant MW as Middleware<br/>(통역사) participant Tool as Tool 함수<br/>(실무자) end box 서빙 프레임워크 (엔진만) participant LLM as 모델 가중치<br/>(두뇌) end User->>SDK: "서울 날씨 알려줘" SDK->>MW: tool 정의 + 질문 전달 Note over MW: 통역사: tool JSON → 프롬프트 텍스트 MW->>LLM: 프로토콜에 맞춘 프롬프트 LLM-->>MW: "...<tool_call>get_weather...</tool_call>" Note over MW: 통역사: 텍스트 → tool call 요청 추출 MW-->>SDK: tool_calls: get_weather("Seoul") Note over SDK: 진행자: tool 실행 루프 자동화 SDK->>Tool: get_weather("Seoul") 실행 Tool-->>SDK: {temperature: "15°C"} SDK->>MW: tool 결과 피드백 MW->>LLM: 결과 포함 프롬프트 LLM-->>MW: 최종 응답 MW-->>SDK: "서울의 현재 날씨는 15°C입니다" SDK-->>User: 최종 응답
서빙 프레임워크에 통역 기능이 있는 경우 (Middleware 불필요):
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 participant SDK as Vercel AI SDK<br/>(진행자) participant Tool as Tool 함수<br/>(실무자) end box 서빙 프레임워크 (엔진 + 통역사) participant API as vLLM/Ollama<br/>(프롬프트 조립 + 출력 파싱) participant LLM as 모델 가중치<br/>(두뇌) end User->>SDK: "서울 날씨 알려줘" SDK->>API: tools JSON + 질문 (OpenAI 호환 API) API->>LLM: 프롬프트 조립 → 추론 LLM-->>API: 출력 파싱 → tool_calls 추출 API-->>SDK: tool_calls: get_weather("Seoul") Note over SDK: 진행자: tool 실행 루프 자동화 SDK->>Tool: get_weather("Seoul") 실행 Tool-->>SDK: {temperature: "15°C"} SDK->>API: tool 결과 피드백 API->>LLM: 추론 LLM-->>API: 최종 응답 API-->>SDK: "서울의 현재 날씨는 15°C입니다" SDK-->>User: 최종 응답
진행자(SDK) 없이, 서빙 프레임워크(엔진 + 통역사)를 직접 사용하는 경우:
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 (직접 구현) participant App as 애플리케이션 코드 participant Tool as Tool 함수<br/>(실무자) end box 서빙 프레임워크 (엔진 + 통역사) participant API as vLLM/Ollama participant LLM as 모델 가중치<br/>(두뇌) end User->>App: "서울 날씨 알려줘" App->>API: POST /v1/chat/completions (tools + 질문) API->>LLM: 프롬프트 조립 → 추론 → 출력 파싱 API-->>App: tool_calls: get_weather("Seoul") Note over App: 직접 구현: tool_calls 확인 → tool 실행 App->>Tool: get_weather("Seoul") 실행 Tool-->>App: {temperature: "15°C"} App->>API: tool 결과 포함 재요청 API->>LLM: 추론 API-->>App: "서울의 현재 날씨는 15°C입니다" Note over App: 직접 구현: tool_calls 없음 → 루프 종료 App-->>User: 최종 응답
진행자(SDK) 없이, Middleware(통역사)를 직접 사용하는 경우 (7편에서 구현한 방식):
sequenceDiagram autonumber participant User as 사용자 box 애플리케이션 (직접 구현) participant App as 애플리케이션 코드 participant MW as Middleware<br/>(통역사) participant Tool as Tool 함수<br/>(실무자) end box 서빙 프레임워크 (엔진만) participant LLM as 모델 가중치<br/>(두뇌) end User->>App: "서울 날씨 알려줘" App->>MW: tool 정의 + 질문 전달 Note over MW: 통역사: tool JSON → 프롬프트 텍스트 MW->>LLM: 프로토콜에 맞춘 프롬프트 LLM-->>MW: "...<tool_call>get_weather...</tool_call>" Note over MW: 통역사: 텍스트 → tool call 요청 추출 MW-->>App: tool_calls: get_weather("Seoul") Note over App: 직접 구현: tool_calls 확인 → tool 실행 App->>Tool: get_weather("Seoul") 실행 Tool-->>App: {temperature: "15°C"} App->>MW: tool 결과 피드백 MW->>LLM: 결과 포함 프롬프트 LLM-->>MW: 최종 응답 MW-->>App: "서울의 현재 날씨는 15°C입니다" Note over App: 직접 구현: tool_calls 없음 → 루프 종료 App-->>User: 최종 응답
4가지 다이어그램의 차이를 정리하면:
| 조합 | 통역 (프롬프트 조립 + 출력 파싱) | 실행 루프 |
|---|---|---|
| Middleware + SDK | Middleware | Vercel AI SDK (자동) |
| 서빙FW(parser O) + SDK | 서빙 프레임워크 | Vercel AI SDK (자동) |
| 서빙FW(parser O) + 직접 구현 | 서빙 프레임워크 | 애플리케이션 코드 (직접) |
| Middleware + 직접 구현 (7편) | Middleware | 애플리케이션 코드 (직접) |
Middleware는 tool을 실행하지 않는다. LLM 출력에서 “get_weather를 Seoul로 호출해야 한다”는 요청을 추출하는 것까지가 역할이다. 실제 실행은 진행자(SDK) 또는 애플리케이션 코드가 실무자(Tool 함수/MCP 서버)에게 위임한다.
모든 조합 한눈에 보기
위 4가지 다이어그램에 Claude/GPT API 케이스까지 포함한 모든 가능한 조합이다. 서빙FW는 Ollama 또는 vLLM이다:
| 레이어 | ① Claude/GPT API | ② Claude/GPT API + SDK | ③ Ollama/vLLM (parser O) | ④ Ollama/vLLM (parser O) + SDK | ⑤ Ollama/vLLM (parser X) + MW | ⑥ Ollama/vLLM (parser X) + MW + SDK |
|---|---|---|---|---|---|---|
| 프롬프트 조립 | API 서버 | API 서버 | Ollama/vLLM | Ollama/vLLM | Middleware | Middleware |
| Tokenization | API 서버 | API 서버 | Ollama/vLLM | Ollama/vLLM | Ollama/vLLM | Ollama/vLLM |
| Constrained Decoding | API 서버 | API 서버 | Ollama/vLLM | Ollama/vLLM | 없음 | 없음 |
| 출력 파싱 | API 서버 | API 서버 | Ollama/vLLM | Ollama/vLLM | Middleware | Middleware |
| Tool 실행 루프 | 직접 구현 | Vercel AI SDK | 직접 구현 | Vercel AI SDK | 직접 구현 | Vercel AI SDK |
| 모델 추론 | 모델 (fine-tuned) | 모델 (fine-tuned) | 모델 (fine-tuned) | 모델 (fine-tuned) | 모델 (in-context learning) | 모델 (in-context learning) |
패턴: SDK 유무로 짝을 이룬다 (①②, ③④, ⑤⑥). 각 짝에서 바뀌는 것은 “Tool 실행 루프” 한 줄뿐이다. 실질적으로 다른 조합은 3가지이고, SDK 추가는 항상 같은 효과(Tool 실행 루프 자동화)다.
”Claude Opus 4.6”은 어디까지인가
Claude/GPT 같은 상용 모델도 구조는 동일하다. Anthropic도 자신들의 모델(Opus 가중치) 위에 서빙 레이어를 만든 것이다.
Anthropic 공식 문서에 따르면:
“When you send a message with the tools parameter, the API constructs a formatted system prompt with your tool definitions”
“Claude Opus 4.6 — 346 tokens for tool use system prompt”
tools를 보내면 API 서버가 346개 토큰의 시스템 프롬프트를 자동 구성한다. 이것이 2편에서 본 "In this environment you have access to..." 텍스트다.
claude-opus-4-6이라는 model ID가 가리키는 범위:
"claude-opus-4-6" (API를 통해 사용할 때)
├── 프롬프트 조립 ← API 서버가 처리 (346 토큰 시스템 프롬프트)
├── Tokenization ← API 서버가 처리
├── 모델 추론 ← Opus 4.6 모델 가중치 (fine-tuned)
├── Constrained Decoding ← API 서버가 처리 (strict: true)
└── 출력 파싱 ← API 서버가 처리 (stop_reason: "tool_use")
포함하지 않는 것:
└── Tool 실행 루프 ← 개발자의 애플리케이션 / MCP 서버
Qwen3와 비교하면:
Claude Opus 4.6 API ≒ Qwen3 모델 + vLLM(--tool-call-parser hermes)
둘 다 “모델 가중치 + 서빙 레이어”인데, Claude는 Anthropic이 묶어서 제공하고, Qwen3는 개발자가 직접 조합하는 차이다.
개발자가 model ID를 claude-opus-4-6에서 claude-sonnet-4-5로 변경하면, 모델 가중치뿐 아니라 모든 서빙 레이어도 해당 모델에 맞게 변경된다. 공식 문서에서 모델별 시스템 프롬프트 토큰 수가 다르다는 것이 이 증거다:
| 모델 | tool use 시스템 프롬프트 토큰 |
|---|---|
| Opus 4.6 | 346 토큰 |
| Haiku 3.5 | 264 토큰 |
| Claude 3 Opus (deprecated) | 530 토큰 |
그러면 7편의 Middleware는 필요 없는 건가?
vLLM/Ollama의 tool parser를 사용하면, 7편에서 직접 구현한 프롬프트 조립과 출력 파싱이 필요 없다. 서빙 프레임워크가 서빙 레벨에서 이 역할을 대신하기 때문이다.
하지만 7편의 Middleware가 여전히 필요한 경우:
- vLLM의 tool parser가 지원하지 않는 모델을 사용할 때
- 서빙 프레임워크 없이 HuggingFace transformers로 직접 추론할 때
- 커스텀 프로토콜이 필요할 때 (XML, YAML-XML 등)
- Ollama의 tool calling이 지원되지 않는 모델에서 tool calling을 사용할 때
Middleware가 필요한지 판단하는 흐름:
flowchart TD A[모델이 tool calling<br/>fine-tuning 되어 있는가?] -->|Yes| B[서빙 프레임워크가<br/>tool parser를 지원하는가?] A -->|No| D[7편 Middleware 필요<br/>in-context learning에 의존<br/>정확도 낮음] B -->|"Yes (vLLM --tool-call-parser<br/>또는 Ollama 지원 모델)"| C[Middleware 불필요<br/>서빙 프레임워크가 처리<br/>Native처럼 사용] B -->|No| E[7편 Middleware 필요<br/>프롬프트 조립 + 출력 파싱]
chat template 확인 방법
2편에서 chat template이 JSON을 모델이 이해하는 텍스트로 변환한다고 했다. 오픈소스 모델에서는 이 template을 직접 확인하고 커스터마이징할 수 있다.
HuggingFace에서 확인
모델의 tokenizer_config.json 파일에 chat template이 Jinja2 형식으로 저장되어 있다:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
# chat template 텍스트 확인
print(tokenizer.chat_template)
# 실제 변환 결과 확인 (3편에서 본 코드)
messages = [{"role": "user", "content": "서울 날씨 알려줘"}]
text = tokenizer.apply_chat_template(messages, tokenize=False)
print(text)tool calling용 chat template 확인
Hermes 2 Pro처럼 tool calling 전용 chat template이 별도로 있는 모델도 있다:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("NousResearch/Hermes-2-Pro-Llama-3-8B")
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"location": {"type": "string"}}}
}
}]
messages = [{"role": "user", "content": "서울 날씨 알려줘"}]
# tool_use 전용 chat template 적용
text = tokenizer.apply_chat_template(
messages,
chat_template="tool_use", # tool calling 전용 template
tools=tools,
tokenize=False,
)
print(text)
# → <tools>[...]</tools> ... <tool_call>... 형식의 프롬프트이 출력이 7편에서 assembleToolPrompt 함수가 만들던 것과 동일한 역할이다. 모델에 내장된 chat template이 프롬프트 조립을 자동으로 처리한다.
vLLM에서의 chat template
vLLM은 모델의 tokenizer_config.json에서 chat template을 자동 로드한다. 별도 설정 없이 vllm serve만 하면 된다. 커스텀 template이 필요하면 --chat-template 옵션으로 Jinja2 파일을 지정할 수 있다.
추천: MVP로 빠르게 시작하려면
6가지 조합 중 MVP/POC에 가장 빠른 조합은 ④ Qwen3 + Ollama + Vercel AI SDK다:
| 단계 | 소요 시간 | 명령어 |
|---|---|---|
| Ollama 설치 | 1분 | brew install ollama |
| 모델 다운로드 | 5~10분 | ollama pull qwen3:8b |
| SDK 설치 | 1분 | npm install ai ollama-ai-provider zod |
| 코드 작성 | 10분 | generateText + tool 정의 |
| 총 | ~20분 | tool calling POC 동작 |
이 조합의 장점:
- Middleware 불필요: Ollama가 통역사 겸임 (프롬프트 조립 + 출력 파싱)
- 실행 루프 자동: SDK가 진행자 역할 (
stopWhen) - 개발자는 Tool 함수만 작성: 실무자(get_weather 등)만 구현하면 끝
- 무료: 로컬 실행, API 비용 없음
다음 편에서 이 조합으로 실제 에이전트를 구현한다.
다음 편: 이 둘을 합치면?
이 글에서 오픈소스 모델을 로컬에 서빙하는 방법을 확인했다. Ollama로 빠르게 시작하고, vLLM으로 프로덕션에 배포할 수 있다. 그리고 서빙 프레임워크의 tool parser가 7편 Middleware의 역할을 대신할 수 있다는 것도 확인했다.
이제 두 가지가 준비되었다:
- 7편: Middleware (프롬프트 조립 + 출력 파싱 + 실행 루프)
- 8편: 로컬 모델 서빙 (Ollama / vLLM)
다음 편에서는 Qwen3 + Ollama + Vercel AI SDK 조합으로 시작하여, MCP server 연결까지 포함한 tool calling이 동작하는 나만의 에이전트를 완성한다.
참고 문서
- Ollama 공식 문서 - 설치, 모델 다운로드, API 사용법
- Ollama Tool Calling - Ollama의 내장 tool calling 지원 모델 및 사용법
- vLLM - Tool Calling - —tool-call-parser, —enable-auto-tool-choice, 지원 파서 목록
- vLLM Quickstart - 설치 및 vllm serve 사용법
- NousResearch Hermes 2 Pro - tool calling fine-tuned 모델, 90% 정확도, chat_template=“tool_use”
- Qwen3 Function Calling - Qwen3의 Hermes-style tool use, vLLM 서빙 가이드
- Anthropic - Tool Use Overview - Claude API의 tool use 구조, 모델별 시스템 프롬프트 토큰 수
- HuggingFace Chat Templates - apply_chat_template, tokenizer_config.json 구조