시리즈: 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 = 에이전트 완성

  • 오픈소스 모델 로컬 구축은 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형식라이선스
Qwen30.6B ~ 235B완료HermesApache 2.0
Hermes 2 Pro8B완료 (90% 정확도)HermesLlama 3 Community
Llama 3.18B, 70B, 405B완료JSONLlama 3.1 Community
Mistral Nemo12B완료Mistral formatApache 2.0
DeepSeek V3671B (MoE)완료JSONMIT

선택 기준 우선순위:

  1. fine-tuning 여부: tool calling fine-tuning이 안 된 모델은 in-context learning에만 의존하므로 정확도가 낮다. Hermes 2 Pro의 90% vs 일반 모델의 불확실한 성공률
  2. 형식 호환성: 7편에서 선택한 Hermes 프로토콜과 맞는 모델이면 가장 좋다. Qwen3, Hermes 2 Pro가 해당
  3. 크기 vs 하드웨어: 8B 모델은 ~5GB VRAM (4-bit 양자화), 70B 모델은 ~40GB VRAM. 로컬 개발이면 8B가 현실적
  4. 라이선스: 상업적 사용이 필요하면 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이 지원하는 파서 목록:

파서대상 모델형식
hermesQwen3, Hermes 2 Pro<tool_call> + JSON
mistralMistral, Mixtral[TOOL_CALLS] + JSON
llama3_jsonLlama 3.1JSON 직접 출력
deepseek_v3DeepSeek V3/R1DeepSeek 고유 형식
internlmInternLMInternLM 형식

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 비교

OllamavLLM
목적개발/프로토타입프로덕션 배포
설치brew install ollamapip install vllm + GPU 드라이버
모델 다운로드ollama pull qwen3:8bHuggingFace에서 자동 다운로드
APIlocalhost:11434/v1localhost:8000/v1
tool calling지원 모델에서 자동--tool-call-parser로 세밀한 제어
성능 최적화기본Continuous Batching, PagedAttention, Tensor Parallelism
guided decoding미지원지원 (JSON schema 강제)
적합한 환경로컬 개발, 소규모 서비스B2B 프로덕션, 대규모 서빙

개발 단계에서는 Ollama로 빠르게 시작하고, 프로덕션으로 전환할 때 vLLM으로 옮기는 것이 일반적인 경로다.

레이어별 비교 - 누가 어디까지 담당하는가

1~7편에 걸쳐 확인한 레이어들을 누가 담당하는가 관점에서 조합별로 비교한다. 위에서 아래로 처리 순서다:

레이어Claude/GPT APIQwen3 + vLLM (parser O)Qwen3 + Ollama범용 모델 + 7편 Middleware+ Vercel AI SDK 추가 시
프롬프트 조립 (2편)API 서버vLLMOllamaMiddleware변화 없음 (SDK가 할 수 없음)
Tokenization (3편)API 서버vLLMOllama서빙 프레임워크변화 없음
Constrained Decoding (4편)API 서버vLLM (guided decoding)없음없음변화 없음
출력 파싱 (4편)API 서버vLLM (hermes parser)OllamaMiddleware변화 없음 (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 + SDKMiddlewareVercel 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/vLLMOllama/vLLMMiddlewareMiddleware
TokenizationAPI 서버API 서버Ollama/vLLMOllama/vLLMOllama/vLLMOllama/vLLM
Constrained DecodingAPI 서버API 서버Ollama/vLLMOllama/vLLM없음없음
출력 파싱API 서버API 서버Ollama/vLLMOllama/vLLMMiddlewareMiddleware
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.6346 토큰
Haiku 3.5264 토큰
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이 동작하는 나만의 에이전트를 완성한다.

참고 문서