구조적 출력

구조적 출력 (Structured Outputs)

vLLM은 xgrammar 또는 guidance 를 백엔드로 사용해 구조적 출력 생성을 지원합니다. 이 문서는 구조적 출력을 생성하는 데 사용할 수 있는 여러 옵션의 예시를 보여줘요.

경고: v0.12.0에서 제거된 다음 deprecated API 필드를 여전히 사용하고 있다면, 이 문서의 나머지 부분에서 보여주는 대로 코드를 structured_outputs 를 사용하도록 업데이트하세요.

  • guided_json{"structured_outputs": {"json": ...}} 또는 StructuredOutputsParams(json=...)
  • guided_regex{"structured_outputs": {"regex": ...}} 또는 StructuredOutputsParams(regex=...)
  • guided_choice{"structured_outputs": {"choice": ...}} 또는 StructuredOutputsParams(choice=...)
  • guided_grammar{"structured_outputs": {"grammar": ...}} 또는 StructuredOutputsParams(grammar=...)
  • guided_whitespace_pattern{"structured_outputs": {"whitespace_pattern": ...}} 또는 StructuredOutputsParams(whitespace_pattern=...)
  • structural_tag{"structured_outputs": {"structural_tag": ...}} 또는 StructuredOutputsParams(structural_tag=...)
  • guided_decoding_backend → 요청에서 이 필드를 제거하세요.

출처: 문서

본문

온라인 서빙 (OpenAI API)

OpenAI의 CompletionsChat API를 사용해 구조적 출력을 생성할 수 있습니다.

다음 파라미터가 지원되며 추가 파라미터로 넣어야 합니다.

  • choice: 출력이 정확히 그 선택지 중 하나가 됩니다.
  • regex: 출력이 정규식 패턴을 따릅니다.
  • json: 출력이 JSON 스키마를 따릅니다.
  • grammar: 출력이 컨텍스트 자유 문법을 따릅니다.
  • structural_tag: 생성된 텍스트 내 지정된 태그 집합 안에서 JSON 스키마를 따릅니다.

지원되는 파라미터의 전체 목록은 OpenAI 호환 서버 페이지에서 볼 수 있어요.

구조적 출력은 OpenAI 호환 서버에서 기본적으로 지원됩니다. --structured-outputs-config.backend 플래그를 vllm serve 에 설정해 사용할 백엔드를 지정할 수 있습니다. 기본 백엔드는 auto 로, 요청 세부 사항에 따라 적절한 백엔드를 선택하려고 합니다. 특정 백엔드와 일부 옵션도 선택할 수 있습니다. 전체 옵션은 vllm serve --help 텍스트에서 확인할 수 있습니다.

가장 쉬운 choice 부터 각 경우의 예시를 보겠습니다.

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

completion = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "user", "content": "Classify this sentiment: vLLM is wonderful!"}
    ],
    extra_body={"structured_outputs": {"choice": ["positive", "negative"]}},
)
print(completion.choices[0].message.content)

다음 예시는 regex 를 사용하는 방법을 보여줍니다. 지원되는 정규식 구문은 구조적 출력 백엔드에 따라 다릅니다. 예를 들어 xgrammar, guidance, outlines 는 Rust 스타일 정규식을 사용하고, lm-format-enforcer 는 Python의 re 모듈을 사용합니다. 간단한 정규식 템플릿으로 이메일 주소를 생성하는 아이디어입니다.

completion = client.chat.completions.create(
    model=model,
    messages=[
        {
            "role": "user",
            "content": "Generate an example email address for Alan Turing, who works in Enigma. End in .com and new line. Example result: [email protected]\n",
        }
    ],
    extra_body={"structured_outputs": {"regex": r"\w+@\w+\.com\n"}, "stop": ["\n"]},
)
print(completion.choices[0].message.content)

구조적 텍스트 생성에서 가장 관련 높은 기능 중 하나는 미리 정의된 필드와 형식으로 유효한 JSON을 생성하는 옵션입니다. 이를 위해 json 파라미터를 두 가지 방식으로 사용할 수 있어요.

  1. JSON Schema 를 직접 사용.
  2. Pydantic 모델 을 정의한 다음 그로부터 JSON Schema를 추출(보통 더 쉬운 옵션).

다음 예시는 Pydantic 모델로 response_format 파라미터를 사용하는 방법을 보여줍니다.

from pydantic import BaseModel
from enum import Enum

class CarType(str, Enum):
    sedan = "sedan"
    suv = "SUV"
    truck = "Truck"
    coupe = "Coupe"

class CarDescription(BaseModel):
    brand: str
    model: str
    car_type: CarType

json_schema = CarDescription.model_json_schema()

completion = client.chat.completions.create(
    model=model,
    messages=[
        {
            "role": "user",
            "content": "Generate a JSON with the brand, model and car_type of the most iconic car from the 90's",
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "car-description",
            "schema": CarDescription.model_json_schema()
        },
    },
)
print(completion.choices[0].message.content)

: 엄격히 필요한 것은 아니지만, 보통 프롬프트에 JSON 스키마와 필드가 어떻게 채워져야 하는지 표시하는 것이 좋습니다. 이는 대부분의 경우 결과를 눈에 띄게 개선할 수 있어요.

마지막으로 grammar 옵션이 있습니다. 사용하기 가장 어렵겠지만 정말 강력합니다. SQL 쿼리 같은 완전한 언어를 정의할 수 있게 해 줘요. 컨텍스트 자유 EBNF 문법으로 동작합니다. 예를 들어 단순화된 SQL 쿼리의 특정 형식을 정의하는 데 사용할 수 있습니다.

simplified_sql_grammar = """
    root ::= select_statement

    select_statement ::= "SELECT " column " from " table " where " condition

    column ::= "col_1 " | "col_2 "

    table ::= "table_1 " | "table_2 "

    condition ::= column "= " number

    number ::= "1 " | "2 "
"""

completion = client.chat.completions.create(
    model=model,
    messages=[
        {
            "role": "user",
            "content": "Generate an SQL query to show the 'username' and 'email' from the 'users' table.",
        }
    ],
    extra_body={"structured_outputs": {"grammar": simplified_sql_grammar}},
)
print(completion.choices[0].message.content)

참고: 전체 예시

추론 출력 (Reasoning Outputs)

추론 모델을 위해 구조적 출력을 reasoning 와 함께 사용할 수도 있습니다.

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

제공된 구조적 출력 기능과 함께 추론을 사용할 수 있습니다. 다음은 JSON 스키마를 사용하는 예시입니다.

from pydantic import BaseModel

class People(BaseModel):
    name: str
    age: int

completion = client.chat.completions.create(
    model=model,
    messages=[
        {
            "role": "user",
            "content": "Generate a JSON with the name and age of one random person.",
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "people",
            "schema": People.model_json_schema()
        }
    },
)
print("reasoning: ", completion.choices[0].message.reasoning)
print("content: ", completion.choices[0].message.content)

참고: 전체 예시

참고: 추론이 활성화된 Qwen3 Coder 모델을 사용할 때, 추론 콘텐츠가 reasoning 필드로 별도 파싱되지 않으면 구조적 출력이 비활성화될 수 있습니다(v0.11.2+). 두 기능을 함께 사용하려면 추론 모드에서 구조적 출력을 명시적으로 활성화해야 합니다. vLLM 서버를 시작할 때 다음 플래그를 추가하세요: --structured-outputs-config.enable_in_reasoning=True. Reasoning Outputs 문서도 참고하세요.

실험적 자동 파싱 (OpenAI API)

이 섹션은 Python 특정 타입과의 더 풍부한 통합을 제공하는 client.chat.completions.create() 메서드에 대한 OpenAI beta 래퍼를 다룹니다.

작성 시점(openai==1.54.4)에 이는 OpenAI 클라이언트 라이브러리의 "beta" 기능입니다. 코드 참조는 여기 에서 찾을 수 있어요.

다음 예시에서는 vLLM이 vllm serve meta-llama/Llama-3.1-8B-Instruct 로 설정되었습니다.

Pydantic 모델을 사용해 구조적 출력을 얻는 간단한 예시입니다.

from pydantic import BaseModel
from openai import OpenAI

class Info(BaseModel):
    name: str
    age: int

client = OpenAI(base_url="http://0.0.0.0:8000/v1", api_key="dummy")
model = client.models.list().data[0].id
completion = client.beta.chat.completions.parse(
    model=model,
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "My name is Cameron, I'm 28. What's my name and age?"},
    ],
    response_format=Info,
)

message = completion.choices[0].message
print(message)
assert message.parsed
print("Name:", message.parsed.name)
print("Age:", message.parsed.age)
ParsedChatCompletionMessage[Testing](content='{"name": "Cameron", "age": 28}', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[], parsed=Testing(name='Cameron', age=28))
Name: Cameron
Age: 28

단계별 수학 해답을 처리하기 위해 중첩 Pydantic 모델을 사용하는 더 복잡한 예시입니다.

from typing import List
from pydantic import BaseModel
from openai import OpenAI

class Step(BaseModel):
    explanation: str
    output: str

class MathResponse(BaseModel):
    steps: list[Step]
    final_answer: str

completion = client.beta.chat.completions.parse(
    model=model,
    messages=[
        {"role": "system", "content": "You are a helpful expert math tutor."},
        {"role": "user", "content": "Solve 8x + 31 = 2."},
    ],
    response_format=MathResponse,
)

message = completion.choices[0].message
print(message)
assert message.parsed
for i, step in enumerate(message.parsed.steps):
    print(f"Step #{i}:", step)
print("Answer:", message.parsed.final_answer)

출력:

ParsedChatCompletionMessage[MathResponse](content='{ "steps": [{ "explanation": "First, let\'s isolate the term with the variable \'x\'. To do this, we\'ll subtract 31 from both sides of the equation.", "output": "8x + 31 - 31 = 2 - 31"}, { "explanation": "By subtracting 31 from both sides, we simplify the equation to 8x = -29.", "output": "8x = -29"}, { "explanation": "Next, let\'s isolate \'x\' by dividing both sides of the equation by 8.", "output": "8x / 8 = -29 / 8"}], "final_answer": "x = -29/8" }', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[], parsed=MathResponse(steps=[Step(explanation="First, let's isolate the term with the variable 'x'. To do this, we'll subtract 31 from both sides of the equation.", output='8x + 31 - 31 = 2 - 31'), Step(explanation='By subtracting 31 from both sides, we simplify the equation to 8x = -29.', output='8x = -29'), Step(explanation="Next, let's isolate 'x' by dividing both sides of the equation by 8.", output='8x / 8 = -29 / 8')], final_answer='x = -29/8'))
Step #0: explanation="First, let's isolate the term with the variable 'x'. To do this, we'll subtract 31 from both sides of the equation." output='8x + 31 - 31 = 2 - 31'
Step #1: explanation='By subtracting 31 from both sides, we simplify the equation to 8x = -29.' output='8x = -29'
Step #2: explanation="Next, let's isolate 'x' by dividing both sides of the equation by 8." output='8x / 8 = -29 / 8'
Answer: x = -29/8

structural_tag 사용 예시는 examples/features/structured_outputs 에서 찾을 수 있어요.

오프라인 추론 (Offline Inference)

오프라인 추론은 동일한 종류의 구조적 출력을 허용합니다. 사용하려면 SamplingParams 안에서 StructuredOutputsParams 클래스로 구조적 출력을 구성해야 합니다. StructuredOutputsParams 안의 주요 옵션은 다음과 같습니다.

  • json
  • regex
  • choice
  • grammar
  • structural_tag

이 파라미터들은 위 온라인 서빙 예시의 파라미터와 같은 방식으로 사용할 수 있습니다. choice 파라미터 사용 예시는 아래와 같습니다.

from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams

llm = LLM(model="HuggingFaceTB/SmolLM2-1.7B-Instruct")

structured_outputs_params = StructuredOutputsParams(choice=["Positive", "Negative"])
sampling_params = SamplingParams(structured_outputs=structured_outputs_params)
outputs = llm.generate(
    prompts="Classify this sentiment: vLLM is wonderful!",
    sampling_params=sampling_params,
)
print(outputs[0].outputs[0].text)

참고: 전체 예시

더 알아보기 (Learn more)