Ác mộng JSON “lỗi” khi làm sản phẩm AI
Làm việc với LLM sướng nhất là lúc thấy nó trả kết quả thông minh. Nhưng cực hình nhất chính là công đoạn lấy dữ liệu đó đổ vào database hoặc hiển thị lên UI. Anh em dev chắc không lạ gì cảnh này: Prompt viết cực kỹ, test trên Playground chạy 10/10, nhưng vừa lên production thì hệ thống crash. Model bỗng dưng “hứng chí” thêm vài câu dông dài kiểu “Dưới đây là JSON của bạn:” hoặc tệ hơn là thiếu một dấu ngoặc nhọn kết thúc.
Hệ quả là hàm json.loads() báo lỗi, user thấy màn hình quay vòng. Mình từng thức trắng đêm chỉ để viết regex xử lý đống text hỗn độn từ GPT-3.5 nhằm trích xuất đúng object cần thiết. Rất may, cả OpenAI và Anthropic hiện đã cung cấp tính năng Structured Output để xử lý triệt để vấn đề này.
Ba cấp độ lấy dữ liệu từ AI API
Trước khi bắt tay vào code, hãy nhìn lại lộ trình tiến hóa của việc ép định dạng dữ liệu. Hiểu rõ từng cấp độ sẽ giúp bạn chọn đúng công cụ cho dự án, tránh lãng phí tài nguyên.
1. Prompting truyền thống (Hên xui)
Cách sơ khai nhất là ghi vào prompt: “Chỉ trả về JSON, không kèm giải thích”. Phương pháp này cực kỳ thiếu ổn định. Với các model nhỏ hoặc khi prompt quá dài, tỉ lệ lỗi parse có thể lên đến 15-20%. Dùng cách này cho production thực sự là một canh bạc mạo hiểm.
2. JSON Mode (Chưa đủ an toàn)
OpenAI từng ra mắt response_format: { "type": "json_object" }. Nó đảm bảo output là JSON hợp lệ về cú pháp. Tuy nhiên, nó không đảm bảo JSON đó đúng các field bạn cần. Ví dụ, bạn cần field user_id nhưng model lại tự ý đổi thành customer_id. Code backend vẫn sẽ lỗi như thường.
3. Structured Output (Giải pháp tối ưu)
Đây là tiêu chuẩn vàng hiện nay. Thay vì hy vọng, chúng ta ép buộc model bằng kỹ thuật Constrained Decoding. Bạn cung cấp một JSON Schema hoặc Pydantic model, và API cam kết trả về đúng 100% cấu trúc đó. Nếu không thể khớp schema, API sẽ báo lỗi thay vì trả về dữ liệu rác.
Tại sao dự án lớn bắt buộc dùng Structured Output?
Mình đã áp dụng kỹ thuật này cho một hệ thống xử lý 5.000 hóa đơn mỗi ngày. Kết quả cho thấy sự khác biệt rõ rệt về độ ổn định.
- Tin cậy tuyệt đối: Loại bỏ hoàn toàn các hàm try-except rườm rà hay các đoạn code regex dọn dẹp dữ liệu.
- Type Safety: Khi dùng Pydantic trong Python, bạn có ngay IntelliSense (gợi ý code) và validation dữ liệu tại chỗ.
- Tối ưu Token: Model không tốn token cho các câu dẫn dắt rườm rà. Nó chỉ tập trung trả về dữ liệu thô bạn cần.
Triển khai với OpenAI API (Strict Mode)
OpenAI hỗ trợ Strict Mode cực mạnh thông qua thư viện Pydantic. Cách này giúp code sạch và dễ bảo trì hơn nhiều.
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI(api_key="your_key")
# 1. Định nghĩa schema rõ ràng
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
priority: int # 1 đến 5
# 2. Gọi API với phương thức parse
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Trích xuất thông tin sự kiện."},
{"role": "user", "content": "Họp team thứ 2 tới với Nam và Lan lúc 9h sáng, việc này gấp."}
],
response_format=CalendarEvent,
)
event = completion.choices[0].message.parsed
print(f"Event: {event.name} - Priority: {event.priority}")
Mấu chốt nằm ở phương thức .parse(). OpenAI tự động chuyển JSON thành object Pydantic. Nếu model vi phạm schema, một exception sẽ được raise ngay lập tức để bạn xử lý.
Ép Claude trả về JSON (Forced Tool Use)
Claude (Anthropic) không có tham số “Strict” riêng biệt. Tuy nhiên, chúng ta có thể dùng kỹ thuật Forced Tool Use để đạt kết quả tương đương.
import anthropic
client = anthropic.Anthropic(api_key="your_key")
# 1. Định nghĩa tool đóng vai trò schema
tools = [{
"name": "print_json",
"description": "Ghi dữ liệu vào hệ thống",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"date": {"type": "string"},
"priority": {"type": "integer"}
},
"required": ["name", "date", "priority"]
}
}]
# 2. Ép Claude phải gọi tool này
response = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "print_json"},
messages=[{"role": "user", "content": "Nhắc mình đi bơi vào 5h chiều mai, ưu tiên cao nhé."}]
)
# 3. Lấy data từ tool_use block
json_output = response.content[0].input
print(json_output)
Bằng cách set tool_choice, Claude sẽ bỏ qua phần hội thoại thông thường. Nó nhảy thẳng vào việc điền dữ liệu vào schema. Claude 3.5 Sonnet xử lý việc này cực kỳ thông minh, ít khi bị “ngáo” format như các dòng model cũ.
Kinh nghiệm thực tế để tránh “ăn hành”
Dù công cụ rất mạnh, khi đưa vào thực tế bạn vẫn cần lưu ý một số điểm sau để hệ thống không bị treo.
- Xử lý lỗi Schema: Đôi khi input của user quá ngắn, không đủ thông tin để điền các field
required. Đừng quên bọctry-exceptquanh đoạn parse dữ liệu. - Chọn đúng Model: Với OpenAI, hãy dùng bản
gpt-4o-2024-08-06trở lên để có hỗ trợ tốt nhất. Với Claude, dòng3.5 Sonnethiện là lựa chọn số 1 về cả tốc độ lẫn độ chính xác. - Viết description chi tiết: Trong JSON Schema, phần
descriptioncho từng field là cực kỳ quan trọng. Hãy mô tả cụ thể: thay vì đặt tên field làdate, hãy viết làdate in ISO 8601 format (YYYY-MM-DD). - Chi phí: Structured Output có thể làm tăng nhẹ độ trễ và token input do schema bị độn vào prompt. Tuy nhiên, nó vẫn rẻ hơn nhiều so với việc phải retry API nhiều lần do lỗi format.
Chuyển từ prompt dạo sang Structured Output là bước ngoặt giúp ứng dụng AI của mình chuyên nghiệp hơn hẳn. Nếu bạn đang xây dựng chatbot hoặc hệ thống trích xuất dữ liệu, hãy áp dụng ngay. Chúc anh em có những bản build ổn định, không còn nỗi lo lỗi parse JSON!

