Structured outputs từ LLMs: JSON mode, Zod và tool use
Grammar-constrained sampling là primitive LLM đáng tin cậy duy nhất. Cách OpenAI, Anthropic, Zod và Vercel AI SDK v6 so sánh — và điểm nào vẫn có thể thất bại.
Bởi Ethan
3.237 từ · 17 phút đọc
Pipeline LLM của bạn chạy tốt trong môi trường test. Rồi lúc 3 giờ sáng, một dấu ngoặc đóng bị thiếu trong JSON response của model khiến job processor crash. Schema validator ném exception, không có gì bắt được nó, và queue bị đình trệ suốt sáu tiếng.
JSON mode không ngăn chặn điều này. Nó chỉ đảm bảo JSON hợp lệ về mặt cú pháp — không đảm bảo JSON khớp với schema của bạn. Structured Outputs thì có. Hiểu rõ sự khác biệt này, và biết implementation của từng provider còn thiếu sót ở đâu, là ranh giới giữa một pipeline đáng tin cậy và một cuộc gọi lúc 3 giờ sáng.
Dành cho ai
Bạn đang xây dựng ứng dụng LLM trên production. Bạn biết Zod schema là gì. Bạn muốn hiểu các đảm bảo cơ chế — không phải một bài hướng dẫn “JSON là gì?”.
Phân biệt cốt lõi: JSON mode và structured outputs
JSON mode (response_format: { type: "json_object" }) yêu cầu model tạo ra JSON hợp lệ về mặt cú pháp. Chỉ vậy thôi. Cấu trúc, tên field, và kiểu dữ liệu vẫn không bị ràng buộc — model đoán dựa trên prompt của bạn.
Structured Outputs sử dụng grammar-constrained sampling ở tầng decoder. Bộ sampler bị ràng buộc chỉ tạo ra các chuỗi token tương ứng với output hợp lệ theo schema. Đây không phải là validation sau hậu kỳ, cũng không phải prompt engineering. Ràng buộc này mang tính cấu trúc.
Thông báo của OpenAI:
“While both ensure valid JSON is produced, only Structured Outputs ensure schema adherence.”
Tài liệu của Anthropic mô tả cơ chế tương tự:
“constraining the model’s token sampling to schema-valid outputs (a technique called grammar-constrained sampling)”
Cả hai implementation đều dẫn đến cùng một kết luận: với production pipeline cần parse output của model thành các struct có kiểu, JSON mode là primitive sai. Structured Outputs mới là thứ bạn cần.
JSON mode: nó là gì và tại sao không đủ
JSON mode tồn tại như một đảm bảo nhẹ nhàng. Nó hữu ích khi thử nghiệm và khi code downstream có thể xử lý bất kỳ shape JSON hợp lệ nào. Pipeline production hiếm khi dễ tính đến vậy.
Các failure mode mang tính định tính — không có benchmark đáng tin cậy nào qua được quá trình adversarial verification về tỷ lệ lỗi của JSON mode. Điều được ghi nhận: model có thể tạo ra JSON hợp lệ cú pháp nhưng thiếu required field, dùng sai kiểu dữ liệu, hoặc có các key bạn không yêu cầu. Validator của bạn sẽ bắt được điều này, nhưng chỉ khi bạn có một cái. Nhiều pipeline không có.
Dùng JSON mode khi: bạn đang prototype, schema đơn giản và prompt-adherence là đủ, hoặc bạn đang dùng model không hỗ trợ Structured Outputs.
Dùng Structured Outputs khi: output được đưa vào typed data pipeline, vi phạm schema sẽ gây lỗi ở downstream, hoặc bạn đang tiếp cận bất kỳ thứ gì gần với production scale.
Function calling: cơ chế của OpenAI và Anthropic
Function calling (tool use) là cơ chế ra đời sớm hơn để tạo structured response. Hai provider triển khai nó theo cách khác nhau.
OpenAI function calling
API function-calling của OpenAI cho phép bạn định nghĩa các tool với input JSON Schema. Model trả về mảng tool_calls khi quyết định gọi một tool. Caller thực thi function và trả về kết quả.
import OpenAI from "openai";
const client = new OpenAI();
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "What is the weather in Paris?" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Returns weather data for a city",
parameters: {
type: "object",
properties: {
city: { type: "string" },
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
},
},
],
});
Function calling không có strict: true vẫn không đảm bảo schema adherence — model có thể bỏ qua required properties hoặc dùng sai kiểu. Để có đảm bảo đó, bạn cần Structured Outputs API (phần bên dưới).
Anthropic tool use
API tool use của Anthropic đạt GA vào ngày 30 tháng 5 năm 2024. Mỗi định nghĩa tool cần có name, description, và input_schema (JSON Schema). Khi Claude gọi một tool, API trả về stop_reason: "tool_use".
Tham số tool_choice điều khiển hành vi gọi tool:
| Mode | Hành vi | Mặc định? |
|---|---|---|
auto | Model tự quyết định có gọi tool hay không | Có, khi có tools |
any | Phải gọi một trong các tool được cung cấp | Không |
tool | Buộc gọi một tool cụ thể theo tên | Không |
none | Không được dùng tool | Có, khi không có tools |
Một hành vi cần lưu ý: khi tool_choice là any hoặc tool, API prefill assistant message. Điều này loại bỏ mọi phần mở đầu ngôn ngữ tự nhiên trước các tool_use block — dù bạn có yêu cầu trong prompt. Hãy tính đến điều này khi thiết kế UI.
OpenAI Structured Outputs API
OpenAI Structured Outputs đạt GA vào ngày 6 tháng 8 năm 2024. API sử dụng response_format: { type: "json_schema" } với strict: true. Cả Python SDK và Node.js SDK đều đi kèm helper zodResponseFormat để chuyển đổi Zod schema sang định dạng cần thiết và trả về kết quả đã parse có kiểu.
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const client = new OpenAI();
const CalendarEventSchema = z.object({
name: z.string(),
date: z.string(),
participants: z.array(z.string()),
});
const completion = await client.beta.chat.completions.parse({
model: "gpt-4o-2024-08-06",
messages: [
{ role: "user", content: "Alice and Bob are going to a science fair on Friday." }
],
response_format: zodResponseFormat(CalendarEventSchema, "event"),
});
const event = completion.choices[0].message.parsed;
// event có kiểu { name: string; date: string; participants: string[] }
Vào tháng 5 năm 2025, OpenAI mở rộng strict mode để hỗ trợ parallel tool calling và bổ sung các tính năng JSON Schema mới: string validation patterns (email, uri, date-time), và range constraints cho số và mảng.
Những gì đảm bảo không bao gồm
Ba failure mode có thể vượt qua đảm bảo schema trên mọi implementation Structured Outputs:
- Safety refusal — model từ chối tạo output vì lý do chính sách. Response không có structured output; code của bạn phải xử lý
message.refusal. - Bị cắt ngắn do token limit — response bị cắt tại
max_tokens. Python SDK némLengthFinishReasonErrorkhifinish_reason == "length". Output không đầy đủ và không hợp lệ theo schema. - Content filter block — output bị chặn sau khi tạo.
Schema adherence chỉ được đảm bảo cho các completion bình thường. Hãy xử lý rõ ràng cả ba trường hợp.
const completion = await client.beta.chat.completions.parse({ ... });
const message = completion.choices[0].message;
if (message.refusal) {
throw new Error(`Model refused: ${message.refusal}`);
}
if (completion.choices[0].finish_reason === "length") {
throw new Error("Response truncated at max_tokens");
}
const data = message.parsed; // an toàn ở đây
Anthropic strict tool use
Strict tool use của Anthropic thêm grammar-constrained sampling vào function calling. Khi đặt strict: true trên định nghĩa tool kết hợp với tool_choice: { type: "any" }, bạn có hai đảm bảo: một tool sẽ được gọi VÀ các input của nó sẽ khớp chính xác với schema.
Strict tool use thoát khỏi public beta vào ngày 29 tháng 1 năm 2026 — không cần beta header sau ngày đó. Tính năng này có sẵn trên Claude Sonnet 4.5, Claude Opus 4.5, Claude Haiku 4.5, và tất cả các model Claude API về sau.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools = [
{
name: "get_weather",
description: "Returns weather data for a city",
input_schema: {
type: "object" as const,
properties: {
city: { type: "string", description: "City name" },
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
strict: true,
},
];
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools,
tool_choice: { type: "any" },
messages: [{ role: "user", content: "What is the weather in Paris?" }],
});
// response.stop_reason === "tool_use"
const toolUseBlock = response.content.find((b) => b.type === "tool_use");
Ba failure mode tương tự ở phần OpenAI cũng áp dụng ở đây — safety refusal, cắt ngắn, và content filter block đều có thể vượt qua đảm bảo.
Claude Mythos Preview không hỗ trợ forced tool use. Các request với tool_choice: { type: "any" } hoặc tool_choice: { type: "tool", name: "..." } trả về lỗi 400 trên model đó. Nếu bạn đang dùng Mythos Preview, hãy dùng tool_choice: auto và dựa vào prompting — ràng buộc schema strict: true vẫn áp dụng cho bất kỳ tool nào model chọn gọi.
Giới hạn schema của Anthropic
Structured outputs của Anthropic không hỗ trợ:
- Recursive schemas
- Ràng buộc số (
minimum,maximum,multipleOf) - Ràng buộc độ dài chuỗi (
minLength,maxLength) additionalPropertiesđặt thành giá trị khácfalse
Các SDK chính thức loại bỏ các ràng buộc không được hỗ trợ ở phía client và chuyển chúng vào các trường description. Việc validation range số và độ dài chuỗi phải diễn ra trong application code của bạn. Đây là giới hạn đáng kể nếu schema của bạn dựa vào các ràng buộc này để đảm bảo tính đúng đắn — một z.number().min(0).max(100) âm thầm trở thành plain number field ở tầng API.
Liệu Vercel AI SDK có xử lý các giới hạn schema của Anthropic một cách hợp lý khi dùng Claude models hay không vẫn là câu hỏi mở. Hành vi ở tầng SDK chưa được xác minh — hãy test với schema cụ thể của bạn trước khi dùng trong production.
Zod để parse có kiểu an toàn
Zod là lớp validation de facto trong TypeScript LLM pipeline. Hai method cần biết:
.parse() ném exception khi thất bại. Trả về bản sao deep clone có kiểu mạnh khi thành công. Dùng khi vi phạm schema luôn là lỗi của developer.
.safeParse() không bao giờ ném exception. Trả về discriminated union: { success: true; data: T } | { success: false; error: ZodError }. Dùng cho LLM output khi response không đầy đủ hoặc không hợp lệ là điều có thể xảy ra.
import { z } from "zod";
const ArticleSchema = z.object({
title: z.string(),
tags: z.array(z.string()),
publishedAt: z.string().datetime(),
});
// Option A: ném exception — dùng khi vi phạm schema luôn là lỗi của developer
const article = ArticleSchema.parse(llmOutput);
// Option B: discriminated union — dùng cho LLM output
const result = ArticleSchema.safeParse(llmOutput);
if (!result.success) {
console.error(result.error.issues);
// e.g. [{ path: ["publishedAt"], code: "invalid_string", message: "Invalid datetime" }]
} else {
const article = result.data; // có kiểu đầy đủ
}
// Option C: async schema cần dùng variant async
const articleAsync = await ArticleSchema.safeParseAsync(llmOutputStream);
Khi có lỗi, dùng result.error.issues (không phải errors — alias errors đã bị xóa trong Zod v4). Đã xác minh trên v4.4.3.
Vercel AI SDK v6: generateText với structured output
Vercel AI SDK v6 là một breaking change. generateObject và streamObject đã deprecated và sẽ bị xóa. Phiên bản thay thế là generateText và streamText với tham số output.
| Khái niệm | v5 (deprecated) | v6 (hiện tại) |
|---|---|---|
| Object có cấu trúc | generateObject({ schema }) | generateText({ output: Output.object({ schema }) }) |
| Streaming | streamObject({ schema }) | streamText({ output: Output.object({ schema }) }) |
| Partial stream | partialObjectStream | partialOutputStream |
| Import | { generateObject } from 'ai' | { generateText, Output } from 'ai' |
Nhiều tutorial vẫn dùng cú pháp v5. Nếu bạn copy-paste ví dụ SDK từ các bài viết trước cuối năm 2025, rất có thể bạn đang xem code đã deprecated.
Migration tự động: npx @ai-sdk/codemod upgrade v6
Ví dụ code v6
import { generateText, streamText, Output } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const ArticleSchema = z.object({
summary: z.string().describe("One-paragraph summary"),
temperature: z.number(),
recommendation: z.string(),
});
// generateText với structured output
const { output } = await generateText({
model: anthropic("claude-sonnet-4-6"),
output: Output.object({ schema: ArticleSchema }),
prompt: "Analyze the weather in San Francisco for a developer blog post.",
});
// output có kiểu z.infer<typeof ArticleSchema>
// Phiên bản streaming
const { partialOutputStream } = streamText({
model: anthropic("claude-sonnet-4-6"),
output: Output.object({ schema: ArticleSchema }),
prompt: "...",
});
for await (const partial of partialOutputStream) {
console.log(partial);
}
v6 cung cấp năm output mode thông qua Output.*:
| Mode | Trường hợp dùng |
|---|---|
Output.object({ schema }) | Object có cấu trúc đơn lẻ với schema validation |
Output.array({ element }) | Mảng typed objects; validation per-element |
Output.choice({ options }) | Phân loại từ tập string cố định |
Output.json() | JSON không có cấu trúc, không có schema enforcement |
Output.text() | Plain text (mặc định) |
Một thay đổi kiến trúc từ v5: structured output giờ được tính là một bước trong vòng lặp multi-step tool-calling. Nếu bạn kết hợp tools với structured output, hãy cấu hình stopWhen rõ ràng. Kiểu lỗi khi thất bại là AI_NoObjectGeneratedError, giữ lại text, response, usage, và cause để debug.
Zod, Valibot, Arktype, và bất kỳ thư viện nào implement Standard JSON Schema interface đều hoạt động tốt trong v6.
Bảng so sánh
| Cơ chế | Đảm bảo schema | Failure mode vượt qua đảm bảo | Giới hạn schema đáng chú ý |
|---|---|---|---|
| JSON mode (OpenAI / Anthropic) | Chỉ hợp lệ cú pháp JSON | N/A — không có đảm bảo schema | Không có |
OpenAI Structured Outputs (strict: true) | Grammar-constrained | Safety refusal, cắt ngắn max_tokens, content filter | Không (hỗ trợ hầu hết tính năng JSON Schema) |
OpenAI function calling (không có strict) | Không có | Tất cả những trường hợp trên | — |
Anthropic strict tool use (strict: true + any) | Grammar-constrained | Safety refusal, cắt ngắn max_tokens, content filter | Không có recursive schemas, không có ràng buộc số/độ dài chuỗi |
Anthropic tool use (không có strict) | Không có | Tất cả những trường hợp trên | — |
Vercel AI SDK v6 Output.object | Phụ thuộc model | Truyền từ provider | Phụ thuộc model — giới hạn của Anthropic sẽ ảnh hưởng |
Khuyến nghị
Với production pipeline mới trên OpenAI: dùng client.beta.chat.completions.parse() với zodResponseFormat. Xử lý rõ ràng message.refusal và finish_reason === "length".
Với production pipeline mới trên Anthropic: dùng strict tool use với tool_choice: { type: "any" }. Loại bỏ các ràng buộc số và độ dài chuỗi khỏi Zod schema trước khi truyền vào API — chúng sẽ bị âm thầm bỏ qua.
Với abstraction đa provider: Vercel AI SDK v6 là lựa chọn hợp lý, nhưng hãy xác minh hành vi schema Anthropic với các schema cụ thể của bạn trước khi release. Hành vi ở tầng SDK cho các tính năng schema không được Anthropic hỗ trợ chưa được ghi nhận đầy đủ.
Về validation layer: dùng .safeParse() ở khắp nơi khi xử lý LLM output. Dùng .parse() chỉ khi vi phạm schema thực sự là lỗi của developer.
Với streaming: dùng streamText với Output.object trong v6. Iterator partialOutputStream cung cấp các partial object có kiểu được xây dựng dần dần.
Về tối ưu chi phí: sau khi schema compliance ổn định, LLM cost routing: khi nào Haiku thắng Opus trình bày khi nào các tác vụ classification và extraction có thể chuyển sang model rẻ hơn mà không ảnh hưởng chất lượng output.
Về quản lý token budget: nếu system prompt dài đẩy structured-output response chạm giới hạn max_tokens, prompt caching năm 2026 — so sánh Anthropic, OpenAI và Gemini giải thích cách cả ba provider xử lý prefix caching, giảm chi phí context lặp lại tới 90%.
Gotcha và edge case
OpenAI Python SDK — nested Pydantic models với field descriptions
Nếu bạn dùng nested Pydantic models trong strict mode và thêm Field(description=...) vào một field có kiểu là một Pydantic model khác, SDK sẽ gửi JSON Schema không hợp lệ đến API và bạn nhận 400 BadRequestError. Nguyên nhân: JSON Schema cho $ref đi kèm extra properties cần inline expansion; code path cũ đã bỏ qua recursive strict coercion trên expanded object.
Cách sửa: nâng cấp openai-python lên phiên bản bao gồm PR #2025 (merged ngày 17 tháng 1 năm 2025). Bất kỳ phiên bản nào sau ngày đó đều an toàn.
# Python — điều gây ra bug (đã sửa trong openai-python sau 2025-01-17)
from openai import OpenAI
from pydantic import BaseModel, Field
class Address(BaseModel):
street: str
city: str
class Person(BaseModel):
# Field(description=...) này trên nested model type gây ra lỗi 400
address: Address = Field(description="Home address")
name: str
Anthropic — không có numeric constraints ở API
z.number().min(0).max(100) trong Zod schema của bạn không tạo ra ràng buộc nào ở tầng Anthropic API. SDK âm thầm loại bỏ minimum, maximum, và multipleOf. Hãy validate range trong application code sau khi parse.
Anthropic — extended thinking không tương thích với forced tool_choice
Khi bật extended thinking, tool_choice: { type: "any" } và tool_choice: { type: "tool", name: "..." } không được hỗ trợ và trả về runtime error. Chỉ tool_choice: { type: "auto" } (mặc định) và tool_choice: { type: "none" } hoạt động cùng với extended thinking.
Khuyến nghị production trong bài này — strict tool use với tool_choice: { type: "any" } — không áp dụng khi bạn thêm extended thinking. Nếu bật cả hai, API sẽ từ chối request. Hãy dùng tool_choice: auto và dựa vào prompting khi cần extended thinking.
Zod v4 — alias errors bị xóa
error.errors là alias cho error.issues trong Zod v3. Nó đã bị xóa trong v4. Nếu bạn đang nâng cấp từ v3, hãy tìm .errors và thay bằng .issues.
Vercel AI SDK v6 — generateObject vẫn còn nhưng đã deprecated
generateObject vẫn hoạt động trong v6 nhưng đã deprecated và sẽ bị xóa. Chạy nó hiện tại không tạo ra cảnh báo runtime. Kiểm tra trong codebase của bạn với:
grep -r "generateObject\|streamObject" src/ --include="*.ts"
Anthropic × Vercel AI SDK — tương thích schema
Liệu Vercel AI SDK v6 có dịch Zod schema cho Anthropic bằng cách loại bỏ các tính năng không được hỗ trợ hay không vẫn chưa được xác minh. Một GitHub issue (#13355) xác nhận API từ chối schema có các property không được hỗ trợ — nhưng liệu SDK có xử lý việc dịch này trước khi request đến API hay không thì chưa được ghi nhận. Hãy test schema cụ thể của bạn với Claude models trước khi dựa vào SDK như một abstraction layer.
Tài liệu tham khảo
- OpenAI Structured Outputs announcement
- OpenAI Structured Outputs guide
- OpenAI changelog
- OpenAI Python SDK PR #2025 (Pydantic $ref fix)
- Anthropic tool use — implement tool use
- Anthropic strict tool use
- Anthropic structured outputs — schema limitations
- Anthropic API release notes
- Zod documentation
- Vercel AI SDK v6 — generating structured data
- Vercel AI SDK v6 migration guide