Tóm tắt: Định nghĩa model pydantic, rồi lấy Model.model_json_schema() làm input_schema của một tool "giả". Ép model gọi tool đó bằng tool_choice={"type": "tool", "name": ...}, sau đó đọc block.input và kiểm tra lại bằng Model.model_validate(). Validate lỗi thì gửi lỗi về dưới dạng tool_result có is_error: true để model sửa. Cách thứ hai là structured outputs (output_config.format / client.messages.parse). Kiến thức tool use cơ bản xem Claude API tool use.
Luồng hoạt động
flowchart LR A["Pydantic model"] -->|"model_json_schema()"| B["tool.input_schema"] B --> C["messages.create + tool_choice ép tool"] C --> D["block tool_use: input (dict)"] D --> E{"model_validate"} E -->|"OK"| F["Object Python"] E -->|"ValidationError"| G["tool_result is_error"] G --> C
Cấu hình
Cách 1: tool use + pydantic (chạy trên mọi model hỗ trợ tool)
uv add anthropic pydanticfrom typing import Literal
import anthropic
from pydantic import BaseModel, Field, ValidationError
client = anthropic.Anthropic()
MODEL = "claude-sonnet-5-5"
class Interface(BaseModel):
name: str = Field(description="Tên interface, vd HundredGigE0/0/0/1")
oper_status: Literal["up", "down"]
description: str | None = None
class LinkDownReport(BaseModel):
device: str = Field(description="Hostname thiết bị")
severity: Literal["critical", "major", "minor"]
interfaces: list[Interface]
summary: str = Field(description="Một câu tóm tắt cho NOC")
TOOL = {
"name": "report_link_down",
"description": "Ghi nhận kết quả phân tích log link down. Luôn gọi tool này để trả kết quả.",
"input_schema": LinkDownReport.model_json_schema(),
}
def extract(log_text: str, max_attempts: int = 2) -> LinkDownReport:
messages = [{"role": "user", "content": f"Phân tích syslog sau:\n\n{log_text}"}]
for _ in range(max_attempts):
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=[TOOL],
tool_choice={"type": "tool", "name": "report_link_down"}, # ép trả qua tool
messages=messages,
)
block = next(b for b in resp.content if b.type == "tool_use")
try:
return LinkDownReport.model_validate(block.input)
except ValidationError as e:
# trả lỗi validate cho model sửa ở lượt sau
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [{
"type": "tool_result", "tool_use_id": block.id, "is_error": True,
"content": f"JSON không hợp lệ, sửa và gọi lại tool:\n{e}",
}]})
raise RuntimeError("Model không trả JSON hợp lệ sau nhiều lần thử")
if __name__ == "__main__":
log = "RP/0/RP0/CPU0:Nov 6 10:01:02 pe1-hni ifmgr: Interface HundredGigE0/0/0/1, changed state to Down"
print(extract(log).model_dump_json(indent=2))Những điểm cần nhớ:
tool_choicekiểutoolbắt buộc model gọi đúng tool đó, nên response không có đoạn văn "Đây là kết quả...". Vì vậystop_reasonsẽ làtool_use.block.inputđã làdict, không cầnjson.loads.- Schema là gợi ý mạnh, không phải bảo đảm: vẫn phải
model_validate. Lỗi hay gặp là saienum, thiếu field bắt buộc, hoặc kiểu số trả về dạng chuỗi. Field(description=...)đi vào schema. Mô tả càng cụ thể (đơn vị, ví dụ) thì kết quả càng đúng.
Cách 2: structured outputs (output_config.format)
SDK Python hiện hành (đã kiểm tra anthropic 0.109) có client.messages.parse(). Hàm này nhận class pydantic, tự chuyển thành output_config={"format": {"type": "json_schema", "schema": ...}}, và trả object đã parse trong parsed_output:
resp = client.messages.parse(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": f"Phân tích syslog sau:\n\n{log}"}],
output_format=LinkDownReport,
)
report = resp.parsed_output # LinkDownReport hoặc NoneNgoài ra có thể đặt "strict": True trong định nghĩa tool để input của tool tuân thủ schema chặt hơn. Hai tính năng này hỗ trợ một tập con của JSON Schema và một số model nhất định. Kiểm tra docs trước khi dùng schema phức tạp ($ref lồng sâu, pattern, minLength...).
| Tiêu chí | Tool use + validate | output_config.format / parse |
|---|---|---|
| Tương thích model/SDK cũ | Cao | Cần model và SDK mới |
| Bảo đảm đúng schema | Không, phải validate và retry | Chặt hơn, vẫn nên validate |
| Kết hợp với tool khác trong agent loop | Tự nhiên (thêm một tool "report") | Output cuối là JSON, tool khác vẫn gọi bình thường |
| Hỗ trợ JSON Schema | Rộng | Một tập con |
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
StopIteration khi tìm block tool_use |
Không ép tool_choice, model trả lời bằng text |
Dùng tool_choice={"type": "tool", "name": ...} |
stop_reason == "max_tokens", JSON thiếu field |
max_tokens nhỏ so với output (list dài) |
Tăng max_tokens, giới hạn số phần tử trong prompt/schema |
ValidationError ở field Literal/enum |
Model dùng từ đồng nghĩa ("Major", "high") | Ghi rõ giá trị hợp lệ trong description, gửi lỗi về để model sửa |
400 khi dùng tool_choice ép tool cùng extended thinking |
Thinking không đi cùng tool_choice kiểu tool/any |
Tắt thinking cho bước trích xuất, hoặc dùng auto + prompt |
400 báo schema không hỗ trợ (structured outputs) |
Dùng keyword JSON Schema ngoài tập được hỗ trợ | Đơn giản hoá schema, chuyển ràng buộc phức tạp sang validator pydantic |
Field tự do (summary) bịa thông tin không có trong log |
Prompt không giới hạn nguồn | Yêu cầu "chỉ dựa vào log", thêm field evidence trích nguyên dòng log |