Как поднять ИИ-анализ документов на сервере
Папка с полусотней договоров, и по каждому нужно вытащить сумму, срок и стороны — не найти нужный файл, а прочитать его и выдать значения. Обычный чат с базой знаний для этого не годится: он ищет похожие куски текста и на один и тот же вопрос честно отвечает по-разному. Ниже — как поднять ИИ-анализ документов на сервере так, чтобы модель читала файл целиком, отвечала предсказуемо и не путала скан с пустым местом.
Содержание
- ИИ-анализ документов и RAG-поиск — разные задачи, разная настройка
- Почему обычный чат для анализа не годится: пересказ, разный ответ и текст вместо значения
- Пространство для анализа: закрепление документа и системный промпт под конкретные поля
- Документ длиннее окна модели: разбор по частям и двухпроходный анализ
- Сканы и фото без OCR: анализ напрямую через vision-модель
- Пакетный анализ через API: обходим папку и получаем таблицу
- Какой сервер под ИИ-анализ документов брать в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →ИИ-анализ документов и RAG-поиск — разные задачи, разная настройка
У AnythingLLM для документа есть два разных режима, и путаница между ними — источник большинства неверных ответов.
- Найти ответ в базе из тысяч файлов — вопрос «где у нас написано про...». Это RAG-поиск: файл режется на чанки, в ответ идут несколько похожих кусков — по умолчанию четыре, — остального для модели не существует. Ему посвящена статья про RAG по своим документам.
- Разобрать конкретный документ или партию однотипных — вопрос «что написано в этом договоре про...», файл известен заранее. Это анализ, и весь материал ниже — про него.
Для второго случая в AnythingLLM есть закрепление (pin): документ целиком вставляется в промпт, минуя поиск по кускам. Документация честна: оно даёт «full-text comprehension… at the expense of speed and cost» и рекомендовано «для документов, которые целиком помещаются в контекстное окно или критически важны» — как крайняя мера.
Для ИИ-анализа документов закрепление — не крайняя мера, а режим по умолчанию: не нужно, чтобы модель угадывала, какие 20% договора ей показать, — нужно видеть договор целиком. Дальше — как это настроить и обойти два его ограничения: контекстное окно и сканы без текста.
Почему обычный чат для анализа не годится: пересказ, разный ответ и текст вместо значения
Первая причина — режим чата. По умолчанию пространство отвечает в Chat: по документации, «uses LLM general knowledge w/custom embeddings to produce output» — не найдя пункта о неустойке, модель подставит типовую формулировку из общих знаний, неотличимую в таблице от настоящей цитаты. Query строже: «will not use LLM unless there are relevant sources... & does not recall chat history» — без найденного текста прямо отвечает, что не нашёл. Третий режим, automatic, — для вызова инструментов; для анализа выбор между query и chat, почти всегда в пользу первого.
Вторая причина — температура. У каждого пространства есть параметр openAiTemp, доступный в интерфейсе и в API при создании. По умолчанию у большинства провайдеров он больше нуля — компромисс в пользу живости ответа, а для анализа это риск: один и тот же вопрос об одном договоре дважды при 0,7 даст разный порядок пунктов, а на пограничных формулировках сумм («порядка полутора миллионов» против «1 500 000 рублей») — разное итоговое число. Для пространства-анализатора ставьте openAiTemp: 0.
Третья причина настройками не лечится — ответ приходит текстом, а не значением. На вопрос «какая сумма договора» модель без инструкции напишет «Согласно договору, сумма составляет 850 000 рублей» — предложение, а не число для ячейки таблицы. Разовая просьба формата в вопросе не спасает — нужен постоянный системный промпт с точной схемой полей, и это следующий шаг.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть AnythingLLMПространство для анализа: закрепление документа и системный промпт под конкретные поля
Пространство под анализ настраивается один раз — дальше через него проходят десятки документов одного типа. Три поля стоит задать сразу при создании:
curl -s -X POST http://127.0.0.1:3001/api/v1/workspace/new \
-H "Authorization: Bearer $ALLM_KEY" -H 'Content-Type: application/json' \
-d '{
"name": "Анализ договоров",
"chatMode": "query",
"openAiTemp": 0,
"topN": 12,
"similarityThreshold": 0,
"queryRefusalResponse": "NO_DATA"
}'
chatMode и openAiTemp — из раздела выше. queryRefusalResponse меняет фразу отказа: вместо There is no relevant information in this workspace to answer your query. пространство ответит вашей строкой — для скрипта это разница между хрупким сравнением подстроки и точным if answer == "NO_DATA".
Дальше загрузка: POST /v1/document/upload возвращает location — путь вида custom-documents/dogovor-142.pdf-3f9b….json, — а привязка к пространству идёт через update-embeddings с этим путём в массиве adds (подробно — в статье про установку AnythingLLM на VPS). Специфика анализа в том, что происходит дальше: документация прямо оговаривает — закрепить можно только уже вшитый документ, эмбеддинг обязателен, даже если сам поиск не нужен. Закрепление включается пуш-пином в интерфейсе либо тем же вызовом из скрипта:
curl -s -X POST http://127.0.0.1:3001/api/v1/workspace/analiz-dogovorov/update-pin \
-H "Authorization: Bearer $ALLM_KEY" -H 'Content-Type: application/json' \
-d '{"docPath":"custom-documents/dogovor-142.pdf-3f9b….json","pinStatus":true}'
Последний и главный кусок настройки — системный промпт с точной схемой: без него модель решает формат сама и от документа к документу меняет порядок полей и даже язык ответа. Задайте схему буквально:
Ты анализируешь один документ, закреплённый в этом пространстве. Отвечай
ТОЛЬКО валидным JSON без markdown-разметки и пояснений до или после:
{
"storona_1": "полное наименование первой стороны",
"storona_2": "полное наименование второй стороны",
"data_dogovora": "ДД.ММ.ГГГГ",
"summa": "число без пробелов и валюты",
"valuta": "RUB|USD|EUR",
"srok_deystviya": "текст пункта о сроке",
"usloviya_rastorzheniya": "текст пункта или NO_DATA",
"citata_summy": "дословная фраза из документа, где указана сумма"
}
Если поля в документе нет — ставь значение "NO_DATA", не придумывай.
Поле citata_summy не лишнее: дословная цитата рядом со значением превращает слепое доверие в факт, который проверяется взглядом за секунду, — единственная защита от красиво оформленной ошибки, которая реально работает.
Документ длиннее окна модели: разбор по частям и двухпроходный анализ
Закрепление честно предупреждает о границе: оно годится, когда документ целиком помещается в контекстное окно. Годовой отчёт на 180 страниц туда не влезет: облачный провайдер вернёт отказ, а локальная модель молча обрежет промпт с начала, и в контексте не окажется первых страниц с реквизитами сторон.
Русский текст даёт примерно 3,5–4 символа на токен — грубая прикидка, не бенчмарк. Договор на 40 000 знаков — это 10–11 тысяч токенов, в окно на 128 тысяч (класс GPT-4o) он войдёт с запасом. А свод регламентов на 600 000 знаков, под 150–170 тысяч токенов, не поместится уже и туда, и тем более в типичные 4–8 тысяч токенов локальной модели через Ollama (num_ctx, разбор — в статье про локальный запуск LLM через Ollama). Превышение лимита у облачных провайдеров выглядит так:
{"type":"error","error":{"type":"invalid_request_error",
"message":"prompt is too long: 215043 tokens > 200000 maximum"}}
Решение — не поиск, это вернуло бы к RAG, — а разбор по частям со склейкой. Документ режется по смысловым границам, каждая часть анализируется отдельным закреплённым проходом по той же JSON-схеме, а результаты — уже компактные данные, не текст — отправляются вторым вызовом на консолидацию.
csplit -z -f chast- -b '%02d.md' otchet.md '/^## Раздел/' '{*}'
for f in chast-*.md; do
# для каждой части: upload → update-embeddings → update-pin → chat (query)
# результат — JSON, накапливается в results.jsonl
:
done
jq -s '.' results.jsonl > svod.json
Второй проход — запрос той же модели с промптом «вот JSON-объекты по частям одного документа, собери их в одну запись, убери дубли, суммируй числовые поля» и svod.json в теле. Разница с нарезкой из статьи про RAG принципиальна: там она нужна, чтобы поиск быстрее находил релевантный кусок, здесь — чтобы ни один кусок не остался непрочитанным.
Сканы и фото без OCR: анализ напрямую через vision-модель
В статье про RAG есть подробный OCR-конвейер — ocrmypdf плюс tesseract-ocr-rus — для поиска по архиву сканов это правильный путь. Для анализа одного скана есть путь короче: показать его модели напрямую, если она умеет читать изображения.
Условие одно — модель должна быть мультимодальной (vision): из облачных это GPT-4o и семейство Claude, из локальных — модели с пометкой vision в реестре Ollama, а в приложение по умолчанию добавлена LLaVA-Llama3 как встроенный вариант. В интерфейсе изображение перетаскивается в чат вместе с вопросом; через API это отдельное поле в теле запроса:
IMG=$(base64 -w0 skan-schet.png)
curl -s -X POST http://127.0.0.1:3001/api/v1/workspace/analiz-schetov/chat \
-H "Authorization: Bearer $ALLM_KEY" -H 'Content-Type: application/json' \
-d @- <<EOF
{
"message": "Извлеки номер счёта, дату и итоговую сумму. Ответ — JSON.",
"mode": "chat",
"attachments": [{
"name": "skan-schet.png",
"mime": "image/png",
"contentString": "data:image/png;base64,${IMG}"
}]
}
EOF
Обратите внимание на mode — для картинки это chat, не query: поиск изображение не индексирует, и Query честно ответит отказом, ведь «relevant sources from vectorDB» у скана попросту нет. Проверка формата минимальна — MIME должен начинаться с image/, — а если модель картинки не понимает, откажет уже сам провайдер своей ошибкой.
Многостраничный скан одним файлом не отправить — вложение ждёт изображение, не PDF на полсотни страниц. Разбейте на страницы заранее:
pdftoppm -png -r 150 skan-dogovora.pdf stranica
Флаг -r 150 — разрешение: ниже мелкий шрифт и цифры в таблицах модель читает с ошибками, для рукописных пометок разумно поднять до 200–300, но и вес файла растёт кратно. Честная граница: точность зависит от качества скана сильнее, чем от выбора модели — кривой угол съёмки телефоном роняет результат заметнее, чем разница между GPT-4o и локальной vision-моделью.
Пакетный анализ через API: обходим папку и получаем таблицу
Ручной проход по десятку договоров — работа на вечер, по трёмстам — работа, которую нужно написать один раз. Собираем части предыдущих разделов в цикл: пространство переиспользуется для всех документов подряд, а закреплённый файл после обработки снимается с закрепления и удаляется из индекса — иначе к сотому договору в контекст начнут проситься все девяносто девять предыдущих.
#!/usr/bin/env bash
API=http://127.0.0.1:3001/api/v1
WS=analiz-dogovorov
KEY="$ALLM_KEY"
OUT="rezultat-$(date +%F).csv"
echo "file,storona_1,storona_2,summa,valuta,citata_summy" > "$OUT"
for f in /root/dogovory/*.pdf; do
loc=$(curl -s -X POST "$API/document/upload" -H "Authorization: Bearer $KEY" \
-F "file=@$f" | jq -r '.documents[0].location')
[ "$loc" = "null" ] && { echo "$f: не разобран, пропуск" >&2; continue; }
curl -s -X POST "$API/workspace/$WS/update-embeddings" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d "{\"adds\":[\"$loc\"],\"deletes\":[]}" > /dev/null
curl -s -X POST "$API/workspace/$WS/update-pin" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d "{\"docPath\":\"$loc\",\"pinStatus\":true}" > /dev/null
raw=$(curl -s -X POST "$API/workspace/$WS/chat" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"message":"Разбери закреплённый документ по схеме.","mode":"query","reset":true}' \
| jq -r '.textResponse')
json=$(echo "$raw" | sed -n '/{/,/}/p')
echo "$json" | jq -e . >/dev/null 2>&1 || { echo "$f: не JSON: $raw" >&2; continue; }
jq -r --arg f "$(basename "$f")" \
'[$f, .storona_1, .storona_2, .summa, .valuta, .citata_summy] | @csv' <<< "$json" >> "$OUT"
curl -s -X POST "$API/workspace/$WS/update-pin" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d "{\"docPath\":\"$loc\",\"pinStatus\":false}" > /dev/null
curl -s -X POST "$API/workspace/$WS/update-embeddings" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d "{\"adds\":[],\"deletes\":[\"$loc\"]}" > /dev/null
done
Флаг "reset":true обнуляет историю треда — без него к десятому договору модель путает реквизиты текущего с упомянутыми раньше: сброс дешевле, чем потом разбираться в путанице. Строка sed -n '/{/,/}/p' защищает от привычки моделей оборачивать JSON в пояснение несмотря на прямой запрет в промпте, а jq -e проверяет, что после вырезки получился валидный объект. И снятие закрепления с удалением из индекса не для порядка: закрепление — флаг pinned в таблице workspace_documents внутри anythingllm.db, и без очистки следующий запрос платит токенами за чужие договоры в контексте.
Честно о скорости: цикл последовательный осознанно — гонка за пин между двумя договорами кончится тем, что один анализ прочитает чужой файл. На сотне документов закладывайте от нескольких минут до получаса в зависимости от провайдера — тот самый «cost of speed» из документации закрепления.
Какой сервер под ИИ-анализ документов брать в MAATRIX
Нагрузка при анализе иная, чем при RAG-поиске: там в модель едет несколько найденных абзацев, здесь — закреплённый документ целиком при каждом обращении. Для внешнего API это счёт за токены, а не нагрузка на сервер: честный минимум — 2 vCPU, 4 ГБ RAM, 40 ГБ NVMe, диск с запасом — скан в 150 DPI весит несколько мегабайт, а закреплённые документы и эмбеддинги хранятся не в одном экземпляре. Для локальной модели разница ощутимее: закрепление гонит в контекст весь промпт, а не пару абзацев, и нагрузка ложится на обработку промпта, а не на генерацию — ответ компактный JSON на десяток строк, вход же иногда десятки тысяч токенов.
На нашем стенде (AMD EPYC 9554, 16 vCPU = 8 физических ядер + HT, qwen2.5:7b Q4_K_M, Ollama 0.33.1) это видно: генерация упирается в память уже на 4 потоках (5,7 / 7,6 / 7,6 / 7,4 ток/с на 2/4/8/16), обработка промпта растёт до 16 (12,6 / 31,0 / 61,7 / 68,0) — а на 32 при тех же 16 vCPU оба показателя обваливаются в двадцать раз (0,35 и 10,9). Вывод: num_thread стоит выставлять по числу физических ядер — здесь 8, — а не по максимуму, который выигрывает у коротких ответов, не у длинного документа.
| Модель | Память в покое | Ток/с генерации при 16 потоках (наш стенд) |
|---|---|---|
qwen2.5:3b | 2,2 ГБ | 34,1 |
mistral:7b | 5,0 ГБ | 12,1 |
qwen2.5:7b | 5,1 ГБ | 7,4 |
llama3.1:8b | 5,6 ГБ | 12,8 |
Это текстовые модели; vision-модель того же класса весит больше за счёт энкодера изображений — цифру для своей сборки покажет ollama show имя-модели --modelfile. Комфортный вариант под локальную модель — 8 vCPU, 16 ГБ RAM, 100+ ГБ NVMe: ядра под расчёт, память под веса плюс контекст под целый документ. Формула по весам — в статье сколько RAM нужно для AnythingLLM.
Локация — Лондон (UK), причины две: доступность (GPT-4o, Claude и эмбеддинги OpenAI с британского адреса отвечают штатно, тогда как с российского на вложения прилетает тот же региональный отказ, что и на текст) и юрисдикция (реквизиты контрагентов из Евросоюза ближе к британской площадке, чем к серверу в России). Для документов строго под 152-ФЗ берите российскую локацию, но тогда и модель, и vision держите локальными.
AnythingLLM разворачивать вручную не нужно: приложение есть в каталоге apps.maatrix.io и ставится автоматически при заказе сервера, работает на Ubuntu и Debian, а адрес панели и ключи появляются в личном кабинете, в разделе «Доступ». Остаётся создать пространство, задать системный промпт под свою схему и загрузить первую партию документов. Оплата — картой российского банка, по СБП, криптовалютой или токеном MAAT.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть AnythingLLMОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Цифрам и датам, которые модель достала из документа, можно верить без проверки?
Нет, относитесь к ним как к черновику. Добавьте в схему поле-цитату (citata_summy из раздела про пространство) — дословный фрагмент рядом со значением превращает слепое доверие в факт, который сверяется взглядом за секунду, особенно на пограничных суммах.
Обязательно ли закреплять документ, или для одного файла хватит вопроса без пина?
Для документа в одну-две страницы обычно хватает вопроса без закрепления, если ответ укладывается в несколько абзацев, что найдёт поиск. Закрепление нужно, когда важна каждая часть текста — договор с приложениями, регламент с перекрёстными ссылками.
Скан документа с персональными данными уходит в облачный vision-провайдер — это утечка?
Да: изображение целиком уезжает к провайдеру при каждом запросе — иной периметр приватности, чем у RAG, где наружу уходят только найденные текстовые фрагменты. Для паспортов и медицинских документов используйте локальную vision-модель через Ollama либо закрашивайте чувствительные поля до загрузки.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.