Представьте консультанта в DNS/Ситилинке, который не навязывает «вот этот блок питания потому что остался на складе», а спокойно объясняет, чем один БП лучше другого под ваш билд, помнит, о чём вы спрашивали раньше, и ещё просит вежливый фидбек.
Я решил собрать такого консультанта в виде Telegram-бота «Кремний» — RAG-бота по железу на бесплатных инструментах: Telegram Bot API, Groq (Llama 3.1 8B), sentence-transformers и чуть-чуть боли с NumPy и Pterodactyl.
Задача: живой консультант по железу в Telegram
Хотелось не просто «ещё одного бота», а что-то ближе к человеческому диалогу: минимум кнопок, максимум свободного текста, плюс нормальный контекст про товары и ответы, основанные на фактах, а не на фантазиях модели.
Требования к «Кремнию» получились такие:
Отвечать на вопросы про комплектующие: видеокарты, материнки, блоки питания, корпуса и т.п. (описания, совместимость, сценарии).
Уметь говорить про цены и основные параметры (TDP, форм-фактор, количество линий PCIe и т.д.) в рамках небольшой базы знаний.
Работать как «почти человек»: пользователь пишет руками, без меню и клавиатур; бот парсит естественный язык и помогает.
Поддерживать RAG: вытаскивать из своей базы релевантные документы и кормить их LLM, чтобы ответы были опорой на данные.
Собирает фидбек, но не навязчиво — отдельной командой, с персональным ответом от LLM на отзыв.
Архитектура: простой стек, много пользы
Чтобы уложиться в «бесплатный/учебный» формат, собрал следующий стек:
Telegram-слой:
python-telegram-bot22.5, async-обвязка вокруг Bot API,ApplicationBuilder,MessageHandler,/start,/help,/feedback.Генерация: Groq API c моделью
llama-3.1-8b-instantчерез Python-клиентgroq(OpenAI-совместимый протокол, бесплатный фритир для прототипов).RAG: эмбеддинги из
sentence-transformers(all-MiniLM-L6-v2) + простой векторный поиск по косинусу черезnumpy.База знаний: вручную собранный mini-каталог комплектующих (материнские платы, видеокарты, БП, корпуса и т.д.) + блоки про доставку и гарантию.
Деплой: локально — обычный venv; в проде — контейнер на Pterodactyl с
requirements.txtи парой сюрпризов от NumPy 2.x и PyTorch.
Компоненты общаются между собой довольно тривиально: Telegram даёт текст → RAG достаёт релевантные документы → LLM отвечает в стиле «Кремния» → опционально запрашиваем отзыв через /feedback.
Личность бота «Кремний»
Сухой консультант по железу — это скучно; на Хабре такие боты долго не живут даже в личке.
Поэтому для системного промпта зашил персоналию:
«Ты — Кремний, виртуальный консультант по железу и сборкам ПК».
Общается на «ты», без токсичности, но с лёгким ироничным тоном (в духе знакомого продавана, который сам собирает себе NAS по ночам).
Делает отсылки к типичным сценариям: «собираю бюджетный билд для шутеров», «тихий домашний NAS», «комп под Stable Diffusion».
Жёстко фиксирован: «если чего-то нет в базе знаний — честно скажи, что не знаешь, и не придумывай характеристики».
Это важно: LLM по умолчанию любит «додумывать», а RAG-боту по железу простительно не всё — особенно, когда дело доходит до совместимости БП и видеокарты.
RAG: от запроса до релевантных железок
База знаний
Базу сделал в виде обычного Python-списка словарей KNOWLEDGE_ITEMS в knowledge_base.py.
Каждый элемент — это либо product, либо блок shipping/warranty, например:
{
"id": "gpu_4070_dual",
"type": "product",
"category": "видеокарты",
"name": "GeForce RTX 4070 Dual",
"description": (
"RTX 4070 с двухвентиляторным охлаждением, TDP около 200 Вт. "
"Подходит для сборок с БП от 650 Вт и хорошей вентиляцией корпуса."
),
"sizes": ["2.5-слота", "длина 270 мм"],
"price_rub": 65000,
"chipset": "NVIDIA RTX 4070",
"power": "1x 8-pin",
}
Такое представление удобно и для RAG (есть чистый текст), и для будущего расширения (можно потом вынести в JSON/БД без переписывания логики).
Эмбеддинги и поиск
RAG-часть лежит в bot.py и использует sentence-transformers/all-MiniLM-L6-v2:
emb_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
DOC_TEXTS = [doc_to_text(item) for item in KNOWLEDGE_ITEMS]
DOC_EMB = emb_model.encode(DOC_TEXTS, normalize_embeddings=True)
DOC_EMB = np.array(DOC_EMB, dtype="float32")
doc_to_text собирает внятный текст из полей продукта: название, категория, описание, питалово, форм-фактор, цены и т.п., чтобы модель embeddings не ломала голову над сырым JSON.
Дальше всё по канону:
def retrieve_relevant_docs(query: str, top_k: int = 3):
query_emb = emb_model.encode([query], normalize_embeddings=True)[0]
sims = DOC_EMB @ query_emb # косинус, т.к. всё уже нормализовано
top_idx = np.argsort(-sims)[:top_k]
...
Так как база небольшая (десятки/сотни элементов), никакой FAISS, Chroma и прочий зоопарк здесь не нужен — обычный numpy вполне достаточно.
LLM: Groq + Llama 3.1 8B по OpenAI протоколу
Чтобы не упираться в платный OpenAI и одновременно не поднимать локальную LLM, взял Groq: он даёт быстрый доступ к Llama 3.1 8B/70B через OpenAI совместимый API.
Инициализация клиента элементарна:
from groq import Groq
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
LLM_MODEL = "llama-3.1-8b-instant"
Вызов чата тоже классический:
completion = groq_client.chat.completions.create(
model=LLM_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "system", "content": rag_context_block},
{"role": "user", "content": user_message},
],
temperature=0.4,
max_tokens=800,
)
answer = completion.choices[0].message.content.strip()
Бонус Groq — для учебных и pet-проектов этого объёма бесплатного лимита хватает с запасом, а задержки по сравнению с OpenAI заметно меньше.
Telegram-слой: минимум кнопок, максимум текста
С точки зрения Telegram бот — обычное приложение на python-telegram-bot 22.5:
/start— приветствие и краткая инструкция: как задавать вопросы, какие темы «Кремний» покрывает./help— краткое повторение возможностей плюс примеры запросов (про «тихий билд в mini-ITX», «комп для ML» и т.п.)./feedback— запуск режима обратной связи; следующее сообщение пользователя считается отзывом.
Все остальные текстовые сообщения без слэшей идут в handle_message, который решает, что делать: ожидать отзыв или обрабатывать запрос через RAG+LLM.
Кнопок по минимуму: никакой клавиатуры с «Узнать цену», «Показать доставку» — пользователь пишет свободным текстом, а задача бота — понять, что имелось в виду (как минимум в пределах базы знаний).
Обратная связь: /feedback вместо спама после каждого ответа
Первую версию я сделал по ТЗ: бот после каждого ответа просил оценить общение. Выглядело это как надоедливый поп-ап на сайте:
Пользователь: «Собери мне билд под 100к» → бот отвечает → сразу «Поставь оценку от 1 до 5».
В реальном сценарии это то, за что хочется снижать карму, а не повышать. Поэтому в версии для «Хабра» логика другая:
Фидбек запрашивается только, если пользователь сам вызывает
/feedback.Бот хранит последнюю реплику пользователя и свой ответ в
USER_STATE[user_id].Когда приходит отзыв, отдельный вызов LLM анализирует, что было в диалоге, и генерит человеческую благодарность/извинение/ответ.
Код примерно такой:
USER_STATE = {
user_id: {
"awaiting_feedback": bool,
"last_user_message": str,
"last_bot_answer": str,
}
}
async def feedback_command(update, context):
state = USER_STATE.setdefault(user_id, {...})
if not state["last_bot_answer"]:
await update.message.reply_text(
"Мы ещё толком не пообщались — сначала задай вопрос, "
"а потом уже можно оценить беседу."
)
return
state["awaiting_feedback"] = True
await update.message.reply_text(
"Напиши, пожалуйста, короткий отзыв о нашей беседе."
)
Заодно такая схема хорошо демонстрирует на собеседовании, как можно использовать LLM не только «для ответов», но и для обработки мета-информации (фидбек, теги, аннотации).
Немного боли: Pterodactyl, NumPy 2.x и PyTorch
Отдельная мини-история достойна раздела «Что пошло не так».
Шаг 1. OOM из-за жирного стека
Сначала я честно сделал pip freeze на локальной машине и скормил этот requirements.txt Pterodactyl-контейнеру.
В результате контейнер радостно начал ставить torch==2.9.1 и заодно потащил за собой кучу nvidia-* пакетов для CUDA, пока не умер с Exit code: 137 и Out of memory: true.
Лекарство: оставить только то, что реально нужно, и явно указать CPU-сборку торча:
python-telegram-bot==22.5
groq==0.36.0
python-dotenv==1.2.1
numpy==1.26.4
sentence-transformers==3.0.1
--extra-index-url https://download.pytorch.org/whl/cpu
torch==2.2.2
Так удалось избавиться от лишних CUDA-зависимостей и ужаться до адекватного размера по памяти.
Шаг 2. NumPy 2.3.5 vs PyTorch
Следующая засада выглядела так:
A module that was compiled using NumPy 1.x cannot be run in NumPy 2.3.5...
RuntimeError: Numpy is not available
Это «подарок» от NumPy 2.x: многие бинарные модули (включая часть PyTorch-сборок) собраны против NumPy 1.x, и при наличии NumPy 2.3.5 PyTorch просто считает, что NumPy у тебя «нет».
Решение максимально приземлённое и советуется во всех обсуждениях: зафиксировать numpy, обычно 1.26.4, как в примере выше.
После понижения NumPy:
Предупреждение исчезло.
sentence-transformersспокойно делаетemb.numpy()внутри.encode, и бот успешно считает эмбеддинги при старте.
Что можно допилить дальше
Даже в таком «учебном» виде у бота уже есть чем похвастаться: живая персоналия, RAG по базе железа, фидбек через LLM и продакшен-подобный деплой на Pterodactyl.
Но если захочется развивать идею, вот несколько направлений:
Нормальный векторный стор: FAISS или аналог, чтобы держать сотни/тысячи позиций, а не десятки.
Переранкер: поверх MiniLM добавить reranking (например, через тот же LLM), чтобы ещё точнее выбирать контекст.
Диалоговая память: хранить не только последний вопрос/ответ, а сессию, и давать LLM больше истории общения (особенно важно, когда пользователь уточняет: «а блок питания к нему подойдёт?»).
Онлайн-цены и наличие: завести лёгкий интеграционный слой с API магазина (если это уже не учебный, а боевой проект).