← Лендинг Hermes Agent

Опубликовано

YandexGPT-5-Lite-8B как генератор над выдачей: третья часть цикла про домашний Perplexity

Автор: Гусев Николай [портфолио]

Обновлено

Темы: llm, rag, local-models

Сложность: средняя

Третья часть: зачем нам ещё одна модель

В первой части цикла я собрал добычу: SearXNG ищет, Camoufox достаёт контент из-под антибота. Во второй - сравнил пять локальных моделей над грязной поисковой выдачей: быстрый qwen3.8 отвечал за сто секунд, LFM2.5 на 32 тысячах контекста честно отказывался натягивать несуществующие связи. Победили универсалы - модели, которые одинаково среднего кодят, рассуждают и резюмируют.

Но у меня оставался вопрос. Весь мой корпус - русский: база знаний по инфраструктуре, документация, поисковая выдача на русском. Универсальные модели всё это читают через переводной токенизатор, собирая русские слова из кусков. А если взять модель, которую русскоязычный корпус учили с нуля?

Сравнение плотности токенизации на одном и том же русском тексте (356 символов):

Барчарт: плотность токенизации русского текста, YandexGPT 5.56 символов на токен против qwen3.8 4.51 и LFM2.5 3.49

smollm3 формально плотнее, но на английском YandexGPT не проигрывает никому, а вот против участников второй части выигрыш заметный: qwen3.8 тратит на тот же русский текст на 23% больше токенов, LFM2.5 - на 59%. Меньше токенов = больше контекста в те же 32k и быстрее генерация. Плотность - не главное достоинство, но приятный бонус нативного инструмента.

Для проверки взял YandexGPT-5-Lite-8B-instruct и прогнал через оба пайплайна из прошлых частей: локальный RAG по базе знаний и ответчик над веб-выдачей. Главный вывод: самое ценное в этой модели не то, как она отвечает, а то, как она отказывается отвечать.

Серия «Домашний Perplexity»

Роль генератора в пайплайне

Агентская схема работает так: retrieval собирает чанки, специализированная модель формулирует по ним ответ, а главная модель агента обрабатывает результат - проверяет, дополняет, решает, что делать дальше. Лёгкий генератор в этой схеме - расходник: он делает дешёвую механическую работу "превратить чанки в связный текст с цитатами", не тратя контекст и токены дорогой основной модели.

Схема: retrieval, генератор YandexGPT 8B, main-модель агента, ответ с атрибуцией

Схема элементарная:

запрос → retrieval (поиск чанков) → контекст с номерами → LLM → ответ с цитатами

Retrieval отвечает за "найти релевантное". Генератор - за "сформулировать ответ строго по найденному". Это разные компетенции, и для второй нужна instruct-модель: base-модель без дообучения продолжит текст вместо ответа на вопрос,_instruction-tuned - поймёт систему ролей и будет держать формат.

Почему YandexGPT-5-Lite-8B-instruct:

  • 8B параметров, квант Q4_K_M весит 4.6 GB - целиком влезает в 8 GB VRAM обычной игровой карты (RTX 4060)
  • около 29 токенов/сек на этой карте
  • контекст 32k - топ-10 чанков с запасом
  • сильный русский - для локальной базы знаний на русском это решающий фактор
  • официальный GGUF от Яндекса, есть в LM Studio из коробки
  • лицензия: коммерческое использование до 10 млн исходящих токенов в месяц, дальше - API в Yandex Cloud

Не путать с новой линейкой Яндекса: осенью 2026 они выпустили AliceAI-Foundation-80B-A3B и Alice AI Search Pretrain - это другие, серверные модели. Foundation 80B в Q4 весит около 45 GB, для домашней карты не предназначен. Для персонального RAG 8B - рабочая точка.

Подключение за пять минут

Качаем GGUF, кладём в папку моделей LM Studio, загружаем. Дальше - обычный OpenAI-совместимый API на localhost:1234. Единственная ловушка на этом пути: если в системе прописан корпоративный прокси, он перехватит и localhost, и вы получите 502 Bad Gateway на запрос к собственной машине. Лечится пустым ProxyHandler:

import json, urllib.request

# пустой ProxyHandler - обход системного прокси
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))

def llm(messages, max_tokens=700):
    req = urllib.request.Request(
        "http://127.0.0.1:1234/v1/chat/completions",
        data=json.dumps({
            "model": "yandexgpt-5-lite-8b-instruct",
            "messages": messages,
            "temperature": 0.3,
            "max_tokens": max_tokens,
        }).encode(),
        headers={"Content-Type": "application/json"},
    )
    with opener.open(req, timeout=240) as r:
        d = json.loads(r.read())
    return d["choices"][0]["message"]["content"], d["usage"]

Пара слов о темплейте. У модели нестандартный формат диалога - реплика генерируется после маркера "Ассистент:[SEP]", переводы строк кодируются особым токеном. Через OpenAI-совместимый endpoint LM Studio всё это обёрнуто автоматически, руками ничего править не надо. Это стало бы проблемой только при прямом запуске через llama.cpp.

Пайплайн 1: RAG-ответчик

Формула простая: ретрив отдаёт чанки, мы нумеруем их и складываем в контекст, модель отвечает со ссылками.

def rag_answer(query, chunks):
    context = "\n\n".join(
        f"### Источник {i+1}\n{c}" for i, c in enumerate(chunks)
    )
    messages = [
        {"role": "system", "content":
         "Ты отвечаешь на вопросы по документации, используя ТОЛЬКО "
         "приведённый контекст. Указывай номера источников. Если ответа "
         "в контексте нет - скажи об этом прямо."},
        {"role": "user", "content": f"Контекст:\n{context}\n\nВопрос: {query}"},
    ]
    return llm(messages)

Живой пример из моего прогона. Вопрос: как переустановить некорректно настроенную реплику домена. Ретрив нашёл два чанка из базы знаний, ответ модели:

Чтобы переустановить некорректно настроенную реплику, выполните:
1. Удалите реплику и выйдите из домена (astra-freeipa-server -U,
   astra-freeipa-client -U).
2. Удалите пакеты: sudo apt purge 'aldpro*' 'freeipa*' 'sssd*' ...
3. Проверьте список профилей в домашнем каталоге.
4. Повторно введите реплику в домен.
Источник: [1]

193 prompt-токена, 6.5 секунды. Атрибуция на месте, шаги по делу.

Пайплайн 2: ответчик над веб-выдачей

Тот же принцип, но retrieval заменён на веб-поиск. Мини-Perplexity на локальной модели:

def search_answer(question):
    hits = web_search(question, limit=4)["data"]["web"]
    pages = web_extract([h["url"] for h in hits[:2]], char_limit=2500)
    parts = [
        f"[{i}] {p.get('title','')}\n{(p.get('content') or '')[:2200]}"
        for i, p in enumerate(pages["results"], 1)
    ]
    messages = [
        {"role": "system", "content":
         "Ты отвечаешь на вопрос по приведённым материалам. Указывай "
         "номера источников в квадратных скобках. Если данных "
         "недостаточно - так и скажи."},
        {"role": "user", "content":
         "Материалы:\n" + "\n\n".join(parts) + f"\n\nВопрос: {question}"},
    ]
    return llm(messages)

Реальный прогон: "как включить отладку регулярных выражений в Python". Поиск нашёл статью, extract вытащил текст, ответ:

Чтобы включить отладку регулярных выражений в Python, нужно
использовать флаг `re.DEBUG` в методе `re.compile` [1].

722 prompt-токена, 1.2 секунды генерации.

Восемь тестов на честность

Теперь главное. Я собрал восемь сценариев, где правильное поведение генератора разное: полный ответ, частичный ответ или отказ. Результаты живых прогонов:

Тест                                    | Результат                      | Метрики
----------------------------------------|--------------------------------|-------------
RAG: поиск по базе, ответ существует    | полный ответ + атрибуция [1]   | 193 tok, 6.5s
RAG: вопроса в контексте нет            | честный отказ                  | 11.8s
RAG: веб-добыча + поиск, ответ есть     | пошаговая инструкция + ссылка  | 193 tok, 6.5s
Web: выдача без ответа (FreeRDP)        | отказ: "нет информации"        | 488 tok, 5.3s
Web: сниппеты дают версию без деталей   | версия названа, детали - отказ | 435 tok, 1.6s
Web: статья НЕ про тот вопрос           | отказ - поймала нерелевантность| 868 tok, 1.1s
Web+extract: релевантная статья         | точный ответ + [1]             | 722 tok, 1.2s
Контраст: противоречивые источники      | хронология, конфликт показан   | 1.4s

Разберу два показательных случая.

Нерелевантная выдача. Искал "фичи Python 3.14", поиск вернул статью про полезные фичи Python вообще - похоже, но не то. Модель не притянула материал за уши:

В предоставленных материалах нет информации о ключевых
особенностях Python версии 3.14 [1][2].

Противоречивые источники. Подал два блока: "релиз 15 марта" и "перенесён на апрель из-за сертификации". Ответ:

Версия X.Y была запланирована к выходу на 15 марта 2027 года,
однако позже выход был перенесён на апрель 2027 года из-за
проблем с сертификацией [1], [2].

Хронология восстановлена, конфликт не замолчан, обе ссылки на месте.

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

Отсюда правило: ответственность за релевантность - на retrieval, не на генератор. Если поиск вернул шум, модель откажется отвечать - и правильно сделает. Улучшать надо поиск, а не промпт.

Скорость зависит от длины контекста: ответы на 200-700 prompt-токенах занимают 1-6 секунд, а с 900 токенами и большим max_tokens - до 12. Для интерактивного чат-бота нормально, для батч-обработки - закладывайте очередь.

Лицензия разрешает коммерческое использование до 10 млн исходящих токенов в месяц. Для внутреннего инструмента на команду - с запасом, для публичного сервиса - считайте экономику или берите API.

Где это применять

Годится: внутренние базы знаний, персональный "Perplexity" над вебом, ответчик в чат-боте по документации, суммаризация найденного.

Не годится: агентные сценарии с вызовом инструментов (function calling у модели есть, но после дообучения слабый), многошаговое планирование, сложный код - там нужны модели классом выше, а 8B - это про другое: дешёвый, быстрый, честный генератор текста по контексту.

Формула простая: эмбеддинги находят, генератор отвечает. Остальное - детали реализации.