[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-R1qwen3_reasoning_parser.py- Qwen3gemma4_reasoning_parser.py- Gemma 4mistral_reasoning_parser.py- Mistralkimi_k2_reasoning_parser.py- Kimi K2granite_reasoning_parser.py- IBM Granitehunyuan_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) 도구 서버와 연동하여, 추론 파서가 구조화된 출력의 시작/종료 태그를 동적으로 결정할 수 있다.
왜 이 설계인가
-
모델별 파서 분리: 각 모델 제공자가 자체적인 추론/도구 호출 형식을 사용하므로, 하나의 범용 파서로는 대응이 불가능하다. 모델별 파서를 분리하여 각자의 토큰화 방식과 형식 규칙을 정확하게 처리한다.
-
지연 로딩의 필요성: 30개 이상의 파서를 모두 즉시 로딩하면, 각 파서가 import하는 의존성(토크나이저 유틸, regex 등)으로 인해 시작 시간이 길어진다. 지연 로딩으로 실제 사용하는 파서만 로딩한다.
-
스트리밍/비스트리밍 분리: 비스트리밍은 완전한 출력을 한 번에 파싱하면 되지만, 스트리밍은 delta 단위로 파싱하면서 상태를 유지해야 한다. 이 두 인터페이스를 별도로 정의하여, 스트리밍 특화 최적화(예: 토큰 단위 검사)가 가능하다.
참고 자료
관련 포스트
vLLM 의 다른글
- 이전글 [vLLM] Compilation Fusion Passes: 컴파일 퓨전 최적화
- 현재글 : [vLLM] Reasoning & Tool Calling: 추론 파서와 도구 호출 파서
- 다음글 [vLLM] Multimodal: Vision, Audio, Video 처리 파이프라인
댓글