Часть 3 из 4
Во второй части я рассказал, как выбрал Piper TTS вместо тяжёлого Silero, как написал полный код новостного агента, как он звучит при первом запуске, и привёл малоизвестные факты о локальных нейросетях. Теперь перехожу к тонкой настройке генерации, решению проблемы с режимом рассуждения в Qwen3, практической оптимизации промптов для озвучки и планам на будущее — RAG-контуру и голосовому ассистенту.
Тонкая настройка генерации: параметры, которые решают всё
Когда я впервые запустил агента с параметрами по умолчанию, результат меня разочаровал. Модель выдавала сводку с маркдауном, нумерованными списками и цифрами. Голос Ирины произносил «один точка первая новость» и «двадцать двадцать шесть год». Слушать это было невозможно.
Проблема не в модели. Проблема в параметрах генерации и в промпте. Я потратил три вечера на подбор комбинации, которая даёт стабильный, чистый, озвучиваемый текст. Вот что влияет на результат.
Температура. Это главный рычаг. При temperature=0.7 модель начинает «креативить»: добавляет вводные слова, риторические вопросы, восклицания, эмфазу. Для письменного текста это бывает уместно. Для озвучки — катастрофа. Синтезатор не понимает риторических конструкций и произносит их с ровной интонацией, что звучит неестественно. При temperature=0.3 модель становится строгой: выдаёт факты, короткие предложения, без украшательств. Оптимально.
Я пробовал temperature=0.1 — слишком сухо, модель иногда пропускает важные детали. Пробовал temperature=0.5 — начинает добавлять оценочные суждения: «к сожалению», «к радости пользователей». Для нейтральной сводки это лишнее. Три десятых — золотая середина.
Максимальное количество токенов. Здесь нюанс, связанный с архитектурой Qwen3. У этой модели есть режим рассуждения — внутренний монолог, в котором она «думает» перед тем, как дать ответ. Этот монолог расходует токены из лимита max_tokens. Если поставить max_tokens=500, модель потратит триста токенов на рассуждение и двести на ответ. Ответ получится обрезанным.
Решение: max_tokens=2000. С запасом. Для сводки из трёх-пяти предложений нужно двести-триста токенов. Остальное — буфер на рассуждение. Да, это замедляет генерацию на пару секунд. Но лучше подождать, чем получить оборванный текст.
Альтернативное решение: отключить рассуждение. Если модель всё равно обрезает ответ, можно явно запретить режим рассуждения через дополнительный параметр в запросе:
response = client.chat.completions.create(
model=LLM_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Новости:\n{news_text}"},
],
temperature=0.3,
max_tokens=2000,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
Параметр enable_thinking: False говорит модели: «Не рассуждай. Просто ответь». Сводка становится короче, генерация быстрее. Но иногда качество чуть падает: модель может упустить неочевидную связь между новостями. Для ежедневного дайджеста это некритично. Я использую этот параметр по умолчанию.
Частота повторений. Параметр frequency_penalty по умолчанию равен нулю. Для суммаризации это нормально: модель не повторяется. Но если в новостях часто встречается одно и то же имя или термин, модель может начать повторять его в каждом предложении. В этом случае frequency_penalty=0.2 решает проблему. Я поставил это значение в конфиге.
Присутствие. Параметр presence_penalty штрафует модель за повторение уже упомянутых тем. Для сводки из пяти новостей это полезно: не даёт модели трижды пересказать одну и ту же новость разными словами. Значение presence_penalty=0.1 — мягкий штраф, не искажающий смысл.
Режим рассуждения Qwen3: проблема и три решения
Отдельно остановлюсь на режиме рассуждения, потому что это самая неочевидная ловушка для тех, кто впервые работает с Qwen3.
Что происходит. Модель Qwen3 обучена с механизмом «цепочки рассуждений». Перед тем как дать финальный ответ, она генерирует внутренний монолог: анализирует задачу, взвешивает варианты, проверяет себя. Этот монолог не показывается пользователю в интерфейсе, но расходует токены и время.
Для сложных задач — математика, логика, программирование — это благо. Модель «думает» и даёт более точный ответ. Для суммаризации новостей это вред. Задача тривиальная: выделить главное и пересказать. Рассуждать здесь не о чем. Но модель по инерции тратит токены на внутренний анализ.
Вот три решения, от простого к сложному.
Решение первое: увеличить лимит токенов. Ставим max_tokens=2000 вместо пятисот. Модель тратит триста токенов на рассуждение, триста на ответ — и всё укладывается в лимит. Просто и надёжно. Недостаток: генерация на две-три секунды дольше.
Решение второе: отключить рассуждение через параметр. Передаём extra_body={"chat_template_kwargs": {"enable_thinking": False}}. Модель пропускает внутренний монолог и сразу генерирует ответ. Быстрее, короче, но иногда чуть менее точно. Для сводки новостей — более чем достаточно.
Решение третье: использовать instruction-tuned версию. Модель qwen3-it-4B не имеет режима рассуждения по умолчанию. Она обучена сразу давать ответ по инструкции. Минус: на сложных задачах чуть слабее базовой. Но для суммаризации разница незаметна.
Я использую второе решение по умолчанию. Если замечаю, что сводка стала неестественно короткой или упускает важную новость — переключаюсь на первое. Третье держу как резерв на случай, если второе перестанет работать после обновления модели.
Оптимизация промпта для TTS: неочевидные правила
Системный промпт, который я привёл ранее, — это финальная версия. Но к ней я пришёл через серию итераций. Каждая строчка появилась не сразу. Рассказываю, какие проблемы я решал и как.
Проблема: модель выводит заголовки жирным. Первый вариант сводки выглядел так: «Первая новость. Компания представила новый чип». Piper произносит звёздочки вслух или делает неестественную паузу. Решение: строка промпта «Без маркдауна».
Проблема: модель нумерует новости. «1. Первая новость. 2. Вторая новость». Голос произносит «один точка» или делает паузу. Слушается как перечисление пунктов в инструкции, а не как связный текст. Решение: строка «Без списков, нумерации».
Проблема: модель пишет цифры. «В 2026 году», «рост на 15%», «более 100 компаний». Синтезатор читает цифры непредсказуемо. Решение: строка «Числа пиши словами». Модель пишет «в две тысячи двадцать шестом году», «рост на пятнадцать процентов», «более ста компаний».
Проблема: модель обрывает предложение. Если лимит токенов исчерпан, генерация останавливается посреди фразы. Голос обрывается на полуслове. Звучит как помеха. Решение: строка «Не обрывай предложения». Модель старается завершить мысль, даже если для этого нужно чуть сократить предыдущие предложения.
Проблема: модель добавляет приветствие. Иногда сводка начинается с «Доброе утро! Сегодня в мире технологий…». Это мило, но каждый день одно и то же приветствие раздражает. Решение: я убрал из промпта любую инструкцию о приветствии и добавил в постобработку удаление первой строки, если она содержит слово «доброе» или «привет».
Проблема: модель добавляет заключение. «Подводя итоги дня…» или «На этом всё, до завтра». Для ежедневного формата это утомительно. Решение: строка промпта «Не добавляй вступление и заключение. Только факты».
Финальный промпт после всех итераций:
Ты — редактор новостного дайджеста.
1. Выдели 3-5 самых важных новостей из списка ниже.
2. Каждую новость опиши одним предложением.
3. Числа пиши словами (TTS не любит цифры).
4. Без маркдауна, списков, нумерации, заголовков.
5. Не обрывай предложения.
6. Не добавляй вступление, приветствие и заключение. Только факты.
7. Пиши связным текстом, как диктор читает новости.
Седьмой пункт появился последним. Без него модель иногда выдаёт набор разрозненных предложений, которые не связаны между собой. С ним — получается связный монолог, как у радиоведущего. Голос Ирины произносит это естественно, с правильными паузами между предложениями.
Сравнение моделей на практике: таблица наблюдений
Я не просто выбрал Qwen3-4B. Я тестировал каждую модель на одних и тех же десяти новостях, с одним и тем же промптом. Вот что получилось.
| Модель | Время генерации | Качество русского | Следование промпту | Режим рассуждения | Итог |
|---|---|---|---|---|---|
| Qwen3-4B | 8-12 секунд | Отличное | Хорошее | Есть, тратит токены | Основная |
| Qwen3-it-4B | 7-10 секунд | Отличное | Отличное | Нет | Альтернатива |
| Llama-3.2-3B | 6-8 секунд | Среднее | Среднее | Нет | Не подходит |
| Phi4-mini-it-4B | 7-9 секунд | Хорошее | Хорошее | Нет | Резерв |
| Qwen3-8B | 18-25 секунд | Отличное | Отличное | Есть | Избыточна |
Llama-3.2-3B генерирует текст быстро, но русский язык хромает. Фразы строятся по кальке с английского: «Компания объявила о запуске нового продукта, который является следующим шагом в их стратегии». Естественный русский был бы: «Компания представила новый продукт — следующий шаг в её стратегии». Для чтения глазами разница терпима. Для озвучки — режет ухо.
Phi4-mini-it-4B лучше, но отдельные формулировки звучат как машинный перевод. «В области полупроводников наблюдается значительный сдвиг» вместо «В полупроводниковой отрасли наметился сдвиг». Мелочи, но из них складывается общее впечатление.
Qwen3-8B выдаёт лучший текст. Но восемнадцать-двадцать пять секунд на сводку из пяти предложений — это слишком. Для утреннего запуска, когда хочется услышать новости, пока ставишь чайник, а не ждать полминуты. Четырёх миллиардов хватает.
Логи и мониторинг: как понять, что всё работает
Агент пишет лог в ~/.config/news-agent/agent.log. Каждая строка с временной меткой. Вот как выглядит типичный запуск:
2026-09-28 07:30:01 [INFO] Загружаю новости из 3dnews.ru
2026-09-28 07:30:03 [INFO] Загружено 10 новостей
2026-09-28 07:30:03 [INFO] Отправляю 10 новостей в модель qwen3:4b
2026-09-28 07:30:14 [INFO] Сводка готова (487 символов)
2026-09-28 07:30:14 [INFO] Синтезирую речь (487 символов)
2026-09-28 07:30:15 [INFO] Аудио сохранено: /home/user/.config/news-agent/audio/digest.wav (234567 байт)
2026-09-28 07:30:15 [INFO] Воспроизвожу /home/user/.config/news-agent/audio/digest.wav
Двенадцать секунд от загрузки до начала синтеза. Плюс секунда на синтез. Плюс время воспроизведения. Итого: пятнадцать секунд до первого звука.
Если что-то идёт не так, лог покажет где именно. Если строка «Загружаю новости» есть, а «Загружено» нет — проблема с сетью или с RSS. Если «Отправляю» есть, а «Сводка готова» нет — сервер не отвечает или модель зависла. Если «Синтезирую» есть, а «Аудио сохранено» нет — проблема с Piper.
Я добавил в скрипт проверку на каждом этапе и понятные сообщения об ошибках. Если сервер не запущен: «Ошибка: сервер на порту 52625 не отвечает. Запустите: flm serve qwen3:4b». Если модель не найдена: «Ошибка: модель не загружена. Выполните: flm pull qwen3:4b». Если Piper не установлен: «Ошибка: бинарник не найден. Установите: pip install piper-tts».
Безопасность: локальный агент и приватность
Один из главных аргументов в пользу локального решения — приватность. Но что это означает на практике?
Что уходит в интернет. Только запрос к RSS-ленте 3DNews. Это обычный HTTP-запрос без авторизации, без куки, без идентификаторов. Сервер 3DNews видит, что кто-то скачал RSS. Не видит кто, не видит зачем, не видит что с этим делают дальше.
Что не уходит в интернет. Текст новостей, который обрабатывает модель. Промпт. Параметры генерации. Результат суммаризации. Аудиофайл. Логи. Ничего из этого не покидает ноутбук.
Что хранится локально. Аудиофайл последней сводки в ~/.config/news-agent/audio/. Лог в ~/.config/news-agent/agent.log. Модель в ~/.config/flm/models/. Всё это на моём диске, под моим контролем. Если хочу — удаляю. Никто не придёт и не скажет: «Мы изменили политику хранения данных».
Что я не отправляю в облако. Мне не нужен аккаунт. Не нужен ключ доступа. Не нужна подписка. Не нужно согласие с условиями использования, которые изменятся в одностороннем порядке. Агент работает на моём железе, по моим правилам, с моими данными.
Это не паранойя. Это вопрос контроля. Я хочу знать, где мои данные. Я хочу знать, кто их видит. Я хочу знать, что с ними происходит. Локальный агент даёт мне этот контроль полностью.
Энергопотребление и нагрев: практические наблюдения
Один вопрос, который мне задавали часто: «Ноутбук сильно греется? Вентиляторы шумят?»
Отвечаю с цифрами. Во время генерации сводки — восемь-двенадцать секунд — процессор потребляет около восемнадцати ватт. Из них NPU — пять-шесть ватт. Температура процессора поднимается на три-пять градусов выше покоя. Вентиляторы чуть ускоряются, но не выходят на слышимый уровень. Через пятнадцать секунд всё возвращается в норму.
Для сравнения: запуск облачного браузера с пятью вкладками нагружает процессор сопоставимо. Просмотр видео в разрешении 4K — больше. Игровой сценарий с нагрузкой на GPU — в пять-десять раз больше.
На практике я не замечаю работу агента. Ноутбук не гудит. Не греется. Не тормозит. Полминуты утром — и всё. Остальное время система в покое.
Батарея. Если ноутбук работает от аккумулятора, один запуск агента расходует около 0.005 ватт-часа. При батарее на шестьдесят ватт-часов это ноль целых восемь сотых процента заряда. Незаметно. Я никогда не думаю о батарее в контексте агента.
Что дальше: RAG-контур и умный поиск по архиву
Ежедневная сводка — это первый этап. Но мне хотелось большего. Не просто «что сегодня», а «что писали про конкретную тему за последний месяц». Не просто «дай мне новости», а «найди все упоминания AMD NPU за неделю и сделай подборку».
Это задача для RAG — Retrieval-Augmented Generation. Идея: накапливать новости в векторной базе данных, а при запросе искать релевантные записи и подавать их модели как контекст.
Архитектура RAG-контура.
Первый слой: сбор. Каждые шесть часов агент загружает RSS, извлекает заголовки и первые абзацы, сохраняет в архив. Не только для суммаризации — для накопления.
Второй слой: эмбеддинги. Каждая новость превращается в вектор — числовое представление смысла. Для этого нужна модель эмбеддингов. В FastFlowLM доступна embed-gemma:300m — лёгкая модель на триста миллионов параметров, которая генерирует векторы размерностью семьсот шестьдесят восемь. Работает на NPU, быстро.
Третий слой: хранилище. Векторы складываются в базу данных. Для локального использования я выбрал ChromaDB. Он работает без сервера, хранит данные на диске, поддерживает фильтрацию по метаданным. Альтернатива — Qdrant, если нужен полноценный сервер. Или FAISS, если хочется минимализма.
Четвёртый слой: запрос. Пользователь задаёт вопрос: «Что писали про Apple за последний месяц?» Вопрос превращается в вектор. Вектор сравнивается со всеми новостями в базе. Топ-десять самых похожих подаются в Qwen3-4B как контекст. Модель генерирует связный ответ.
Пятый слой: озвучка. Ответ синтезируется через Piper и воспроизводится.
Код-заготовка для RAG:
import chromadb
from openai import OpenAI
client = OpenAI(base_url="http://localhost:52625/v1", api_key="sk-local")
def embed(text: str) -> list[float]:
response = client.embeddings.create(
model="embed-gemma:300m",
input=text,
)
return response.data[0].embedding
chroma = chromadb.PersistentClient(path="~/.config/news-agent/rag")
collection = chroma.get_or_create_collection("news")
def add_news(news_items):
for item in news_items:
collection.add(
documents=[item["title"] + "\n" + item["summary"]],
metadatas=[{"link": item["link"], "date": item["published"]}],
ids=[item["link"]],
)
def search(query: str, top_k: int = 10):
query_vec = embed(query)
results = collection.query(query_embeddings=[query_vec], n_results=top_k)
return results["documents"][0]
План развития по этапам:
Этап первый — текущий. Ежедневная сводка свежих новостей. Работает.
Этап второй. Накопление архива в ChromaDB. Каждые шесть часов — загрузка и индексация. Через месяц в базе будет полторы-две тысячи новостей. Через три месяца — пять-шесть тысяч.
Этап третий. Поиск по архиву через командную строку. Вопрос — ответ. Без голоса.
Этап четвёртый. Голосовой поиск. Вопрос голосом — ответ голосом. Здесь понадобится распознавание речи. Whisper на NPU — логичный кандидат.
Этап пятый. Интеграция с мессенджером. Телеграм-бот или локальный веб-интерфейс. Запрос с телефона — ответ из ноутбука. Без облака.
Голосовой ассистент: следующий рубеж
Если всё пойдёт по плану, через полгода у меня будет не просто новостной агент, а полноценный голосовой ассистент. Локальный. Приватный. Работающий без интернета.
Что для этого нужно.
Распознавание речи. Модель Whisper, адаптированная для NPU. Преобразует голос в текст. Работает локально. Задержка — две-три секунды для короткой фразы.
Обработка запроса. Qwen3-4B получает текст и решает, что делать. Если вопрос о новостях — ищет в RAG-базе. Если общий вопрос — отвечает из своих знаний. Если команда — выполняет действие.
Синтез ответа. Piper озвучивает результат.
Контекст диалога. Модель помнит последние пять-десять реплик и может поддерживать связный разговор. Не бесконечный — память ограничена. Но для коротких сессий хватает.
Это не фантастика. Все компоненты уже существуют. Whisper работает на локальном железе. Qwen3 отвечает на вопросы. Piper говорит. Осталось связать их в единую цепочку и добавить распознавание команд.
Малоизвестный факт: архитектура Whisper, которую разработали в лаборатории, была задумана как модель, обученная на шестистах восьмидесяти тысячах часов размеченного аудио на девяноста девяти языках. При этом для русского языка в обучающей выборке было около восьми тысяч часов — достаточно для хорошего качества распознавания, но заметно меньше, чем для английского. Отсюда и рекомендация: для русского голоса лучше использовать модель размера «medium» или «large», а не «base». «Base» на русском ошибается чаще.
Интеграция с домашней автоматизацией
Ещё одна идея, которая крутится в голове: связать агента с умным домом. Не через облако. Локально.
Сценарий: утром, в семь тридцать, агент включается, генерирует сводку и воспроизводит её через колонку в кухне. Не через динамик ноутбука, а через отдельную колонку по локальной сети. По протоколу. Без стриминговых сервисов. Без подписки. Без интернета.
Технически это несложно. Вместо aplay — отправка WAV-файла на колонку по протоколу. Или генерация на ноутбуке и воспроизведение на колонке. Задержка — полсекунды. Незаметно.
Или другой сценарий: голосовая команда «Новости» из любой точки квартиры. Микрофон ловит, передаёт на ноутбук, тот распознаёт, генерирует, озвучивает через колонку. Цикл — три-четыре секунды. Локально. Приватно. Без единого облачного запроса.
Это уже не утренний ритуал. Это полноценный голосовой интерфейс для дома. Который работает без интернета. Который не подслушивает. Который не передаёт данные. Который принадлежит мне.
Работа с несколькими источниками новостей
Сейчас агент читает один источник. Но один источник — это одна точка зрения. Один редактор. Один алгоритм отбора. Мне хочется шире.
Решение: несколько RSS-лент в конфигурации. Не три, не пять — десять-пятнадцать. Из разных тематик: технологии, наука, железо, софт, космос. Агент загружает все, смешивает, прогоняет через модель. Модель выделяет главное из общего потока.
Код для нескольких источников:
RSS_SOURCES = [
"https://3dnews.ru/breaking/rss/",
"https://www.ixbt.com/export/news.rss",
"https://habr.com/ru/rss/news/",
]
def fetch_all_news():
all_entries = []
for url in RSS_SOURCES:
feed = feedparser.parse(url)
all_entries.extend(feed.entries[:5])
return all_entries
Из каждого источника — по пять записей. Итого пятьдесят-семьдесят пять новостей. Модель выбирает пять-семь самых важных. Сводка становится разнообразнее. Появляются перекрёстные связи: одна тема в разных источниках подаётся по-разному, и модель может это отразить.
Единственное ограничение: время генерации растёт. Десять новостей — восемь секунд. Пятьдесят — двадцать-двадцать пять секунд. Для утреннего запуска терпимо. Для интерактивного запроса — многовато. Решение: кэшировать. Загружать и суммаризировать раз в шесть часов, а при запросе отдавать готовый результат.
Обработка ошибок в бою: что я узнал за месяц
Месяц работы агента — это месяц мелких поломок и быстрых исправлений. Вот типичные ситуации.
Сервер не запущен. Утром таймер срабатывает, но flm serve не работает. Агент пишет в лог: «Сервер не отвечает». Решение: добавить в systemd-юнит команду ExecStartPre, которая запускает сервер перед агентом. Или запускать сервер как отдельный сервис, который стартует при входе в систему.
Модель не в памяти. После перезагрузки ноутбука модель не загружена автоматически. Первый запрос к серверу возвращает ошибку. Решение: добавить ExecStartPre с командой flm pull qwen3:4b, если модели нет в кэше. Или держать сервер постоянно запущенным.
RSS недоступен. Редко, но бывает: сервер 3DNews на минуту-две не отвечает. Агент падает. Решение: добавить повтор с задержкой. Три попытки по пять секунд. Если все три не удались — пропустить запуск и записать в лог. Не критично: завтра будет новый запуск.
Аудиофайл занят. Если предыдущее воспроизведение ещё идёт, а новый запуск пытается писать в тот же файл. Решение: уникальное имя файла с датой и временем. digest_2026-09-28_0730.wav. Конфликтов нет.
Память заполнена. Модели занимают три гигабайта. Плюс система. Плюс браузер. На тридцати двух гигабайтах оперативки это терпимо, но если моделей много — может стать тесно. Решение: держать только одну-две модели. Остальные удалить через flm rm.
Часть 3 из 4.
В следующей, финальной части: полный чек-лист настройки с нуля за один вечер, FAQ с ответами на частые вопросы, справочник команд для диагностики и восстановления, итоговые рекомендации и выводы. А также размышления о том, куда движется рынок локальных нейросетей и почему через два-три года каждый второй ноутбук будет работать так же, как мой.


Добавить комментарий