Дообучаем Gemma 26B на MacBook с 24 ГБ памяти: как устроен голосовой помощник, который работает без интернета
Полгода я собираю голосового помощника для macOS, который не просто отвечает, а выполняет действия на компьютере: открывает папки, ищет файлы, включает фильмы, читает почту. Главное условие, которое я себе поставил, — языковая модель должна работать локально, на том же Mac, без облака и подписок. В статье расскажу, из чего состоит такая система, как я дообучал модель на ноутбуке и на какие грабли наступил по дороге.
Общая схема
Приложение написано на Electron. Вокруг окна работают несколько отдельных процессов:
Слух — процесс на Python: детектор речи (VAD) и распознавание через Whisper в сборке под MLX (Apple Silicon).
Мозг — llama.cpp (
llama-server) с моделью Gemma 4 26B A4B в 4-битной квантизации и LoRA-насадкой в формате GGUF.Голос — синтез речи на Qwen-TTS по эталонной записи голоса.
Помощник на Swift — системные вещи macOS: звук, микрофон, эхо.
Процессы общаются с окном сообщениями. Если какой-то из них падает, его перезапускает сторож, — например, голос поднимается заново примерно за 7 секунд.
Мозг: модель выбирает инструмент, а не пишет ответ
Чат-бот отвечает текстом. Помощнику нужно другое: понять, какое действие выполнить и с какими параметрами. Поэтому на каждую реплику модель возвращает один шаг в фиксированном формате:
{"think": "...", "tool": "list_folder", "args": {"place": "Загрузки"}, "say": "Открываю загрузки", "next": false}tool — имя инструмента из списка (папки, поиск файлов, видео, почта, браузер, заметки и т. д.), args — параметры, say — что сказать вслух, next — нужен ли следующий шаг. Окно выполняет инструмент и, если надо, отдаёт результат обратно модели. Всё, что меняет или удаляет файлы, выполняется только после голосового подтверждения.
Тот же формат шага отдают и внешние модели (ChatGPT, Claude, Gemini), если пользователь их подключил: для них инструменты передаются родными вызовами функций, а окно не замечает разницы. Когда выбран внешний мозг, локальный llama-server останавливается и освобождает около 13 ГБ памяти.
Зачем дообучать
Без дообучения Gemma с подробной системной подсказкой (около 5 тысяч токенов правил и примеров) справлялась неплохо: 89 % верных шагов на моём наборе проверок при медиане 1,94 секунды на ответ. Но длинная подсказка дорога по времени, а ошибки были системные: модель путала похожие команды («поставь» фильм или музыку), забывала параметры, переспрашивала там, где всё ясно.
Идея — перенести правила из подсказки в веса: дообучить небольшую LoRA-насадку и обходиться короткой подсказкой.
Стенд — главное, что я сделал
Прежде чем учить, я собрал стенд: набор фраз с правильными шагами, на котором никогда не учусь. Сейчас в нём 766 разговоров: команды папкам, поиск фильмов, почта, вопросы к документам, цепочки из нескольких реплик, отказы, ослышки распознавания. Стенд заморожен: файл не меняется, и любые две версии мозга сравниваются только на нём.
Два урока, которые дорого мне стоили:
Сравнивать надо весь разговор, а не одну реплику. Пока проверка шла по отдельным репликам, из неё выпадало больше 400 примеров с цепочками — и картина была слишком радужной.
Примеры, собранные по ошибкам стенда, завышают оценку на этом же стенде. Для честной проверки нужен свежий набор, на котором не учились ни модель, ни я сам, когда писал примеры.
Как учить 26B на ноутбуке
Обучение — QLoRA через mlx-lm на MacBook с 24 ГБ общей памяти. Модель 26B — MoE с ~4B активных параметров, в 4 битах она помещается, но впритык:
Длина примера — не больше ~1400 токенов, иначе Metal падает с
Insufficient Memory. Поэтому учить с полной подсказкой на Mac невозможно — ещё одна причина переносить правила в веса. Перед обучением отдельный скрипт отбрасывает примеры, которые не влезают.Скорость — 0,25–0,4 шага в секунду, один проход — около трёх часов. Учу ночью.
Mac засыпает. Однажды он уснул после обучения, и проверка на стенде шла шесть часов. Теперь вся цепочка (обучение → перевод в GGUF → стенд) запускается под одним
caffeinate -i -s, а не каждый шаг отдельно.Обучение срывается по памяти. Рядом работают браузер, индексация Spotlight, своп доходит до 6–7 ГБ. Срывы совпадали с проверкой на валидационной выборке каждые 200–400 шагов. Решение: сохранять точки, автоматически продолжать с последней (
--resume-adapter-file), а валидацию на поздних этапах отключить.Не правьте shell-скрипт, пока он выполняется. Bash читает файл по ходу, и правка на лету уронила цепочку с
syntax errorна строке, которой в момент запуска ещё не было.Для Gemma 4 понадобились две небольшие правки в
mlx-lm, а для перевода насадки в GGUF — отдельное окружение с torch.
Что получилось по версиям
Версия | Стенд | Медиана |
|---|---|---|
Без насадки, полная подсказка | 475 / 533 (89 %) | 1,94 с |
v3, полная подсказка | 487 / 533 (91 %) | 1,63 с |
v4 (новые разделы) | 684 / 766 | — |
v4b, короткая подсказка | 703 / 766 | 1,64 с |
Самое интересное — версия v4. Я добавил в обучение новые разделы (вопросы к документам, чертежи, английский, цепочки), и на них она выросла, а на старых командах просела: «включи <фильм>» стало запускать музыку, «найди фильм» потеряло тип поиска, «свою папку» модель начала путать с папкой заметок, потому что слово часто встречалось в новых примерах.
Помогла не переделка с нуля, а ремонтный проход: 416 примеров ровно на эти случаи плюс случайная выборка старых и новых данных (всего около 1100), доучивание v4 ещё на ~1000 шагов с меньшим шагом обучения (1,5e-5 вместо 3e-5), без валидации. Это заняло около 70 минут — и дало лучший результат за всё время. Обучение с нуля, наоборот, заставляло модель забывать старое.
Слух: имя должно быть услышано
Помощник выполняет команды только после своего имени. Сложность в том, что во время фильма имя тонет в звуке колонок, и Whisper его просто не слышит.
Решение — приглушать фильм не по имени, а по началу речи. Детектор речи считает, на сколько децибел голос громче «плавающего пола» — медленного среднего громкости без речи. Если речь громче фона на 6 дБ и это не эхо собственного голоса помощника, звук фильма сразу становится тише. Если за 2,5 секунды имя не прозвучало — громкость возвращается.
Голос: «сэр» и «сёр»
Синтез детерминирован: для одной и той же фразы всегда выбирается один и тот же «дубль» (seed считается по тексту). Выяснилось, что примерно каждый третий дубль произносит «сэр» как «сёр». Менять написание бесполезно: «сер», «сээр», «с эр» ломают соседние слова.
Помогла акустика. У гласной «э» вторая форманта около 2000–2300 Гц, у «ё» и английского sir — около 750–900 Гц. После синтеза слово находится по таймингам Whisper, гласная измеряется LPC-анализом (12-й порядок), и если F2 слишком низкая — фраза пересинтезируется другим дублем. Проверка занимает около 3 мс; на наборе проверок детектор ловит 39 сбоев из 46. Сам Whisper как детектор не годится: он пишет «сэр» по контексту, даже когда слышно «сёр».
«Второй мозг» без своего индекса
Вопрос «какая сумма в смете на ремонт?» должен находить ответ в документах, хотя пользователь не называет файл. Свою векторную базу я делать не стал — на macOS уже есть Spotlight с полнотекстовым индексом. Слова вопроса (без стоп-слов) ищутся в содержимом документов, копии одного файла схлопываются, два-три лучших файла читаются, и модели отдаются окна текста по ~600 знаков вокруг найденных слов. Ответ — только по этим выдержкам и с названием документа; если в них ничего нет, помощник так и говорит. Поиск занимает 60–700 мс и не ест память.
Чертежи DWG без AutoCAD
Формат DWG закрытый, своего просмотрщика у macOS нет. Его читает LibreDWG, собранная в WebAssembly: чертёж разбирается в отдельном потоке (чтобы окно и голос не замирали) и превращается в SVG. Результат кэшируется по пути и времени изменения файла, поэтому повторное открытие мгновенное.
Выводы
Замороженный стенд важнее любой модели: без него невозможно понять, стало лучше или хуже.
На ноутбуке можно дообучить 26B, если резать длину примеров, сохранять точки и держать машину без сна.
Новые умения лучше доучивать поверх прежней насадки на смеси старых и новых данных; переобучение с нуля забывает старое.
Перенос правил из подсказки в веса дал и точность, и скорость: короткая подсказка с насадкой обогнала длинную.
Следующий шаг — личная память и самообучение: помощник будет запоминать собственные слова пользователя и учиться на фразах, которые не понял. Об этом, если будет интересно, расскажу отдельно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.