Опубликовано
Домашний Perplexity, часть 2: кто думает над выдачей
Автор: Гусев Николай [портфолио]
Темы: ai-agents, benchmarks, local-models, prompting
**Сложность: **средняя
В первой части персональный ассистент научился доставать факты из веба: лестница инструментов прошла поиск, антибот и капчу. Остался вопрос, кому доверить мышление над найденным. Пять локальных моделей, грязный контекст, ловушка дат и два режима поиска: быстрый за полторы минуты и глубокий за четверть часа. Все параметры, промпты и подводные камни внутри, опыт можно повторить.
Что осталось после первой части
Лестница из первой части решала задачу доставки. SearXNG ищет, trafilatura и Camoufox читают страницы, модель bge-m3 отбирает релевантное, компактная smollm3 собирает ответ. Для вопроса "сколько стоит сок" этого достаточно, потому что ответ лежит в одном-двух фактах.
У Perplexity есть и вторая половина способности. Пользователь спрашивает не про цену, а про устройство: сравни варианты, выстрой хронологию, где источники противоречат друг другу. Тогда ассистент собирает десять документов по несколько тысяч знаков, и сводка превращается в исследование. Модель на 3 миллиарда параметров в такой работе либо тонет, либо тихо сочиняет недостающее.
Значит, требовалось ответить на два вопроса. Какая локальная модель способна думать над выдачей, а какая годится только на пересказ. И как собрать это в два режима, чтобы быстрый вопрос не ждал четверть часа, а глубокий не получал ответ на скорую руку. Железо то же, что в первой части: процессор Ryzen и видеокарта на 8 гигабайт. Бюджет прежний, нулевой.
Участники: пять моделей
Все модели крутятся в LM Studio на локальной видеокарте и бесплатны. Вот точные ключи из каталога и их роли в эксперименте.
`Модели для эксперимента (ключ в LM Studio, размер, роль):
qwen3.8-4b-distill
4B, факты, быстрый режим
google/gemma-4-e4b
4B, универсал, лучший на чистом входе
smollm3-3b
3B, компактная, сводки
granite-4.1-3b
3B, жадный токенайзер
liquidai_lfm2.5-2.6b
2.6B, аналитика, всегда думает`
Источники моделей: Qwen (дистиллят qwen3.8), Google (семейство gemma), HuggingFaceTB (smollm3), ibm-granite (granite 4.1), LiquidAI (LFM2.5). Модели можно скачать в LM Studio поиском по этим именам.
Методика: сначала чисто, потом грязно
Первый раунд был вежливым. Каждая модель получала одинаковый подготовленный контекст: аккуратные выжимки источников, примерно одинаковый объем. Пять моделей, пять ответов, оценки от 4.2 до 4.75 балла. Разница в полбалла ничего не решала, и это само по себе было находкой: на чистом входе локальные модели почти неотличимы.
Настоящую работу показал второй раунд. Контекст испачкали: реранкинг отключили, шумовые сниппеты из выдачи сохранили как есть, корпус собрали из десяти источников по 5 тысяч знаков, итого 50 тысяч. Дальше многошаговая цепочка: ответ первого шага подавался на вход второго, и модель обязана была строить вывод на собственном прошлом выводе. Так работает реальный поиск, и так не работают красивые демо.
В корпус заложили ловушку. Часть источников называла одну дату события, часть другую, и обе правы: программа PRISM началась в 2007 году, а публично о ней стало известно в 2013, со скандалом Сноудена. Слабая модель склеит эти даты в кашу или возьмет первую попавшуюся. Проверка тривиальна: ищем в ответе обе даты.
`def trap_verdict(msg):
has_2007 = "2007" in msg
has_2013 = "2013" in msg
return "both" if has_2007 and has_2013 else \
"попался" if has_2013 else \
"2007-only" if has_2007 else "no-dates"`
И последний штрих в стиле Perplexity: каждый прогон писался в локальный кэш по хэшу ссылки. Повторные запуски не дергали веб и работали с тем же материалом. Это спасало и время, и нервы, когда прогон умирал на середине.
Системный промпт один на всех, и в нем три правила дисциплины: только по предоставленным источникам, синтез с номерами ссылок в квадратных скобках, противоречия между источниками отмечать обязательно.
`SYS = ("Отвечай по-русски только по предоставленному содержимому страниц. "
"Синтезируй из разных источников, ссылки на источники давай "
"в квадратных скобках [N]. "
"Если источники противоречат друг другу или содержат разные "
"даты - обязательно отметь противоречие. "
"Чего нет в источниках - не выдумывай.")`
Арифметика контекста: почему модели падали
Первый грязный прогон умер с ошибкой 500 от сервера моделей. Причина оказалась скучной и поучительной: 50 тысяч знаков это примерно 12.5 тысячи токенов, а контекст у моделей стоял 8192. Промпт просто не влез. Всем поставили 16384.
После расширения снова упала одна модель, причем дважды. Ее токенайзер режет русский текст жаднее остальных: там, где другие модели видели 13 тысяч токенов, granite насчитал 18 151. Русский текст раскладывается на мелкие кусочки, и это надо учитывать при выборе размера контекста. Помогла установка 32 тысячи.
`# LM Studio REST API: загрузка модели с нужным контекстом
POST http://127.0.0.1:1234/api/v1/models/load
{"model": "granite-4.1-3b", "context_length": 32768,
"offload_kv_cache_to_gpu": false, "flash_attention": true}`
Вывод простой: контекст считается в токенах, а токен у каждой модели свой. Для русского языка запас нужен всем, а жадным токенайзерам вдвойне.
Результаты грязного бенчмарка
Итог по времени и глубине ответа на грязном контексте в 16 тысяч токенов:
`модель время фундамент ответа заметки
qwen3.8-4b-distill 104 c ~1500 знаков сжато и точно
google/gemma-4-e4b 104 c ~1470 знаков лучший на чистом входе
smollm3-3b 165 c ~1360 знаков стабильно середняк
granite-4.1-3b 202 c ~940 знаков жадный токенайзер, аутсайдер
liquidai_lfm2.5-2.6b 962 c 3857 знаков глубже всех в 2.6 раза`
Ловушку дат прошли все пять моделей: обе даты названы, обе объяснены, противоречия между источниками показаны. При достаточном бюджете контекста даже небольшие модели не путаются в чужих датах. Пустых ответов тоже не осталось.
Быстрый режим: выключить раздумья
Для фактовых вопросов победил дистиллят qwen3.8 на 4 миллиардах параметров. Точная конфигурация: внутренние рассуждения выключены явным полем, лимит ответа 400 токенов, контекст 16384, температура ноль.
`# пресет быстрого режима, продакшн
{"model": "qwen3.8-4b-distill",
"reasoning": "off", # отправлять ТОЛЬКО для qwen
"max_output_tokens": 400,
"context_length": 16384}`
`# сам вызов
POST http://127.0.0.1:1234/api/v1/chat
{"model": "qwen3.8-4b-distill",
"input": "Вопрос: ...\n\nСодержимое страниц: [1] ... [N]",
"system_prompt": SYS, "temperature": 0.0,
"max_output_tokens": 400, "context_length": 16384,
"store": false, "reasoning": "off"}`
Ответ готов примерно за 100 секунд при скорости 5-6 токенов в секунду. Два решения здесь делают всю разницу. Выключенные рассуждения не оставляют модели выбора: она не тратит квитанцию на размышления, а сразу пересказывает найденное. А жесткий лимит в 400 токенов дисциплинирует формат: короткие ответы на короткие вопросы. Спросил цену - получил цену и источники.
Глубокий режим и эксперимент с 32 тысячами
Для аналитики выбор пал на LFM2.5 на 2.6 миллиарда параметров от LiquidAI. Ее особенность в том, что она всегда рассуждает перед ответом: сначала несколько тысяч токенов размышлений, потом сам вывод. Сравнение провели честно, два прогона в обе стороны.
На контексте 16384 глубокий ответ вышел шаблонным: аккуратная выжимка на 1152 знака, без собственного исследования. Затем контекст расширили до 32768 и скармливали корпус из 20 источников, примерно 21.6 тысячи токенов. Результат изменился качественно: первый шаг дал 3723 знака фундамента за 778 секунд с 3916 токенами размышлений, второй, построенный на первом, еще 3454 знака анализа за 828 секунд и почти 6 тысяч токенов размышлений. Глубина выросла втрое.
`# пресет глубокого режима, продакшн
{"model": "liquidai_lfm2.5-2.6b",
# поле reasoning НЕ СЛАТЬ ВООБЩЕ: модель always-think,
# лишнее поле дает HTTP 400
"max_output_tokens": 16384,
"context_length": 32768}`
Самое ценное случилось в рассуждениях. На просьбу связать два события модель отказалась натягивать прямую связь между крахом СССР и программой PRISM. Она разложила хронологию по годам: поправка к FISA 2008, запуск PRISM 2007, публичное раскрытие 2013, показала, где связь прослеживается, а где ее нет, и честно сказала, что прямой причинности источники не подтверждают. Облачные ассистенты в таких местах охотно сочиняют красивую линию. Локальная модель на 2.6 миллиарда параметров повела себя эпистемически честнее.
Цена известна: длинный контекст не влезает в 8 гигабайт видеопамяти целиком, таблица контекста частично уходит в оперативку, скорость падает до 3.3 токена в секунду, полный проход занимает 13-14 минут. Для фактов недопустимо, для исследования нормально: дешево и глубоко.
Подводные камни локального API
По пути вскрылись три особенности, которые стоит знать заранее.
Первая: у LFM2.5 размышления и ответ делят общий бюджет вывода. При малом лимите, скажем 2048, модель тратила все токены на размышления и возвращала пустоту: подумала в столе, отвечать уже нечем. Лечится только большим лимитом на выход, 16384. Поле выключения рассуждений сервер у этой модели не принимает вовсе, лишний параметр дает HTTP 400.
Вторая: ответ приходит сегментами, и content одного сообщения может быть как строкой, так и списком кусков. Склеивать их через перевод строки нельзя: разрез рвет предложения посередине. Сегменты одного сообщения конкатенируются напрямую, без разделителя.
Третья: перед загрузкой очередной модели чужие загруженные инстансы надо выгружать, а своей не выгружать и переиспользовать. Иначе видеокарта на 8 гигабайт быстро превращается в очередь из наполовину живых моделей.
`# разбор ответа: message отдельно, reasoning отдельно
msg = "".join(c.get("content","") if isinstance(c.get("content"),str)
else "".join(p_.get("text","") for p_ in c["content"])
for c in r["output"] if c.get("type")=="message")
reasoning_len = len("".join(c.get("content","")
for c in r["output"] if c.get("type")=="reasoning"))`
Два режима, два скила
Так родились два скила в Hermes Agent. Первый, perplexity-fast: дистиллят qwen3.8, рассуждения выключены, лимит 400, контекст 16384. Факты, цены, даты, короткие сводки. Второй, perplexity-deep: LFM2.5, всегда думает, контекст 32768, лимит 16384. Сравнения, хронологии, разбор противоречий. Правило выбора простое: фактовый вопрос идет быстрым маршрутом, аналитический глубоким.
Встраивание в поиск: домен веб-вопросов
Оба режима встроили в основной поисковый контур ассистента. Первым запрос встречает доменный роутер DCD: он смотрит на тему и раскладывает вопросы по доменам. Продуктовые вопросы уходят в локальную базу знаний, векторный магазин ZVec хранит ее индексы по коллекциям, а эпизодная память memvid до классификации вспоминает, не отвечал ли ассистент на похожее раньше, и записывает удачные ответы новыми эпизодами. Для веба же локальная база авторитетом быть не может: цена в магазине и сегодняшняя новость не живут в базе знаний. Поэтому в роутере появился отдельный домен веб-поиска, и внутри него локальные ступени пропускаются, веб первичен.
Второй механизм - кэш страниц. Найденные страницы режутся на фрагменты, векторизуются моделью bge-m3 и складываются в ZVec. Срок жизни записи 30 дней, идентификатор - хэш ссылки, так что одна страница не дублируется. Повторный вопрос сначала проверяет кэш: если нашлось два релевантных фрагмента и больше, веб вообще не дергается.
Здесь ждал подводный камень из документации баз: при гибридном индексе выданный score - это расстояние, а не сходство, один минус косинус. Сравнивать порог с ним напрямую нельзя. Поэтому сходство пересчитывается явно, и порог установлен на истинном косинусном сходстве 0.55.
Случай с кофе: когда своя база врет уверенно
Сквозной прогон сразу нашел настоящий баг. Вопрос "актуальные цены на кофе в ашане" роутер корректно отправил в веб-домен. Но семантический поиск по пути заглянул в локальную базу, а там в чат-логах нашлись разговоры про кофе. Векторное сходство сработало, качество ответа назначено "верное", и веб-контур не запустился. Спросили про цены - получили воспоминания о кофе из переписок.
Это класс ошибок гибридных систем: попадание по вектору глушит живой поиск там, где локальная база не авторитет. Починили точечно: для веб-домена локальные ступени семантического поиска пропускаются целиком, какой бы уверенной ни выглядела находка. Контур прикрыли тестом, который специально подсовывает похожую на правильную находку и проверяет, что веб все равно откроется.
После правки прогон встал на место. Первый запрос ушел в живой поиск: три источника, сводка по фактам. Повторный вопрос с тем же текстом пришел из кэша страниц: веб молчал, ответ собран из вчерашней находки. Полный цикл поиск, сводка, запись в кэш, ответ из кэша работает целиком. Контур прикрыт 35 тестами, полный набор после всех переносов проходит 487.
Как повторить опыт
Нужно: видеокарта на 8 гигабайт, LM Studio с REST API на порту 1234, SearXNG с выдачей JSON, python с библиотеками trafilatura и camoufox. Порядок действий такой.
Шаг 1. Скачать пять моделей в LM Studio по ключам из таблицы выше. В настройках включить REST API сервер.
Шаг 2. Собрать корпус: запрос в SearXNG, из выдачи берется до 10-20 неповторяющихся ссылок, каждая страница читается trafilatura, а если текста меньше 200 знаков, то Camoufox в скрытом режиме. Каждый документ усекается до 5 тысяч знаков и получает номер в квадратных скобках.
Шаг 3. Прогнать три задачи: фундамент, анализ с подачи собственного прошлого ответа, ловушка дат. Системный промпт из этой статьи, температура ноль. Результаты писать в json после каждой задачи, а не в конце.
Шаг 4. Сравнить ответы по глубине фундамента и вердикту ловушки. Увидите то же, что здесь: на чистом входе модели почти одинаковы, на грязном расходятся в разы.
Карта стека, часть 2
`запрос владельца
-> эпизодная память memvid: похожий ответ уже был? -> выдать сразу
-> доменный роутер DCD: это веб-вопрос?
да -> домен веб-поиска: локальные ступени пропущены, веб первичен
проверка кэша страниц (bge-m3 + ZVec, 30 дней)
найдено -> ответ из кэша, веб молчит
пусто -> SearXNG, отбор релевантного, локальная модель
фактовый -> qwen3.8-4b-distill, без раздумий, ~100 секунд
анализ -> liquidai_lfm2.5-2.6b, 32K, 13-14 минут
-> запись страниц в кэш, эпизода в memvid
нет -> ZVec базы знаний, как раньше
-> ответ со ссылками на источники`
Принцип
Одна большая модель на все вопросы - это подписка и чужой сервер. Пара маленьких с разделением ролей - это свой бюджет под каждую задачу: полторы минуты, когда нужен факт, и четверть часа, когда нужно понимание. Лестница из первой части доставляет факты, а распределение мышления из второй решает, сколько думать над ними.
Главный урок серии не в инструментах. Локальная модель на 2.6 миллиарда параметров, посаженная на честный контекст и достаточный бюджет, отвечает дисциплинированнее облачной: не склеивает даты, не тянет связь там, где ее нет, и молчит, когда источников недостаточно. Это не компромисс ради экономии. Это качество, которое вырастишь сам.
Это вторая часть серии о локальных свободных решениях внутри персонального ассистента Hermes Agent.
Ссылки
Первая часть: как собрать свой домашний Perplexity на нулевом бюджете
Модели: Qwen | Gemma от Google | SmolLM3 | Granite от IBM | LFM2.5 от LiquidAI | bge-m3 для отбора
Инструменты: LM Studio | SearXNG | trafilatura
Подписывайся: @hermesagentru
hermes-agent.ru