Бумажный ад и его цифровой призрак
У вас сотни тысяч PDF, сканов счетов, договоров с кривыми подписями. Каждый документ — уникальный сюрприз: шрифт то мелкий, то цветной, таблицы расползаются, а где-то вообще рукописный текст. И всё это надо распарсить, извлечь поля, загнать в CRM или ERP. Классические OCR-пайплайны тут ломаются на первом же развороте. LLM — спасение, но как их готовить?
Amazon Bedrock к середине 2026 года оброс мускулами: Claude 4 Opus, LLaMA 4, Mistral Large 3 — все они умеют читать документы нативно. Но ключевой вопрос: как собрать пайплайн, который не сожрёт бюджет в real-time и не заставит ждать результатов неделю, если данные нужны вчера?
Ответ — комбинация on-demand и batch режимов с динамическим выбором модели. Звучит как магия, но на деле просто инженерная дисциплина. Давайте разберём на примере, как построить такую систему, не выстрелив себе в ногу.
Когда on-demand душит бюджет, а batch заставляет ждать
Представьте: клиент присылает документ — нужно извлечь данные сейчас и показать в интерфейсе. Вы зовёте Claude 4 Opus, он тратит 5000 токенов на анализ, и через 3 секунды готово. Всё круто, пока таких запросов не становится 10 тысяч в час. Тут бюджет летит в космос, а latency начинает плавать.
С другой стороны — ночной batch. Вы скидываете 50 тысяч документов в S3, запускаете batch inference, и через час получаете результат. Дешевле в 3-5 раз, но как быть с теми, кому ответ нужен сейчас?
Решение — гибридный пайплайн: для критичных документов on-demand с быстрой моделью (например, Claude 4 Haiku), для массовой обработки — batch с более мощной моделью (Claude 4 Opus или LLaMA 4). Но чтобы это работало дешево и предсказуемо, нужно динамически выбирать модель, а не хардкодить.
Архитектура: как не наступить на грабли
Схема простая, но дьявол в деталях. Основные компоненты:
- S3 Input/Output — сырые документы (PDF, JPG, PNG) и результат извлечения.
- Lambda + API Gateway — для on-demand запросов. Lambda принимает документ (или ссылку), определяет его сложность и выбирает модель.
- Bedrock (Converse API / InvokeModel) — on-demand вызов модели.
- Bedrock Batch Inference — для массовых задач. Конфигурация лежит в S3 (input manifest), результаты тоже в S3.
- DynamoDB / SQS — для оркестрации и отслеживания статусов.
- Prompt Registry — хранилище промптов в Parameter Store или отдельный репозиторий.
Критично: для on-demand используйте Converse API (он поддерживает structured output и system prompt), для batch — ModelInvocationJob с конфигурацией в JSON. Не путайте API — будет больно.
Пример конфигурации batch inference на S3:
{
"jobName": "invoice-extraction-2026-06-11",
"modelId": "anthropic.claude-4-sonnet-20260501",
"inputDataConfig": {
"s3InputDataConfig": {
"s3Uri": "s3://my-bucket/batch-input/manifest.json"
}
},
"outputDataConfig": {
"s3OutputDataConfig": {
"s3Uri": "s3://my-bucket/batch-output/"
}
},
"roleArn": "arn:aws:iam::123456789012:role/BedrockBatchRole"
}
Manifest — это JSONL-файл, где каждая строчка содержит параметры запроса. Например:
{"modelId": "anthropic.claude-4-sonnet-20260501", "prompt": "...", "maxTokens": 4096}
Подробнее про работу с большими документами (когда 128K токенов — это смешно) читайте в статье про AgentCore и Recursive Language Models. Там же рассматривается, как не потерять контекст.
Промпты: заставьте LLM читать документы, а не писать стихи
LLM без хорошего промпта — это генератор галлюцинаций. Для извлечения данных нужно принудить модель отдавать строгий JSON. Тут на помощь приходит structured output, который в Bedrock можно реализовать через tool_use или кастомное response_format. В Structured Outputs в Amazon Bedrock разобрано, как создать валидные JSON-ответы без ручной проверки — обязательно прочтите, иначе половина ответов будет мусором.
Базовый шаблон промпта для извлечения полей из счёта:
Вы — система извлечения данных. Извлеките строго следующие поля из документа в формате JSON.
Поля: invoice_number, date, total_amount, vendor_name, currency, line_items (массив с полями description, quantity, unit_price, amount).
Если поле не найдено, используйте null.
Не добавляйте комментарии и лишний текст. Только JSON.
Документ:
{OCR_TEXT}
Но это только начало. В реальности документы бывают с таблицами, подписями, водяными знаками. Тут помогает контекстная сегментация: разбить документ на страницы, каждую обработать отдельно, а потом смержить. Для мультимодальных документов (картинки, графики) используйте мультимодальный RAG от Bedrock — он умеет индексировать не только текст, но и визуальные элементы.
Динамический выбор модели: дешево и сердито
Зачем гонять Opus на простом счёте, когда Haiku справится за копейки? И наоборот: сложный договор на 50 страниц с кучей исключений — Haiku нагенерирует ошибок. Нужен механизм, который на лету выбирает модель.
Как это работает:
- Определяем сложность документа: количество страниц, наличие таблиц, объём текста, язык.
- Сопоставляем с порогами: если страниц <5 и нет таблиц — используем Haiku; если от 5 до 20 — Sonnet; если больше 20 или есть сложная вёрстка — Opus.
- Для batch: можно в манифесте указывать modelId для каждого документа индивидуально. Bedrock Batch Inference это поддерживает.
Ошибка: многие делают статический выбор модели для всего batch'а. Не делайте так — вы либо переплачиваете за простые документы, либо недобираете качество на сложных. Используйте подход «модель на каждый документ».
Пример кода Lambda, которая определяет модель (Python, boto3):
import boto3
import json
def select_model(document_text: str, num_pages: int) -> str:
# Простая эвристика
if num_pages <= 3 and len(document_text) < 5000:
return "anthropic.claude-4-haiku-20260501"
elif num_pages <= 15:
return "anthropic.claude-4-sonnet-20260501"
else:
return "anthropic.claude-4-opus-20260501"
def lambda_handler(event, context):
body = json.loads(event['body'])
doc_text = body['text']
pages = body.get('pages', 1)
model_id = select_model(doc_text, pages)
bedrock = boto3.client('bedrock-runtime')
response = bedrock.converse(
modelId=model_id,
messages=[{"role": "user", "content": [{"text": doc_text}]}]
)
return {"statusCode": 200, "body": response['output']['message']['content'][0]['text']}
Для более точного выбора можно использовать классификатор на основе Titan Text — он сам оценит сложность и вернёт рекомендуемую модель. Это даёт дополнительную экономию (см. опыт Ring в статье про масштабирование мультиязычной RAG-поддержки — там похожая оптимизация дала 21% экономии).
Batch пайплайн: когда время терпит, но счетов боишься
Для массовой обработки (например, ежемесячный разбор 100 тысяч счетов) batch — король. Схема:
- Сырые документы попадают в S3.
- Lambda-триггер (или Step Functions) генерирует манифест для batch, обогащая каждый документ метаданными (сложность, модель, промпт).
- Запускается job через Bedrock Batch Inference.
- Результаты пишутся в S3, затем парсятся и загружаются в базу.
Важный момент: batch-задачи выполняются асинхронно, статус можно отслеживать через CloudWatch Events или подписку на SNS. При падении job — автоматический перезапуск (но с ограничением retry, чтобы не уйти в минус).
Один из наших клиентов (Associa) обрабатывал 26 ТБ документов именно batch-пайплайном. Как они это сделали — читайте в кейсе про 48 миллионов бумаг. Там много идей про предобработку и дедупликацию.
On-demand пайплайн: реальный опыт и грабли
On-demand нужен для интерактивных сценариев: пользователь загружает документ и ждёт результат в UI. Тут главные проблемы — latency и throttling.
- Latency: даже Claude 4 Haiku может отвечать 2-10 секунд на сложных документах. Нужно асинхронное взаимодействие (WebSocket, Polling).
- Throttling: Bedrock в on-demand имеет quota на TPM (токенов в минуту). При пиках — 429 ошибки. Решение — очередь (SQS) и пул консьюмеров с exponential backoff.
- Cost: on-demand дороже batch. Если документ можно подождать минуту — лучше отправлять в mini-batch с задержкой.
Хороший пример асинхронной обработки в реальном времени — система сбора доказательств для аудита. Там каждый документ попадает в пайплайн и обрабатывается с учётом контекста. Читайте пошаговый гайд по автоматизации аудита — принципы те же.
Ошибки и грабли: что я видел на продакшене
За два года внедрения Bedrock для извлечения данных я собрал коллекцию шишек:
- Игнорирование системного промпта. Многие передают инструкцию в user message, но system prompt даёт более стабильный результат. Используйте поле
systemв Converse API. - Забывают про context window. Документ на 100 страниц может не влезть. Используйте чанкование или AgentCore (см. статью про AgentCore).
- Не проверяют structured output. Даже с tool_use иногда приходят битые JSON. Добавьте валидацию в конце (и оберните retry с другим промптом).
- Экономия на промптах. Один раз написали промпт — и он работает. Нет, нужно A/B тестирование. Промпты должны жить в реестре и версионироваться.
- Не чистят PII. Если документы содержат персональные данные, их нужно маскировать до обработки. Подход описан в статье про автоматическую чистку PII.
- Batch без мониторинга. Job упал, а вы узнали через день. Настройте CloudWatch Alarms на неудачные завершения.
Что дальше? Прогноз на конец 2026
Amazon уже анонсировал Prompt Management и Prompt Flows в Bedrock — это позволит версионировать промпты и делать A/B тесты прямо в консоли. Ожидается, что cost-оптимизация станет встроенной: модели сами будут переключаться на более дешёвые, если качество обработки совпадает.
Мой совет: не пытайтесь построить универсальный пайплайн на все случаи. Разделите документы на типы (счета, договоры, письма) и для каждого типа сделайте отдельный промпт + правило выбора модели. Это сэкономит вам 30-40% бюджета и нервов. И да, не забудьте про дедупликацию — если один документ попадает и в batch, и в on-demand, вы заплатите дважды.