구조화된 출력으로 LLM 제한: Ollama, Qwen3 및 Python 또는 Go
Ollama에서 구조화된 출력을 얻는 몇 가지 방법
대규모 언어 모델(LLMs) 은 강력하지만, 실제 운영 환경에서는 일반적인 문장이 아닌 예측 가능한 데이터를 원합니다. 즉, 앱에 입력할 수 있는 속성, 사실 또는 구조화된 객체를 원합니다. 이에 대해 LLM 구조화된 출력을 살펴보겠습니다.
일반적으로 Ollama는 구조화된 출력을 지원하는 기능을 도입했습니다 (공지사항), 이는 모델의 응답을 JSON 스키마에 맞추어 제한할 수 있게 해줍니다. 이는 LLM 기능 목록화, 모델 벤치마킹, 시스템 통합 자동화와 같은 작업에 일관된 데이터 추출 파이프라인을 가능하게 합니다.
이 게시물에서는 다음과 같은 내용을 다룹니다:
- 구조화된 출력이 무엇인지, 왜 중요한지
- LLM에서 구조화된 출력을 얻는 간단한 방법
- Ollama의 새 기능이 어떻게 작동하는지
- LLM 기능 추출 예시:
구조화된 출력이란 무엇인가요?
일반적으로 LLM은 자유로운 텍스트를 생성합니다:
“모델 X는 사고 체인을 사용하여 추론을 지원하고, 200K의 컨텍스트 창을 가지며, 영어, 중국어, 스페인어를 지원합니다.”
이 텍스트는 읽기 쉬우나, 파싱하기 어렵습니다.
대신, 구조화된 출력을 사용하면 엄격한 스키마를 요청할 수 있습니다:
{
"name": "Model X",
"supports_thinking": true,
"max_context_tokens": 200000,
"languages": ["English", "Chinese", "Spanish"]
}
이 JSON은 쉽게 검증하고, 데이터베이스에 저장하거나, UI에 전달할 수 있습니다.
LLM에서 구조화된 출력을 얻는 간단한 방법
LLMs는 때때로 스키마를 이해하고, 특정 스키마를 사용하여 JSON 형식으로 출력을 요청할 수 있습니다. 알리바바의 Qwen3 모델은 추론과 구조화된 응답에 최적화되어 있습니다. 이를 명시적으로 지시하여 JSON 형식으로 응답하도록 할 수 있습니다.
예시 1: Python에서 ollama
와 함께 Qwen3 사용, 스키마를 사용한 JSON 요청
import json
import ollama
prompt = """
구조화된 데이터 추출기입니다.
JSON만 반환합니다.
텍스트: "Elon Musk는 53세이며, 오스틴에 거주합니다."
스키마: { "name": string, "age": int, "city": string }
"""
response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']
# JSON 파싱
try:
data = json.loads(output)
print(data)
except Exception as e:
print("JSON 파싱 오류:", e)
출력:
{"name": "Elon Musk", "age": 53, "city": "Austin"}
Pydantic을 사용한 스키마 검증
잘못된 출력을 방지하기 위해 Python에서 Pydantic 스키마에 대해 검증할 수 있습니다.
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
city: str
# Qwen3에서 얻은 JSON 문자열이 'output'이라고 가정
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)
이렇게 하면 출력이 예상되는 구조에 맞게 보장됩니다.
Ollama의 구조화된 출력
Ollama는 이제 format
매개변수에 스키마를 전달할 수 있게 되었습니다. 이로 인해 모델은 스키마에 맞는 JSON만 응답하도록 제한됩니다 (문서).
Python에서는 일반적으로 Pydantic을 사용하여 스키마를 정의하고, Ollama가 이를 JSON 스키마로 사용하도록 합니다.
예시 2: LLM 기능 메타데이터 추출
LLM의 기능을 설명하는 텍스트 스니펫이 있다고 가정해보겠습니다:
“Qwen3은 강력한 다국어 지원(영어, 중국어, 프랑스어, 스페인어, 아랍어)을 제공합니다. 추론 단계(사고 체인)를 허용합니다. 컨텍스트 창은 128K 토큰입니다.”
구조화된 데이터를 원합니다:
from pydantic import BaseModel
from typing import List
from ollama import chat
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
prompt = """
다음 설명을 분석하고, 모델의 기능을 JSON 형식으로만 반환하세요.
모델 설명:
'Qwen3은 강력한 다국어 지원(영어, 중국어, 프랑스어, 스페인어, 아랍어)을 제공합니다.
추론 단계(사고 체인)를 허용합니다.
컨텍스트 창은 128K 토큰입니다.'
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=LLMFeatures.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
가능한 출력:
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}
예시 3: 여러 모델 비교
여러 모델의 설명을 입력하고 구조화된 형식으로 추출할 수 있습니다:
from typing import List
class ModelComparison(BaseModel):
models: List[LLMFeatures]
prompt = """
각 모델의 기능을 JSON 형식으로 추출하세요.
1. Llama 3.1은 추론을 지원합니다. 컨텍스트 창은 128K입니다. 언어: 영어만 지원합니다.
2. GPT-4 Turbo는 추론을 지원합니다. 컨텍스트 창은 128K입니다. 언어: 영어, 일본어.
3. Qwen3은 추론을 지원합니다. 컨텍스트 창은 128K입니다. 언어: 영어, 중국어, 프랑스어, 스페인어, 아랍어.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=ModelComparison.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
출력:
{
"models": [
{
"name": "Llama 3.1",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English"]
},
{
"name": "GPT-4 Turbo",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Japanese"]
},
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}
]
}
이렇게 하면 모델의 기능에 따라 벤치마킹, 시각화, 필터링이 매우 간단해집니다.
예시 4: 누락된 항목 자동 감지
필드가 누락되었을 때 null
값을 허용할 수도 있습니다:
from typing import Optional
class FlexibleLLMFeatures(BaseModel):
name: str
supports_thinking: Optional[bool]
max_context_tokens: Optional[int]
languages: Optional[List[str]]
이렇게 하면 일부 정보가 알려지지 않았을 때도 스키마가 유효하게 유지됩니다.
이점, 주의사항 및 최선의 실천 방법
Ollama(또는 이를 지원하는 시스템)를 통해 구조화된 출력을 사용하면 많은 이점이 있지만, 몇 가지 주의사항도 있습니다.
이점
- 더 강력한 보장: 모델은 JSON 스키마에 맞게 요청되며, 자유로운 텍스트가 아닌 JSON 스키마에 맞게 응답하도록 합니다.
- 더 쉬운 파싱:
json.loads
또는 Pydantic/Zod를 사용하여 직접 파싱하거나, 정규식 또는 휴리스틱 대신 사용할 수 있습니다. - 스키마 기반 진화: 스키마를 버전 관리하고, 필드를 추가(기본값 포함)하여 뒷받침되는 호환성을 유지할 수 있습니다.
- 상호 운용성: 후속 시스템은 구조화된 데이터를 기대합니다.
- 결정성 (저온도 시 더 좋음): 온도가 낮을수록(예: 0), 모델이 스키마에 더 엄격하게 따르는 경향이 있습니다. Ollama 문서는 이 방법을 권장합니다.
주의사항 및 함정
- 스키마 불일치: 모델이 여전히 편차를 보일 수 있습니다—예: 필수 속성을 누락하거나, 키 순서를 변경하거나, 추가 필드를 포함할 수 있습니다. 검증이 필요합니다.
- 복잡한 스키마: 매우 깊거나 재귀적인 JSON 스키마는 모델을 혼란스럽게 하거나 실패로 이어질 수 있습니다.
- 프롬프트의 모호함: 프롬프트가 모호할 경우, 모델이 필드나 단위를 잘못 추측할 수 있습니다.
- 모델 간 불일치: 일부 모델은 구조화된 제약을 준수하는 데 더 잘하거나 못할 수 있습니다.
- 토큰 제한: 스키마 자체는 프롬프트 또는 API 호출에 토큰 비용을 추가할 수 있습니다.
최선의 실천 방법 및 팁 (Ollama 블로그 + 경험에서 도출됨)
- Pydantic (Python) 또는 **Zod (JavaScript)**를 사용하여 스키마를 정의하고 JSON 스키마를 자동 생성합니다. 이는 수작업 오류를 피할 수 있습니다.
- 프롬프트에 항상 “JSON만 응답하라” 또는 **“추가 설명이나 텍스트를 포함하지 말라”**와 같은 지시문을 포함하세요.
- 온도 = 0 (또는 매우 낮은 값)을 사용하여 무작위성을 최소화하고 스키마 준수를 최대화하세요. Ollama는 결정성을 권장합니다.
- JSON 파싱 실패 또는 스키마 검증 실패 시 항상 검증하고, 필요 시 대체(예: 재시도 또는 정리)를 수행하세요.
- 먼저 간단한 스키마부터 시작하고, 점차 확장하세요. 처음에는 복잡하게 하지 마세요.
- 유용하지만 제한된 오류 지시문을 포함하세요: 예를 들어, 모델이 필수 필드를 채우지 못할 경우
null
을 반환하도록 하세요(스키마가 허용할 경우).
Go 예시 1: LLM 기능 추출
다음은 Qwen3에 대한 구조화된 출력을 요청하는 간단한 Go 프로그램입니다.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ollama/ollama/api"
)
type LLMFeatures struct {
Name string `json:"name"`
SupportsThinking bool `json:"supports_thinking"`
MaxContextTokens int `json:"max_context_tokens"`
Languages []string `json:"languages"`
}
func main() {
client, err := api.ClientFromEnvironment()
if err != nil {
log.Fatal(err)
}
prompt := `
다음 설명을 분석하고, 모델의 기능을 JSON 형식으로만 반환하세요.
설명:
"Qwen3은 강력한 다국어 지원(영어, 중국어, 프랑스어, 스페인어, 아랍어)을 제공합니다.
추론 단계(사고 체인)를 허용합니다.
컨텍스트 창은 128K 토큰입니다."
`
// 구조화된 출력을 위한 JSON 스키마 정의
formatSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
}
// 스키마를 JSON으로 변환
formatJSON, err := json.Marshal(formatSchema)
if err != nil {
log.Fatal("JSON 스키마 변환 실패:", err)
}
req := &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: formatJSON,
Options: map[string]any{"temperature": 0},
}
var features LLMFeatures
var rawResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// 스트리밍된 내용 누적
rawResponse += response.Response
// 응답이 완료되면 파싱
if response.Done {
if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
return fmt.Errorf("JSON 파싱 오류: %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("파싱된 구조: %+v\n", features)
}
이 예제 Go 프로그램을 컴파일하고 실행하려면, ollama-struct
라는 폴더에 main.go
파일이 있다고 가정합니다.
이 폴더 내에서 실행해야 합니다:
# 모듈 초기화
go mod init ollama-struct
# 의존성 다운로드
go mod tidy
# 빌드 및 실행
go build -o ollama-struct main.go
./ollama-struct
예시 출력
파싱된 구조: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}
Go 예시 2: 여러 모델 비교
이를 확장하여 비교 목록을 추출할 수 있습니다.
type ModelComparison struct {
Models []LLMFeatures `json:"models"`
}
prompt = `
다음 모델 설명에서 기능을 추출하고 JSON 형식으로 반환하세요:
1. PaLM 2: 이 모델은 추론 능력이 제한되어 있으며, 기본 언어 이해에 집중합니다. 컨텍스트 창은 8,000 토큰입니다. 주로 영어만 지원합니다.
2. LLaMA 2: 이 모델은 중간 정도의 추론 능력을 가지며, 일부 논리 작업을 처리할 수 있습니다. 컨텍스트는 최대 4,000 토큰입니다. 영어, 스페인어, 이탈리아어를 지원합니다.
3. Codex: 이 모델은 프로그래밍 및 코드 분석에 특화된 강력한 추론 능력을 가집니다. 컨텍스트 창은 16,000 토큰입니다. 영어, 파이썬, 자바스크립트, 자바를 지원합니다.
"models" 배열을 포함하는 JSON 객체를 반환하세요.
`
// 모델 비교를 위한 JSON 스키마 정의
comparisonSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"models": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
},
},
},
"required": []string{"models"},
}
// 스키마를 JSON으로 변환
comparisonFormatJSON, err := json.Marshal(comparisonSchema)
if err != nil {
log.Fatal("비교 스키마 변환 실패:", err)
}
req = &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: comparisonFormatJSON,
Options: map[string]any{"temperature": 0},
}
var comp ModelComparison
var comparisonResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// 스트리밍된 내용 누적
comparisonResponse += response.Response
// 응답이 완료되면 파싱
if response.Done {
if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
return fmt.Errorf("JSON 파싱 오류: %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
for _, m := range comp.Models {
fmt.Printf("%s: Context=%d, Languages=%v\n", m.Name, m.MaxContextTokens, m.Languages)
}
예시 출력
PaLM 2: Context=8000, Languages=[English]
LLaMA 2: Context=4000, Languages=[English Spanish Italian]
Codex: Context=16000, Languages=[English Python JavaScript Java]
참고로, 이 예제에서 qwen3:4b
도 잘 작동하며, qwen3:8b
와 동일합니다.
Go 개발자에게 권장 사항
- 온도를 0으로 설정하여 스키마 준수를 최대화하세요.
json.Unmarshal
을 사용하여 검증하고, 파싱 실패 시 대체를 수행하세요.- 스키마를 간단하게 유지하세요 — 깊고 재귀적인 JSON 구조는 문제가 될 수 있습니다.
- 필드가 누락될 수 있을 경우,
omitempty
를 Go 구조체 태그에 사용하세요. - 모델이 가끔 잘못된 JSON을 생성할 경우, 재시도를 추가하세요.
전체 예시 - LLM 사양으로 차트 그리기 (구조화된 JSON에서 비교 표로 단계별)
- 원하는 데이터에 대한 스키마 정의
Pydantic을 사용하여 Ollama에 JSON 스키마를 생성하고, 모델의 응답을 검증할 수 있습니다.
from pydantic import BaseModel
from typing import List, Optional
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
- Ollama에 오직 해당 형식의 JSON만 반환하도록 요청
스키마를 format=
에 전달하고, 결정성을 위해 온도를 낮춥니다.
from ollama import chat
prompt = """
각 모델의 기능을 추출하고, 스키마에 맞는 JSON만 반환하세요.
1) Qwen3은 사고 체인을 지원하며, 128K 컨텍스트, 영어, 중국어, 프랑스어, 스페인어, 아랍어를 지원합니다.
2) Llama 3.1은 사고 체인을 지원하며, 128K 컨텍스트, 영어를 지원합니다.
3) GPT-4 Turbo는 사고 체인을 지원하며, 128K 컨텍스트, 영어, 일본어를 지원합니다.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format={"type": "array", "items": LLMFeatures.model_json_schema()},
options={"temperature": 0}
)
raw_json = resp.message.content # JSON 형식의 LLMFeatures 목록
- 검증 및 정규화
생산 환경에서 사용하기 전에 항상 검증하세요.
from pydantic import TypeAdapter
adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json) # -> list[LLMFeatures]
- 비교 표 작성 (pandas)
검증된 객체를 DataFrame으로 변환하여 정렬, 필터링, 내보내기 가능하게 합니다.
import pandas as pd
df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))
# 가독성을 위해 열 순서 재정렬
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]
# 추후 사용을 위해 CSV로 저장
df.to_csv("llm_feature_comparison.csv", index=False)
- (선택사항) 간단한 시각화
모델 간 차이를 빠르게 눈으로 확인할 수 있는 간단한 차트를 생성합니다.
import matplotlib.pyplot as plt
plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("모델별 최대 컨텍스트 창 (토큰)")
plt.xlabel("모델")
plt.ylabel("최대 컨텍스트 토큰")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")
TL;DR
Ollama의 새 구조화된 출력 지원을 통해 LLM을 단순한 챗봇이 아닌 데이터 추출 엔진으로 사용할 수 있습니다.
위 예시에서는 LLM 기능(예: 사고 지원, 컨텍스트 창 크기, 지원 언어)에 대한 구조화된 메타데이터를 자동으로 추출하는 방법을 보여주었습니다. 이 작업은 일반적으로 취약한 파싱이 필요한 작업이었습니다.
LLM 모델 카탈로그, 평가 대시보드, 또는 AI 기반 연구 보조기를 구축하려는 경우, 구조화된 출력은 통합을 부드럽고, 신뢰성 있게, 그리고 운영 가능한 상태로 만들어줍니다.