Представьте что вы получили 500 кредитных заявок. В каждой — паспорт, банковская выписка, справка о доходах, налоговая форма. Всё в PDF. Имена файлов: upload1.pПредставьте что вы получили 500 кредитных заявок. В каждой — паспорт, банковская выписка, справка о доходах, налоговая форма. Всё в PDF. Имена файлов: upload1.p

От OCR до ADE: как машины научились не просто читать, а понимать документы

2026/03/10 17:15
15м. чтение
Для обратной связи или замечаний по поводу данного контента, свяжитесь с нами по адресу [email protected]

Документы — это не просто текст. Это таблицы где смысл в структуре строк и столбцов. Графики где тренд закодирован в форме линии. Блок-схемы где логика закодирована в стрелках. Рукописные пометки, печати с изогнутым текстом, чекбоксы — всё это несёт информацию, но совершенно по-разному.

Долгое время единственным способом извлечь данные из таких документов был OCR — технология которая умеет одно: переводить пиксели в буквы. Посмотрим что с ней не так, и почему её оказалось недостаточно.


Часть 1. OCR — машина которая видит буквы но не понимает страницу

Как это работает под капотом

Tesseract появился в 1985 году в лабораториях HP. Принцип работы: разбить изображение на строки → строки на слова → слова на символы → каждый символ сравнить с эталоном.

Классический OCR работал через жёстко заданные признаки — замкнутые контуры (буква O содержит замкнутый контур, F — нет), соотношения сторон, количество пересечений с горизонтальными линиями. Обученный классификатор говорил: "это больше похоже на B чем на 8". На чистом тексте — работает. При малейшем отклонении от идеала — разваливается.

Современные движки вроде PaddleOCR заменили ручные признаки на нейросети:

  • CNN (свёрточная сеть) сама учится что важно: первые слои замечают края и линии, средние — части букв, последний — целые символы

  • Трансформер (SVTR) читает символы не изолированно, а с учётом контекста соседей — размытую букву в слове "привет" легче угадать зная что рядом стоит "_ривет"

  • Ниже простой пример использования:

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='en') result = ocr.ocr('document.jpg') for line in result[0]: bbox, (text, confidence) = line print(f"'{text}' (уверенность: {confidence:.2f})") # 'Total amount due' (уверенность: 0.97) # '$155.15' (уверенность: 0.94)

PaddleOCR возвращает не просто текст, но и bounding box — координаты прямоугольника вокруг каждого текстового блока. Теперь мы знаем не только что написано, но и где.

Фундаментальная проблема

Вот что происходит с двухколоночной научной статьёй. OCR читает горизонтально — строку за строкой. Результат: текст из левой и правой колонок перемешивается в бессмыслицу. Таблица где числа отрываются от заголовков столбцов. График где подписи осей оказываются посреди основного текста.

(Читает все слева направо, не разделил абзацы, таблицы осмысленно между собой - это проблема)
(Читает все слева направо, не разделил абзацы, таблицы осмысленно между собой - это проблема)

Что наприме видит OCR в двухколоночном документе: "Methods Results" "We used The experiment" "N=100 showed p<0.05" Что получается после OCR: "Methods Results We used The experiment N=100 showed p<0.05"

OCR решает задачу распознавания символов. Он не решает задачу понимания документа.

И это не баг который можно починить — это принципиальное ограничение архитектуры. OCR смотрит на страницу через трубочку: видит один символ, потом следующий, потом следующий. Никакого понимания структуры.


Часть 2. Layout Detection — наконец поднимаем голову и смотрим на страницу целиком

Идея

Layout Detection — это отдельная модель которая смотрит на страницу как на изображение и отвечает на вопрос: какие типы регионов здесь есть?

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

Без Layout Detection: Изображение → Text Detection → Text Recognition → "стена текста" С Layout Detection: Изображение → Layout Detection (какие регионы?) ↓ Text Detection (где текст внутри каждого региона?) ↓ Text Recognition (что написано?) ↓ Структурированный результат с типами блоков (Теперь каждый блок разделен по смыслу)

(Теперь каждый блок разделен по смыслу)


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

from paddleocr import PPStructure layout_engine = PPStructure(show_log=False) result = layout_engine('document.jpg') for region in result: print(f"Тип: {region['type']}") # text, table, figure, title... print(f"bbox: {region['bbox']}") # координаты региона

Порядок чтения — это отдельная задача

Layout Detection говорит где блоки и что они из себя представляют. Но порядок чтения — отдельная задача которую он не решает.

Проблема в том что OCR возвращает просто облако слов с координатами — никакого порядка. В простом одноколоночном документе можно отсортировать сверху вниз и этого хватит. Но в реальных документах — статья с врезкой, презентация с несколькими блоками, газетная полоса — такая сортировка сломает текст.

LayoutReader решает именно эту задачу. Он берёт координаты текстовых блоков от OCR и определяет правильный порядок чтения. В основе — LayoutLM от Microsoft, трансформер обученный понимать пространственное расположение текста на странице. Обучен на 500 тысячах размеченных страниц где люди показывали правильный порядок.

На выходе LayoutReader отдаёт просто текст - уже собранный в правильном порядке. Никаких координат, никакой нумерации. Агент этих деталей вообще не видит.

Зачем это нужно агенту? Агент получает текст документа как контекст в системном промпте и отвечает на вопросы пользователя. Если текст перемешан - колонки смешались, абзацы перепутались — он просто не сможет адекватно читать документ. Порядок чтения нужен чтобы контекст в промпте был связным.

Layout Detection при этом работает параллельно и независимо — он нужен для другого: чтобы агент знал какие визуальные регионы есть на странице и мог вызвать нужный инструмент, например попадается бокс с лейблом (картинка/таблица) агент понимает что нужно вызвать VLM инструмент:

Итого агент получает в системный промпт две независимые вещи:

LayoutReader → связный текст документа ──┐ ├──→ системный промпт агента Layout Detection → карта регионов с типами ──┘ (Каждый блок получает свой порядковый номер чтения)

(Каждый блок получает свой порядковый номер чтения)

Часть 3. Визуальное понимание — то чего не хватало

Почему текста всё равно недостаточно

Даже с правильным порядком чтения мы теряем огромный пласт информации.

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

db60fb04a2d09ec325cd81a81f441ba7.png

OCR захватывает текст. Всё остальное теряется.

Как устроена Vision-Language Model

VLM — это обычный LLM перед которым стоит стек обработки изображений:

[Изображение] → Vision Encoder → Projector → LLM → [Текст ответа] (CLIP/SigLIP) (переводной (обычная языковая слой) модель)

Vision Encoder (например CLIP от OpenAI) превращает пиксели в векторы. CLIP обучен на сотнях миллионов пар "изображение + текст" и понял что картинка кошки и слово "кошка" описывают одно и то же — их векторы близки в пространстве эмбеддингов.

Projector — переходный слой. Визуальные векторы имеют другую природу чем текстовые токены. Projector конвертирует одно в другое.

LLM получает смешанную последовательность: визуальные токены + текстовые токены вопроса. Рассуждает над всем этим вместе.

Никакой магии — просто совместное обучение на данных где изображения и тексты связаны.

Гибридная архитектура на практике

VLM используется не вместо OCR и Layout Detection, а вместе с ними:

Layout Detection + LayoutReader → точная структура, правильный порядок ↓ Маршрутизация по типу: Текст → OCR (быстро, точно, дёшево) Таблица → специализированная модель или VLM График → VLM с целевым промптом ↓ Агент получает весь контекст

Layout Detection даёт детерминированную основу. VLM обрабатывает то что требует визуального понимания.


Часть 4. Агентный подход — система которая принимает решения

ReAct: явное рассуждение перед каждым действием

Собрав OCR, Layout Detection и VLM вместе, мы получили набор инструментов. Но кто решает какой инструмент вызвать и когда? Это задача агента.

ReAct (Reason + Act) — паттерн где система явно рассуждает перед каждым действием:

💭 Thought → Что нужно? Есть ли ответ в уже имеющемся тексте? ⚡ Action → Вызвать нужный инструмент 👀 Observe → Изучить результат 💭 Thought → Достаточно? Нужен ещё шаг? ✅ Answer → Сформулировать ответ

Это принципиально отличается от "одного прохода". Агент может заметить что OCR вернул странное число ($7.99 вместо $7.95), усомниться, попытаться перепроверить. Человек так и работает с документами.

Что реально происходит

Агент получает системный промпт в котором содержится весь упорядоченный текст документа и список всех регионов с типами и ID:

## Текст документа (в порядке чтения) [результат OCR + LayoutReader] ## Регионы документа - region_id="text_0", type="text", page=0 - region_id="table_1", type="table", page=0 - region_id="chart_0", type="figure", page=1

Дальше агент сам решает:

Вопрос: "Какой тренд показывает график?" Thought: это визуальный вопрос, из текста не отвечу Action: AnalyzeChart(region_id="chart_0") Observe: {"trend": "declining", "x_axis": "Year 2020-2023"} Answer: "График показывает снижающийся тренд" --- Вопрос: "Какой заголовок у документа?" Thought: это есть в OCR тексте Answer: "US Economic Report Q3 2024"

Агент сам решает — тратить ли дорогой API вызов к VLM или ответить из уже имеющегося текста. Это и есть агентность в практическом смысле.


Часть 5. LandingAI DPT — когда пайплайн становится одной моделью

Проблема самодельного пайплайна

Всё описанное выше — OCR + Layout Detection + LayoutReader + VLM + LangChain агент — работает. Мы только что его и описали. Но у него есть фундаментальная проблема:

Каждый компонент настраивается отдельно. Стыки между ними хрупки. Обновление одного компонента требует перепроверки всей цепочки. Новый тип документа — снова тюнинг каждого звена. Всё это сложно, дорого и ненадёжно в продакшене.

LandingAI решила эту проблему иначе: вместо того чтобы собирать пайплайн из кубиков, они обучили специализированную модель которая делает всё это сразу.

Три принципа DPT

Vision-First. Документ воспринимается как визуальный объект с самого начала. Не "сначала OCR, потом понимаем структуру" — а одновременное восприятие текста, расположения, структуры и визуальных отношений.

Data-Centric. Правильно подобранные обучающие данные дают такой же прирост как улучшение архитектуры. Тысячи примеров обведённых слов, чекбоксов разных стилей, рукописных формул, печатей с изогнутым текстом — всё это размечено и вошло в обучение.

Agentic. Система не делает всё за один проход. Сложная таблица обрабатывается иначе чем параграф. Система итерирует до достижения порога качества.

Как обучены чекбоксы и обведённые слова (пример выше)

Это вопрос который возникает сразу: как модель понимает что слово обведено кружком? Никакой магии — просто обучение. Модели показали тысячи примеров с разметкой:

  • Вот чекбокс с галочкой → в ответе [x]

  • Вот пустой чекбокс → в ответе [ ]

  • Вот слово "No" обведено кружком → в ответе No (circled)

Модель выучила сопоставление визуального паттерна с текстовым представлением. Она не "понимает" что такое галочка в человеческом смысле — она выучила: этот визуальный паттерн всегда маппится на этот текстовый символ.
Как раз это и есть Data-Centic, модель обучена на таком количестве примеров что способна решать даже такие задачи.

b14d3389bd88f51f334bc190a605dd2e.png730c257aa48dc930998b02571ac06f47.png

Производительность

DocVQA — стандартный бенчмарк: вопросы и ответы по реальным отсканированным документам (датасет UCSF Industry Documents Library). Вопросы типа "какой домашний телефон указан в этой форме?" — и ответ нужно найти в рукописном поле.

239484bbdae250a2b8b9f492e15daeb7.png

Система

Точность

Человек

~98%

Лучшие опубликованные модели

< 99%

LandingAI DPT-2

99.15%

API

from landingai_ade import LandingAIADE client = LandingAIADE() # Весь пайплайн — один вызов parse_result = client.parse( document="contract.pdf", model="dpt-2-latest" )

Результат — иерархически организованные данные:

parse_result └── splits[] # одна запись на страницу ├── .markdown # чистый markdown с сохранённой структурой └── .chunks[] # структурные единицы страницы ├── chunk_id # UUID ├── text # содержимое ├── chunk_type # text|table|figure|logo|attestation ├── bbox # координаты [0..1] от размера страницы └── page # номер страницы

Чанк — не просто строка текста, а осмысленная структурная единица: логотип, таблица целиком, параграф, график, подпись к рисунку. Координаты нормализованы от 0 до 1 — работает для любого разрешения. То есть теперь каждый чанк хранит в себе и реализацию ORC и Layout Detection.

Что умеет с реально сложными документами

Тип attestation — новый тип чанка которого нет в обычных OCR системах. Печати и подписи. Изогнутый текст внутри круглой печати с фоновым шумом + отдельная подпись рядом. DPT читает и то и другое.

ec7979971357c2f25b21dbed1de7aba6.png

Рукописные математические формулы√(√2/2) в рукописном виде возвращается в markdown с правильными математическими символами.

Мегатаблицы с тысячей ячеек — обычный LLM галлюцинирует потому что не может удержать такой объём в контекстном окне. Агентный подход обрабатывает по частям.

(потом можно преобразовать output в csv и вывести в удобном виде)
(потом можно преобразовать output в csv и вывести в удобном виде)
141ab6401fa161267011fba2dfbc6b70.png

Документы без единого текста — инструкция IKEA, только иллюстрации. DPT-1 возвращает детальное текстовое описание каждого рисунка: "инструкция не собирать на твёрдой поверхности, рекомендуется защитный коврик".

24ab5683ef5505451e5a3d10a5a8f1ca.png695bcc0abb4026b88507f255b6438127.png

Часть 6. Извлечение структурированных данных

Parse даёт структурированное представление документа. Extract вытаскивает конкретные поля по заданной схеме. Разделение имеет смысл: один распаршенный документ можно запрашивать с разными схемами без повторного парсинга.

from pydantic import BaseModel, Field from landingai_ade.lib import pydantic_to_json_schema class UtilityBillSchema(BaseModel): total_amount_due: float = Field( description="Total amount currently due on this bill" ) max_consumption_month: str = Field( description="Month with highest consumption in the last 12 months, " "determined from the usage history bar chart" ) extraction = client.extract( schema=pydantic_to_json_schema(UtilityBillSchema), markdown=parse_result.markdown, model="extract-latest" ) print(extraction.extraction) # {'total_amount_due': 155.15, 'max_consumption_month': 'January'} print(extraction.extraction_metadata) # {'total_amount_due': {'value': 155.15, 'references': ['0-e', '0-h']}}

Чем подробнее описание поля — тем точнее извлечение. Модель использует description чтобы понять что именно искать. "Total amount currently due" работает лучше чем просто "amount".

References в метаданных — ссылки на конкретные чанки (или ячейки таблицы) из которых получено значение. Короткие ID вида '0-e' — ячейки таблицы. Длинные UUID — фигуры или текстовые блоки. Это позволяет построить интерфейс где пользователь видит значение и может кликнуть чтобы увидеть точное место в оригинальном документе.

Реальный сценарий: обработка кредитных заявок

from enum import Enum from pydantic import BaseModel class DocumentType(str, Enum): ID = "ID" W2 = "W2" bank_statement = "bank_statement" investment_statement = "investment_statement" # Шаг 1: парсим документ с разбивкой по страницам parse_result = client.parse( document=document, split="page", # markdown разбивается по страницам → parse_result.splits[] model="dpt-2-latest" ) # Для категоризации достаточно первой страницы first_page_markdown = parse_result.splits[0].markdown doc_type = client.extract(schema=doc_type_json_schema, markdown=first_page_markdown) # Шаг 2: применить правильную схему для этого типа schema = schema_map[doc_type.extraction["type"]] data = client.extract(schema=schema, markdown=parse_result.markdown)

Трюк с первой страницей не случаен: для категоризации обычно достаточно шапки документа, а значит тратим значительно меньше токенов
Логика элегантная: сначала дёшево узнаём тип, потом точно извлекаем данные с правильной схемой (для нужного типа документа).

Extract принимает схему в двух форматах. Первый — чистый JSON Schema, удобен если схема генерируется динамически или приходит извне. Второй — Pydantic модель через конвертер pydantic_to_json_schema(), удобен когда схема описывается прямо в коде. Под капотом это одно и то же — Pydantic просто конвертируется в JSON Schema перед отправкой.


Часть 7. RAG — задаём вопросы документам

Почему keyword-поиск не работает

74-страничный отчёт Apple. Аналитик спрашивает: "Какая была выручка в 2023?"

Keyword-поиск ищет слово "revenue". Документ использует "net sales". Ноль результатов — хотя информация есть. Даже если найдём совпадение — "revenue" встречается 75 раз, и keyword-поиск не знает какое упоминание отвечает на конкретный вопрос. Ответ на "какие основные риски компании" вообще разбросан по страницам 12, 15 и 18 — его нужно синтезировать.

Нужно семантическое понимание.

Как работает RAG

RAG (Retrieval-Augmented Generation) — три фазы:

Фаза 1: Препроцессинг (делается один раз)

Каждый чанк из ADE превращается в embedding — вектор из 1536 чисел кодирующий смысл текста. Семантически похожие тексты получают близкие векторы. Именно поэтому запрос "revenue" находит чанк с "net sales": их векторы близки в 1536-мерном пространстве.

import chromadb from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings CHROMA_DB_PATH = Path("./chroma_db") COLLECTION_NAME = "ade_documents" EMBEDDING_MODEL = "text-embedding-3-small" vectordb = Chroma( collection_name=COLLECTION_NAME, embedding_function=OpenAIEmbeddings(model=EMBEDDING_MODEL), persist_directory=str(CHROMA_DB_PATH) )

ChromaDB хранит вектор, текст и метаданные в одной записи. Векторы используются для поиска, текст достаётся и передаётся LLM — модель никогда не видит числа.

Фаза 2: Retrieval (при каждом запросе)

retriever = vectordb.as_retriever()

Можно добавить фильтрацию по метаданным — гибридный поиск:

q_embed = openai.embeddings.create( model=EMBEDDING_MODEL, input="What was Apple's total revenue in 2023?", ).data[0].embedding results = collection.query( query_embeddings=[q_embed], n_results=5, include=["documents", "metadatas", "distances"], where={"chunk_type": "table"}, )

Фаза 3: Generation

LLM получает найденные чанки как контекст. Ключевая инструкция в системном промпте — защита от галлюцинаций:

from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain.chains import create_retrieval_chain system_prompt = ( "Use the following pieces of retrieved context to answer the " "user's question. " "If you don't know the answer, say that you don't know." "\n\n" "{context}" ) prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), ("human", "{input}"), ]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=1) rag_chain = create_retrieval_chain(retriever, prompt | llm) response = rag_chain.invoke({"input": "What were Apple net sales in 2023?"}) print(response["answer"])

Visual Grounding в RAG

Для каждого найденного чанка есть bbox в метаданных. Это позволяет восстановить точный фрагмент из оригинального PDF — изображение генерируется на лету из координат, ничего не хранится в базе. В продакшене на AWS: PDF в S3, координаты из ChromaDB, изображение генерируется по запросу и отдаётся как presigned URL.

Почему это важно: аналитик получает ответ "выручка $383 млрд" и видит точную таблицу со страницы 28. Проверить правильность — один клик. Через полгода аудиторы спрашивают откуда число — есть конкретная страница, конкретная таблица, конкретная ячейка.


Итоговая картина

Весь путь который мы прошли — это не просто улучшение точности распознавания. Это смена парадигмы на каждом шаге:

Tesseract (1985) └─ Смотрит через трубочку: один символ за раз └─ Знает: "это буква B" └─ Не знает: что это заголовок PaddleOCR └─ Трубочка расширилась до строки └─ Знает: "это слово, вот где оно на странице" └─ Не знает: порядок чтения и структуру Layout Detection + LayoutReader └─ Наконец смотрит на страницу целиком └─ Знает: "это таблица, это колонки, вот правильный порядок" └─ Не знает: что нарисовано на графике VLM └─ Понимает визуальный смысл └─ Знает: "график показывает снижение, блок-схема идёт вправо" └─ Не знает: как всё это объединить надёжно Агентный пайплайн (всё выше + ReAct) └─ Принимает решения: какой инструмент вызвать └─ Знает: когда использовать VLM, а когда достаточно OCR └─ Хрупкий: сложно поддерживать, каждый стык может сломаться LandingAI DPT └─ Та же идея, реализованная как единая специализированная модель └─ Знает: всё вышеперечисленное из коробки └─ Точнее человека на стандартном бенчмарке

Самодельный агентный пайплайн и LandingAI DPT — это одна и та же идея. Разница в реализации: первый собран из разрозненных кубиков и требует постоянной поддержки, второй — единая модель обученная на миллионах документов с нуля.

Tesseract

PaddleOCR

Агентный пайплайн

LandingAI DPT

Простой текст

Многоколоночный текст

⚠️

Таблицы без линий

⚠️

Графики и блок-схемы

⚠️

Рукопись + чекбоксы

⚠️

⚠️

Математические формулы

⚠️

⚠️

Печати и подписи

Visual Grounding

⚠️

Настройка под новый тип документа

Много кода

Много кода

Очень много кода

JSON схема

Надёжность в продакшене

Низкая

Средняя

Низкая

Высокая


Заключение

Если вы дочитали до сюда — у вас теперь есть ответ на вопрос из начала статьи. 500 кредитных заявок с безымянными файлами обрабатываются так:

  1. DPT парсит каждый документ, понимает структуру

  2. Extract с DocType схемой определяет тип по первой странице

  3. Extract с документо-специфичной схемой вытаскивает нужные поля

  4. Автоматическая валидация: совпадают ли имена, актуальны ли документы, сколько суммарно активов

  5. Visual Grounding: каждое значение привязано к конкретному месту в оригинале

Неделя ручной работы → несколько минут автоматической обработки.

Документы перестали быть чёрными ящиками.


Материал основан на курсе Document AI: From OCR to Agentic Doc Extraction от DeepLearning.AI и LandingAI

Источник

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу [email protected] для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.