Зачем вообще это нужно? Проблема студента 2026 года
Сидишь, готовишься к экзамену. Нужно быстро найти определение в учебнике на 500 страниц. Или понять сложную формулу из лекции, которую преподаватель объяснил криво. ChatGPT платный, у бесплатного ограничения, а главное – все твои вопросы улетают в облако. Конфиденциальность? Забудь. Интернет отвалился? Ты отвалился.
Решение – коробка под столом (или просто твой ноутбук), которая знает ВСЕ твои учебники, конспекты и статьи. Отвечает мгновенно, без интернета, и никогда не скажет декану, какие глупые вопросы ты задавал в 3 часа ночи. Собираем такого монстра.
Важно: На дворе 05.03.2026. Qwen2.5 – не вчерашняя новость, а текущий стандарт для локальных моделей с отличным балансом качества и размера. Используем самые свежие инструменты.
Архитектура: что внутри чёрного ящика
Система простая, но эффективная. Не Agentic RAG, о котором я писал в другом гайде, а точный инструмент для вопрос-ответа.
- Документы: PDF, EPUB, TXT – всё, что ты сбросил в папку
data/. - Индексатор: Разбивает текст на чанки, создаёт эмбеддинги (векторные представления).
- ChromaDB: Векторная база данных. Хранит чанки и их эмбеддинги. Ищет похожие.
- Qwen2.5 (GGUF): Мозги системы. Запускается через llama.cpp. Принимает вопрос и найденные чанки, генерирует ответ.
- Gradio: Веб-интерфейс. Просто открываешь в браузере и общаешься.
1 Готовим среду: не пропусти этот шаг
Создаём чистую песочницу. Иначе через месяц не поймёшь, почему всё сломалось.
mkdir study_assistant && cd study_assistant
python -m venv venv
# Активация для Linux/macOS
source venv/bin/activate
# Для Windows
# venv\Scripts\activate
2 Ставим зависимости: не просто pip install
Тут есть подводные камни. Llama-cpp-python нужно собирать под твою систему, особенно если хочешь использовать GPU (CUDA).
# Базовые для работы с документами
pip install chromadb langchain langchain-community pypdf2 python-docx
# Для интерфейса
pip install gradio
# Критически важная зависимость - собираем llama-cpp-python с поддержкой GPU
# Для CUDA (если есть NVIDIA карта)
CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python --force-reinstall --upgrade
# Для чистого CPU (работает, но медленнее)
# pip install llama-cpp-python
Если сборка с CUDA падает с ошибкой – забей. Запусти на CPU. Да, будет не так шустро, но для учёбы сойдёт. Главное – начать.
3 Качаем модель: берём правильную версию
Не качай первое попавшееся. Нам нужен GGUF формат (оптимизированный для llama.cpp) и желательно версия с квантованием Q4_K_M – лучший баланс качества и размера. На 05.03.2026 актуальна модель Qwen2.5-7B-Instruct.
Иди на Hugging Face, например, к TheBloke: https://huggingface.co/TheBloke/Qwen2.5-7B-Instruct-GGUF. Скачивай файл qwen2.5-7b-instruct-q4_k_m.gguf. Положи в папку models/.
mkdir models
cd models
wget https://huggingface.co/TheBloke/Qwen2.5-7B-Instruct-GGUF/resolve/main/qwen2.5-7b-instruct-q4_k_m.gguf
# Или качай вручную через браузер
Если 7B велика, есть Qwen2.5-3B или даже 1.5B. Они слабее, но намного шустрее.
4 Пишем индексатор: не просто split_text
Самый важный код во всей системе. Плохо разобьёшь текст – модель будет получать мусор и генерировать ерунду. Не используй наивное разбиение по символам.
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
def load_documents(data_dir="data"):
documents = []
for filename in os.listdir(data_dir):
filepath = os.path.join(data_dir, filename)
try:
if filename.endswith('.pdf'):
loader = PyPDFLoader(filepath)
elif filename.endswith('.txt'):
loader = TextLoader(filepath, encoding='utf-8')
elif filename.endswith('.docx'):
loader = Docx2txtLoader(filepath)
else:
continue
loaded_docs = loader.load()
documents.extend(loaded_docs)
print(f"Загружен: {filename}")
except Exception as e:
print(f"Ошибка загрузки {filename}: {e}")
return documents
def chunk_documents(documents, chunk_size=512, chunk_overlap=50):
# Перекрытие (overlap) критически важно для сохранения контекста
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"Создано {len(chunks)} чанков.")
return chunks
Ключевой параметр – chunk_size. Для Qwen2.5 и учебных текстов 512-1024 токена – нормально. Overlap в 50-100 токенов не даст потерять мысль на границе чанков.
5 Запускаем ChromaDB: не усложняй
Локальная база в папке db/. Для эмбеддингов используем легковесную модель all-MiniLM-L6-v2 – она быстро работает и нормально отрабатывает для академических текстов.
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
def create_vector_store(chunks, persist_directory="db"):
# Модель для эмбеддингов - локальная и быстрая
embedding_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cpu'}, # Можешь попробовать 'cuda', если есть GPU
encode_kwargs={'normalize_embeddings': False}
)
# Создаём и сохраняем базу
vector_db = Chroma.from_documents(
documents=chunks,
embedding=embedding_model,
persist_directory=persist_directory
)
vector_db.persist()
print(f"Векторная база сохранена в {persist_directory}")
return vector_db
# Использование:
docs = load_documents()
chunks = chunk_documents(docs)
vector_db = create_vector_store(chunks)
6 Собираем конвейер RAG: где магия
Теперь свяжем поиск по базе и модель. Промпт – это святое. Если напишешь криво, Qwen2.5 начнёт выдумывать или цитировать не то.
from langchain.llms import LlamaCpp
from langchain.chains import RetrievalQA
from langchain.callbacks.manager import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
def load_llama_model(model_path, n_ctx=2048):
# n_ctx - размер контекста. 2048 хватит для вопроса + несколько чанков.
callback_manager = CallbackManager([StreamingStdOutCallbackHandler()])
llm = LlamaCpp(
model_path=model_path,
temperature=0.1, # Низкая температура для фактовых ответов
max_tokens=512, # Не даём болтать лишнего
top_p=0.9,
n_ctx=n_ctx,
callback_manager=callback_manager,
verbose=False,
n_gpu_layers=33 # Раскомментируй и укажи число слоёв для GPU, если используешь CUDA
# n_gpu_layers=-1 # Для загрузки всех слоёв на GPU
)
return llm
def create_rag_chain(vector_db, llm):
# Промпт-шаблон. Обрати внимание на строгое указание контекста.
QA_PROMPT = """Используй только следующий контекст для ответа на вопрос. Если ответа нет в контексте, скажи 'В предоставленных материалах этого нет.' Не выдумывай.
Контекст: {context}
Вопрос: {question}
Точный и краткий ответ:"""
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vector_db.as_retriever(search_kwargs={"k": 3}), # Берём 3 самых похожих чанка
chain_type_kwargs={"prompt": QA_PROMPT},
return_source_documents=True
)
return qa_chain
7 Лепим интерфейс на Gradio: просто и функционально
Делаем одну страницу с полем ввода, кнопкой и областью вывода. Добавим чекбокс, чтобы показывать источники (по каким чанкам был дан ответ).
import gradio as gr
def gradio_interface(rag_chain):
def respond(question, show_sources):
result = rag_chain({"query": question})
answer = result["result"]
sources = ""
if show_sources and "source_documents" in result:
sources = "\n\n--- Источники ---\n"
for i, doc in enumerate(result["source_documents"]):
sources += f"{i+1}. {doc.page_content[:200]}...\n"
return answer + sources
demo = gr.Interface(
fn=respond,
inputs=[
gr.Textbox(label="Задай вопрос по материалам", lines=3),
gr.Checkbox(label="Показать источники", value=False)
],
outputs=gr.Textbox(label="Ответ ассистента", lines=10),
title="Локальный ИИ-ассистент для учёбы",
description="Загрузи учебники в папку 'data' и задавай вопросы. Работает без интернета."
)
return demo
# Финальная сборка и запуск
if __name__ == "__main__":
# Загрузка модели (указывай свой путь)
llm = load_llama_model("models/qwen2.5-7b-instruct-q4_k_m.gguf")
# Загрузка существующей базы (если индексировал заранее)
embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vector_db = Chroma(persist_directory="db", embedding_function=embedding_model)
rag_chain = create_rag_chain(vector_db, llm)
demo = gradio_interface(rag_chain)
demo.launch(server_name="0.0.0.0", server_port=7860) # Доступно по http://localhost:7860
Где собака зарыта: нюансы, которые сломают твой проект
Теперь о том, что обычно умалчивают в туториалах.
- Формат PDF. Если PDF – это сканы страниц (картинки), PyPDFLoader вытащит только пустоту. Нужен OCR. Для начала используй текстовые PDF или конвертируй через Adobe или онлайн-сервисы.
- Размер контекста (n_ctx). У Qwen2.5 в GGUF он часто ограничен 4096 или 8192 токенов. Если поставишь больше, чем поддерживает модель – упадёшь с ошибкой. Проверяй спецификацию файла GGUF.
- Первый запуск модели. Он всегда долгий – модель загружается в память. Может занять минуту. Не пугайся.
- Температура (temperature). Для учёбы ставь 0.1 или даже 0. Это снизит креативность, но повысит фактологическую точность.
- Индексация больших библиотек. 100 учебников? Индексация может занять часы. Делай это один раз и сохраняй базу (
persist_directory).
| Ошибка | Причина | Решение |
|---|---|---|
| "Failed to load module..." при запуске llama-cpp-python | Нет совместимых библиотек C++ (например, libstdc++). | Установи build-essential на Ubuntu или Xcode Command Line Tools на macOS. |
| Медленный ответ (секунды) | Модель работает на CPU или чанков слишком много. |
|
| Модель "галлюцинирует" | Слабый промпт или чанки не релевантны. | Ужесточи промпт (как в примере). Увеличь chunk_size до 1024. |
А что дальше? Куда развивать систему
Базовая система работает. Но это только начало. Вот что можно добавить, когда освоишься:
- Память диалога. Чтобы ассистент помнил, о чём вы говорили 5 минут назад. Реализуется через сохранение истории в промпт или отдельную базу. Есть отдельный гайд на эту тему.
- Голосовой интерфейс. Говоришь вопрос – получаешь голосовой ответ. Пригодится, когда руки заняты. Смотри проект с n8n.
- Автоматическое обновление базы. Кинул новый PDF в папку – система сама переиндексировала его. Реализуется через watchdog.
- Агентские возможности. Чтобы ассистент не только отвечал, но и что-то делал – искал в интернете (осторожно!), считал на Python. Это уровень AI-монстра.
Самое главное – теперь у тебя есть фундамент. Система, которая принадлежит только тебе. Не зависит от аптайма OpenAI, не продаёт твои данные, и ты можешь ковыряться в её кишках сколько угодно. Это чувство свободы стоит тех трёх часов, которые ты потратишь на сборку.
Хочешь глубже понять, как работать с ИИ не как пользователь, а как инженер? Курс "Нейросети с нуля: ваш ИИ-ассистент для жизни и работы" даст системные знания – от основ ML до развёртывания таких систем, как наша.
Последний совет. Не пытайся сделать всё идеально с первого раза. Сначала добейся, чтобы система просто запустилась и ответила на вопрос по маленькому текстовому файлу. Потом добавляй PDF. Потом улучшай промпт. Итерации – ключ к тому, чтобы не забросить проект на полпути. Удачи.