Mục lục
- Mục tiêu bài học
- Recap Bài 24 — vì sao cần lớp schema-enforced
- Ba vấn đề của prompt-only JSON
- OpenAI JSON Mode — valid JSON, không enforce schema
- OpenAI Structured Output — json_schema strict
- Cơ chế constrained decoding phía server
- Pydantic v2 — schema validation phía client
- Pydantic → JSON Schema
- client.beta.chat.completions.parse() — pattern modern
- Instructor — wrapper đa provider, auto retry
- Anthropic — tool use cho structured output
- Anthropic — prefill trick là fallback
- Thiết kế schema tốt — Type, Literal, Field
- Nested schema và limit độ sâu
- Optional, default, required
- Validation error handling
- Use case thực tế
- Cost — schema thêm token ở đâu
- Failure mode khi schema phức tạp
- Best practice production
- JSON repair libraries
- Landscape 2025-2026 — provider so sánh
- Outlines — constrained generation cho local model
- Code Python — bốn pattern chạy được
- Bài tập
Mục tiêu bài học
Sau bài này, bạn cần phân biệt được:
- Ba mức structured output của OpenAI: prompt-only JSON, JSON Mode (
json_object), và Structured Output (json_schemastrict). - JSON Mode chỉ guarantee cú pháp — không enforce schema. Structured Output 08/2024 mới enforce 100% schema bằng constrained decoding.
- Pydantic v2 là contract phía client:
model_json_schema()sinh JSON Schema,model_validate_json()validate response. client.beta.chat.completions.parse(response_format=Model)là API modern — trả Pydantic object đã validate, không cần loads thủ công.- Instructor giấu retry + đa provider sau cùng interface.
- Anthropic dùng tool use làm cách native cho structured; prefill
{chỉ là fallback đơn giản. - Outlines + Pydantic enforce schema cho local model (Qwen, Llama) qua logit masking.
- Schema phức tạp (nested sâu, recursive) làm model hallucinate và tăng latency — phải thiết kế phẳng khi có thể.
Bài này nối thẳng từ Bài 24 (Output format) và Bài 28 (Self-consistency / ToT). Sau bài này, sampling parameter ở Bài 30 sẽ giải thích vì sao temperature thấp giúp structured output ổn định hơn.
Recap Bài 24 — vì sao cần lớp schema-enforced
Bài 24 đã liệt kê các format (JSON, XML, CSV, YAML, table) và pattern viết yêu cầu format ngay trong prompt. Cách prompt-only đủ cho prototype, nhưng production gặp ba ràng buộc làm prompt-only không đủ:
- SLA parse rate: app phải parse được > 99% response. Prompt-only thường đạt 90-97% với model lớn, 80-95% với model nhỏ — chưa đủ.
- Schema lock: hợp đồng với downstream service (DB column, gRPC field) không cho phép drift. Hallucinated field hay miss field gây alert prod.
- Latency budget: retry thủ công đẩy P95 latency lên 2-3 lần. Giải pháp phía server (constrained decoding) giảm retry về 0.
Bài này tách rõ hai lớp:
- Lớp wire format (JSON, XML…): chữ trên dây.
- Lớp schema (Pydantic / JSON Schema): cái gì hợp lệ ở mức semantic — field nào bắt buộc, type gì, enum gì.
Bài 24 chủ yếu nói lớp 1. Bài này tập trung lớp 2 — schema-enforced output.
Ba vấn đề của prompt-only JSON
Khi chỉ dùng prompt mô tả schema ("Trả JSON với keys ..."), ba lớp lỗi xuất hiện đều đặn:
- Schema violation: model bỏ field
email, đổi"sentiment"thành"emotion", hoặc trả"age": "30"(string) thay vì int. Pydantic v2 coerce một phần (string số → int) nhưng không phải tất cả. - Preamble + wrapper: "Sure, here is the JSON: ```json {...} ```". JSON nằm trong markdown fence + có text trước/sau. Parser strict raise
JSONDecodeErrorngay token đầu. - Cú pháp lỗi: trailing comma
{"a":1,}, single quote{'a':1}, unescaped quote{"text":"anh nói "hi""}, comment// ...kiểu JS. Hay gặp với SLM (Qwen 1.5B, Phi-3) và Claude Haiku.
Giải pháp đã có: structured output API phía server + Pydantic validate phía client. Ba phần còn lại của bài đi qua từng tầng.
OpenAI JSON Mode — valid JSON, không enforce schema
JSON Mode (DevDay 11/2023, từ gpt-4-1106-preview): API guarantee output parse được bằng json.loads. Bật bằng response_format={"type": "json_object"}:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Output valid JSON."},
{"role": "user", "content":
"Extract name, age, email từ: 'Tôi tên John, 30 tuổi, [email protected].'"},
],
response_format={"type": "json_object"},
)
print(response.choices[0].message.content)
# {"name": "John", "age": 30, "email": "[email protected]"}
Hai điểm cần nhớ:
- JSON Mode chỉ ràng buộc cú pháp. Field nào có, kiểu gì — vẫn do model quyết định. Vì vậy phải mô tả schema trong prompt (system hoặc user) và validate Pydantic sau khi nhận.
- API yêu cầu chuỗi "JSON" xuất hiện trong messages. Nếu không, OpenAI trả
BadRequestError. Đây là safeguard tránh model rơi vào loop sinh JSON vô tận khi prompt không yêu cầu structured.
JSON Mode hữu ích khi không muốn phụ thuộc Pydantic phía server (ví dụ schema động sinh runtime). Nhược điểm: không khác mấy so với prompt-only về độ tin cậy schema — vẫn cần validate.
OpenAI Structured Output — json_schema strict
Tháng 08/2024 OpenAI release Structured Output (gpt-4o-2024-08-06 trở đi). API enforce 100% schema:
response_format = {
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name", "age"],
"additionalProperties": False,
},
"strict": True,
},
}
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user",
"content": "Extract: 'John, 30 tuổi.'"}],
response_format=response_format,
)
Khác biệt với JSON Mode:
- API nhận trực tiếp JSON Schema. Không cần mô tả schema bằng văn bản trong prompt nữa (vẫn nên thêm description vào field để model hiểu intent).
- Field
strict: Truebật constrained decoding (xem bước 6). Tất cả field trongrequiredsẽ có, không thiếu, không thừa nhờadditionalProperties: False. - Subset JSON Schema được hỗ trợ: object, array, string, number, boolean, enum, anyOf, $ref. Không hỗ trợ
patternregex,format(email/url validate), recursive sâu > 5 cấp. - Lần đầu dùng schema lạ, request có latency cao hơn (compile schema thành state machine). Lần sau cache.
Available cho gpt-4o, gpt-4o-mini, o1, o3, o4-mini và fine-tuned variant của các model này.
Cơ chế constrained decoding phía server
Để hiểu vì sao Structured Output đảm bảo 100% schema, cần hiểu sơ về cách model decode:
- Tại mỗi step, model sinh logit cho ~150K token.
- Sampler (xem Bài 30) chọn token theo distribution.
- Với strict mode, OpenAI biến JSON Schema thành finite-state machine: ở mỗi state, chỉ một subset token là hợp lệ (ví dụ sau
{phải là"; sau"name":phải là"mở string). - Trước khi sample, các token không hợp lệ bị mask (logit =
-∞). Model chỉ chọn từ token hợp lệ.
Kết quả: output không thể vi phạm schema vì token vi phạm bị chặn ngay tại sampler. Đây là kỹ thuật chung gọi constrained decoding hay grammar-constrained sampling. Cộng đồng open-source dùng cùng kỹ thuật trong Outlines, guidance, xgrammar, lm-format-enforcer.
Chi phí: state machine cần compile (chi phí một lần) và check ở mỗi step (vài %). Đổi lại không có invalid output, không retry.
Pydantic v2 — schema validation phía client
Pydantic v2 (release 06/2023, core viết bằng Rust) là chuẩn de facto cho schema Python. Một model định nghĩa:
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
email: str | None = None
Ý nghĩa:
name: str,age: intlà field bắt buộc.email: str | None = Nonelà field optional, defaultNone.- Pydantic tự coerce kiểu:
{"age": "30"}→age=30(int). Hành vi này có thể tắt bằngStrictBool,StrictInt.
Validate dữ liệu:
person = Person(name="John", age=30) # từ dict
person = Person.model_validate({"name": "John", "age": 30})
person = Person.model_validate_json('{"name": "John", "age": 30}')
Pydantic v2 nhanh hơn v1 5-50 lần (core Rust). Phần lớn library 2024-2026 (FastAPI, LangChain v0.3, Instructor, OpenAI SDK .parse()) đều dựa trên v2.
Pydantic → JSON Schema
Mọi BaseModel đều biết tự sinh JSON Schema:
schema = Person.model_json_schema()
# {
# "type": "object",
# "properties": {
# "name": {"type": "string", "title": "Name"},
# "age": {"type": "integer", "title": "Age"},
# "email": {"anyOf": [{"type": "string"}, {"type": "null"}],
# "default": None}
# },
# "required": ["name", "age"],
# "title": "Person"
# }
Schema này có thể paste vào response_format.json_schema của OpenAI hoặc input_schema của Anthropic tool use. Đây là cách hai thế giới ghép vào nhau:
- Code Python tự nhiên với class.
- LLM API nhận JSON Schema chuẩn.
Lưu ý nhỏ: Pydantic sinh schema không bật additionalProperties: False mặc định. Để dùng với OpenAI strict mode, thêm config:
class Person(BaseModel):
model_config = {"extra": "forbid"}
name: str
age: int
client.beta.chat.completions.parse() — pattern modern
OpenAI SDK Python >= 1.40 thêm helper kết hợp Structured Output + Pydantic trong một call:
from openai import OpenAI
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "Extract: 'John, 30 tuổi.'"},
],
response_format=Person, # truyền class trực tiếp
)
person: Person = response.choices[0].message.parsed
print(person.name, person.age) # John 30
Sau hậu trường, SDK làm 3 việc:
- Gọi
Person.model_json_schema(), buildresponse_formatvớistrict: True. - Gọi API như bình thường — server constrained decoding.
- Validate response qua
Person.model_validate_json(), setmessage.parsed= instance Pydantic.
Một số trường hợp model "từ chối" (refuse) — ví dụ schema yêu cầu PII và model phán đoán không an toàn — message.refusal sẽ có giá trị, message.parsed là None. App cần check refusal trước khi dùng parsed.
Instructor — wrapper đa provider, auto retry
Instructor (Jason Liu, OSS từ 2023) bao một interface chung trên nhiều provider:
import instructor
from openai import OpenAI
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
client = instructor.from_openai(OpenAI())
person = client.chat.completions.create(
model="gpt-4o-mini",
response_model=Person,
max_retries=3,
messages=[{"role": "user", "content": "Extract: 'John, 30 tuổi.'"}],
)
Cơ chế:
- Bind schema vào API bằng cách phù hợp với provider: OpenAI dùng Structured Output, Anthropic dùng tool use, Mistral dùng function call, Ollama dùng JSON Mode.
- Khi validate fail, append
ValidationErrorvào history và gọi lại — model tự sửa theo error message. Số vòng domax_retries. - Tương thích Anthropic, Mistral, Cohere, Groq, Ollama, Vertex AI, Bedrock — code đa provider không phải đổi.
Khi nào dùng Instructor thay vì .parse() trực tiếp:
- Cần đa provider hoặc fallback (OpenAI down → Anthropic).
- Schema phức tạp dễ fail validation lần đầu, retry cứu rate.
- Stream Pydantic field-by-field — Instructor có
Partial[Person]để yield instance từng phần.
Anthropic — tool use cho structured output
Đến 2026, Anthropic chưa có JSON Mode tương đương response_format={"type": "json_object"} của OpenAI. Pattern chính thức (theo Anthropic doc, "Tool use for structured output"): định nghĩa một tool kèm input_schema và bắt model dùng tool đó.
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "extract_person",
"description": "Extract person info",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name", "age"],
},
}]
response = client.messages.create(
model="claude-opus-4",
max_tokens=512,
tools=tools,
tool_choice={"type": "tool", "name": "extract_person"},
messages=[{"role": "user", "content": "Extract: 'John, 30 tuổi.'"}],
)
person_data = response.content[0].input # dict đã đúng schema
Ưu điểm so với prompt-only:
- Schema validate phía Anthropic — output là dict đúng type, không phải string.
- Schema có thể sinh từ Pydantic:
Person.model_json_schema()→input_schema. - Tool use có path tối ưu trong model (Claude 3.5/4/Opus được train heavy với tool use), parse rate cao hơn prompt-only đáng kể.
Đây là cách Instructor dùng khi gọi qua instructor.from_anthropic().
Anthropic — prefill trick là fallback
Nếu không muốn dùng tool use (ví dụ schema rất đơn giản, hoặc tool đã dùng cho mục đích khác), prefill { ở assistant message bắt model tiếp tục bằng JSON:
messages = [
{"role": "user", "content":
"Extract name, age. Trả CHỈ JSON: {\"name\": ..., \"age\": ...}"},
{"role": "assistant", "content": "{"}, # prefill
]
response = client.messages.create(
model="claude-opus-4",
max_tokens=256,
messages=messages,
stop_sequences=["}\n\n"],
)
raw = "{" + response.content[0].text # nối lại ký tự đã prefill
data = json.loads(raw)
Đã nói ở Bài 24, nhắc lại để so sánh:
- Prefill không enforce schema. Field có thể thiếu, type sai. Vẫn cần Pydantic validate.
- Đơn giản về code — không phải khai tool, không phải parse
content[0].input. - Phù hợp khi cần JSON đơn giản trong agent loop đã dùng tool cho action khác.
Khuyến nghị 2026: cho Anthropic, mặc định tool use. Prefill chỉ dùng khi tool slot đã hết hoặc schema rất nhỏ.
Thiết kế schema tốt — Type, Literal, Field
Schema tốt giúp model hiểu intent nhanh và giảm hallucinated value. Bốn nguyên tắc:
- Type cụ thể:
int,float,str,list[str],Literal["a", "b"]— không dùngAnyhaydictkhông structure. - Literal cho enum: thay vì
sentiment: str, dùngLiteral["positive", "negative", "neutral"]. JSON Schema sẽ cóenum: [...], model không thể trả"mixed". - Field description:
Field(description="...")— model dùng làm hint. Description ngắn, mô tả intent (đơn vị, format ngày), không cần văn vẻ. - Constraint số:
Field(ge=0, le=100)cho range. OpenAI strict mode hỗ trợ một phần (minimum, maximum); với phần Pydantic không xuất, validate phía client bắt thêm.
from pydantic import BaseModel, Field
from typing import Literal
class Article(BaseModel):
title: str = Field(description="Title of the article")
authors: list[str] = Field(description="List of author names")
year: int = Field(ge=1900, le=2026,
description="Publication year")
category: Literal["tech", "science", "business"]
Với schema này, model gần như luôn trả year trong khoảng 1900-2026 và category trong 3 giá trị cho phép. Hallucinated value gần về 0.
Nested schema và limit độ sâu
Pydantic cho phép nested model rất tự nhiên:
class Address(BaseModel):
street: str
city: str
country: str
class Person(BaseModel):
name: str
address: Address
emergency_contact: Address | None = None
JSON Schema sinh ra có $ref trỏ về definition của Address. OpenAI strict mode hỗ trợ $ref trong giới hạn:
- Tổng số properties cộng dồn < 100.
- Độ sâu nest < 5.
- Không có recursive (Person có self-reference): hỗ trợ nhưng hay fail compile, nên tránh.
Mô hình nhỏ (Haiku, Llama 3.1 8B, Qwen 7B) bắt đầu vi phạm schema khi nest sâu > 3. Khuyến nghị flat khi có thể: thay vì person.address.street dùng address_street ở top level. Phân tích bóc tách sang object sau khi parse.
Optional, default, required
Ba cú pháp Pydantic v2 hay nhầm lẫn:
class User(BaseModel):
name: str # required, không default
email: str | None = None # optional (nullable), default None
age: int = 18 # required ở semantic, có default
role: str = Field(default="user") # tương đương age
Ý nghĩa:
name: str— nếu thiếu,ValidationError.email: str | None = None— chấp nhậnnullhoặc string.age: int = 18— có thể thiếu, Pydantic fill default. Model thường vẫn trả vì OpenAI strict mode require tất cả keys khiadditionalProperties: False— workaround: dùngemail: str | Noneđể model trảnullkhi không biết.
Quan trọng cho structured output: với strict mode OpenAI, tất cả field trong properties phải nằm trong required. Pydantic v2 sinh điều này tự động nếu không có default. Để "optional" thật, dùng kiểu nullable + default None, không dùng default value khác.
Validation error handling
Khi không dùng strict mode (ví dụ Anthropic prefill, SLM local, JSON Mode), validate ở client:
from pydantic import ValidationError
try:
person = Person.model_validate_json(raw_response)
except ValidationError as e:
for err in e.errors():
print(err["loc"], err["type"], err["msg"])
# Retry hoặc fallback
e.errors() trả list dict mỗi error có:
loc: path đến field lỗi, ví dụ("address", "city").type: kind error, ví dụmissing,int_parsing,literal_error.msg: thông điệp người-đọc-được.
Pattern retry tự sửa: format e.errors() thành text, append vào messages, gọi lại model. Đây là việc Instructor làm tự động — nếu tự viết, chỉ vài chục dòng code.
Use case thực tế
Structured Output xuất hiện đều đặn trong các tầng:
- Data extraction: bóc field từ document, email, contract. Schema tương đương với DB table.
- Form filling: parse user input free-text thành form fields (đặt vé máy bay, đặt phòng).
- LLM-powered API: backend trả JSON cho frontend, dùng LLM sinh response thay vì code rule. Strict mode đảm bảo response không break client.
- Tool / function calling (Bài 42+): tool argument là JSON Schema; model fill argument bằng structured output.
- Multi-agent (Bài 56+): agent A gọi agent B với message có schema chung (task, params, context); structured giúp parse 100%.
- Evaluation pipeline: LLM-as-judge trả
{"score": 1-5, "rationale": "..."}để metric tự động.
Một quan sát: trong code agent thực tế (LangGraph, CrewAI), hầu như mọi inter-node communication đều là structured output. Free-text chỉ còn ở leaf node hiển thị cho user.
Cost — schema thêm token ở đâu
Structured Output không miễn phí về token:
- Output token: JSON tốn key, dấu nháy, brace — ~2-3× plain text cho cùng nội dung (xem Bài 24).
- Input token: với prompt-only, schema mô tả nằm trong prompt (~50-200 token).
- Với
json_schemaOpenAI: schema KHÔNG tính vào input token user-facing — OpenAI compile riêng. Đây là khoản tiết kiệm khi chuyển từ prompt-only sang strict. - Với Anthropic tool use: tool definition tính vào input token, nhưng vẫn rẻ hơn lặp schema trong system prompt mỗi turn nếu cache turn được.
Trade-off thực tế: chuyển từ prompt-only JSON sang Structured Output thường tiết kiệm 30-50 input token mỗi request (bỏ schema mô tả) đổi lấy chi phí compile (một lần). Reliability lên ~100% bỏ hết retry. Cost gross thường giảm.
Failure mode khi schema phức tạp
Strict mode đảm bảo cú pháp + type, nhưng không đảm bảo nội dung đúng. Các fail mode còn lại:
- Schema quá complex: > 50 field, nest > 4 cấp, anyOf nhiều nhánh — model điền nhưng giá trị không thật, hallucinate dữ liệu.
- Free-text field bên trong JSON: field
description: strdài 500 từ — model có thể "tràn" sang ký tự special làm OpenAI compile thất bại trong vài corner case (escape \n trong string). - Schema mâu thuẫn intent: required
age: intnhưng input không có age — model "đoán" 25, 30, 40. Schema không thay được dữ liệu bị thiếu. - Compile timeout: lần đầu dùng schema lớn, OpenAI trả 408 / 500. Cần retry sau vài giây để cache xong.
- Refusal: với content nhạy cảm, model trả
refusalthay vìparsed.
Quan sát: dù strict mode bật, app vẫn phải có code path xử lý None, refusal, và validate nội dung (ví dụ regex email sau khi nhận).
Best practice production
- Flat schema khi có thể. Nest sâu chỉ khi mô hình domain bắt buộc.
- Literal cho enum: bỏ
strtự do nếu giá trị hữu hạn. - Field description ngắn: 1 câu, có format mẫu nếu cần ("ISO 8601 date, e.g. 2026-05-25").
- Test với edge case: input thiếu thông tin, input nhiễu, input rất dài — kiểm tra model trả gì.
- Validate phía client ngay cả khi strict mode bật: Pydantic catch refusal, content invalid (email sai format), business rule (ngày trong tương lai).
- Auto retry với Instructor hoặc tự viết loop khi không dùng strict mode.
- Log validation error vào telemetry: rate fail trên schema cụ thể là tín hiệu schema cần simplify.
- Temperature thấp (Bài 30) cho task extraction — 0.0-0.3 thường đủ.
JSON repair libraries
Khi làm việc với SLM local hoặc model không có structured mode, JSON malformed là chuyện thường ngày. Hai library chuyên repair:
json-repair(Stefano Baccianella, PyPI): nhận JSON lỗi, trả JSON valid. Fix trailing comma, missing brace, single quote, unescaped newline.from json_repair import repair_json raw = "{'name': 'John', 'age': 30,}" # single quote + trailing comma fixed = repair_json(raw) # '{"name": "John", "age": 30}'dirtyjson: parser relax, accept comment, single quote, trailing comma. Trả Python dict trực tiếp.
Vị trí trong pipeline: extract_json custom (Bài 24, bước 21) → json.loads thử → nếu fail dùng json-repair → vẫn fail thì retry với LLM. Pattern này đẩy parse rate từ ~93% lên ~99% mà không cần upgrade model.
Landscape 2025-2026 — provider so sánh
Provider Native structured Pattern phổ biến
─────────────────────────────────────────────────────────────────────────────
OpenAI json_schema strict (08/2024) .beta.chat.completions.parse()
Anthropic Tool use (since Claude 3, 2024) tool_choice forced
Google responseSchema (Gemini 1.5+, 2024) config.response_mime_type=
"application/json"
Mistral Function calling (2024) qua Instructor
Cohere Tool use + JSON Mode qua Instructor
Ollama JSON Mode + grammar (llama.cpp) qua Instructor / Outlines
HuggingFace Outlines, xgrammar, lm-format- Outlines + Pydantic
enforcer (OSS)
Cùng một Pydantic model, hầu hết provider đều bind được nhờ wrapper (Instructor, LiteLLM với structured mode, LangChain with_structured_output). Code đa provider 2026 không phải viết lại schema mỗi nơi.
Outlines — constrained generation cho local model
Với SLM local (Qwen 2.5, Phi-3, Llama 3.1, Gemma 2) đã giới thiệu ở bài 18. Để bắt model tuân schema, dùng outlines (library Python, PyPI outlines):
import outlines
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
model = outlines.models.transformers("Qwen/Qwen2.5-1.5B-Instruct")
generator = outlines.generate.json(model, Person)
person: Person = generator(
"Extract name and age from: 'Tôi tên John, 30 tuổi.'"
)
print(person.name, person.age)
Cơ chế: Outlines compile Pydantic / JSON Schema / regex / grammar thành finite-state machine, tại mỗi decoding step mask logit của token không hợp lệ — y hệt kỹ thuật OpenAI dùng phía server. Khác biệt: chạy local trên HF Transformers, không gửi data ra ngoài.
Trade-off với local + Outlines:
- Schema 100% enforce, không tốn API cost.
- Latency cao hơn (SLM nhỏ chạy GPU consumer ~5-50 token/s).
- Nội dung không bằng GPT-4o với task khó — flat schema, extraction thẳng, OK.
Code Python — bốn pattern chạy được
(a) OpenAI Structured Output với Pydantic:
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
class Person(BaseModel):
name: str
age: int = Field(ge=0, le=120)
role: Literal["engineer", "manager", "designer"]
client = OpenAI()
resp = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user",
"content": "Extract: 'Alice, 28 tuổi, engineer.'"}],
response_format=Person,
)
person = resp.choices[0].message.parsed
print(person) # name='Alice' age=28 role='engineer'
(b) Instructor với auto retry:
import instructor
from openai import OpenAI
client = instructor.from_openai(OpenAI())
person = client.chat.completions.create(
model="gpt-4o-mini",
response_model=Person,
max_retries=3,
messages=[{"role": "user",
"content": "Extract: 'Alice, 28, engineer.'"}],
)
(c) Anthropic tool use cho structured:
import anthropic
import json
aclient = anthropic.Anthropic()
schema = Person.model_json_schema()
tools = [{
"name": "save_person",
"description": "Save extracted person info",
"input_schema": schema,
}]
resp = aclient.messages.create(
model="claude-opus-4",
max_tokens=512,
tools=tools,
tool_choice={"type": "tool", "name": "save_person"},
messages=[{"role": "user",
"content": "Extract: 'Alice, 28, engineer.'"}],
)
person = Person.model_validate(resp.content[0].input)
(d) Outlines local model:
import outlines
model = outlines.models.transformers("Qwen/Qwen2.5-1.5B-Instruct")
generator = outlines.generate.json(model, Person)
person = generator("Extract: 'Alice, 28, engineer.'")
Bốn cách trên đều trả về cùng một Person Pydantic instance. App downstream không quan tâm provider — chỉ làm việc với object Python type-safe.
Bài tập
- Viết 3 Pydantic model:
Contact(name, email, phone, company),Product(name, price, currency, category, in_stock),Review(rating 1-5, sentiment Literal, pros/cons list). DùngLiteralcho enum,Fieldvới constraint phù hợp. - Lấy 5 đoạn text thật (description sản phẩm Tiki, review Shopee, signature email). Với mỗi text, gọi
client.beta.chat.completions.parse()tương ứng model. Đếm: bao nhiêu lần parse thành công, bao nhiêu lầnrefusal, bao nhiêu lần raiseValidationError. - So sánh 3 cách: (i) prompt-only JSON, (ii) JSON Mode
response_format={"type": "json_object"}, (iii) Structured Outputjson_schemastrict. Chạy 100 input cho mỗi cách, đo: parse rate, latency P50/P95, tổng cost (đếm tokentiktoken). - Implement schema
Reviewở bài 1 với Instructor,max_retries=3. Chạy vớigpt-4o-minivàclaude-haiku-4. Đếm số lần retry trung bình mỗi request. - (Tùy chọn) Cài
outlines+ một SLM (Qwen 2.5 1.5B). Chạy 20 input extractContactvới Outlines vs prompt-only. So sánh parse rate và latency.
- OpenAI — Structured Outputs guide
- OpenAI — Introducing Structured Outputs in the API (08/2024)
- OpenAI — JSON Mode
- Anthropic — Tool use overview
- Anthropic — Structured output via tool use
- Anthropic — Prefill Claude's response
- Pydantic v2 documentation
- Pydantic — JSON Schema generation
- Instructor — Structured outputs for LLMs
- Instructor — GitHub repository
- Outlines — Structured generation for LLMs
- Outlines — GitHub repository
- json-repair — GitHub repository
- dirtyjson — PyPI
- JSON Schema specification
- Google Gemini — Structured output
