추론 출력

추론 출력 (Reasoning Outputs)

vLLM은 DeepSeek R1 같은 추론 모델을 지원합니다. 이 모델들은 추론 단계와 최종 결론을 모두 포함하는 출력을 생성하도록 설계됐어요.

추론 모델은 출력에 추가 reasoning 필드를 반환합니다. 이 필드는 최종 결론에 이르는 추론 단계를 담고 있으며, 다른 모델의 출력에는 존재하지 않습니다.

경고: reasoning 은 이전에 reasoning_content 라고 불렸습니다. 마이그레이션하려면 reasoning_contentreasoning 으로 직접 바꾸세요. 클라이언트 코드도 함께 업데이트하는 것이 중요합니다. 그렇지 않으면 reasoning 이 채워져 있어도 클라이언트 코드가 빈 reasoning_content 를 조용히 읽을 수 있습니다.

출처: 문서

본문

지원 모델 (Supported Models)

vLLM은 현재 다음 추론 모델을 지원합니다.

모델 시리즈 파서 이름 (Parser Name) 구조적 출력 지원 도구 호출
Cohere Command A Reasoning cohere_command3 json, regex
Cohere Command A Plus cohere_command4 json, regex
DeepSeek R1 series deepseek_r1 json, regex
Gemma 4 series gemma4 json, regex
DeepSeek-V3.1 deepseek_v3 json, regex
ERNIE-4.5-VL series ernie45 json, regex
ERNIE-4.5-21B-A3B-Thinking ernie45 json, regex
GLM-4.5 series glm45 json, regex
Holo2 series holo2 json, regex
Hunyuan A13B series hunyuan_a13b json, regex
IBM Granite 3.2 language models granite
MiniMax-M2 minimax_m2_append_think json, regex
Qwen3 series qwen3 json, regex
QwQ-32B deepseek_r1 json, regex

참고: IBM Granite 3.2와 DeepSeek-V3.1 추론은 기본적으로 비활성화되어 있습니다. 활성화하려면 chat_template_kwargsthinking=True 도 전달해야 해요. Qwen3 시리즈의 추론 기능은 기본적으로 활성화되어 있습니다. 비활성화하려면 chat_template_kwargsenable_thinking=False 를 전달하세요. Gemma 4 추론은 기본적으로 비활성화되어 있으며, chat_template_kwargsenable_thinking=True 를 전달하거나 reasoning_effort 를 설정하면(자동 활성화) 활성화됩니다. DeepSeek-V3.1 도구 호출은 비-thinking 모드에서 지원됩니다. Holo2 추론은 기본적으로 활성화되어 있으며, 비활성화하려면 chat_template_kwargsthinking=False 도 전달해야 합니다.

퀵스타트 (Quickstart)

추론 모델을 사용하려면 채팅 완료 엔드포인트에 요청할 때 --reasoning-parser 플래그를 지정해야 해요. --reasoning-parser 플래그는 모델 출력에서 추론 콘텐츠를 추출하는 데 사용할 추론 파서를 지정합니다.

vllm serve deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B \
    --reasoning-parser deepseek_r1

그 다음 응답에 추론 콘텐츠를 반환해야 하는 모델에 요청을 보냅니다.

from openai import OpenAI

# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

models = client.models.list()
model = models.data[0].id

# Round 1
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
# For granite, add: `extra_body={"chat_template_kwargs": {"thinking": True}}`
# For Qwen3 series, if you want to disable thinking in reasoning mode, add:
# extra_body={"chat_template_kwargs": {"enable_thinking": False}}
response = client.chat.completions.create(model=model, messages=messages)

reasoning = response.choices[0].message.reasoning
content = response.choices[0].message.content

print("reasoning:", reasoning)
print("content:", content)

reasoning 필드는 최종 결론에 이르는 추론 단계를 담고, content 필드는 최종 결론을 담습니다.

스트리밍 채팅 완료 (Streaming chat completions)

추론 모델에 대한 스트리밍 채팅 완료도 지원됩니다. reasoning 필드는 채팅 완료 응답 청크delta 필드에서 사용할 수 있어요.

{
    "id": "chatcmpl-123",
    "object": "chat.completion.chunk",
    "created": 1694268190,
    "model": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
    "system_fingerprint": "fp_44709d6fcb",
    "choices": [
        {
            "index": 0,
            "delta": {
                "role": "assistant",
                "reasoning": "is",
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

OpenAI Python 클라이언트 라이브러리는 스트리밍 출력의 reasoning 속성을 공식적으로 지원하지 않습니다. 하지만 클라이언트는 응답의 추가 속성을 지원합니다. getattr 을 사용해 응답에 reasoning 속성이 있는지 확인할 수 있어요.

from openai import OpenAI

# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

models = client.models.list()
model = models.data[0].id

messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
# For granite, add: `extra_body={"chat_template_kwargs": {"thinking": True}}`
# For Qwen3 series, if you want to disable thinking in reasoning mode, add:
# extra_body={"chat_template_kwargs": {"enable_thinking": False}}
stream = client.chat.completions.create(
    model=model,
    messages=messages,
    stream=True,
)

print("client: Start streaming chat completions...")
printed_reasoning = False
printed_content = False

for chunk in stream:
    # Safely extract reasoning and content from delta,
    # defaulting to None if attributes don't exist or are empty strings
    reasoning = (
        getattr(chunk.choices[0].delta, "reasoning", None) or None
    )
    content = getattr(chunk.choices[0].delta, "content", None) or None

    if reasoning is not None:
        if not printed_reasoning:
            printed_reasoning = True
            print("reasoning:", end="", flush=True)
        print(reasoning, end="", flush=True)
    elif content is not None:
        if not printed_content:
            printed_content = True
            print("\ncontent:", end="", flush=True)
        # Extract and print the content
        print(content, end="", flush=True)

접근하기 전에 응답에 reasoning 이 존재하는지 확인하는 것을 잊지 마세요. 예시 를 확인할 수 있습니다.

도구 호출 (Tool Calling)

도구 호출과 추론 파서가 모두 활성화되어 있을 때도 추론 콘텐츠를 사용할 수 있어요. 또한 도구 호출은 content 필드에서만 함수를 파싱하며 reasoning 에서는 파싱하지 않습니다.

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City and state, e.g., 'San Francisco, CA'"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location", "unit"],
            }
        },
    }
]

response = client.chat.completions.create(
    model=client.models.list().data[0].id,
    messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}],
    tools=tools,
    tool_choice="auto",
)

print(response)
tool_call = response.choices[0].message.tool_calls[0].function

print(f"reasoning: {response.choices[0].message.reasoning}")
print(f"Function called: {tool_call.name}")
print(f"Arguments: {tool_call.arguments}")

더 많은 예시는 examples/reasoning/openai_chat_completion_tool_calls_with_reasoning.py 를 참고하세요.

서버 수준 기본 채팅 템플릿 Kwargs (Server-Level Default Chat Template Kwargs)

--default-chat-template-kwargs CLI 인자를 사용해 서버 수준에서 기본 chat_template_kwargs 를 설정할 수 있어요. 이는 클라이언트가 각 요청에서 지정할 필요 없이 모든 요청에 걸쳐 추론 동작을 구성할 때 유용합니다.

기본적으로 thinking 모드 비활성화 (Disabling Thinking Mode by Default)

Qwen3처럼 thinking이 기본 활성화된 모델의 경우 서버 전체에서 비활성화할 수 있습니다.

vllm serve Qwen/Qwen3-8B \
    --reasoning-parser qwen3 \
    --default-chat-template-kwargs '{"enable_thinking": false}'

기본적으로 thinking 모드 활성화 (Enabling Thinking Mode by Default)

IBM Granite 3.2나 DeepSeek-V3.1처럼 thinking이 기본 비활성화된 모델의 경우 서버 전체에서 활성화할 수 있습니다.

vllm serve ibm-granite/granite-3.2-2b-instruct \
    --reasoning-parser granite \
    --default-chat-template-kwargs '{"thinking": true}'

요청 수준 오버라이드 (Request-Level Override)

요청 수준 chat_template_kwargs 는 항상 서버 기본값보다 우선합니다. 예를 들어 서버가 enable_thinking=false 로 시작된 경우에도 클라이언트는 특정 요청에서 활성화할 수 있습니다.

response = client.chat.completions.create(
    model=model,
    messages=messages,
    extra_body={"chat_template_kwargs": {"enable_thinking": True}}  # Overrides server default
)

thinking 예산 제어 (Thinking Budget Control)

Qwen3, DeepSeek, Nemotron3 같은 일부 모델은 추론에 사용되는 최대 토큰 수를 제한하는 thinking 예산을 지원합니다.

토큰 카운트는 reasoning_start_str 부터 시작됩니다. 추론 토큰 수가 구성된 thinking_token_budget 에 도달하면 vLLM은 모델이 reasoning_end_str 을 생성하도록 강제하고, 효과적으로 reasoning 블록을 종료합니다.

이 기능을 사용하려면:

  • --reasoning-parser 가 추론 추출을 활성화합니다.
  • --reasoning-config 가 reasoning 경계 토큰(예: reasoning_start_str, reasoning_end_str)을 정의합니다. 설정하지 않으면 vLLM이 추론 파서에서 이 토큰들을 자동으로 초기화하려고 시도합니다.
  • thinking_token_budget(샘플링 파라미터)이 요청별 추론 토큰 제한을 설정합니다.

thinking_token_budget 를 지정하지 않으면 max_tokens 같은 일반 생성 제약 외에 명시적인 추론 제한이 적용되지 않습니다.

--reasoning-configReasoningConfig 에 해당하는 JSON 객체를 받습니다. 필드는 다음과 같습니다.

필드 타입 설명
reasoning_start_str str | null 추론 콘텐츠의 시작을 표시하는 문자열
reasoning_end_str str | null 추론 콘텐츠의 끝을 표시하는 문자열

참고: reasoning_end_str 에는 reasoning 종료 토큰 앞에 오는 전환 문구를 포함할 수 있습니다. 예를 들어 reasoning_end_str"I have to give the solution based on the reasoning directly now. response" 로 설정하면 예산이 소진될 때 모델이 그 문구를 출력하도록 지시해 추론 종료를 더 자연스럽게 만듭니다.

온라인 서빙 (Online Serving)

vllm serve Qwen/Qwen3-0.6B \
    --reasoning-parser qwen3 \
    --reasoning-config '{"reasoning_start_str": " thinking", "reasoning_end_str": "I have to give the solution based on the reasoning directly now. response"}'

그 다음 thinking_token_budget 으로 요청을 보내 추론 토큰을 제한합니다.

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3-0.6B",
    "messages": [
      { "role": "user", "content": "9.11 and 9.8, which is greater?" }
    ],
    "thinking_token_budget": 10
  }'

오프라인 추론 (Offline Inference)

from vllm import LLM, SamplingParams
from vllm.config import ReasoningConfig

llm = LLM(
    model="Qwen/Qwen3-0.6B",
    reasoning_config=ReasoningConfig(
        reasoning_start_str=" thinking",
        reasoning_end_str="I have to give the solution based on the thinking directly now. response",
    ),
)

sampling_params = SamplingParams(thinking_token_budget=10)

messages = [
    {"role": "user", "content": "9.11 and 9.8, which is greater?"},
]

outputs = llm.chat(messages, sampling_params=sampling_params)

for output in outputs:
    print("text:", output.outputs[0].text)

자동 enable_thinking 활성화 (Automatic enable_thinking Activation)

Gemma 4, DeepSeek-V4-Pro, IBM Granite 3.2 같은 일부 모델은 thinking 모드를 활성화하려면 채팅 템플릿 kwargs에 enable_thinking: true 가 필요합니다. 없으면 다른 설정과 무관하게 추론 토큰이 절대 생성되지 않습니다.

Chat Completions 요청에 reasoning_effort(또는 Responses API 요청에 reasoning.effort)를 설정하면 vLLM이 자동으로 enable_thinking 을 채팅 템플릿 kwargs에 주입합니다.

  • reasoning_effort = "low", "medium", 또는 "high"enable_thinking = true
  • reasoning_effort = "none"enable_thinking = false
  • reasoning_effort 미설정 → enable_thinking 이 주입되지 않음(기존 동작 보존)

reasoning_effort 를 사용할 때 chat_template_kwargs: {"enable_thinking": true} 를 수동으로 전달할 필요가 없습니다. 자동으로 처리됩니다.

참고: chat_template_kwargs 에서 enable_thinking 을 명시적으로 설정하면 사용자의 값이 자동 주입보다 우선합니다. 필요하면 동작을 오버라이드할 수 있어요. 템플릿이 enable_thinking 을 선언하지 않는 모델(예: DeepSeek R1)의 경우 주입된 kwarg는 resolve_chat_template_kwargs 에 의해 무해하게 걸러집니다.

예시:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

# reasoning_effort automatically enables thinking for models that need it
response = client.chat.completions.create(
    model="google/gemma-4-26B-A4B-it",
    messages=[{"role": "user", "content": "What is 15 * 37?"}],
    reasoning_effort="high",  # Automatically sets enable_thinking=true
)

print(response.choices[0].message.reasoning)
print(response.choices[0].message.content)

추론 출력 억제 (Suppressing Reasoning Output)

include_reasoning 파라미터로 API 응답에서 추론 콘텐츠를 억제할 수 있어요. false 로 설정하면 추론 토큰은 여전히 생성되지만(따라서 모델 품질은 영향을 받지 않음) 응답에서 제외됩니다. 이는 추론 동작을 바꾸지 않으면서 네트워크 트래픽을 줄입니다.

이 파라미터는 Chat Completions API와 Responses API 모두에서, 스트리밍 및 비스트리밍 요청에서 지원됩니다.

include_reasoning=false 일 때 vLLM은 per-token 메타데이터(logprobs와 토큰 IDs)도 억제해 로그프로브 항목의 디코딩된 토큰 텍스트나 원시 토큰 ID를 통한 추론 콘텐츠 유출을 방지합니다.

Chat Completions API

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
model = client.models.list().data[0].id

# Reasoning is included by default (include_reasoning=True)
response = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": "What is 15 * 37?"}],
    extra_body={"include_reasoning": False},
)

msg = response.choices[0].message
assert msg.content  # Content is still present
assert not getattr(msg, "reasoning", None)  # Reasoning is suppressed

스트리밍도 같은 방식으로 동작하며, reasoning 델타가 청크에서 생략됩니다.

stream = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": "What is 15 * 37?"}],
    stream=True,
    extra_body={"include_reasoning": False},
)

for chunk in stream:
    delta = chunk.choices[0].delta
    # delta.reasoning will always be None
    if delta.content:
        print(delta.content, end="", flush=True)

Responses API

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")

response = client.responses.create(
    model=client.models.list().data[0].id,
    input="What is 15 * 37?",
    include_reasoning=False,
)

# No "reasoning" items in output
types = [item.type for item in response.output]
assert "reasoning" not in types

제한 사항 (Limitations)

  • 추론 콘텐츠는 온라인 서빙의 채팅 완료 엔드포인트(/v1/chat/completions), Anthropic Messages API(/v1/messages), Responses API(/v1/responses)에서만 사용할 수 있습니다.

새 추론 모델 지원 방법 (How to support a new reasoning model)

vllm/reasoning/deepseek_r1_reasoning_parser.py 와 유사한 ReasoningParser 를 추가할 수 있습니다.

# import the required packages

from vllm.reasoning import ReasoningParser, ReasoningParserManager
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.engine.protocol import DeltaMessage

# define a reasoning parser and register it to vllm
# the name list in register_module can be used
# in --reasoning-parser.
class ExampleParser(ReasoningParser):
    def __init__(self, tokenizer: TokenizerLike):
        super().__init__(tokenizer)

    def extract_reasoning_streaming(
        self,
        previous_text: str,
        current_text: str,
        delta_text: str,
        previous_token_ids: Sequence[int],
        current_token_ids: Sequence[int],
        delta_token_ids: Sequence[int],
    ) -> DeltaMessage | None:
        """
        Instance method that should be implemented for extracting reasoning
        from an incomplete response; for use when handling reasoning calls and
        streaming. Has to be an instance method because  it requires state -
        the current tokens/diffs, but also the information about what has
        previously been parsed and extracted (see constructor)
        """

    def extract_reasoning(
        self,
        model_output: str,
        request: ChatCompletionRequest | ResponsesRequest,
    ) -> tuple[str | None, str | None]:
        """
        Extract reasoning content from a complete model-generated string.

        Used for non-streaming responses where we have the entire model response
        available before sending to the client.

        Parameters:
        model_output: str
            The model-generated string to extract reasoning content from.

        request: ChatCompletionRequest
            The request object that was used to generate the model_output.

        Returns:
        tuple[Optional[str], Optional[str]]
            A tuple containing the reasoning content and the content.
        """
# Register the reasoning parser
ReasoningParserManager.register_lazy_module(
    name="example",
    module_path="vllm.reasoning.example_reasoning_parser",
    class_name="ExampleParser",
)

추가로, 구조적 출력을 활성화하려면 vllm/reasoning/deepseek_r1_reasoning_parser.py 에 있는 것과 유사한 Reasoner 를 만들어야 합니다.

@dataclass
class DeepSeekReasoner(Reasoner):
    """
    Reasoner for DeepSeek R series models.
    """
    start_token_id: int
    end_token_id: int

    start_token: str = " thinking"
    end_token: str = " response"

    @classmethod
    def from_tokenizer(cls, tokenizer: PythonBackend) -> Reasoner:
        return cls(
            start_token_id=tokenizer.encode(" thinking", add_special_tokens=False)[0],
            end_token_id=tokenizer.encode(" response", add_special_tokens=False)[0],
        )

    def is_reasoning_end(self, input_ids: list[int]) -> bool:
        return self.end_token_id in input_ids

    def is_reasoning_end_streaming(self, input_ids: list[int], delta_ids: list[int]) -> bool:
        return self.end_token_id in delta_token_ids
    ...

xgrammar 같은 구조적 출력 엔진은 end_token_id 를 사용해 모델 출력에 추론 콘텐츠가 있는지 확인하고, 그런 경우 구조적 출력을 건너뜁니다.

마지막으로 --reasoning-parser 플래그로 모델의 추론을 활성화할 수 있어요.

vllm serve <model_tag> --reasoning-parser example

더 알아보기 (Learn more)