본문으로 건너뛰기

[vLLM] Reasoning & Tool Calling: 추론 파서와 도구 호출 파서

들어가며

최신 LLM들은 단순 텍스트 생성을 넘어, 추론(reasoning) 과정을 <think>...</think> 태그로 노출하거나, 도구 호출(tool calling)을 JSON 형식으로 출력한다. vLLM은 vllm/reasoning/vllm/tool_parsers/에서 모델별로 다른 이 출력 형식을 통합적으로 파싱하는 시스템을 제공한다.

공식 문서

vLLM 공식 문서: Reasoning Outputs vLLM 공식 문서: Tool Calling

핵심 구조/코드 분석

ReasoningParser 추상 클래스

class ReasoningParser:
    def __init__(self, tokenizer: "TokenizerLike", *args, **kwargs):
        self.model_tokenizer = tokenizer

    @abstractmethod
    def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
        """추론 콘텐츠가 끝났는지 확인"""

    @abstractmethod
    def extract_reasoning(self, model_output: str, request) -> tuple[str | None, str | None]:
        """완전한 출력에서 추론 콘텐츠와 일반 콘텐츠를 분리"""

    @abstractmethod
    def extract_reasoning_streaming(self, previous_text, current_text, delta_text, ...):
        """스트리밍 중 추론 콘텐츠 추출"""

핵심 메서드는 세 가지다: 추론 종료 감지(is_reasoning_end), 완전 응답 파싱(extract_reasoning), 스트리밍 파싱(extract_reasoning_streaming). xgrammar 같은 구조화된 출력 엔진이 is_reasoning_end를 사용하여 추론 구간에서는 문법 제약을 적용하지 않는다.

ReasoningParserManager: 지연 로딩 레지스트리

class ReasoningParserManager:
    reasoning_parsers: dict[str, type[ReasoningParser]] = {}
    lazy_parsers: dict[str, tuple[str, str]] = {}

    @classmethod
    def get_reasoning_parser(cls, name: str) -> type[ReasoningParser]:
        if name in cls.reasoning_parsers:
            return cls.reasoning_parsers[name]
        if name in cls.lazy_parsers:
            return cls._load_lazy_parser(name)  # 첫 접근 시 import
        raise KeyError(f"Reasoning parser '{name}' not found.")

지연 로딩(lazy loading)을 지원하여, 실제로 사용될 때만 해당 파서 모듈을 import한다. 20개 이상의 파서가 있으므로 시작 시간 절약에 효과적이다.

지원되는 추론 파서들

vllm/reasoning/ 디렉토리에는 다양한 모델 전용 파서가 있다:

  • deepseek_r1_reasoning_parser.py - DeepSeek-R1
  • qwen3_reasoning_parser.py - Qwen3
  • gemma4_reasoning_parser.py - Gemma 4
  • mistral_reasoning_parser.py - Mistral
  • kimi_k2_reasoning_parser.py - Kimi K2
  • granite_reasoning_parser.py - IBM Granite
  • hunyuan_a13b_reasoning_parser.py - Hunyuan A13B
  • 그 외 다수

도구 호출 파서 시스템

vllm/tool_parsers/에도 유사한 구조로 30개 이상의 파서가 있다:

hermes_tool_parser.py          # Hermes 형식
llama_tool_parser.py           # LLaMA
mistral_tool_parser.py         # Mistral
deepseekv3_tool_parser.py      # DeepSeek V3
openai_tool_parser.py          # OpenAI 호환
pythonic_tool_parser.py        # Pythonic 형식
qwen3xml_tool_parser.py        # Qwen3 XML
gemma4_tool_parser.py          # Gemma 4

각 모델이 도구 호출을 표현하는 방식이 다르다. Hermes는 <tool_call> XML 태그를, Mistral은 [TOOL_CALLS] 토큰을, DeepSeek는 JSON 블록을 사용한다.

추론 토큰 카운팅

def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
    """시퀀스에서 추론 토큰 수를 카운트.
    기본 구현은 0을 반환하여 기존 파서는 변경 없이 동작."""
    return 0

추론 토큰 카운팅은 옵트인 방식이다. 이를 구현하면 API 응답에 추론 토큰과 출력 토큰을 분리하여 보고할 수 있다.

구조화된 태그 준비

def prepare_structured_tag(self, original_tag, tool_server):
    """구조화된 출력용 태그를 준비. 기본값은 None."""
    return None

MCP(Model Context Protocol) 도구 서버와 연동하여, 추론 파서가 구조화된 출력의 시작/종료 태그를 동적으로 결정할 수 있다.

왜 이 설계인가

  1. 모델별 파서 분리: 각 모델 제공자가 자체적인 추론/도구 호출 형식을 사용하므로, 하나의 범용 파서로는 대응이 불가능하다. 모델별 파서를 분리하여 각자의 토큰화 방식과 형식 규칙을 정확하게 처리한다.

  2. 지연 로딩의 필요성: 30개 이상의 파서를 모두 즉시 로딩하면, 각 파서가 import하는 의존성(토크나이저 유틸, regex 등)으로 인해 시작 시간이 길어진다. 지연 로딩으로 실제 사용하는 파서만 로딩한다.

  3. 스트리밍/비스트리밍 분리: 비스트리밍은 완전한 출력을 한 번에 파싱하면 되지만, 스트리밍은 delta 단위로 파싱하면서 상태를 유지해야 한다. 이 두 인터페이스를 별도로 정의하여, 스트리밍 특화 최적화(예: 토큰 단위 검사)가 가능하다.

참고 자료

댓글

관련 포스트

vLLM 의 다른글