Bỏ qua điều hướng

Structured Outputs là gì? Cách ép LLM trả về JSON hợp lệ

Structured outputs là kỹ thuật ép LLM trả về dữ liệu đúng chuẩn schema bằng giải mã có ràng buộc (constrained decoding), loại bỏ lỗi parse.

Tuan Tran Van
13 phút đọc
Mục lục (9 phần)
  1. Structured outputs là gì?
  2. Vì sao chỉ dặn trong prompt là chưa đủ?
  3. Mô hình bị ép vào khuôn như thế nào?
  4. OpenAI, Claude và Gemini làm điều này khác nhau ra sao?
  5. Cái giá phải trả: JSON hợp lệ nhưng câu trả lời sai
  6. Suy luận trước, đóng khuôn sau
  7. Khi nào bạn vẫn cần một lớp kiểm tra riêng?
  8. Khi nào nên — và không nên — dùng structured outputs?
  9. Tài liệu tham khảo

Structured outputs là kỹ thuật cho phép bạn ép mô hình ngôn ngữ lớn (LLM) phải trả về dữ liệu tuân thủ tuyệt đối một schema (lược đồ dữ liệu) định sẵn, thường là JSON Schema.

Thay vì chỉ sử dụng prompt engineering (kỹ thuật viết câu lệnh) và hy vọng mô hình nghe lời, kỹ thuật này can thiệp trực tiếp vào bước giải mã (decoding) ở tầng suy luận (inference). Nhờ vậy, output đi thẳng từ văn bản lộn xộn về đúng định dạng mà hệ thống của bạn cần, không kèm theo những lỗi cú pháp bất ngờ.

Quá trình này đảm bảo mọi token được sinh ra đều hợp lệ về mặt cấu trúc ngay từ đầu. Khi bạn tích hợp LLM vào các đường ống dữ liệu (pipeline) tự động, structured outputs xóa sổ các lỗi phân tích cú pháp (parse error) và biến phản hồi của mô hình thành dữ liệu đầu vào mà code của bạn dám tin.

Đây không còn là chuyện "dặn dò" mô hình nữa, mà là dựng một rào chắn kỹ thuật không thể vượt qua ngay ở tầng suy luận của engine.

Minh hoạ structured outputs: đầu ra tự do của mô hình ngôn ngữ lớn được ép vào khuôn JSON có cấu trúc

Structured outputs là gì?

Về mặt kỹ thuật, structured outputs chạy trên nền phương pháp giải mã có ràng buộc (constrained decoding). Khi mô hình chuẩn bị chọn token tiếp theo từ bộ từ vựng, hệ thống sẽ can thiệp để ép phân phối xác suất chỉ tập trung vào những token hợp lệ theo quy tắc của schema hoặc ngữ pháp (grammar). Những token làm sai cấu trúc bị chặn đứng ngay tại chỗ, nên kết quả đầu ra luôn "đúng về mặt cấu trúc" (correct by construction), tức là đúng từ lúc sinh ra chứ không phải sửa lại sau khi đã sai.

Ba mức ràng buộc đầu ra: biểu thức chính quy, JSON Schema và ngữ pháp GBNF

Bạn cần phân biệt rõ giữa việc "yêu cầu lịch sự" trong prompt và việc "ép buộc" ở tầng suy luận. Trong khi prompt engineering dễ bị mô hình phớt lờ mỗi khi gặp tác vụ khó, structured outputs thực thi ràng buộc ở mức toán học, ngay trong quá trình tính toán. Cách tiếp cận này giúp hệ thống của bạn không còn phải loay hoay với các đoạn code xử lý ngoại lệ cồng kềnh hoặc phải tốn chi phí gọi API nhiều lần để mô hình "sửa sai" định dạng.

Kỹ thuật này hỗ trợ nhiều loại khuôn mẫu khác nhau, từ các biểu thức chính quy (Regex) đơn giản cho đến JSON Schema phức tạp hay các ngữ pháp như GBNF (GGML BNF). Chọn định dạng nào là tùy vào độ phức tạp của dữ liệu bạn cần. Chẳng hạn, GBNF rất mạnh khi bạn cần ép mô hình sinh ra mã SQL hoặc các ngôn ngữ đặc tả riêng (DSL) có cấu trúc lồng nhau sâu mà các bộ lọc thông thường khó lòng kiểm soát nổi.

Vì sao chỉ dặn trong prompt là chưa đủ?

Chỉ dùng prompt để ép mô hình "trả về JSON" là cách làm thiếu tin cậy trong môi trường sản xuất. Mô hình rất hay tự ý chèn thêm lời dẫn, bọc kết quả trong thẻ markdown, hoặc trả về sai kiểu dữ liệu như dùng chuỗi (string) thay cho số nguyên (integer). Ngay cả những mô hình hàng đầu cũng thường xuyên bỏ sót các trường thông tin bắt buộc hoặc làm sai cấu trúc dấu ngoặc khi phản hồi quá dài, gây lỗi cho hệ thống hạ tầng phía sau.

Bốn kiểu lỗi thường gặp khi chỉ dặn mô hình trả về JSON bằng prompt: thêm lời dẫn, bọc markdown, sai kiểu dữ liệu, thiếu trường bắt buộc

Chạy ở quy mô lớn mà chỉ dựa vào prompt, tỷ lệ lỗi phân tích cú pháp (parse error) có thể rơi vào khoảng 1-5%. Tỷ lệ này đủ để làm sụp đổ các hệ thống tự động. Thậm chí, nghiên cứu "Let Me Speak Freely?" đo được 100% phản hồi từ GPT-3.5 Turbo trong JSON-mode đặt trường "answer" trước trường "reason" khi được yêu cầu: mô hình bám vào thói quen dự đoán token chứ không bám vào thứ tự logic bạn mong muốn.

Bản chất của LLM là tính bất định (non-deterministic). Khi phải xử lý các bài toán suy luận nặng, mô hình dồn sức cho nội dung câu trả lời và lơ là chuyện giữ đúng định dạng. Việc ép mô hình "chỉ trả về JSON" thông qua prompt không giải quyết được gốc rễ vấn đề là sự tự do của decoder. Structured outputs giải quyết vấn đề này bằng cách tách bạch: mô hình lo việc chọn nội dung, còn bộ giải mã lo việc đảm bảo nội dung đó nằm đúng trong khuôn khổ cấu trúc.

Mô hình bị ép vào khuôn như thế nào?

Cơ chế kỹ thuật cốt lõi ở đây là logits masking. Khi mô hình tính toán xác suất cho token tiếp theo, những token làm sai cấu trúc schema bị gán giá trị âm vô cùng (-inf) trước khi qua hàm softmax, tức là bước biến điểm số thành xác suất. Kết quả là các token lỗi này sẽ có xác suất bằng 0, buộc mô hình phải chọn từ tập hợp các token hợp lệ. Để làm được điều này, hệ thống dùng máy trạng thái hữu hạn (FSM) hoặc ô-tô-mát đẩy xuống (Pushdown Automaton) để bám theo trạng thái đầu ra qua từng bước, và xác định chính xác token nào là "hợp lệ" tiếp theo.

Cơ chế logits masking: token sai cấu trúc bị gán âm vô cùng trước hàm softmax nên xác suất về 0

Rắc rối nhất ở đây là "mismatched tokenizer", xảy ra khi các mảnh token của mô hình không khớp hoàn toàn với quy tắc ngữ pháp. Các bộ giải mã hiện đại xử lý vấn đề này bằng cách đối soát ngữ pháp với bộ từ vựng để tìm ra những token là "tiền tố hợp lệ" (valid prefixes) của một chuỗi hoàn chỉnh theo ngữ pháp. Điều này đảm bảo tính hợp lệ ở mức từng ký tự, ngăn chặn lỗi từ khi token chưa được sinh ra.

Khác với các hệ thống dùng FSM thường gặp khó khăn với các cấu trúc đệ quy (như UI lồng nhau), cách tiếp cận dựa trên ngữ pháp phi ngữ cảnh (CFG) của OpenAI cho phép xử lý các schema phức tạp và có tính đệ quy cao. Dưới đây là một ví dụ về schema đơn giản mà bạn có thể dùng để ép mô hình trả về thông tin cơ bản:

json
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer" }
  },
  "required": ["name", "age"],
  "additionalProperties": false
}

OpenAI, Claude và Gemini làm điều này khác nhau ra sao?

OpenAI triển khai response_format: { type: "json_schema" } với tham số strict: true. Họ chuyển đổi schema sang ngữ pháp phi ngữ cảnh (CFG), giúp đạt tỷ lệ chính xác 100% trong các bài thử nghiệm nội bộ, một bước nhảy vọt so với mức dưới 40% của các bản gpt-4-0613 cũ. Tuy nhiên, cái giá phải trả là độ trễ biên dịch ngữ pháp (grammar compilation) trong lần gọi đầu tiên: schema thông thường mất dưới 10 giây, còn schema phức tạp có thể lên tới một phút, dù các lần gọi sau sẽ nhanh hơn nhờ bộ nhớ đệm (caching), nên schema của bạn càng hay thay đổi thì cái giá này càng khó chịu.

So sánh cách OpenAI, Claude và Gemini triển khai structured outputs qua các tham số API khác nhau

Claude của Anthropic sử dụng output_config.format và tính năng "strict tool use". Điểm hay của SDK Claude là nó tự dọn dẹp trước: nó gỡ các ràng buộc không hỗ trợ trực tiếp ở tầng suy luận như minLength hay maximum ra khỏi schema, sau đó đưa chúng vào phần mô tả trường (description). Nhờ vậy mô hình vẫn nhận thức được ràng buộc thông qua prompt ngầm định, còn bộ giải mã thì không phải gánh thêm việc.

Google Gemini hỗ trợ JSON Schema trực tiếp qua REST API và cho phép dùng Pydantic hoặc Zod để định nghĩa. Gemini xử lý được các cấu trúc phức tạp như anyOf, nhờ đó bạn thiết kế được những schema có điều kiện linh hoạt. Gemini cũng tương thích tốt với các thư viện phổ biến bên Python và JavaScript.

Cái giá phải trả: JSON hợp lệ nhưng câu trả lời sai

Siết khuôn quá chặt có thể làm mô hình suy luận kém hẳn đi. Nghiên cứu "Let Me Speak Freely?" chỉ ra rằng hạn chế không gian token ngay từ đầu làm giảm đáng kể khả năng suy luận khi giải toán (GSM8K) hay khi xử lý logic. Khi không có không gian để "suy nghĩ" tự do (Chain of Thought), mô hình dễ bị kẹt vào các lựa chọn sai lầm nhưng vẫn phải cố gắng lấp đầy cấu trúc JSON bạn yêu cầu.

Đối lập giữa JSON đúng cú pháp và nội dung đúng logic: cấu trúc hợp lệ không bảo đảm câu trả lời đúng

Chuyện này thường xảy ra khi mô hình bị bắt "chốt" đáp án ngay ở những token đầu tiên của một schema hẹp. Ví dụ, nếu bạn thiết kế schema yêu cầu trường "answer" đứng trước trường "reason", mô hình buộc phải đưa ra kết quả ngay lập tức mà không được phép tính toán trung gian. Kết quả là bạn nhận được một JSON hoàn hảo về cú pháp nhưng nội dung giá trị bên trong lại hoàn toàn sai lệch so với logic thực tế.

Đặt lên bàn cân, để mô hình trả về văn bản tự do thường đạt độ chính xác cao hơn so với ép vào "JSON-mode" mà không chừa chỗ cho suy luận. Cấu trúc JSON vô hình trung đã chặn đứng đúng những token trung gian mô hình cần để nghĩ. Vì vậy, thứ tự các key trong schema là một yếu tố kỹ thuật ảnh hưởng trực tiếp đến trí thông minh của hệ thống, chứ không phải chuyện thẩm mỹ.

Suy luận trước, đóng khuôn sau

Để khắc phục nhược điểm trên, bạn nên đưa trường reasoning hoặc thought lên vị trí đầu tiên trong schema. Điều này tạo ra một "không gian nháp" (scratchpad) để mô hình tính toán từng bước trước khi điền vào kết quả cuối cùng. Cách sắp xếp này giữ được khả năng suy luận của mô hình mà đầu ra vẫn đúng cấu trúc cho code xử lý phía sau.

So sánh thứ tự trường trong schema: đặt trường suy luận trước đáp án thay vì đặt đáp án lên đầu

Kỹ thuật "hai giai đoạn" cũng là một lựa chọn tốt: giai đoạn 1 cho mô hình suy luận tự do, giai đoạn 2 dùng structured outputs để trích xuất dữ liệu từ đoạn văn đó. Cách này tách biệt hoàn toàn việc tư duy và việc định dạng. Các dòng mô hình suy luận như o1 hoặc Claude với tag <thinking> đã tự động hóa quy trình này bằng cách tách phần tư duy không cấu trúc khỏi phần kết quả có cấu trúc cuối cùng.

Với các tác vụ khó, khi thiết kế schema tôi luôn đặt trường suy luận lên trước, để mô hình không bị bắt chốt đáp án khi chưa kịp nghĩ và sai số logic giảm xuống. Dưới đây là ví dụ minh họa cho một schema tối ưu cho suy luận:

json
{
  "type": "object",
  "properties": {
    "reasoning": { "type": "string", "description": "Suy luận chi tiết trước khi trả lời" },
    "final_answer": { "type": "integer" }
  },
  "required": ["reasoning", "final_answer"]
}

Khi nào bạn vẫn cần một lớp kiểm tra riêng?

Structured outputs chỉ đảm bảo cái "khuôn" (cấu trúc), không đảm bảo cái "thịt" (tính chính xác của nội dung). Mô hình vẫn có thể ảo giác (hallucinate) dữ liệu nằm trong một JSON đẹp đẽ. Sau khi nhận dữ liệu, bạn vẫn phải chạy các bước kiểm tra logic nghiệp vụ để biết những giá trị bên trong có thực tế và hợp lý hay không.

Hai lớp phòng thủ: structured outputs bảo đảm cấu trúc, lớp kiểm tra nghiệp vụ bảo đảm giá trị hợp lý

Có những ngoại lệ hệ thống mà structured outputs không thể xử lý hoàn toàn. Ví dụ, khi mô hình từ chối trả lời vì lý do an toàn (refusal), nó có thể trả về một thông báo văn bản thay vì JSON. Hoặc nếu quá trình sinh token bị dừng đột ngột do hết giới hạn max_tokens, bạn sẽ nhận được một đoạn JSON bị cắt cụt không thể parse được. Bạn cần code xử lý các trường hợp stop_reason này để hệ thống không bị treo.

Để chắc tay hơn, bạn nên dùng các thư viện như Instructor kết hợp với Pydantic. Các công cụ này hỗ trợ xác thực (validation) sâu hơn và có cơ chế tự động thử lại (retry) kèm thông báo lỗi chi tiết cho mô hình nếu dữ liệu bên trong không đạt yêu cầu. Đây là lớp phòng thủ thứ hai, giúp hệ thống bền bỉ hơn trước những lỗi logic mà bản thân structured outputs ở tầng suy luận không thể nhận diện.

Khi nào nên — và không nên — dùng structured outputs?

Lời khuyên kỹ thuật của tôi là hãy dùng structured outputs cho các tác vụ trích xuất dữ liệu, phân loại, hoặc làm cổng kết nối với API nơi mà sai một dấu phẩy cũng gây lỗi. Ở giao diện giữa LLM và chương trình (LLM-to-program), đây là mặc định hợp lý nếu bạn muốn hạ tầng phần mềm của mình đứng vững.

Tuy nhiên, đừng dùng nó cho các tác vụ sáng tạo, viết lách tự do, hoặc những bài toán suy luận cực khó nếu bạn chưa thiết kế được một schema có đủ chỗ cho mô hình "tư duy". Thứ bạn phải cân là độ chặt của cấu trúc và chỗ trống cho suy luận, và như phần trên đã cho thấy, chỉ cần đặt sai thứ tự hai trường trong schema là câu trả lời hỏng.

Tài liệu tham khảo

Chia sẻ bài viết

X / TwitterFacebookLinkedIn