Перейти к содержимому
11. Структурированный вывод и батч

Раздел 11 — Структурированный вывод через tool_use, пакетная обработка и многопроходное ревью

Что покрывает этот раздел

Три связанных архитектурных решения: как заставить Claude выдавать машиночитаемый вывод (tool_use + JSON Schema, strict: true, более новая фича Structured Outputs); когда переносить работу на Message Batches API ради стоимости и пропускной способности (и когда этого не делать); и почему независимый ревьюер либо разделение на проход по файлу + проход интеграции лучше самопроверки.

Экзамен проверяет все три темы конкретными сценариями — проверки до merge против ночных отчётов, единый проход против прохода по файлу, промпты «только JSON» против tool_use. Правильные ответы вырастают из одного принципа: подбирайте технику под ограничения нагрузки по задержке, надёжности и бюджету внимания.

Исходный материал (из официального руководства)

4.3 Структурированный вывод через tool_use и JSON Schema

  • tool_use с JSON Schema — самый надёжный подход для гарантированно соответствующего схеме вывода; он устраняет синтаксические ошибки JSON.
  • Режимы tool_choice: "auto" (модель может вернуть текст), "any" (обязана вызвать инструмент, может выбрать какой) или принудительный {"type": "tool", "name": "..."}.
  • Строгие схемы устраняют только синтаксические ошибки — но не семантические (позиции, не сходящиеся в сумму, значения не в тех полях, выдуманное содержимое вместо отсутствующих данных).
  • Дизайн схемы: обязательные против опциональных/nullable, перечисления с "other" + строкой-детализацией ради расширяемости, "unclear" для неоднозначности, правила нормализации формата в промпте.

4.5 Стратегия пакетной обработки

  • Message Batches API: экономия 50% по стоимости, окно обработки до 24 часов, без SLA по задержке.
  • Хорошо подходит для неблокирующих, толерантных к задержке нагрузок (ночные отчёты, еженедельные аудиты, ночная генерация тестов). Плохо подходит для блокирующих процессов (проверки до merge).
  • По руководству: Batch API не поддерживает многоходовое агентное исполнение инструментов внутри одного запроса — вы не можете приостановиться посреди запроса, чтобы выполнить инструмент и подать результаты обратно.
  • custom_id соотносит запрос и ответ; провалившиеся custom_id можно переотправить после исправлений (например, разбить документы, превысившие контекст).

4.6 Многоинстансное и многопроходное ревью

  • Модель, удерживающая своё генерационное рассуждение, реже подвергает сомнению собственные решения, поэтому самопроверка структурно слаба.
  • Независимые инстансы ревью ловят проблемы, которые самопроверка и extended thinking упускают.
  • Ревью многих файлов должно делиться на локальные проходы по файлу плюс проход интеграции по файлам, чтобы избежать размывания внимания и противоречивых замечаний.
  • Проходы верификации могут просить модель самостоятельно сообщать уверенность на каждое замечание, чтобы включить калиброванную маршрутизацию.

Структурированный вывод через tool_use

Почему tool_use лучше «отвечай только JSON»

Просьба к модели «ответить только JSON» работает большую часть времени и падает достаточно часто, чтобы быть продакшен-опасностью: случайная проза, «умные» кавычки, висячие запятые, markdown-ограждения, извиняющиеся преамбулы. tool_use убирает весь этот класс отказов — модель не выдаёт свободный текст, она выдаёт структурированный блок tool_use, чьё поле input гарантированно является JSON-объектом.

Добавление "strict": true (сэмплирование, ограниченное грамматикой) дополнительно гарантирует, что JSON соответствует типам, перечислениям и обязательным полям схемы. Строгий режим поддерживается на Opus 4.7/4.6/4.5, Sonnet 4.6/4.5 и Haiku 4.5 (Strict tool use). Более новая фича Structured Outputs (output_config.format = { "type": "json_schema", "schema": ... }) даёт ту же гарантию без определения фиктивного инструмента (Structured outputs); обе используют один и тот же подмножество JSON Schema и одну и ту же оговорку о семантических ошибках.

import anthropic

extract_invoice = {
    "name": "extract_invoice",
    "description": "Extract structured invoice data from the document.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "additionalProperties": False,
        "required": ["vendor", "invoice_number", "line_items",
                     "stated_total", "calculated_total", "currency", "category"],
        "properties": {
            "vendor": {"type": "string"},
            "invoice_number": {"type": "string"},
            "issue_date": {"type": ["string", "null"], "format": "date"},
            "currency": {"type": "string", "enum": ["USD", "EUR", "GBP", "other"]},
            "currency_other": {"type": ["string", "null"]},
            "category": {"type": "string",
                         "enum": ["saas", "hardware", "travel",
                                  "professional_services", "other", "unclear"]},
            "category_other": {"type": ["string", "null"]},
            "line_items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "additionalProperties": False,
                    "required": ["description", "amount"],
                    "properties": {
                        "description": {"type": "string"},
                        "amount": {"type": "number"}
                    }
                }
            },
            "stated_total":     {"type": "number"},
            "calculated_total": {"type": "number"}
        }
    }
}

client = anthropic.Anthropic()
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    tools=[extract_invoice],
    tool_choice={"type": "tool", "name": "extract_invoice"},
    messages=[{"role": "user", "content": INVOICE_TEXT}],
)
data = next(b.input for b in resp.content if b.type == "tool_use")

Шпаргалка по tool_choice

tool_choiceПоведение моделиКогда использовать
{"type": "auto"}Может ответить текстом или вызвать любой инструментАгентные циклы, где текстовые ответы допустимы
{"type": "any"}Обязана вызвать инструмент; выбирает какойИзвлечение по нескольким схемам, когда тип документа неизвестен
{"type": "tool", "name": "extract"}Обязана вызвать именно этот инструментПринудить известный шаг извлечения до обогащения
{"type": "none"}Не может вызвать ни один инструментВыключить инструменты на ход без перестройки запроса

Сочетайте любой из вариантов с "disable_parallel_tool_use": true, если вам нужен не более чем один вызов инструмента за ход — полезно, когда нижестоящий код ожидает один структурированный payload или когда параллельные вызовы инструментов нарушали бы инварианты порядка (Parallel tool use).

Паттерны дизайна схем

  • Обязательные против опциональных / nullable. Обязательные поля принуждают модель их заполнять, провоцируя выдумывание, когда источник в действительности не содержит данных. Помечайте опционально присутствующие поля как "type": ["string", "null"], чтобы модель возвращала null вместо галлюцинаций.
  • Перечисление с "other" + детализацией. Закрытые перечисления хрупки. Перечисление category [..., "other"] + соседнее category_other: string сохраняет информацию для новых категорий, не ломая нижестоящий код.
  • "unclear" для неоднозначности. Значение перечисления "unclear" позволяет модели сигнализировать о низкой уверенности; маршрутизируйте такие строки на человеческое ревью.
  • Самовалидирующиеся поля. Выдавайте одновременно stated_total (из документа) и calculated_total (сумма из line_items); постобработочная проверка ловит семантические ошибки, которые строгие схемы поймать не могут.

Поддерживаемое подмножество JSON Schema

Компилятор strict-режима / Structured Outputs принимает подмножество JSON Schema. Большинство отказов «почему моя схема даёт 400?» приходятся на этот список:

  • Поддерживается: object/array/string/integer/number/boolean/null, enum (только примитивы), const, anyOf/allOf (ограниченно), внутренние $ref/$def, default, required, additionalProperties: false, форматы (date-time, date, email, uri, uuid, …), minItems равное 0 или 1.
  • Не поддерживается: внешние $ref, рекурсивные схемы, сложные типы в перечислениях, числовые ограничения (minimum/maximum/multipleOf), ограничения длины строки, additionalProperties != false.
  • Регулярные выражения: квантификаторы, классы символов и группы работают; обратные ссылки, lookaround и границы слов — нет.

Что tool_use НЕ решает

Строгие схемы гарантируют парсимость, но не корректность. Они с радостью выдадут валидный по схеме счёт, где позиции суммируются в $812, а stated_total указан как $1 200, или где имя поставщика оказывается в invoice_number. Чтобы это ловить, нужны валидация на уровне приложения (например, трюк с calculated_total выше), циклы повторных попыток с обратной связью по ошибке (домен 4.4) или проход независимого ревьюера (домен 4.6).

Message Batches API

Что вы получаете и от чего отказываетесь

ИзмерениеСинхронный Messages APIMessage Batches API
ТарификацияСтандартная−50% на ввод + вывод (например, Sonnet 4.6 $1.50/$7.50 за MTok)
ЗадержкаСекундыДо 24 часов; большинство батчей завершается за час; SLA нет
Максимум запросов / батчн/д100 000 запросов или 256 МБ, что наступит раньше
Использование инструментовПолный агентный циклИнструменты могут быть определены; многоходовое исполнение инструментов посреди запроса не поддерживается
СтримингДаНе поддерживается (результаты забираются по завершении батча)
Prompt cachingДаДа (по принципу best-effort; сочетайте с часовым кэшем для общего контекста)
Срок хранения результатовн/д29 дней
МоделиВсе активныеВсе активные модели

Источники: Batch processing.

Когда использовать и когда нет

Используйте Message Batches API, когда:

  • Нагрузка неблокирующая — ночные отчёты о техдолге, еженедельные аудиты, генерация регрессионных тестов, очереди модерации контента, массовые оценки.
  • Объёмы достаточно велики, чтобы скидка 50% существенно влияла.
  • Каждый запрос самодостаточен (нет необходимости подставлять результаты инструментов между ходами).

Не используйте его, когда:

  • Человек ждёт результат (проверки до merge, чат, интерактивные интерфейсы).
  • Процессу нужно, чтобы модель вызвала инструменты, увидела результаты и продолжала рассуждать в том же запросе.
  • Вам нужен стриминг или задержка ниже минуты.

Это в точности структура Sample Question 11: переведите ночной отчёт о техдолге на батч (A), оставьте блокирующую проверку до merge на синхронном API. Все неправильные ответы предлагают надеяться, что батчи «обычно успевают», или добавлять fallback по тайм-ауту — ни одно из этого не приемлемо, когда SLA — «разработчик смотрит в экран».

custom_id и обработка ошибок

Каждый запрос в батче несёт custom_id (1–64 символа, [a-zA-Z0-9_-]). Это единственный механизм соотнесения результатов с вводами, потому что порядок вывода не гарантирован. Закладывайте в custom_id достаточно метаданных, чтобы поднять исходную запись — doc_42891-v3-2026q1 подойдёт, а req_001 будет вас преследовать.

При получении результатов у каждой записи есть result со значением succeeded, errored, canceled или expired. Стандартный паттерн обработки отказов:

  1. Подтяните поток результатов и разделите по result.type.
  2. Для записей с errored посмотрите код ошибки: разбейте слишком большие документы, исправьте невалидные параметры, затем переотправьте только провалившиеся custom_id новым меньшим батчем.
  3. Для записей с expired (батч не завершился за 24 часа) переотправьте меньшим размером батча или в часы низкой нагрузки.

Разобранный пример: ночное извлечение из 100 документов

from anthropic import Anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

client = Anthropic()

requests = [
    Request(
        custom_id=f"invoice-{doc.id}",
        params=MessageCreateParamsNonStreaming(
            model="claude-sonnet-4-6",
            max_tokens=2048,
            tools=[extract_invoice],
            tool_choice={"type": "tool", "name": "extract_invoice"},
            messages=[{"role": "user", "content": doc.text}],
        ),
    )
    for doc in documents
]

batch = client.messages.batches.create(requests=requests)

while True:
    batch = client.messages.batches.retrieve(batch.id)
    if batch.processing_status == "ended":
        break
    time.sleep(60)

for entry in client.messages.batches.results(batch.id):
    if entry.result.type == "succeeded":
        msg = entry.result.message
        payload = next(b.input for b in msg.content if b.type == "tool_use")
        store(entry.custom_id, payload)
    else:
        log_failure(entry.custom_id, entry.result)

Обратите внимание на сочетание: tool_use для безопасного по схеме вывода внутри батчевого запроса. У одноразового извлечения нет вызовов инструментов посреди запроса, поэтому ограничение Batch API не кусается.

Математика SLA: с какой частотой отправлять

Если бизнес-SLA — «результаты в течение N часов», а Batch API может занять до 24 часов, отправляйте с частотой такой, что задержка отправки + 24 часа обработки ≤ N. Для SLA N = 30 ч отправляйте каждые 4 часа (в худшем случае запись 4 часа ждёт, пока следующий батч её подхватит, плюс 24 часа обработки = 28 часов, комфортно укладывается в 30). Для SLA 26 ч — каждый час. Для SLA 24 ч одним только Message Batches API уложиться нельзя — переходите на синхронные вызовы или соглашайтесь на пропуски SLA.

Архитектура многоинстансного и многопроходного ревью

Ловушка самопроверки

Когда тот же инстанс Claude, что написал код, его же и ревьюит, генерационное рассуждение всё ещё в контексте — модель воспринимает свои предыдущие выборы как посылки, а не как гипотезы, которые надо подвергнуть сомнению. Даже явная инструкция «критикуй свой предыдущий ответ» слабее, чем свежий инстанс без прежних обязательств, которые ему пришлось бы защищать.

Собственная система Code Review от Anthropic это отражает: она запускает несколько специализированных агентов параллельно (разные промпты для логики, безопасности, граничных случаев) и проводит шаг верификации против реального поведения кода, чтобы отфильтровать ложные срабатывания до публикации замечаний (Claude Code Code Review). Разделите работу, используйте независимый контекст, сводите результаты в конце.

Паттерн «проход по файлу + проход интеграции»

Это правильный ответ на Sample Question 12 (PR из 14 файлов с непоследовательной глубиной и противоречивыми замечаниями):

                    ┌────────────────────────────┐
                    │  Per-file local passes     │
PR (14 files) ──►  │  - one Claude call per file │  ──► findings_local[]
                    │  - focused prompt           │
                    │  - no other files in ctx    │
                    └────────────────────────────┘
                                  │
                                  ▼
                    ┌────────────────────────────┐
                    │  Integration pass           │
                    │  - all diffs + module map   │  ──► findings_integration[]
                    │  - cross-file data flow     │
                    │  - API contracts, types     │
                    └────────────────────────────┘
                                  │
                                  ▼
                          dedupe + rank + post

Проходы по файлу дают каждому файлу одинаковый бюджет внимания, устраняя режим отказа «глубоко на файле 1, поверхностно на файле 14». Проход интеграции специально промптится под межфайловые темы (дрейф сигнатур вызывающего/вызываемого, изменения общих схем, транзакционные инварианты), чтобы не повторять то, что локальные проходы уже сделали.

Паттерн независимого ревьюера

Для критичных одиночных артефактов (сгенерированная миграция, отчёт для клиента) делайте два вызова Claude:

  1. Генератор — производит артефакт с полным рассуждением.
  2. Ревьюерновый запрос, новый системный промпт, без транскрипта генератора, получает только артефакт и спецификацию. Его единственная задача — находить дефекты.

Отсутствие контекста у ревьюера — это фича, а не баг: он не может рационализировать решения, которые сам не принимал.

Проходы верификации с аннотацией уверенности

Пусть ревьюер возвращает замечания с явным перечислением confidence ("high", "medium", "low") и однострочным rationale:

{
  "findings": [
    {"severity": "high", "confidence": "high",
     "file": "billing.py", "line": 142,
     "issue": "Off-by-one in proration when subscription starts on month boundary",
     "rationale": "Integration test billing_test.py:88 covers mid-month only."}
  ]
}

Затем маршрутизируйте по уверенности: high confidence + high severity идёт прямо в PR как блокирующий комментарий; замечания с low confidence уходят в очередь triage или запускают проход-арбитр. Это и есть калиброванная маршрутизация ревью, на которую ссылается официальный навык.

Матрица решений: какая техника под какую задачу

НагрузкаТребование к задержкеГлубина ревьюРекомендуемый стек
Блокирующая проверка до mergeСекундыПроход по файлу + интеграцияСинхронный Messages API + tool_use(strict) + многопроходное ревью
Ночной отчёт о техдолгеЧасыПроход по файлу + интеграцияMessage Batches API + tool_use(strict) + многопроходное ревью
Извлечение полей из 100 тыс. документовЗа ночьТолько выборочный QCMessage Batches API + принудительный tool_choice + самовалидирующиеся поля
Интерактивный чат с шагом извлеченияСекундыНетСинхронный Messages API + принудительный tool_choice + nullable-поля
QC регулирующих документовМинуты–часыНезависимый ревьюерСинхронно (или батч) + разделение генератор/ревьюер + маршрутизация по уверенности
Еженедельный кроссрепозиторный аудитДниТолько по репозиториюMessage Batches API + tool_use + пропустить проход интеграции

Ключевые акценты для экзамена

  • tool_use против «отвечай JSON»: правильный ответ всегда тяготеет к tool_use / строгим схемам ради гарантированной парсимости. Промпты «голый JSON» — отвлекающий вариант.
  • Выбор tool_choice: "any" для неизвестного типа документа среди нескольких инструментов извлечения; принудительный {"type":"tool","name":"..."} чтобы гарантировать запуск конкретного инструмента до обогащения; "auto" — только когда текстовые ответы допустимы.
  • Дизайн схемы: nullable, когда источник может не содержать данных (предотвращает выдумывание); "other" + детализация для расширяемости; "unclear" для неоднозначности; самовалидирующиеся поля для семантических проверок.
  • Соответствие Message Batches API: только неблокирующие, толерантные к задержке, ≤24 часа нагрузки. Проверки до merge — каноничный неподходящий случай. −50%, лимит 100 тыс. запросов / 256 МБ, результаты валидны 29 дней, без стриминга, без исполнения инструментов посреди запроса.
  • custom_id: обязателен для соотнесения; переотправляйте только провалившиеся идентификаторы после исправления первопричины.
  • Многопроходное ревью: проход по файлу + проход интеграции по файлам лучше единого прохода на многофайловых PR; независимые инстансы лучше самопроверки; замечания с аннотацией уверенности позволяют маршрутизацию.
  • Заблуждения, которых надо избегать: «большее контекстное окно решает размывание внимания» (нет), «три полных прохода + голосование большинством» (заглушают реальные баги, ловимые лишь иногда), «fallback по тайм-ауту с батча на синхрон» (переусложнённо; подбирайте правильный API под нагрузку).

Ссылки

Последнее обновление