← Модуль 4: Architect
4.3

Структурированный вывод: 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 экзамена.