Tools

Bắt Claude trả JSON chuẩn schema: dùng tool use thay vì “làm ơn trả JSON”

Tóm tắt

Khai báo một tool với input_schema (JSON Schema), đặt tool_choice bắt buộc gọi tool đó, rồi đọc input của khối tool_use. Kết hợp validate phía server và retry khi sai. Claude API cũng có tính năng structured outputs chính thức — xem docs để chọn cách phù hợp.

“Hãy trả về JSON, không giải thích gì thêm” — câu prompt này chạy được 95% số lần. 5% còn lại là lúc model thêm “Đây là kết quả:” trước JSON, viết "High" thay vì "high", quên một trường bắt buộc… và parser của bạn văng lỗi lúc 2 giờ sáng. Với Claude, có hai cách chắc chắn hơn nhiều: structured outputs (ràng buộc output theo JSON Schema) và tool use với schema chặt.

Ba cách lấy JSON từ Claude

CáchĐộ tin cậyKhi nào dùng
Prompt “hãy trả JSON”Thấp — phụ thuộc may rủiPrototype nhanh, không đưa vào production
JSON outputs (output_config.format)Cao — constrained decoding bám đúng schemaCâu trả lời cuối cùng phải parse được: trích xuất, phân loại, báo cáo
Strict tool use (strict: true)Cao — tên tool và tham số đúng schemaAgent gọi hàm của bạn, tham số sai là hỏng hệ thống

Hai tính năng sau có thể dùng riêng hoặc cùng lúc trong một request — ví dụ agent vừa gọi tool chính xác, vừa trả kết quả cuối có cấu trúc.

Cách 1: JSON outputs

Khai báo schema trong output_config.format. Claude được ràng buộc khi sinh token nên output luôn khớp schema (trừ vài trường hợp ngoại lệ ở dưới), không cần vòng retry vì sai định dạng:

import json, anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=500,
    messages=[{"role": "user", "content": ticket_text}],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["bug", "billing", "feature", "other"]},
                    "priority": {"type": "string", "enum": ["P1", "P2", "P3"]},
                    "summary":  {"type": "string"}
                },
                "required": ["category", "priority", "summary"],
                "additionalProperties": False
            }
        }
    },
)
data = json.loads(next(b.text for b in resp.content if b.type == "text"))

Dùng Pydantic cho gọn (Python SDK)

from pydantic import BaseModel
from typing import Literal

class Ticket(BaseModel):
    category: Literal["bug", "billing", "feature", "other"]
    priority: Literal["P1", "P2", "P3"]
    summary: str

resp = client.messages.parse(
    model="claude-sonnet-5-5", max_tokens=500,
    messages=[{"role": "user", "content": ticket_text}],
    output_format=Ticket,
)
ticket = resp.parsed_output   # đối tượng Ticket đã được validate

SDK tự chuyển model Pydantic thành schema, gửi lên dưới dạng output_config.format, rồi validate kết quả. Những ràng buộc API không hỗ trợ (như minimum, maxLength) được SDK chuyển vào phần mô tả trường và vẫn được kiểm tra lại trong code của bạn.

Cách 2: Strict tool use

Khi agent gọi hàm thật của bạn (đặt lịch, tạo ticket, truy vấn DB), thêm "strict": true vào định nghĩa tool để tên tool và tham số luôn đúng schema:

tools = [{
    "name": "save_ticket",
    "description": "Lưu ticket đã phân loại vào hệ thống",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["bug", "billing", "feature", "other"]},
            "priority": {"type": "string", "enum": ["P1", "P2", "P3"]},
            "summary":  {"type": "string"}
        },
        "required": ["category", "priority", "summary"],
        "additionalProperties": False
    }
}]

msg = client.messages.create(
    model="claude-sonnet-5-5", max_tokens=500, tools=tools,
    tool_choice={"type": "tool", "name": "save_ticket"},   # bắt buộc gọi tool này
    messages=[{"role": "user", "content": ticket_text}])

data = next(b.input for b in msg.content if b.type == "tool_use")

Mẹo cũ “ép gọi tool bằng tool_choice rồi đọc input” vẫn hữu ích; thêm strict là chuyển từ “thường đúng” sang “đảm bảo đúng schema”.

Giới hạn của JSON Schema cần biết

Hỗ trợKhông hỗ trợ (trả lỗi 400)
object, array, string, integer, number, boolean, nullSchema đệ quy
enum, const, anyOf, allOfminimum, maximum, multipleOf
$ref nội bộ, default, requiredminLength, maxLength
Format: date-time, date, email, uri, uuid, ipv4…additionalProperties khác false
pattern regex đơn giảnRegex có backreference, lookahead/lookbehind

Một số giới hạn độ phức tạp: tối đa 20 strict tool mỗi request, 24 tham số tuỳ chọn và 16 tham số dạng union trên tổng các schema strict. Schema quá phức tạp sẽ bị lỗi “too complex for compilation” — hãy làm phẳng cấu trúc, chuyển tham số sang required, hoặc chỉ đánh dấu strict cho tool quan trọng.

Những trường hợp vẫn có thể “lệch”

  • stop_reason: "refusal" — khi Claude từ chối, thông điệp từ chối được ưu tiên nên output có thể không khớp schema.
  • stop_reason: "max_tokens" — output bị cắt giữa chừng, JSON không hợp lệ. Tăng max_tokens rồi gửi lại.
  • Chữ hoa/thường trong enum — đôi khi khác schema mà không báo lỗi. So sánh không phân biệt hoa thường, và đừng tạo hai giá trị enum chỉ khác nhau chữ hoa.

Hiệu năng và chi phí

  • Request đầu tiên với một schema mới chậm hơn do phải biên dịch grammar; grammar được cache 24 giờ kể từ lần dùng cuối.
  • Đổi cấu trúc schema làm mất cache; chỉ đổi name hay description thì không.
  • Tính năng này thêm một đoạn system prompt mô tả định dạng → tốn thêm ít token input.
  • Không dùng chung được với citations (lỗi 400) hay kỹ thuật prefill câu trả lời; nhưng chạy tốt với Batch API (giảm 50%), streaming và token counting.

Checklist production

  1. Dùng JSON outputs hoặc strict tool use thay vì chỉ dặn trong prompt.
  2. Vẫn validate lại ở server (Pydantic / Zod) — bắt các trường hợp refusal và max_tokens.
  3. Kiểm tra stop_reason trước khi parse.
  4. Log input/output của các ca lỗi để cải thiện prompt và schema.
  5. Trích xuất đơn giản → dùng model nhỏ; chạy hàng loạt → kết hợp Batch API.
  6. Không đặt dữ liệu nhạy cảm (ví dụ thông tin sức khoẻ) vào tên trường, giá trị enum hay pattern — schema được cache, chỉ để dữ liệu thật trong nội dung message.

Áp dụng thế nào?

  • Dev: Rà các chỗ đang json.loads() output từ prompt “trả JSON”, chuyển sang messages.parse() với Pydantic. Xoá luôn các vòng retry vì sai định dạng.
  • Tech Lead: Chuẩn hoá: mọi tool gọi vào hệ thống có side effect đều bật strict và có schema review như API contract.
  • PM: Khi team ước tính “tính năng AI trích xuất dữ liệu”, phần parse và retry giờ nhỏ hơn nhiều — thời gian nên dồn vào thiết kế schema và bộ test đánh giá chất lượng nội dung.

Nguồn & tham khảo

  1. Structured outputs — Claude Docs