Статьи
May 10

Создаём Telegram бота с aiogram3 и llama3-7B

Пара слов

Итак, будем писать простого чат бота для telegram с локальной llama3 8B (даешь опенсорс!). Я всё буду делать на linux, какие-то не очевидные моменты для windows буду подсвечивать, в остальном - всё примерно тоже самое, я буду писать что конкретно делаю, а вы сможете загуглить, как оно там на винде работает - в 90% случаев команды те же самые. Предполагается, что вы можете создать и активировать виртуальное окружение самостоятельно, всё остальное я буду показывать поэтапно.

Создание проекта, выбор модели

Итак, первые шаги:

  • Создаём директорию проекта
  • Создаём виртуальное окружение и активируем его
  • Создаём папку models, кидаем туда файлы моделей

Модельки ламы можно скачать отсюда, тут лежат сжатые версии, кратко по сжатию:

Quantinized версии моделей позволяют снизить требования по железу, при этом у них снижается качество комплитов.
Изначально точность несжатой модели - 16bit, однако, 8bit модельки почти такие же хорошие, как и их несжатые версии, при этом требуют значительно меньше ресурсов, вы скорее всего сможете запустить llama3-Q8 даже на CPU ноутбука.
Ещё одно правило - количество параметров превалирует над силой сжатия, например сжатая llama3-70B всё равно будет работать лучше чем несжатая 8B.

Исходя из этого, 8Q даст нам лучшее соотношение скорость/качество, её и скачаю, заодно и ещё одну, 2Q - посмотреть, поиграться :)

В итоге, получилось как-то так (я ещё дополнительно создал git репозиторий для удобства)

Как выглядит проект после создания директорий и загрузки моделей

Подготовка к созданию Telegram бота

Установим aiogram3 в наше окружение

pip install aiogram
pip install python-dotenv

Добавим сразу это всё в файл requirements.txt, я для удобства это делаю одной командой, она создаст файл, если он ещё не существует и добавит туда строчку из pip freeze, по совпадению "aiogram"

pip freeze | grep aiogram >> requirements.txt
pip freeze | grep python-dotenv >> requirements.txt

Пишем BotFather, создаём там бота и получаем токен, кладём его в .env файл, переменную назовём BOT_TOKEN

содержимое .env файла

Пишем hello world на aiogram3

Создадим два файла, message_router.py и bot.py.

Вот что будет в message_router.py:

from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message

router = Router()

@router.message(Command("start"))
async def cmd_start(message: Message):
    await message.answer(f"hello world!")

Очень кратко по функционалу - @router.message будет отслеживать любое сообщение боту, Command это фильтр команд, то есть следующая функция будет срабатывать на любое сообщение боту, которое содержит команду /start. Этот router мы сейчас прицепим на главный роутер - диспетчер.

А теперь пишем и сам bot.py:

import asyncio
import logging
import sys
import os

from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from dotenv import load_dotenv, find_dotenv

import message_router  # тот самый роутер

load_dotenv(find_dotenv())


async def main() -> None:
    bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="HTML"))
    await bot.delete_webhook(drop_pending_updates=True)
    
    dp = Dispatcher()
    dp.include_routers(message_router.router)  # тут добавляем наши роутеры
    await dp.start_polling(bot)
    

if __name__ == "__main__":
    BOT_TOKEN = os.getenv("BOT_TOKEN") # получаем токен бота из переменной окружения
    if BOT_TOKEN is None:
        raise ValueError("BOT_TOKEN is not set")
        
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())

Тут всё просто, сначала получаем токен бота, потом в главной функции создаём объект Bot, создаём Dispatcher, крепим к нему наш роутер и запускаем polling.

Запустим бота, можно пойти и убедиться что он отвечает на /start.

python3 main.py

Запуск сервера llama.cpp

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

Если позже захотите переустановить сервер с другими настройками, то добавьте эти флаги --no-cache-dir --force-reinstall --upgrade

Для CPU

Команда будет такой

pip install 'llama-cpp-python[server]'

А запускать сервер находясь в папке с проектом вот так

python3 -m llama_cpp.server --model models/<Имя модели>.gguf

Для Nvidia

Если у вас стоит GPU, то советую выбрать этот вариант, так как скорость генерации на видеокарте гораздо выше.

Cначала нужно установить CUDA Toolkit, сделать это можно тут. Просто выбираете параметры своей системы и получаете готовую инструкцию. Если вы на Windows, всё делается через один installer, если на Linux, не забудьте поставить ещё и драйверы, они сразу за основной инструкцией.

После установки CUDA, установим сам llama.cpp сервер

CUDACXX=/usr/local/cuda-12/bin/nvcc CMAKE_ARGS="-DLLAMA_CUBLAS=on -DCMAKE_CUDA_ARCHITECTURES=native" FORCE_CMAKE=1 pip install 'llama-cpp-python[server]'

И чтобы запустить его, команда такая же как и для CPU варианта, только дополнительно определяем сколько задач мы хотим отдать на саму карточку

python3 -m llama_cpp.server --model models/<Имя модели>.gguf --n_gpu_layers 35

Чтобы определить оптимальное количество слоёв, на linux можно воспользоваться утилитой nvidia-msi, пока-что можете выставить совсем немного 5-10, позже сможем протестировать, во время работы бота

watch -n 0.5 nvidia-smi

Соединяем всё вместе

давайте немного отредактируем наш код, почти полностью перепишем наш message_router:

from aiogram import Router
from aiogram.types import Message

import openai

router = Router()

history = [
    {
        "role": "system",
        "content": "You are an intelligent assistant."
    }
]


@router.message()
async def any_message(message: Message, client: openai.Client):
    history.append({"role": "user", "content": message.text})
    completion = client.chat.completions.create(
        model="local-model",
        messages=history,
        temperature=0.7,
    )
    content = completion.choices[0].message.content
    new_message = {"role": "assistant", "content": content}
    history.append(new_message)
    await message.answer(content)

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

Теперь дополним пару строк в bot.py, функция main теперь выглядит так:

async def main() -> None:
    bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="HTML"))
    await bot.delete_webhook(drop_pending_updates=True)

    client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
    dp = Dispatcher(client=client)
    dp.include_routers(message_router.router)
    
    await dp.start_polling(bot)

Создали клиента, закинули его в диспетчер, теперь он доступен из любой функции под декораторами роутеров

и не забудем про импорт OpenAI, теперь заголовок файла такой

import asyncio
import logging
import sys
import os

from openai import OpenAI
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties

import message_router

Всё готово, запускаем сервер, ждём пока загрузится моделька, потом запускаем бота и идём проверять в чат

Хьюстон, как слышно?

Итоги

Мы сделали простенького телеграм бота с локальной llama3, задачи по улучшению и доработке функционала на вас, например сейчас все сообщения хранятся в оперативной памяти :)

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶

Автор: @thisizmyusername