Nỗi ám ảnh mang tên JSONDecodeError từ LLM
Tích hợp LLM vào hệ thống thực tế luôn đi kèm một bài toán muôn thuở: làm sao để model trả về đúng định dạng JSON mà backend parse được ngay.
Anh em làm automation hay backend chắc không lạ gì cảnh này. Script chạy 100 lần thì ngon lành 85 lần. Đến lần 86, OpenAI bất ngờ nhét thêm block code ```json ... ```, dư một dấu phẩy ở cuối, hoặc bỏ quên một field quan trọng. Kết quả là pipeline gãy ngang lúc 2 giờ sáng chỉ vì json.loads() quăng lỗi JSONDecodeError.
4 cách lấy structured output phổ biến hiện nay
Tùy độ phức tạp của bài toán, anh em thường chọn 1 trong 4 giải pháp:
- Cách 1: Prompt ép kiểu thuần túy. Bạn nhồi vào cuối prompt câu lệnh quen thuộc: “Chỉ trả về JSON thuần túy, không markdown, không giải thích”. Sau đó, code dùng
json.loads()để parse chuỗi nhận được. - Cách 2: OpenAI Function Calling / Tool Calling. Bạn truyền JSON Schema vào tham số
tools. Model sẽ cố gắng sinh arguments theo đúng schema đó. - Cách 3: Native Structured Outputs. Kích hoạt
response_formatvới JSON Schema hoặc Pydantic class trực tiếp trên API của OpenAI (tính năng ra mắt từ tháng 8/2024). - Cách 4: Thư viện Instructor kết hợp Pydantic. Wrapper bọc quanh client của OpenAI, Anthropic hay Gemini. Instructor biến class Pydantic thành schema, tự parse dữ liệu đầu ra và tự kích hoạt vòng lặp sửa lỗi (self-healing) khi schema bị vi phạm.
So sánh thực tế: Chọn cách nào cho dự án?
1. Prompt Engineering thuần túy
Ưu điểm: Triển khai siêu nhanh. Cách này không đòi hỏi cài thêm thư viện và chạy được với cả các model local nhỏ như Llama 3 8B hay Qwen.
Nhược điểm: Thiếu ổn định. Thử nghiệm trên 1.000 request phức tạp, tỉ lệ lỗi parse thường dao động từ 8% đến 15%. Quan trọng hơn, bạn không thể ép kiểu chi tiết (như regex email hay khoảng giá trị số nguyên) nếu chỉ dựa vào prompt.
2. Function Calling và Native Structured Outputs
Ưu điểm: Tỉ lệ tuân thủ schema đạt gần 100% trên các model đời mới như gpt-4o.
Nhược điểm: Native API chỉ đảm bảo tính hợp lệ về mặt cú pháp JSON và kiểu dữ liệu cơ bản. Nó không thể tự chạy các logic kiểm tra nghiệp vụ phức tạp, ví dụ như kiểm tra số CMND đủ 12 số hay ngày sinh phải ở quá khứ. Nếu vi phạm, request vẫn fail và bạn phải tự viết vòng lặp gọi lại API bằng tay.
3. Thư viện Instructor
Ưu điểm:
- Tận dụng toàn bộ sức mạnh của Pydantic v2: hỗ trợ type hint, auto-complete chuẩn chỉnh trên VS Code và PyCharm.
- Cơ chế Self-Healing cực kỳ thông minh. Khi dữ liệu vi phạm validator, Instructor tự gom trace log lỗi rồi gửi ngược lại cho LLM để nó tự sửa.
- Một cú pháp cho mọi model: Bạn dễ dàng đổi từ OpenAI sang Anthropic Claude 3.5 Sonnet, Google Gemini 1.5 Pro hay Groq mà không cần sửa logic validation.
Nhược điểm: Tăng thêm dependency vào dự án. Mỗi lần model phải retry để sửa lỗi, bạn sẽ tốn thêm token và độ trễ phản hồi tăng từ 500ms đến 1.5s.
Khi nào nên dùng Instructor?
Với các script cào dữ liệu cá nhân chạy một lần, bạn có thể dùng prompt thuần cho nhanh. Ngược lại, nếu đang dựng API production, bot hỗ trợ khách hàng, hoặc worker xử lý giao dịch, Instructor là cứu cánh giúp loại bỏ hoàn toàn các lỗi crash vặt do LLM gây ra.
Từng bước triển khai Instructor trong thực tế
Bước 1: Cài đặt môi trường
Cài đặt ba package chính qua pip:
pip install instructor openai pydantic
Bước 2: Khởi tạo Client
Instructor bọc quanh client của OpenAI bằng hàm instructor.from_openai() mà không làm thay đổi các thiết lập kết nối sẵn có:
import os
from openai import OpenAI
import instructor
# Khởi tạo client tiêu chuẩn
raw_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# Patch thêm tính năng structured output từ Instructor
client = instructor.from_openai(raw_client)
Bước 3: Định nghĩa cấu trúc dữ liệu với Pydantic
Xây dựng class kế thừa từ BaseModel. Hãy tận dụng Field(description=...) để model hiểu rõ mục tiêu trích xuất của từng trường:
from typing import List
from pydantic import BaseModel, Field
class DeveloperProfile(BaseModel):
name: str = Field(description="Họ và tên của lập trình viên")
years_of_experience: int = Field(description="Số năm kinh nghiệm làm việc thực tế")
primary_skills: List[str] = Field(description="Danh sách các ngôn ngữ hoặc framework thành thạo")
is_senior: bool = Field(description="Đánh dấu True nếu kinh nghiệm >= 5 năm, ngược lại False")
Bước 4: Gọi API và nhận về Object có Type Check
Bạn chỉ cần truyền class vừa tạo vào tham số response_model:
raw_text = """
Nguyễn Văn An đã code Python được hơn 6 năm nay. Anh chuyên làm
với FastAPI, Docker, PostgreSQL và gần đây có làm thêm dự án RAG với LangChain.
"""
profile: DeveloperProfile = client.chat.completions.create(
model="gpt-4o-mini",
response_model=DeveloperProfile,
messages=[
{"role": "user", "content": f"Trích xuất thông tin ứng viên: {raw_text}"}
],
)
# IDE nhận diện đúng thuộc tính, không sợ gõ nhầm key
print(f"Họ tên: {profile.name}")
print(f"Kinh nghiệm: {profile.years_of_experience} năm")
print(f"Kỹ năng chính: {', '.join(profile.primary_skills)}")
print(f"Level Senior: {profile.is_senior}")
Bước 5: Thiết lập Custom Validator và kích hoạt Self-Healing
Đây là điểm giá trị nhất của Instructor. Bạn có thể cài cắm logic kiểm tra tính hợp lệ bằng @field_validator:
from pydantic import BaseModel, Field, field_validator
class CustomerLead(BaseModel):
full_name: str = Field(description="Tên khách hàng")
phone_number: str = Field(description="Số điện thoại Việt Nam gồm 10 chữ số")
requirement: str = Field(description="Nhu cầu của khách")
@field_validator("phone_number")
@classmethod
def validate_phone(cls, v: str):
cleaned = "".join(filter(str.isdigit, v))
# Kiểm tra đúng 10 số và đầu số hợp lệ tại VN
if len(cleaned) != 10 or not cleaned.startswith("0"):
raise ValueError(f"Số điện thoại không hợp lệ ({v}). Phải gồm đúng 10 chữ số và bắt đầu bằng số 0.")
return cleaned
# Thử nghiệm với chuỗi đầu vào lộn xộn
lead_text = "Khách tên Trần Minh Hùng, gọi lại số 0912-345-678 hỏi mua gói server 16 core."
lead = client.chat.completions.create(
model="gpt-4o-mini",
response_model=CustomerLead,
max_retries=3, # Số lần tối đa cho phép LLM tự sửa lỗi validation
messages=[
{"role": "user", "content": f"Xử lý lead sau: {lead_text}"}
],
)
print(lead.model_dump_json(indent=2))
Nếu LLM vô tình để nguyên ký tự gạch nối khiến validator bắn ra ValueError, Instructor sẽ lập tức chặn lại. Nó gửi thông báo lỗi ngược lại API kèm lệnh yêu cầu chỉnh sửa. LLM sẽ tự làm sạch chuỗi thành 0912345678 và trả về kết quả chuẩn mà backend không hề bị gián đoạn.
Kinh nghiệm thực chiến cần nhớ
- Viết mô tả trong
Fieldthật tường minh: LLM dựa vàodescriptionđể đưa ra phán đoán. Mô tả càng rõ ràng, tỉ lệ trích xuất chính xác ở lần gọi đầu tiên càng cao. - Cân nhắc chi phí với
gpt-4o-mini: Thay vì dùng các model lớn tốn kém,gpt-4o-minikết hợp Instructor là combo vừa rẻ vừa nhanh cho 95% tác vụ parse dữ liệu. - Luôn giới hạn
max_retries: Đặt ngưỡng an toàn từ 2 đến 3 lần. Nếu văn bản đầu vào hoàn toàn không chứa thông tin cần tìm, việc retry liên tục chỉ làm cạn hạn mức token của bạn.
