Ваша любимая LLM — гениальный дилетант. Она перечитала пол-интернета, но застыла во времени. Спросите у llama3.1 про pandas 2.2 — она с умным видом выдаст API двухлетней давности, а если надавить — начнет галлюцинировать несуществующими параметрами. Знакомо? Я тоже через это прошел.
Проблема в том, что веса LLM фиксированы после обучения. Новые фичи fastapi, httpx или sqlalchemy 2.0 в них не просочились. И переучивать модель каждый раз — разорительно (и бессмысленно). Но есть лазейка: RAG (Retrieval-Augmented Generation). Вместо того чтобы нашпиговывать знаниями саму модель, мы подкладываем ей шпаргалку из актуальной документации прямо в момент вопроса.
Итог: вы получаете локального ассистента, который точно знает, как выглядит сигнатура pd.DataFrame.map в 2026 году, и не выдумывает чушь. Вся магия — в 50 строках Python, Ollama и одной векторной базе.
Почему просто скормить документацию в промпт — плохая идея
Технически вы можете запихнуть всю доку requests в промпт. Но у локальных LLM контекстное окно обычно 8-32K токенов, а документация — сотни тысяч. Результат: либо обрежете, либо модель забудет начало. RAG решает это элегантно — держим в памяти только то, что нужно в текущем диалоге, а база знаний лежит рядом в индексе.
Если вы ещё не знакомы с основами RAG, советую сначала пробежаться по полному руководству по RAG — там разобрана архитектура и типовые грабли. А для быстрого старта есть гайд по RAG за 15 минут.
Инструменты на май 2026: что ставим
| Компонент | Что используем | Почему он |
|---|---|---|
| Локальная LLM | deepseek-coder-33b (через Ollama) |
Отличное знание Python, 33B параметров — золотая середина для одной RTX 4090 |
| Модель эмбеддингов | nomic-embed-text:v1.5 (Ollama) |
768-мерные векторы, хорошо понимает код и текст |
| Векторная БД | ChromaDB 0.6.3 | Встраивается в Python-скрипт, не требует отдельного сервера |
| Чанкер | RecursiveCharacterTextSplitter из langchain-text-splitters 0.4 |
Умно режет по естественным границам (заголовки, пустые строки) |
Предупреждение: Если у вас нет мощной видеокарты, можно использовать меньшие модели (например, codestral-22b через API или арендовать GPU на Vast.ai). Но RAG-пайплайн сам по себе легкий — эмбеддинги считаются на CPU или слабой GPU.
Как НЕ надо делать: лобовой напильник
Допустим, вы скачали всю документацию pandas в один гигантский Markdown-файл и скормили его в промпт. Что будет? Модель либо ничего не ответит из-за переполнения контекста, либо начнёт выдумывать. RAG потому и Retrieval — мы НЕ даём всё, мы достаём только то, что релевантно вопросу.
Ещё одно распространённое заблуждение: «обучу LoRA на документации». Не надо. LoRA — для смены стиля или добавления новых фактов в веса. RAG дешевле, быстрее и не требует пересборки модели. Только когда вы упрётесь в скорость поиска и не сможете ужать знания в разумные чанки — задумайтесь о fine-tune. А это бывает редко.
Пошаговый план: от документа до работающего чата
1 Собираем документацию
Нам нужны тексты — желательно в Markdown. Большинство проектов на ReadTheDocs отдают HTML. Используем html2text, чтобы конвертировать. Для примера возьмём httpx 0.28 — новейшую версию на май 2026.
import requests
from html2text import HTML2Text
# Скачиваем страницу документации
url = "https://www.python-httpx.org/advanced/"
resp = requests.get(url)
resp.encoding = 'utf-8'
# Конвертируем в Markdown
converter = HTML2Text()
converter.ignore_links = False
markdown = converter.handle(resp.text)
# Сохраняем
with open("httpx_advanced.md", "w", encoding="utf-8") as f:
f.write(markdown)
Повторите для всех нужных разделов. Или, если документация лежит в репозитории как .rst/.md, клонируйте и используйте прямо их — это даже лучше.
2 Режем на чанки (с умом)
Просто резать по 1000 символов — варварство. Вы порубите пополам описание функции или пример кода. Используем рекурсивный сплиттер с пониманием структуры: он делит по двум переносам строки, потом по заголовкам, потом по предложениям.
from langchain_text_splitters import RecursiveCharacterTextSplitter
with open("httpx_advanced.md", "r", encoding="utf-8") as f:
text = f.read()
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""],
)
chunks = splitter.split_text(text)
print(f"Получилось {len(chunks)} чанков")
nomic-embed-text хорошо работают с такими длинами. Если документы содержат много кода, увеличьте до 1500.3 Создаём эмбеддинги и загружаем в ChromaDB
Теперь превращаем чанки в векторы. Ollama поддерживает эмбеддинги из коробки — достаточно указать модель. ChromaDB хранит всё в локальной папке, можно переиспользовать между сессиями.
import chromadb
from ollama import Embed
# Инициализируем Chroma (persistent — сохраняет на диск)
client = chromadb.PersistentClient(path="./chroma_httpx")
collection = client.create_collection(
name="httpx_advanced",
metadata={"hnsw:space": "cosine"} # косинусная близость
)
# Эмбеддинги через Ollama
embeddings = []
for chunk in chunks:
response = Embed(model="nomic-embed-text:v1.5", input=chunk)
embeddings.append(response["embedding"])
# Загружаем в коллекцию
collection.add(
ids=[f"chunk_{i}" for i in range(len(chunks))],
embeddings=embeddings,
documents=chunks,
)
print("Индекс создан")
4 Поиск и формирование промпта
Прилетает вопрос пользователя. Мы ищем в индексе 3-5 самых похожих чанков, склеиваем их в контекст и передаём LLM. Вот функция поиска:
def search_and_answer(query: str, n_results: int = 3):
# Эмбеддинг запроса
q_emb = Embed(model="nomic-embed-text:v1.5", input=query)["embedding"]
# Поиск в Chroma
results = collection.query(
query_embeddings=[q_emb],
n_results=n_results
)
retrieved_docs = results["documents"][0]
context = "\n\n---\n\n".join(retrieved_docs)
# Составляем промпт
prompt = f"""Ты — ассистент по Python библиотеке httpx.
Используй только информацию из контекста ниже.
Если ответа нет, скажи, что не знаешь.
Контекст:
{context}
Вопрос: {query}
Ответ:"""
# Отправляем в LLM (Ollama)
response = ollama.chat(
model="deepseek-coder:33b",
messages=[{"role": "user", "content": prompt}]
)
return response["message"]["content"]
# Пример
print(search_and_answer("Как настроить таймауты для всего клиента?"))
5 Обёртка для диалога
Добавим простой цикл ввода-вывода, чтобы не ходить в терминал снова.
while True:
q = input("\nВаш вопрос (или 'exit'): ")
if q.lower() == 'exit':
break
ans = search_and_answer(q)
print(f"\nОтвет:\n{ans}\n")
Нюансы, которые сожгут вам час (я уже сжёг)
- Версионирование документации: если вы проиндексировали
requests 2.31, а спрашиваете про 2.32 — модель выдаст по сути то же самое, но новая сигнатура не попадёт. Решение: в метаданных чанка хранить версию и при поиске фильтровать по версии. - Контекстное окно: 3-5 чанков по 1000 токенов = 3000-5000 токенов. Плюс диалог — может выйти за 8K. Если модель часто «забывает» чат, либо используйте модель с большим окном (32K+), либо сокращайте количество чанков.
- Качество эмбеддингов:
nomic-embed-textхорош, но для кода лучшеbge-m3(он поддерживает многократность языков). Поменять модель легко — просто пересчитайте эмбеддинги и пересоздайте коллекцию. - Фильтрация мусора: в документации бывают навигационные меню, колонтитулы. Их надо вырезать. Или используйте уже очищенные дампы с ReadTheDocs JSON API.
- Параметр «temperature»: для кода лучше ставить 0.1-0.3, чтобы модель не импровизировала с API.
Когда RAG паллиатив, а когда — серебряная пуля
RAG идеален для конкретных фактов: «какой параметр у pd.read_csv для обработки пропусков?». Если вы хотите, чтобы модель сама разбиралась в концептуальных вопросах (например, «спроектируй систему очередей») — RAG просто даст больше релевантных примеров, но не заменит архитектурное мышление. И это нормально.
Для полного погружения в локальные альтернативы взгляните на статью про NotebookLM на минималках — там похожий принцип, но для документов. А если захотите ускорить поиск по миллионам страниц, добро пожаловать в мир LLMSearchIndex.
Главный совет напоследок: не пытайтесь впихнуть невпихуемое. RAG не заменит память модели — он даёт ей шпаргалку. Делайте чанки осмысленными, фильтруйте лишнее, и ваш локальный ассистент станет лучшим другом, а не диванным экспертом.