Структурированный вывод: tool_use + JSON schema + retry loops
Цель: получать от Claude **стабильный, валидируемый JSON** для production пайплайнов. Без "иногда ломается". Это Домен 4 экзамена (20%).
Почему это проблема
Просто просить Claude "return JSON" — ненадёжно. Синтаксические ошибки, лишние markdown-обрамления, неправильные типы полей. Для скриптов и ETL это фатально.
Решение: tool_use + JSON schema
Самый надёжный подход — определить "инструмент" который Claude "вызывает", с JSON-схемой как input.
extract_invoice_data = {
"name": "extract_invoice_data",
"description": "Extract structured data from invoice document",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"issue_date": {"type": "string", "format": "date"},
"total_amount": {"type": "number"},
"currency": {"type": "string", "enum": ["USD", "EUR", "RUB"]},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"price": {"type": "number"}
},
"required": ["description", "quantity", "price"]
}
}
},
"required": ["invoice_number", "total_amount", "currency", "line_items"]
}
}response = client.messages.create(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": [
{"type": "document", "source": {"type": "base64", "data": pdf_bytes}},
{"type": "text", "text": "Extract invoice data"}
]}],
tools=[extract_invoice_data],
tool_choice={"type": "tool", "name": "extract_invoice_data"}
)
data = response.content[0].input # гарантированно валидный JSON по схемеtool_choice: "any" или {"type": "tool", "name": "..."} гарантирует вызов инструмента вместо текстового ответа.
Что tool_use НЕ решает
Устраняет синтаксические ошибки (невалидный JSON, лишние кавычки). Не устраняет семантические — суммы могут не сходиться, значения в неправильных полях, поле валюты содержит название компании.
Для семантики — нужна валидация и retry loops.
Pydantic validate-retry pattern
from pydantic import BaseModel, ValidationError
class Invoice(BaseModel):
invoice_number: str
total_amount: float
currency: Literal["USD", "EUR", "RUB"]
line_items: list[LineItem]
# Семантическая валидация
@validator("line_items")
def items_sum_matches_total(cls, items, values):
if abs(sum(i.price * i.quantity for i in items) - values["total_amount"]) > 0.01:
raise ValueError("Line items sum doesn't match total_amount")
return items
def extract_with_retry(document, max_retries=3):
for attempt in range(max_retries):
response = claude_extract(document)
try:
return Invoice(**response.content[0].input)
except ValidationError as e:
# Retry with specific error
document = add_retry_context(document, response, e)
raise ExtractionFailed(f"Failed after {max_retries}")Ключ — передать конкретную ошибку в retry
def add_retry_context(original_doc, failed_response, error):
return f"""
Previous attempt produced:
{failed_response}
Validation error:
{error}
Retry extraction. Address this specific issue.
Original document:
{original_doc}
"""Без конкретной ошибки retry бесполезен — модель не знает что было не так.
Когда retry НЕ поможет
- Информация отсутствует в документе — не поможет сколько ни повторяй
- Документ противоречив сам себе — нужен human review, не retry
Правило: отличай retryable (ошибки формата/структуры) vs non-retryable (отсутствие информации).
Проектирование схем
Обязательные vs опциональные
{
"required": ["invoice_number", "total_amount"],
"properties": {
"invoice_number": {"type": "string"},
"total_amount": {"type": "number"},
"notes": {"type": ["string", "null"]} // nullable для опциональных
}
}Nullable ["string", "null"] когда поле может отсутствовать в источнике. Не путать с required — required значит поле должно быть в выводе (возможно как null).
Enum с "other"
{
"category": {
"type": "string",
"enum": ["food", "transport", "utilities", "entertainment", "other"]
},
"category_other_description": {
"type": ["string", "null"]
}
}Когда есть вероятность что документ попадёт в категорию которую ты не предусмотрел — other + описание. Иначе модель будет форсить под существующие категории неточно.
Поля самокоррекции
{
"calculated_total": {"type": "number"},
"stated_total": {"type": "number"}
}Извлечение и того что написано, и того что посчиталось — позволяет автоматически выявлять расхождения.
Few-shot для неоднозначных случаев
Когда один тип документа может быть разных форматов — примеры в system prompt:
Example 1: Invoice with inline line items
[document preview]
→ {"format": "inline", "line_items": [...]}
Example 2: Invoice with attached table
[document preview]
→ {"format": "table", "line_items": [...]}
Example 3: Receipt (not invoice) — should be rejected
[document preview]
→ {"format": "unclear", "reject_reason": "..."}
Few-shot эффективнее длинных инструкций для форматных нюансов.
Батчи: Message Batches API
Когда обрабатываешь 100+ документов и можешь подождать до 24ч:
batch = client.messages.batches.create(
requests=[
{"custom_id": "doc_1", "params": {...}},
{"custom_id": "doc_2", "params": {...}},
# ...
]
)50% экономия. Окно до 24 часов, без гарантий SLA по задержке.
Когда подходит
- Ночные отчёты
- Еженедельные аудиты
- Разовые массовые миграции данных
Когда НЕ подходит
- Pre-merge проверки (ждут ответа синхронно)
- Real-time интеграции
custom_id для корреляции
Без custom_id ты не сможешь сопоставить ответы с исходными запросами когда придут. Всегда указывай его.
detected_pattern для анализа качества
Для извлечения с частыми ложными срабатываниями:
{
"finding": {"type": "string"},
"severity": {"enum": ["critical", "warning", "info"]},
"detected_pattern": {"type": "string"}
}Модель сохраняет какой паттерн привёл к находке. Потом анализируешь detected_pattern в базе → видишь где модель путается.
Human-review routing
Для high-stakes извлечений:
if result.confidence < 0.8 or result.has_ambiguity:
route_to_human_review(result)
else:
auto_process(result)Калибруй пороги на размеченных валидационных наборах — не доверяй самооценке уверенности модели без проверки.
Практика (30 минут)
Задача 1. Схема
Возьми реальную задачу извлечения (инвойсы / чеки / резюме / любой документ). Спроектируй JSON schema:
- required vs nullable
- enum с
other - поля самокоррекции если применимо
Задача 2. Retry loop
Имплементируй Pydantic модель + retry. На 10 тестовых документах посмотри:
- Сколько прошло с 1 попытки?
- Сколько потребовало retry?
- Какие ошибки неретрайабельны?
Задача 3. Batch (опционально)
Если есть 50+ документов для обработки — попробуй Message Batches API с custom_id. Сравни цену с синхронным API.
Что дальше
Урок 4.4: multi-agent research — координатор + подагенты для исследовательских задач. Сценарий 3 экзамена.