localaiforum
로그인
가이드 › 모델 활용

구조화 출력과 함수 호출

모델에게 JSON을 요청하면 거의-JSON을 기꺼이 뱉습니다. 끝에 붙은 쉼표, 따옴표 없는 키, 첫 중괄호 앞의 친절한 문장. 자유형 샘플링은 언제나 유효하지 않은 토큰을 뱉을 수 있습니다. 샘플러는 당신의 스키마에 관해 아무것도 모르기 때문입니다. 구조화 출력이 그 간극을 막고, 함수 호출은 같은 메커니즘이 다른 모자를 쓴 것입니다.

자유형 JSON이 깨지는 이유

생성된 모든 토큰은 분포의 샘플입니다. 매 단계에서 약간의 확률 질량이 — 크기는 작지만 temperature 0 위에서는 절대 0이 아닙니다 — 형식을 깨는 토큰 위에 놓입니다. 여는 괄호 전의 닫는 괄호, 배열 마지막 원소 뒤의 쉼표, 학습 데이터에 구워진 파이썬 습관인 소문자 true의 True 철자. "거의 항상 유효"를 출력 토큰 200개에 곱하면 간헐적이고 재현 불가능한 파손이 됩니다 — 최악의 종류의 버그입니다. temperature 0조차 출력을 결정론적으로 만들 뿐 스키마-유효하게 만들지는 않습니다.

해결 사다리

단 메커니즘 보장 어디에
프롬프트뿐 ("valid JSON으로 답하라") 정중한 부탁 없음 — 샘플링은 여전히 실패 가능 어느 엔진이나
JSON 모드 프롬프트 + JSON 문법 제한 문법적으로 유효한 JSON; 모양은 미보장 OpenAI 호환 API, Ollama format: "json"
스키마 제약 스키마 → 문법 → logits 마스크 출력이 항상 파싱되고 스키마와 일치 llama.cpp (json_schema / GBNF), vLLM guided_json, Ollama structured outputs, outlines / xgrammar

각 단은 유연성을 지불하고 결정론을 삽니다. 소비자가 코드라면 언제나 최상위 단을 쓰세요.

문법-제약 디코딩

스키마를 문법으로 컴파일하세요. 모든 디코딩 단계에서, 유효한 문서를 확장할 수 없는 각 토큰의 logit을 샘플링 전에 −무한대로 설정합니다. 유효하지 않은 출력은 희박한 것이 아니라 불가능합니다. llama.cpp는 GBNF 문법을 배송합니다. 십진수 문법은 한 줄입니다:

root ::= "-"? [0-9]+ ("." [0-9]+)?

같은 아이디어가 JSON Schema를 감쌉니다. 로컬 llama.cpp 서버(OpenAI 호환 엔드포인트)에 대해:

import json, requests

schema = {
    "type": "object",
    "properties": {
        "title":     {"type": "string"},
        "component": {"type": "string",
                      "enum": ["auth", "upload", "ui", "api"]},
        "severity":  {"type": "string",
                      "enum": ["low", "medium", "high"]}
    },
    "required": ["title", "component", "severity"],
    "additionalProperties": False
}

r = requests.post("http://localhost:8080/v1/chat/completions", json={
    "messages": [{"role": "user",
                  "content": "Extract the bug: " + report}],
    "response_format": {
        "type": "json_schema",
        "json_schema": {"name": "bug", "strict": True,
                        "schema": schema}
    },
    "temperature": 0
})
bug = json.loads(r.json()["choices"][0]["message"]["content"])

Ollama는 format 필드의 JSON Schema로, vLLM은 guided_json으로 같은 일을 합니다. 같은 메커니즘, 철자 셋 — 당신 엔진의 정확한 필드 이름은 엔진 파라미터에 있습니다.

제약이 곧 올바름은 아닙니다

문법은 진실이 아니라 모양을 보장합니다. 모델은 크래시 리포트에 {"severity": "low"}를 뱉을 수 있고, 문자열 필드를 채움물로 채울 수 있습니다. 마스킹은 개방형 필드를 해칠 수도 있습니다. 모델이 문단 나눔을 원하는데 문법이 줄바꿈을 금지하면 품질이 떨어집니다. 실용 규칙들:

  • 값 공간을 아는 경우 자유 문자열 대신 좁은 enum.
  • 소비하는 모든 필드에 required, 발명을 막는 additionalProperties: false.
  • 문자열 필드는 짧게. 긴 산문은 제약된 응답 밖으로 돌려 보세요.
  • 원하는 출력의 완성 예제 하나를 그래도 프롬프트에 넣으세요 — 문법이 못 하는 내용을 조종합니다.

함수와 도구 호출

도구 호출은 구조화 출력에 규약 하나를 얹은 것입니다. 모델이 어느 함수를 어떤 인자로 호출할지 반환하고, 당신의 코드가 그것을 실행해 결과를 덧붙입니다. OpenAI tools 형태가 로컬 엔진들이 모방하는 사실상의 방언입니다:

{
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Current weather for a city",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {"type": "string"},
          "unit": {"type": "string", "enum": ["C", "F"]}
        },
        "required": ["city"]
      }
    }
  }]
}

모델은 메시지에 tool_calls로 답하고, 당신은 출력을 role: "tool" 메시지로 돌려주며 루프가 계속됩니다. 모델은 특정 도구 방언으로 학습되었습니다 — 로컬 엔진에서 가장 잘 지원되는 것은 Hermes와 Qwen 혈통 템플릿입니다(llama.cpp의 --jinja, Ollama, 자동 도구 선택의 vLLM). 도구 학습 없는 모델은 무엇으로 제약하든 그럴듯한 인자 이름이 환각된 그럴듯한 호출을 냅니다. 그러니 먼저 과제에 맞는 모델을 고르세요: 모델 선택.

파싱과 내용 둘 다 검증하세요

모든 호출 뒤 검사 두 개. 첫째, 파싱 — json.loads 성공 (문법이 보통 보장합니다. JSON 모드는 항상은 아닙니다). 둘째, 의미론: 필수 키 존재, enum 범위 내, 숫자 그럴듯함, 문자열 비어 있지 않음. 실패하면 검증 오류를 프롬프트에 되먹여 한 번 재시도하세요 — 제약된 재시도는 싸고 보통 첫 번에 성공합니다.

이 정확한 루프 — 호출 내뿉기, 검증, 실행, 관찰 — 가 에이전트가 하루 종일 도는 것입니다: 에이전트.

관련: 에이전트 · 프롬프팅 · 샘플링 · RAG