AI · 7 мин чтения

RAG чат-бот по базе знаний: разработка, архитектура и код

RAG чат-бот по базе знаний ищет ответ в ваших документах и только потом формулирует его человеческим языком - этим он и отличается от «голого» ChatGPT, который уверенно врёт про то, чего не знает. RAG чат бот разработка сводится к сборке пайплайна из пяти-шести компонентов, и ниже я разложу архитектуру по частям и покажу рабочий код на Python, который сам ставил в прод для базы знаний магазина на WooCommerce и для поддержки на aiogram.

Как работает RAG: retrieval-augmented generation по-человечески

У большой языковой модели два слабых места под бизнес-задачи: она не знает ваших внутренних данных и охотно придумывает факты, когда данных не хватает. RAG (retrieval-augmented generation) закрывает оба. Схема из двух шагов. Сначала retrieval - по вопросу пользователя система находит в базе знаний несколько релевантных кусков текста. Потом generation - эти куски подкладываются модели в промпт как контекст, и она отвечает, опираясь на факты, а не на «память». Модель перестаёт галлюцинировать, потому что ей буквально дали текст, из которого нужно взять ответ.

Меня часто спрашивают, почему не дообучить модель (fine-tuning) на своих документах. Дообучение дорогое, требует переобучения при каждом изменении базы и всё равно не гарантирует точных цитат. RAG обновляется добавлением одного документа в индекс за секунды. Для регламентов поддержки, FAQ, каталога товаров это единственный вменяемый вариант.

Подход Что под капотом Когда беру
Голая LLM Отвечает по общим знаниям из обучения Когда своих данных нет и точность не критична
Fine-tuning Модель переобучают на ваших примерах Свой стиль/формат ответов, редко меняющиеся данные
RAG Поиск по базе + генерация с контекстом Живая база знаний, нужны точные ответы по документам

Архитектура RAG-бота: компоненты пайплайна

Любой умный ассистент по документам собирается из одних и тех же кубиков. Порядок такой:

  1. Загрузчик - вытягивает исходники: PDF, статьи Notion, выгрузку из CRM, страницы сайта.
  2. Чанкер - режет длинные документы на куски по 500-800 символов, чтобы поиск был точечным.
  3. Эмбеддер - превращает каждый чанк в вектор (набор чисел), отражающий смысл текста.
  4. Векторное хранилище - база, где эти векторы лежат и по ним быстро ищут.
  5. Ретривер - по вопросу достаёт топ‑K самых близких чанков.
  6. LLM + оркестратор - Claude или другая модель формулирует ответ, а обвязка соединяет всё с интерфейсом (Telegram, виджет на сайте).

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

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

База знаний: чанкинг и эмбеддинги

Качество ответов на 70% зависит от того, как порезана база. Слишком крупные чанки тащат в контекст лишний шум, слишком мелкие теряют смысл. На практике держусь диапазона 500-800 символов с перекрытием в 100-150, чтобы мысль не обрывалась на границе куска.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,      # символов на чанк
    chunk_overlap=120,   # перекрытие, чтобы не резать мысль
)
chunks = splitter.split_text(document_text)

Дальше - эмбеддинги. У Anthropic своей модели эмбеддингов нет, в связке с Claude обычно беру Voyage AI - она заточена под ретрив и даёт заметно более релевантную выдачу, чем базовые варианты. Складываю векторы в Chroma: для старта её хватает, разворачивается локально одной строкой.

import voyageai, chromadb

vo = voyageai.Client()
vectors = vo.embed(chunks, model='voyage-3',
                   input_type='document').embeddings

client = chromadb.PersistentClient(path='./kb')
col = client.get_or_create_collection('docs')
col.add(
    ids=[f'chunk-{i}' for i in range(len(chunks))],
    embeddings=vectors,
    documents=chunks,
)

Загрузчики под PDF, выгрузку из CRM и парсинг страниц я не пишу каждый раз с нуля - часть готовых парсеров держу у себя в библиотеке готовых скриптов и оттуда беру основу, допиливая под формат конкретного клиента.

Векторный поиск: где хранить эмбеддинги

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

Хранилище Плюс Когда беру
Chroma Локально, ноль инфраструктуры Прототип, база до ~50 тыс. чанков
pgvector Живёт внутри вашего PostgreSQL Уже есть Postgres, не хочется новый сервис
Qdrant Быстрый, фильтры по метаданным Прод, большая база, гибридный поиск

Сам поиск - несколько строк:

def retrieve(query, k=5):
    q = vo.embed([query], model='voyage-3',
                 input_type='query').embeddings[0]
    res = col.query(query_embeddings=[q], n_results=k)
    return res['documents'][0]

Важный момент: для документов и для запроса Voyage использует разный input_type (document и query). Пропустишь это - релевантность просядет, а причину будешь искать полдня. Проверено на своей шкуре.

Генерация ответа: промпт и Claude API

На этом шаге собираем найденные чанки в контекст и отдаём модели вместе с вопросом. Главная защита от галлюцинаций живёт в системном промпте: прямо запрещаю выдумывать и требую отвечать только по переданному тексту. Модель - claude-sonnet-5, температуру для поддержки держу низкой, чтобы ответы были предсказуемыми.

from anthropic import Anthropic

client = Anthropic()
SYSTEM = (
    'Ты — ассистент поддержки. Отвечай строго по контексту ниже. '
    'Если ответа в контексте нет — скажи об этом и не выдумывай.'
)

def rag_answer(query):
    context = 'nn'.join(retrieve(query))
    msg = client.messages.create(
        model='claude-sonnet-5',
        max_tokens=1024,
        system=SYSTEM,
        messages=[{
            'role': 'user',
            'content': f'Контекст:n{context}nnВопрос: {query}',
        }],
    )
    return msg.content[0].text

Если база большая и одни и те же документы летят в контекст постоянно, включаю prompt caching - Anthropic кеширует статичную часть промпта, и на объёмных инструкциях это срезает и деньги, и задержку. Ещё полезно возвращать пользователю ссылку на источник: рядом с чанком храню метаданные (URL, название раздела) и подставляю их в ответ, чтобы человек мог перепроверить.

Интерфейс: Telegram-бот и виджет на сайт

Рантайм готов - осталась обёртка. Чаще всего это Telegram: aiogram, вебхук или long polling, функция rag_answer внутри хендлера.

import asyncio
from aiogram import Bot, Dispatcher
from aiogram.types import Message

dp = Dispatcher()

@dp.message()
async def handle(message: Message):
    await message.answer(rag_answer(message.text))

async def main():
    bot = Bot(token='YOUR_TOKEN')
    await dp.start_polling(bot)

asyncio.run(main())

Для сайта делаю то же самое, но обёрнутое в HTTP-эндпоинт на FastAPI и виджет-скрипт. На Tilda подключаю его через блок T123: кнопка «Спросить бота», окошко чата, запросы уходят на мой бэкенд. Если не хочется писать бэкенд вообще, простой сценарий можно собрать в n8n - там есть готовые ноды под векторные хранилища и LLM, но для нагруженного прода я всё же держу код у себя под контролем.

Сроки, бюджет и грабли

По срокам: рабочий MVP на одной базе знаний с Telegram-интерфейсом собираю за 1-2 недели, полноценного бота с виджетом на сайт, метаданными и админкой для обновления базы - за 3-4 недели. Чат-бот с базой знаний на RAG у меня стоит от 50 000 ₽, точная цифра зависит от объёма документов и числа каналов. Для сравнения: в студиях на рынке за похожую задачу называют суммы заметно шире, от 150 000 ₽ и выше, - это рыночный ориентир, а не мой прайс.

Типичные грабли, на которые натыкаются при самостоятельной сборке:

  • Плохой чанкинг. Режут по фиксированному числу символов без перекрытия - ответы рвутся на полуслове.
  • Один input_type на всё. Документы и запросы эмбедятся одинаково, релевантность падает.
  • Нет фолбэка. Когда в базе ответа нет, бот всё равно что-то придумывает - лечится жёстким системным промптом.
  • Забыли про обновление базы. Индексацию никто не автоматизировал, и через месяц бот отвечает по устаревшим ценам.

Последний пункт вижу чаще всего. База знаний живая, поэтому переиндексацию сразу вешаю на расписание - по вебхуку из CRM или ночным крон-джобом, чтобы бот не отставал от реальности.

Чат-бот с базой знаний

AI / RAG

от 50 000 ₽

Подробнее →

Частые вопросы

Сколько документов потянет RAG-бот?

Практически без ограничений - вопрос лишь в выборе хранилища. Chroma спокойно держит десятки тысяч чанков, для сотен тысяч и миллионов беру Qdrant с фильтрами по метаданным. В контекст модели при этом всё равно уходит только топ‑5-10 найденных кусков, так что размер базы на скорость ответа почти не влияет.

Можно ли обойтись без OpenAI и Claude, на локальной модели?

Можно: связка локальной LLM (например, из семейства open-weight) и локального эмбеддера работает и держит данные внутри контура. Но качество ответов и понимание русского у облачных моделей вроде Claude пока ощутимо выше, а инфраструктура под локальный инференс стоит своих денег. Для чувствительных данных локальный вариант оправдан, для большинства задач - облако.

Как бот отвечает, если в базе нет ответа?

Это закладывается в системный промпт: модели прямо предписано сказать, что информации нет, и не выдумывать. Дополнительно можно проверять релевантность найденных чанков по порогу близости - если ближайший кусок слишком далёк от вопроса, бот честно отвечает, что не знает, и предлагает связаться с оператором.

Чем RAG лучше обычного бота на кнопках или интентах?

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

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

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно