Bắt LLM trả JSON đúng chuẩn: ba lớp và vòng repair
Ba lớp ép LLM trả JSON đúng cấu trúc, vì sao JSON mode chưa đủ, và vòng repair lấy chính thông báo lỗi của parser làm input để sửa kết quả.
Bạn prompt model "trả về JSON", jsonDecode kết quả, demo chạy ngon. Rồi 2 giờ sáng pager kêu vì model bọc JSON trong một khối ```json.
Đây là pipeline ba lớp để chuyện đó không lặp lại — và lớp cuối cùng, vòng repair, là lớp parser của bạn không bao giờ nhìn thấy.
Vì sao "trả về JSON" hỏng ở production
Tác giả bài viết gốc gọi các kiểu hỏng này là "boring and relentless" — nhàm chán nhưng dai dẳng. Danh sách anh liệt kê:
- Markdown fence — model bọc object trong khối
```json, chuỗi thô bắt đầu bằng backtick chứ không phải{. - Lời dẫn hoặc lời kết — "Sure! Here's the JSON:" chèn phía trước, hoặc "Let me know if you need anything else!" nối phía sau.
- Dấu phẩy thừa và nháy đơn — người đọc thấy hợp lệ, parser JSON nghiêm ngặt thì không.
- Type drift — bạn xin string, nhận number; xin array, nhận chuỗi nối bằng dấu phẩy. Đúng cú pháp, sai ngữ nghĩa.
- Thiếu field hoặc bịa field — model bỏ đúng cái field bạn cần, hoặc thêm một
confidence: 0.9bạn không hề xin.
Câu chốt của bài đáng dán lên tường: một model đúng 97% số lần vẫn sai hàng nghìn request mỗi ngày. Sai lầm là coi đó là từng con bug để diệt lẻ, thay vì một lớp lỗi cần hấp thụ bằng kiến trúc.
Ba lớp, mạnh nhất trước
Bài viết xếp ba công cụ ép cấu trúc theo thứ tự đảm bảo giảm dần:
- Constrained decoding (JSON mode / output ép theo schema). Provider ràng buộc việc lấy mẫu token nên output chắc chắn hợp lệ cú pháp, và nếu bạn đưa JSON Schema thì chắc chắn đúng hình dạng. Mạnh nhất vì nó tác động ở tầng sampling chứ không phải tầng prompt — model không thể sinh ra token phá vỡ grammar.
- Tool/function calling. Bạn mô tả một hàm với schema tham số có kiểu, model phát ra lời gọi có cấu trúc. Vẫn là bộ máy constrained decoding đó, khoác áo khác — và hợp nhất khi object bạn cần thực chất là một hành động:
create_invoice,extract_contact,schedule_meeting. - Prompt-and-pray kèm validation. Xin JSON trong prompt rồi kiểm tra thứ nhận về. Yếu nhất, và là nơi bạn rơi xuống khi provider, model hoặc gateway không hỗ trợ hai lớp trên — model đời cũ, vài bản open-weights sau proxy, một số đường streaming.
Chỗ nhiều người bỏ sót: kể cả lớp mạnh cũng không miễn cho bạn khâu validate. JSON mode bảo đảm bạn jsonDecode được. Nó không bảo đảm model đã điền field bạn cần, tôn trọng enum của bạn, hay không thả một null vào chỗ bạn bắt buộc phải có string.
Validate bằng schema của chính bạn
Schema cũng không diễn đạt được mọi business rule. Ví dụ bài viết đưa ra: ngày này phải sau ngày kia; mảng này phải khác rỗng khi type == "premium"; email này phải thuộc domain bạn hỗ trợ.
Nên kiến trúc luôn là ba bước: sinh ra dưới ràng buộc chặt nhất mà provider cho phép, validate theo nguồn sự thật của riêng bạn, rồi repair.
Và đây là chi tiết quyết định chất lượng vòng repair: parser chỉ có một việc — biến input bất kỳ thành một object đúng kiểu, hoặc một lỗi có cấu trúc mô tả chính xác chỗ sai. Theo tác giả, một parser ném ra lỗi chung chung kiểu "invalid" là vô dụng ở bước này, vì độ cụ thể của thông báo lỗi quyết định chất lượng của lần sửa. Lỗi đó chính là input cho bước repair.
Và đừng rải các câu if (json['name'] == null) khắp codebase. Định nghĩa contract một lần rồi validate theo nó — bằng JSON Schema, hoặc một validator kiểu Zod/Pydantic vừa để kiểm tra vừa làm schema gửi cho provider.
Làm gì tuần này
Mở đúng một chỗ trong service đang gọi LLM lấy dữ liệu có cấu trúc. Xem nó nằm ở lớp nào trong ba lớp trên, và lỗi khi parse fail có đủ cụ thể để làm input cho một lần sửa không. Nếu không, đó là việc cần làm trước khi đụng tới prompt.
Không spam, hủy đăng ký bất kỳ lúc nào.
Bài viết liên quan
Chạy LLM local trên MacBook M3 36GB để code: được và mất
06 thg 9, 2026
Cursor có 5 lớp cấu hình: bạn đang dùng đúng một lớp
05 thg 9, 2026