Вы запустили LocalAI, загрузили крутую модель, попытались засунуть в нее PDF с отчетом - и получили тишину. Или, что хуже, кракозябры. Знакомо? LocalAI из коробки не умеет читать PDF. Он ждет чистый текст, а вы даете ему бинарный файл с графиками, таблицами и, возможно, сканами. Это как пытаться накормить супом человека, который ест только через трубочку. Нужен адаптер.
PDF - это не текст, а слоеный пирог проблем
Почему так сложно? PDF создавался для единого отображения на любом устройстве, а не для удобства машинного чтения. Внутри может быть:
- Текст как векторные контуры (да, иногда буквы - это картинки).
- Сканированные страницы с картинками (нужен OCR).
- Таблицы, которые при извлечении превращаются в кашу.
- Метаданные и многоуровневая структура.
LocalAI, особенно в базовой конфигурации, ждет чистый промпт. Ему все равно, откуда текст - из файла, базы или сети. Ваша задача - достать этот текст из PDF и красиво подать.
Ошибка номер один: пытаться загрузить PDF напрямую в API LocalAI. Он его не поймет. Вы получите ошибку или, если очень "повезет", модель начнет интерпретировать бинарные данные как токены, породив бессмысленный вывод.
Решение: цепочка инструментов, а не волшебная кнопка
Нет единого плагина "PDF for LocalAI". Будем строить конвейер. Основная идея: PDF -> Текст (с помощью OCR если надо) -> Чанки -> Эмбеддинги -> LocalAI.
Для коротких документов можно обойтись просто извлечением текста и отправкой в модель. Для длинных - нужен RAG, чтобы не перегружать контекстное окно.
1 Ставим LocalAI и готовим рабочее место
Начнем с основ. У вас должен быть работающий LocalAI. Самый простой способ - через Docker.
docker run -p 8080:8080 localai/localai:latest-cpu
Это запустит сервер на localhost:8080. Проверьте, что он отвечает:
curl http://localhost:8080/v1/models
Вы увидите список моделей (пока пустой). Теперь нужно скачать саму языковую модель и, что критично, модель для эмбеддингов. Без эмбеддингов RAG не построить.
# Создаем директории для моделей
mkdir -p ./models
# Скачиваем, например, модель для эмбеддингов (all-MiniLM-L6-v2 часто используется)
# и языковую модель (например, Mistral-7B)
# LocalAI использует файлы в формате .gguf. Модели можно найти на Hugging Face.
# Для простоты используем встроенный скрипт загрузки:
# docker exec -it /bin/bash -c "local-ai download --backend bert-embeddings all-MiniLM-L6-v2"
# Но лучше подготовить модели заранее и смонтировать volume.
2 Выбираем и настраиваем бэкенд для обработки PDF
LocalAI поддерживает разные бэкенды. Нас интересует два типа: для эмбеддингов (например, bert-embeddings) и для генерации текста (например, llama.cpp). Для обработки PDF сам LocalAI не предоставляет бэкенд - это наша задача.
Создадим простой Python-скрипт, который будет разбирать PDF. Установим зависимости:
pip install pypdf2 pillow pytesseract pdf2image
Для OCR также нужен установленный Tesseract в системе:
# На Ubuntu/Debian
sudo apt install tesseract-ocr tesseract-ocr-rus # для русского языка
# На macOS
brew install tesseract
Не используйте PyPDF2 для сканированных PDF! Она вытащит только текст, который есть в слое текста (часто его нет). Вы получите пустую строку и будете думать, что все сломалось. Для сканов нужен OCR.
3 Пишем скрипт для извлечения текста из PDF
Вот базовый скрипт, который определяет тип PDF и применяет нужный метод.
import sys
from pathlib import Path
from PyPDF2 import PdfReader
import pytesseract
from pdf2image import convert_from_path
def extract_text_from_pdf(pdf_path):
"""Извлекает текст из PDF, используя OCR если нужно."""
text = ""
# Пробуем извлечь текст напрямую
reader = PdfReader(pdf_path)
for page in reader.pages:
page_text = page.extract_text()
if page_text.strip():
text += page_text + "\n"
# Если текста мало или нет, пробуем OCR
if len(text.strip()) < 100: # эвристика: если меньше 100 символов
print("Мало текста, применяем OCR...")
images = convert_from_path(pdf_path)
ocr_text = ""
for image in images:
ocr_text += pytesseract.image_to_string(image, lang='rus+eng') + "\n"
text = ocr_text
return text
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Укажите путь к PDF файлу")
sys.exit(1)
pdf_path = sys.argv[1]
result = extract_text_from_pdf(pdf_path)
print(result[:1000]) # покажем первые 1000 символов
Сохраните как pdf_extractor.py и запустите: python pdf_extractor.py ваш_файл.pdf.
4 Делим текст на чанки и создаем эмбеддинги
Длинный текст нельзя целиком пихать в модель. Делим на куски (чанки) по 500-1000 символов с перекрытием.
def split_text(text, chunk_size=500, overlap=50):
"""Делит текст на чанки с перекрытием."""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap
return chunks
# После получения чанков, создаем эмбеддинги через LocalAI API
import requests
def get_embedding(text, model="all-MiniLM-L6-v2"):
"""Получает эмбеддинг текста от LocalAI."""
response = requests.post(
"http://localhost:8080/v1/embeddings",
json={
"model": model,
"input": text
}
)
if response.status_code == 200:
return response.json()["data"][0]["embedding"]
else:
raise Exception(f"Ошибка эмбеддинга: {response.text}")
Теперь у вас есть векторные представления каждого чанка. Их нужно где-то хранить. Для простоты используйте локальную векторную базу, например, chromadb.
pip install chromadb
import chromadb
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.create_collection(name="pdf_docs")
# Добавляем чанки с эмбеддингами
for i, chunk in enumerate(chunks):
embedding = get_embedding(chunk)
collection.add(
embeddings=[embedding],
documents=[chunk],
ids=[str(i)]
)
5 Задаем вопросы через RAG
Теперь, когда у нас есть база знаний, можно задавать вопросы. Принцип: находим чанки, наиболее похожие на вопрос, и отправляем их вместе с вопросом в языковую модель.
def ask_question(question, top_k=3):
"""Ищет ответ на вопрос в PDF через RAG."""
# Получаем эмбеддинг вопроса
question_embedding = get_embedding(question)
# Ищем похожие чанки
results = collection.query(
query_embeddings=[question_embedding],
n_results=top_k
)
# Собираем контекст из найденных чанков
context = "\n\n".join(results["documents"][0])
# Формируем промпт для модели
prompt = f"""Используй только следующий контекст для ответа на вопрос.
Если в контексте нет ответа, скажи \"Я не нашел ответа в документе\".
Контекст:
{context}
Вопрос: {question}
Ответ:"""
# Отправляем в LocalAI для генерации
response = requests.post(
"http://localhost:8080/v1/chat/completions",
json={
"model": "mistral-7b", # ваша модель
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1
}
)
if response.status_code == 200:
return response.json()["choices"][0]["message"]["content"]
else:
return f"Ошибка: {response.text}"
# Пример использования
answer = ask_question("Какие основные выводы в отчете?")
print(answer)
Где спрятаны грабли: нюансы и подводные камни
Теперь о том, что может пойти не так. (Спойлер: многое).
| Проблема | Причина | Решение |
|---|---|---|
| OCR работает медленно и съедает всю память | Обработка изображений высокого разрешения, Tesseract не оптимизирован для больших объемов | Уменьшайте DPI при конвертации PDF в изображения. Используйте pdf2image с параметром dpi=150. Для пакетной обработки рассмотрите более быстрые движки, например, OCRmyPDF. |
| Эмбеддинги для русского языка получаются плохие | Модель all-MiniLM-L6-v2 обучена в основном на английском | Используйте мультиязычные модели, например, paraphrase-multilingual-MiniLM-L12-v2. Или русскоязычные, как от Cointegrated. |
| LocalAI падает при запросе эмбеддингов для многих чанков | Не хватает оперативной памяти или модель для эмбеддингов не загружена | Убедитесь, что модель эмбеддингов скачана и указана в конфиге LocalAI. Ограничьте параллельные запросы. |
| Таблицы и формулы извлекаются как бессвязный текст | Обычные инструменты не понимают структуру таблиц | Используйте специализированные библиотеки: Camelot (для текстовых таблиц) или Tabula (для PDF с табличными слоями). Или смиритесь с тем, что ИИ будет видеть таблицу как текст. |
Частые вопросы, которые вы зададите себе в 3 часа ночи
Можно ли обойтись без RAG для коротких документов?
Да. Если PDF на 2-3 страницы и текст извлекается чисто, можно просто скормить весь текст в модель, уложившись в контекстное окно. Но помните про ограничения токенов. Для Mistral 7B это обычно 4096 токенов, что примерно 3000 слов на английском. На русском меньше.
Как ускорить обработку множества PDF?
Параллелизация. Обрабатывайте несколько файлов одновременно, но следите за памятью. Используйте очередь задач (Celery, RQ) или просто multiprocessing.Pool. Самое узкое место - OCR. Если документы в основном текстовые, скорость возрастет в разы.
Что делать, если качество ответов низкое?
Скорее всего, проблема в чанках. Если они разрывают предложения или смысловые блоки, контекст будет искажен. Попробуйте чанкировать по абзацам или использовать более умное разделение, например, с помощью библиотеки langchain.text_splitter (но это добавит зависимостей). Также проверьте, что модель для эмбеддингов адекватно представляет семантику вашего текста.
LocalAI не видит мою модель для эмбеддингов. В чем дело?
Проверьте конфигурационный YAML файл LocalAI. Модели должны быть объявлены там с указанием бэкенда. И да, модели эмбеддингов и языковые модели - это разные файлы. Убедитесь, что вы скачали правильный файл .gguf для эмбеддингов и положили его в папку models.
Что дальше? Выходим за рамки базового RAG
Базовая цепочка работает. Но если вам нужно что-то серьезнее, смотрите в сторону агентного RAG, где система сама решает, как искать и обновлять информацию. Или используйте гибридный поиск, комбинируя эмбеддинги с ключевыми словами для точности.
Самый неочевидный совет: иногда проще конвертировать PDF в Markdown с помощью инструментов вроде pandoc или pdf2md, а уже потом кормить в LocalAI. Структура Markdown сохраняет заголовки и списки, что помогает модели лучше понять документ. Попробуйте - это может сэкономить часы настройки сложных пайплайнов.
И последнее: не ждите от локального стека скорости и качества Claude или Gemini. Вы платите за приватность и контроль - иногда буквально временем и нервами. Но когда все настроено, это работает автономно, без интернета и слежки. И это того стоит.