Self-hosted RAG для запитань до документів: LangChain, pgvector і змінні LLM
Як open-source RAG-застосунок перетворює PDF і DOCX на відповіді: розбиття на чанки, pgvector, перемикання між OpenAI і Claude одним налаштуванням та уроки розробки з невеликим бюджетом.
Документи рідко самі відповідають на запитання про себе. Регламенти, звіти й договори накопичуються у вигляді PDF, файлів Word і сканів, і майже ні в кого немає часу читати їх від початку до кінця. Система retrieval-augmented generation (RAG), яка відповідає на запитання щодо завантажених файлів, — практичний спосіб це виправити, а open-source проєкт AI-DocumentIntelligence добре показує, як зробити таку систему простою й вартою довіри.
Яку прогалину це закриває
Та сама ситуація виникає в будь-якій організації, де багато паперів. Соцпрацівнику потрібен один рядок із 40-сторінкового регламенту. Аналітику потрібна одна цифра з 22-ї сторінки звіту. Файл формально доступний, але знайти в ньому щось можна цілу вічність.
Зазвичай у команд лишається два варіанти. Перший — закритий продукт для роботи з документами на основі ШІ з непрозорими цінами й розмитими правилами щодо того, що відбувається з даними. Другий — і далі шукати вручну.
Відкрита альтернатива, яку можна розгорнути в себе, дає третій шлях. Мета не в тому, щоб зробити ще одну тонку обгортку над чиїмось API. Потрібне щось достатньо просте, щоб команда, яка працює з конфіденційними документами, могла це вивчити, замінити окремі частини й довіряти результату.
Як це працює: завантажити, розбити, векторизувати, запитати
Застосунок виконує чотири завдання:
- Приймає файли PDF, DOCX або TXT.
- Розбиває кожен файл на невеликі змістовні чанки.
- Перетворює чанки на ембедінги й зберігає їх у PostgreSQL через розширення
pgvector. - Відповідає на запитання до документа, поставлені звичайною мовою, за допомогою OpenAI або Anthropic Claude, а перемикання між ними робиться одним налаштуванням.
Останній пункт важливіший, ніж здається. Багато навчальних RAG-проєктів прив'язують вас до одного постачальника моделей. У реальних проєктах провайдера часто доводиться змінювати через вартість, вимоги регуляторів чи доступність, і ніхто не хоче переписувати застосунок, коли це стається. Тут LLM_PROVIDER — звичайне значення конфігурації, а не щось закопане глибоко в коді.
Стек
Список технологій навмисно короткий:
- Фронтенд: React 18 із TypeScript
- Бекенд: Node.js і Express на TypeScript
- Оркестрація: LangChain
- Векторне сховище: PostgreSQL з pgvector
- Моделі: OpenAI або Anthropic Claude на вибір
- Інструменти: Docker, ts-node і dotenv
Ніщо тут не вимагає від команди вчитися новому способу випуску софту. Будь-хто, хто вже вміє запускати сервіс на Node.js поруч із базою PostgreSQL, запустить і це.
Архітектура
Загалом схема така: інтерфейс на React спілкується з API на Express, який передає завантаження обробнику документів:
┌────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ React UI │ ───► │ Express API │ ───► │ Document Processor │
│ (upload+chat)│ │ (TypeScript) │ │ (chunking / split) │
└────────────┘ └──────────────────┘ └────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌────────────────────┐
│ LangChain Layer │ ◄──► │ PostgreSQL + pgvector│
│ (OpenAI / Claude) │ │ (embeddings store) │
└──────────────────┘ └────────────────────┘
Розбиття й векторизація відбуваються один раз, під час завантаження. Після цього кожне запитання йде тим самим коротким маршрутом. Запитання перетворюється на ембедінг, pgvector повертає найближчі чанки, ці чанки разом із запитанням ідуть у вибрану модель, а відповідь повертається разом з історією чату щодо цього документа.
У такій архітектурі немає нічого екзотичного, і в цьому весь сенс. Цінність у тому, щоб правильно зробити прості речі: акуратно вибирати провайдера, розумно ділити текст і коректно обробляти ситуацію, коли ключа чи налаштування бракує.
Код, який має значення
Основні ідеї видно лише в кількох фрагментах.
Перемикання провайдерів зводиться до читання змінної оточення:
// backend/src/llm/provider.ts (simplified)
const provider = process.env.LLM_PROVIDER; // "openai" | "anthropic"
Далі невелика фабрична функція повертає потрібну чат-модель залежно від цього значення:
export function getChatModel() {
if (provider === "anthropic") {
return new ChatAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
model: process.env.ANTHROPIC_MODEL,
});
}
return new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
Розбиття на чанки спирається на рекурсивний сплітер LangChain, налаштований радше під запитання й відповіді, ніж під просте зберігання:
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 150,
});
const chunks = await splitter.splitDocuments(rawDocs);
Перекриттю варто приділяти більше уваги, ніж зазвичай. Якщо воно замале, відповідь може розрізатися навпіл між двома чанками. Якщо завелике, ви платите за зберігання й пошук того самого речення тричі. Перекриття в 150 символів при чанках по 1000 символів дало найкращі результати на змішаному наборі тестових файлів PDF і DOCX.
Пошук і генерація теж розділені. Кожен етап можна тестувати окремо, тож можна переконатися, що пошук знаходить потрібні чанки, не витрачаючи кредити API на генерацію. На безкоштовному акаунті OpenAI така економія дуже помітна.
Що спрацювало, а що далося важко
Рішення, які окупилися
- Змінний провайдер від першого дня. Більшу частину логіки пошуку написали й протестували на безкоштовних локальних ембедінгах ще до того, як справа дійшла до платного API, тож витрати під час розробки залишалися майже нульовими.
- Відокремлення розбиття й векторизації від відповідей на запитання. Коли відповідь виглядає неправильною, можна швидко зрозуміти, винен пошук чи генерація.
- Docker Compose для всього стеку (бекенд, фронтенд і Postgres). Будь-хто може клонувати репозиторій і запустити його за кілька хвилин, а не воювати із залежностями пів дня.
Що виявилося складнішим, ніж очікувалося
- Робота без платних кредитів API. Відсутність доступу до платного акаунта OpenAI чи Claude — реальне обмеження. Обхідний шлях: спершу перевірити пошук на безкоштовних локальних моделях ембедінгів, а платний крок генерації додати пізніше. Це слушна порада для будь-якого RAG-проєкту з обмеженим бюджетом.
- Вибір розміру чанка. Нудна й водночас критично важлива річ. Жодне налаштування промптів не врятує погане розбиття, а його виправлення дало найбільше покращення за весь проєкт.
- Запуск на чужій машині. Ноутбук, де все працює, і чистий
docker compose upна комп'ютері незнайомої людини — різні речі. Зрозумілий.env.example, явні налаштування імені бази даних і описане розв'язання конфліктів портів забрали більше часу, ніж сама логіка RAG, але саме від них залежить, чи зможе хтось іще користуватися проєктом.
Як запустити самому
Репозиторій відкритий і запускається локально кількома командами:
git clone https://github.com/Srameshgitnow/AI-DocumentIntelligence.git
cd AI-DocumentIntelligence
docker compose up --build
Змінні оточення, повна інструкція з налаштування й нотатки щодо розв'язання проблем є в README проєкту. Повідомлення про помилки, ідеї щодо підходу до перемикання провайдерів і пул-реквести — усе це можна залишати в трекері задач.
Ширша ідея таких проєктів — застосовувати оркестрацію LLM з LangChain або LangGraph до реальних робочих задач, як-от запитання й відповіді щодо регламентів для держпослуг, агенти для код-рев'ю чи перетворення голосових нотаток на користувацькі історії для Agile, і обходитися без відшліфованих демо, які ніколи не стикаються зі справжніми документами. Просте, змінне й легке для перевірки зазвичай виграє в хитромудрого.