Архитектура решения: как собрать IDP-пайплайн на AWS
Пайплайн начинается с триггера от EventBridge Scheduler, который по расписанию запускает Lambda-функцию. Функция подключается к Gmail API, забирает непрочитанные письма с вложениями и сохраняет файлы в S3. Следующий этап - Amazon Textract извлекает текст и таблицы из PDF или изображений. Результат передаётся в Amazon Bedrock, где модель Claude Sonnet 4.6 классифицирует документ и извлекает персональные данные. Весь процесс оркеструется через AWS Step Functions - это даёт визуальный контроль, повторные попытки при сбоях и ветвление логики. OAuth-токены Gmail хранятся в Secrets Manager и никогда не попадают в код. Готовый результат сохраняется в DynamoDB, а при низкой уверенности модели задача уходит на ручную проверку через Human-In-The-Loop.
Компоненты пайплайна и их роли
Каждый сервис решает одну задачу. Разберём их по порядку.
- EventBridge Scheduler - cron-подобный триггер. Запускает пайплайн каждый час без необходимости держать постоянно работающий сервер. Настройка занимает 5 минут через консоль или CloudFormation.
- Lambda - бессерверные функции на Python 3.12. Первая забирает письма из Gmail, вторая вызывает Textract, третья формирует промпт для Bedrock и парсит ответ. Память 256–1024 МБ, таймаут до 15 минут.
- Amazon Textract - OCR-сервис, который возвращает не просто текст, а структурированные блоки: строки, слова, таблицы, формы. Для счетов и договоров это критично - данные из таблиц приходят с координатами ячеек.
- Amazon Bedrock - управляемый API к Claude Sonnet 4.6. Модель получает текст от Textract и промпт с инструкцией. Ответ приходит в JSON - тип документа и список найденных PII-полей.
- Step Functions - оркестратор. Описывает workflow как JSON (ASL). Ветвление по типу документа, повторные попытки при ошибках API, параллельная обработка нескольких вложений.
- Secrets Manager - хранилище OAuth-токенов с автоматической ротацией. Lambda получает токен через вызов get_secret_value, секрет никогда не логируется.
Почему именно AWS: сравнение с альтернативами
Три фактора определяют выбор стека: интеграция, стоимость и надёжность. Сравним с альтернативами.
Prentis - специализированная IDP-платформа с готовыми коннекторами. Минус: закрытая архитектура, невозможность кастомизировать логику извлечения под специфичные типы документов. В нашем пайплайне промпт для Bedrock меняется за минуту, без релиза вендора.
GPT-5.4 через Azure OpenAI - сильный соперник по качеству извлечения. Но при объёме 10 000 страниц в месяц разница в стоимости инференса с Bedrock достигает 30–40% в пользу AWS за счёт on-demand pricing и отсутствия надбавки за managed API. Плюс Textract уже встроен в экосистему - не нужно настраивать сетевые доступы между облаками.
Claude Opus 4.6 - более мощная модель, чем Sonnet 4.6. В тестах на извлечении PII из русскоязычных документов Opus показывает точность на 3–5% выше. Но для задач классификации и извлечения стандартных полей (ФИО, ИНН, паспорт) Sonnet справляется с достаточной надёжностью, а стоимость токена в 3 раза ниже. Подробный разбор различий моделей - в статье про архитектуру ChatGPT 5.6, где мы разбирали критерии выбора модели под задачу.
Pay-as-you-go модель означает, что за простой системы вы не платите. EventBridge Scheduler + Lambda + Bedrock on-demand - это счета от $50 в месяц на прототипе. При росте объёмов подключаются резервные capacity-юниты Bedrock, которые фиксируют стоимость и гарантируют latency.
Получение документов: интеграция Gmail и EventBridge
Первый этап пайплайна - автоматический сбор входящих документов. Вместо ручного сохранения вложений из писем Lambda-функция забирает их по Gmail API и кладёт в S3. EventBridge Scheduler запускает функцию каждый час - этого достаточно для большинства бизнес-сценариев. При пиковых нагрузках интервал сокращается до 15 минут.
Код Lambda-функции для получения писем:
import boto3
import google.auth
from googleapiclient.discovery import build
def get_gmail_service():
secret = get_secret()
creds = google.oauth2.credentials.Credentials(
token=secret['access_token'],
refresh_token=secret['refresh_token'],
token_uri="https://oauth2.googleapis.com/token",
client_id=secret['client_id'],
client_secret=secret['client_secret']
)
return build('gmail', 'v1', credentials=creds)
def lambda_handler(event, context):
service = get_gmail_service()
messages = service.users().messages().list(
userId='me', q='has:attachment is:unread'
).execute()
s3 = boto3.client('s3')
for msg in messages.get('messages', []):
attachment = service.users().messages().attachments().get(
userId='me', messageId=msg['id'], id=attachment_id
).execute()
s3.put_object(
Bucket='idp-documents',
Key=f"inbox/{msg['id']}.pdf",
Body=base64.urlsafe_b64decode(attachment['data'])
)
service.users().messages().modify(
userId='me', id=msg['id'],
body={'removeLabelIds': ['UNREAD']}
).execute()Фильтр has:attachment is:unread забирает только новые письма с вложениями. После обработки письмо помечается прочитанным - дубли исключены. S3-бакет idp-documents имеет версионирование и шифрование SSE-KMS по умолчанию.
Безопасное хранение OAuth-токенов в Secrets Manager
Токены Gmail API - это ключ к почтовому ящику. Хранить их в переменных окружения Lambda или в коде нельзя. Secrets Manager решает задачу: токены шифруются KMS-ключом, доступ выдаётся только IAM-роли Lambda-функции.
Пошаговая настройка:
- Создайте OAuth 2.0 Client ID в Google Cloud Console. Тип - Desktop application. Скачайте JSON с client_id и client_secret.
- Выполните OAuth flow один раз локально, получите refresh_token. Инструкция - в руководстве по Amazon Bedrock Managed Knowledge Base, раздел настройки IAM и секретов.
- В AWS Secrets Manager создайте секрет типа "Other type". Вставьте JSON:
{"client_id":"...","client_secret":"...","refresh_token":"...","access_token":"..."}. - Прикрепите к IAM-роли Lambda политику:
secretsmanager:GetSecretValueна ARN секрета. - В коде Lambda получайте токен через
get_secret_value. Access_token обновляется автоматически при каждом вызове - Gmail API возвращает новый, функция сохраняет его обратно в Secrets Manager черезupdate_secret.
Ротация токенов происходит прозрачно. Если refresh_token протухает (раз в 6 месяцев при неиспользовании), EventBridge запускает alert-функцию, которая отправляет уведомление в Slack через SNS.
Распознавание текста с Amazon Textract
Textract принимает на вход PDF, JPEG, PNG или TIFF из S3-бакета. Ответ приходит в виде иерархии блоков: PAGE → LINE → WORD. Для таблиц добавляется блок CELL с координатами строки и столбца. Это даёт структуру, которую удобно передавать в LLM для дальнейшего анализа.
Вызов Textract из Lambda:
textract = boto3.client('textract')
def extract_text(bucket, key):
response = textract.detect_document_text(
Document={'S3Object': {'Bucket': bucket, 'Name': key}}
)
lines = []
for block in response['Blocks']:
if block['BlockType'] == 'LINE':
lines.append(block['Text'])
return '\n'.join(lines)Для документов с таблицами используйте start_document_analysis с FeatureTypes: ['TABLES', 'FORMS']. Это асинхронный вызов - результат придёт в SNS-топик, который триггерит следующую Lambda. Step Functions ждёт callback через .waitForTaskToken.
Обработка ошибок: Textract возвращает UnsupportedDocumentException для файлов не-изображений и InvalidS3ObjectException для битых PDF. Обе ошибки ловятся в Step Functions блоком Catch и маршрутизируются в Dead Letter Queue для ручного разбора.
Оптимизация затрат: выбор между Synchronous и Asynchronous API
Синхронный API (detect_document_text) обрабатывает документы до 10 МБ и 10 страниц. Время ответа - 1–3 секунды. Стоимость: $1.50 за 1000 страниц.
Асинхронный API (start_document_analysis) берёт файлы до 500 МБ и 3000 страниц. Время обработки - до 30 минут. Стоимость: $0.60 за 1000 страниц. Дополнительно оплачивается хранение промежуточных результатов в S3.
Практическая рекомендация: для одностраничных счетов и удостоверений - синхронный вызов. Для многостраничных договоров и пакетов документов - асинхронный. В Step Functions ветвление по размеру файла: $.file_size > 10MB ? async : sync. Это сокращает счёт за Textract на 25–30% по сравнению с универсальным асинхронным подходом.
Если вы рассматриваете open-source OCR-решения, сравнение точности и стоимости инференса мы делали в статье про открытые модели OCR 2026. Для production-сценариев Textract выигрывает за счёт нулевого администрирования и интеграции с KMS.
Классификация и извлечение PII с помощью Amazon Bedrock и Claude Sonnet 4.6
Текст от Textract - это сырая строка. Нужно понять, что за документ перед нами, и вытащить из него структурированные данные. Claude Sonnet 4.6 решает обе задачи за один вызов. Модель получает промпт с инструкцией и текстом, возвращает JSON с типом документа и списком PII-полей.
Вызов Bedrock из Lambda:
bedrock = boto3.client('bedrock-runtime')
def classify_and_extract(text):
prompt = build_prompt(text)
response = bedrock.invoke_model(
modelId='anthropic.claude-sonnet-4-6-20250514',
body=json.dumps({
'anthropic_version': 'bedrock-2023-05-31',
'max_tokens': 1000,
'messages': [{'role': 'user', 'content': prompt}]
})
)
result = json.loads(response['body'].read())
return json.loads(result['content'][0]['text'])Температура 0 для детерминированного извлечения данных. Max tokens - 1000, этого хватает на JSON с 10–15 полями. Средняя latency - 1.2 секунды для одностраничного документа.
Промпт для классификации и извлечения: примеры и best practices
Промпт построен по принципу «роль → задача → формат ответа → few-shot примеры». Это стандартный подход, который даёт стабильный JSON без галлюцинаций.
Ты - система извлечения данных из документов на русском языке.
Проанализируй текст документа и выполни две задачи:
1. Определи тип документа из списка: passport, inn, snils, contract, invoice, other.
2. Извлеки все персональные данные (PII), которые найдёшь.
Верни ответ строго в формате JSON:
{
"document_type": "тип_документа",
"pii": {
"full_name": "ФИО полностью",
"passport_series": "1234",
"passport_number": "567890",
"inn": "123456789012",
"snils": "123-456-789 01",
"birth_date": "01.01.1990",
"address": "адрес регистрации"
},
"confidence": "high|medium|low"
}
Включай только те поля, которые реально найдены в тексте.
Если поле не найдено - не включай его в JSON.
Пример 1:
Текст: "Паспорт 4510 123456 выдан ОВД района Сокол г. Москвы 15.03.2015 на имя Иванова Ивана Ивановича 01.01.1990 г.р."
Ответ: {"document_type":"passport","pii":{"full_name":"Иванов Иван Иванович","passport_series":"4510","passport_number":"123456","birth_date":"01.01.1990"},"confidence":"high"}
Пример 2:
Текст: "ИНН 7707083893 присвоен Петрову Петру Петровичу 05.12.2000"
Ответ: {"document_type":"inn","pii":{"full_name":"Петров Петр Петрович","inn":"7707083893"},"confidence":"high"}
Текст документа:
{text}Почему такой формат: JSON в ответе парсится без regex, few-shot примеры задают ожидаемую структуру, поле confidence позволяет маршрутизировать сомнительные результаты на ручную проверку. Промпт не содержит доменно-специфичных допущений - он работает для счетов, договоров и удостоверений одинаково.
Обработка ошибок и неоднозначных результатов
Модель может вернуть невалидный JSON, пропустить поле или выдать confidence: low. Каждый случай обрабатывается по своей логике.
Невалидный JSON: повторный вызов с тем же промптом, но с префиксом «Предыдущий ответ был невалидным JSON. Верни строго валидный JSON». После трёх попыток документ уходит в очередь на ручную обработку.
Confidence: low - документ маршрутизируется в Human-In-The-Loop. В Step Functions это реализовано через Choice State: $.confidence == "low" → A2I Task. Оператор видит оригинал документа из S3, текст от Textract и ответ модели. Подтверждает или корректирует данные.
Пропущенные обязательные поля: если документ классифицирован как passport, а поле passport_number отсутствует - это аномалия. Step Functions отправляет такой кейс на повторный прогон с более детальным промптом, где явно указано: «Найди серию и номер паспорта в формате XXXX XXXXXX».
Сравнение с Claude Opus 4.6: на тестовой выборке из 100 русскоязычных документов Sonnet показал точность извлечения 94.2%, Opus - 97.8%. Разница в 3.6% критична для финансовых документов, но для стандартного документооборота Sonnet достаточно. Переключение между моделями в Bedrock - это смена одной строки modelId, без изменения промпта.
Оркестрация с AWS Step Functions
Step Functions связывает Lambda-функции в отказоустойчивый workflow. ASL-определение описывает машину состояний: последовательные шаги, параллельные ветки, перехват ошибок, повторные попытки. Визуализация в консоли показывает, на каком шаге застрял документ.
Структура workflow:
- GetEmails - Lambda получает письма из Gmail, возвращает список S3-ключей.
- Map State - параллельная обработка каждого вложения.
- TextractSync / TextractAsync - ветвление по размеру файла.
- ClassifyAndExtract - вызов Bedrock, парсинг ответа.
- Choice: Confidence Check - если low, переход на A2I Human Loop.
- SaveResults - запись в DynamoDB, генерация отчёта.
ASL-фрагмент с параллельной обработкой:
"ProcessAttachments": {
"Type": "Map",
"InputPath": "$.attachments",
"ItemsPath": "$.files",
"MaxConcurrency": 5,
"Iterator": {
"StartAt": "TextractSync",
"States": {
"TextractSync": {
"Type": "Task",
"Resource": "arn:aws:lambda:...",
"Next": "ClassifyAndExtract"
},
"ClassifyAndExtract": {
"Type": "Task",
"Resource": "arn:aws:lambda:...",
"End": true
}
}
}
}MaxConcurrency: 5 - это лимит одновременных вызовов Bedrock для одного аккаунта по умолчанию. При большем количестве документов они встают в очередь внутри Map State.
Обработка ошибок и повторные попытки в Step Functions
Каждый шаг workflow имеет блок Retry и Catch. Это страхует от временных сбоев API и не требует писать логику повторных попыток внутри Lambda.
"TextractSync": {
"Type": "Task",
"Resource": "arn:aws:lambda:...",
"Retry": [
{
"ErrorEquals": ["ThrottlingException", "ProvisionedThroughputExceededException"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2
},
{
"ErrorEquals": ["ModelTimeoutException"],
"IntervalSeconds": 10,
"MaxAttempts": 2,
"BackoffRate": 1.5
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "DeadLetterQueue"
}
]
}ThrottlingException от Textract - частая проблема при параллельной обработке. BackoffRate: 2 означает, что вторая попытка через 10 секунд, третья через 20. Этого достаточно для восстановления квоты.
Dead Letter Queue - это SQS-очередь, в которую попадают документы после исчерпания всех попыток. Раз в сутки Lambda выгребает очередь и отправляет сводку в Slack с причинами ошибок. Оператор решает: перезапустить обработку вручную или признать документ нечитаемым.
Уведомления о статусе пайплайна идут через SNS. При успешной обработке пакета документов - сообщение в канал #idp-success с количеством обработанных файлов. При ошибках - алерт в #idp-alerts с ARN execution и стектрейсом.
Доработки для production: Human-In-The-Loop и зашифрованные отчёты
Прототип обрабатывает документы полностью автоматически. Production-решение добавляет два слоя: ручную проверку сомнительных результатов и генерацию готовых отчётов для клиентов или внутренних потребителей.
Human-In-The-Loop реализуется через Amazon A2I (Augmented AI). В Step Functions после шага ClassifyAndExtract стоит Choice State. Если confidence == "low" или документ классифицирован как "other", задача уходит в A2I. Оператор видит интерфейс с тремя панелями: оригинал PDF слева, распознанный текст в центре, предложенный моделью JSON справа. Подтверждает или редактирует поля. Результат возвращается в workflow через callback.
Альтернативный подход - кастомный UI на базе S3 static website + API Gateway. Файлы для проверки лежат в S3 с presigned URL (срок действия 24 часа), оператор открывает веб-интерфейс, вносит правки, нажимает «Подтвердить». Lambda получает POST-запрос и продолжает execution Step Functions через send_task_success. Этот вариант дешевле A2I при количестве проверок до 100 в день - не платите за интеграцию с SageMaker Ground Truth.
Автоматическая генерация зашифрованных PDF-отчётов
Результат обработки - это не просто запись в DynamoDB. Бизнесу нужен документ, который можно отправить клиенту или подшить в архив. Lambda формирует PDF с помощью ReportLab: шапка с логотипом, таблица извлечённых данных, метаданные (дата обработки, идентификатор документа, confidence score).
Код генерации PDF:
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
def generate_report(data, output_key):
buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
c.setFont("Helvetica", 12)
c.drawString(50, 800, f"Отчёт обработки документа #{data['doc_id']}")
c.drawString(50, 780, f"Тип: {data['document_type']}")
c.drawString(50, 760, f"Уверенность: {data['confidence']}")
y = 730
for field, value in data['pii'].items():
c.drawString(50, y, f"{field}: {value}")
y -= 20
c.save()
buffer.seek(0)
kms = boto3.client('kms')
s3 = boto3.client('s3')
encrypt_response = kms.encrypt(
KeyId='alias/idp-report-key',
Plaintext=buffer.read()
)
s3.put_object(
Bucket='idp-reports',
Key=output_key,
Body=encrypt_response['CiphertextBlob'],
ServerSideEncryption='aws:kms',
SSEKMSKeyId='alias/idp-report-key'
)Шифрование через KMS с ключом idp-report-key. Без доступа к ключу файл не прочитает никто, включая администратора S3-бакета. Это закрывает требования по защите персональных данных - ФЗ-152 и GDPR для европейских контрагентов.
Для массовой генерации отчётов (1000+ в день) узким местом становится KMS. Решение: шифрование конвертом (envelope encryption). Генерируете data key, шифруете им PDF, а сам data key заворачиваете в KMS. Это снижает нагрузку на KMS API в 1000 раз и ускоряет генерацию.
Заключение: итоги и следующие шаги
Мы собрали полностью автоматизированный IDP-пайплайн на AWS. EventBridge Scheduler запускает процесс каждый час. Lambda забирает письма из Gmail, OAuth-токены хранятся в Secrets Manager. Textract извлекает текст и таблицы. Claude Sonnet 4.6 через Bedrock классифицирует документ и вытаскивает PII. Step Functions оркеструет весь поток с повторными попытками и ветвлением. Результаты сохраняются в DynamoDB, при низкой уверенности модели подключается ручная проверка. Готовые отчёты генерируются в зашифрованных PDF через KMS.
Ключевые takeaways:
- Serverless-стек означает нулевые затраты на простой. Счёт начинается с $50 в месяц.
- Промпт с few-shot примерами даёт стабильный JSON без галлюцинаций. Меняйте примеры под свои типы документов.
- Step Functions с Retry и Catch заменяет километры кода обработки ошибок в Lambda.
- Human-In-The-Loop через A2I или кастомный UI - обязательный слой для production.
Направления для развития системы:
- Потоковая обработка - замена EventBridge на Kinesis Data Streams. Документы обрабатываются мгновенно при поступлении, а не по расписанию. Актуально при объёме 1000+ писем в час.
- Fine-tuning модели - дообучение Claude на доменно-специфичных документах. Повышает точность извлечения до 99% и сокращает количество кейсов для ручной проверки.
- Мультиканальный приём - добавление Telegram Bot API и WhatsApp Business API как источников документов. Архитектура не меняется: новый источник → новый блок в начале Step Functions → общий пайплайн обработки.
- Агентный RAG для сложных документов - связка Bedrock Knowledge Base + агент, который ищет связанные документы и проверяет перекрёстные ссылки. Подход разобран в статье про четыре этапа Context Engineering - те же принципы применимы к юридическим и финансовым документам.
Архитектура готова к масштабированию. Начните с прототипа на 50 документах в день, отладьте промпты и настройки Retry. Через неделю вы получите систему, которая обрабатывает документы быстрее и точнее ручного оператора, а стоит в 10 раз дешевле.