Files
view-mate/TASK-llm-interface.md
subochev 7527cf8848 LLM: интерфейс + две реализации (Qwen удалённо / Gemma 4 E2B локально) + переключатель
- LlmClient интерфейс; RemoteLlmClient (OkHttp, llm.binom.pw) и LocalLlmClient (LiteRT-LM, Gemma 4 E2B)
- LocalLlm: Gemma4E2B (SHA-256, GPU/CPU-бэкенды), buildLocalLlmPrompt (SYSTEM→systemInstruction, TOOL→user), GemmaModelDownloader (стриминг, прогресс, фолбэк источников), спекулятивное декодирование
- LlmPrefs (персистентность выбора), PhoneApp: фабрика по llmChoice, пересоздание ассистента, скачивание/отмена
- UI: GlassesInfoScreen — выбор сервер/локальная + прогресс; ChatScreen — индикатор активной модели
- Тесты: +12 (LlmLocalTest) — phone 154 зелёных
2026-08-23 02:52:22 +03:00

9.0 KiB
Raw Permalink Blame History

Задача: интерфейс LLM + две реализации (Qwen удалённо / Gemma E2B локально) + переключатель

Дизайн — TASK-local-llm.md. Заметка с разбором litertlm (скопирована в репо, читай её): docs/litertlm-openassistant-notes.md. Полная архитектура агента (тулсеты/скиллы) — TASK-agent-tools.md. Реализуй в app-phone.

Контекст

  • Сейчас LlmClient — класс с прямым OkHttp к https://llm.binom.pw/v1/chat/completions (Qwen3.8-27B-NVFP4). Файл: app-phone/src/main/kotlin/pw/binom/viewmate/phone/agent/LlmClient.kt (НЕ app-phone/agent/ — путь с src/main/kotlin!). Вызывается из PhoneApp.ensureAssistant() (app-phone/src/main/kotlin/pw/binom/viewmate/phone/PhoneApp.kt) — создаётся один раз и передаётся в Assistant(chat = { history, system -> llm.chat(history, system) }).
  • Нужно: интерфейс + две реализации + ручной глобальный переключатель в UI телефона. В будущем всё должно работать оффлайн.
  • Модель для локальной: Gemma 4 E2B (2B), формат .litertlm (LiteRT LM, Google). Разбор — в docs/litertlm-openassistant-notes.md (скопирован в репо — читай его, не /root/notes/).

Что сделать

1. Интерфейс LlmClient

Превратить LlmClient в интерфейс (или добавить интерфейс-обёртку):

interface LlmClient {
    suspend fun chat(messages: List<ChatMessage>, system: String? = null): String
}
  • Существующий класс → RemoteLlmClient (реализация интерфейса, код не менять по сути — только implements).
  • Все использования (PhoneApp.ensureAssistant) — через интерфейс.

2. Локальная реализация — LocalLlmClient (Gemma 4 E2B, LiteRT LM)

По заметке /root/notes/openassistant-lite-llm.md (секция «Две нейронки + токенайзер», «Как повторить», «Скорость»):

  • Зависимость: com.google.ai.edge.litertlm:litertlm-android:0.14.0 в app-phone/build.gradle.kts (репозиторий — google(), у нас в settings должен быть; проверить). ВАЖНО: litertlm 0.14.0 требует kotlinx-coroutines 1.11.0 (иначе NoSuchMethodError close$default на SendChannel — подтверждено автором Modern-Apps, см. его build.gradle.kts:47-48). Поднять версию kotlinx-coroutines в gradle/libs.versions.toml с 1.10.2 до 1.11.0 — проверить, что ничего не ломается (тесты после этого обязаны быть зелёными).
  • Модель: файл gemma-4-E2B-it.litertlm (SHA256 181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c), ~0.8 ГБ. Хранить в getExternalFilesDir(null) (как у автора). НЕ класть в APK (большой) — загружается отдельно.
  • Загрузка: если файла нет — скачать с зеркала (автор: https://data.vayunmathur.com/models/, но у нас доступнее HF: https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/<файл>), SHA256-проверка. Сделай через OkHttp (уже есть в проекте) + поток в файл, с прогрессом (лог).
  • Запуск (из заметки):
    • Engine(EngineConfig(modelPath, backend = GPU(), visionBackend = GPU(), audioBackend = CPU(), cacheDir))
    • ExperimentalFlags.enableSpeculativeDecoding = true ПЕРЕД engine.initialize() (спекуляция у Gemma 4 встроена в архитектуру — MTP, флаг включает рантайм-путь)
    • ConversationConfig(systemInstruction, initialMessages, tools?, automaticToolCalling?) + sendMessageAsync стриминг
  • Chat: перевести List<ChatMessage> + system в формат litertlm (initialMessages — история, systemInstruction — system). Вернуть накопленный текст ответа. Стриминг можно агрегировать (не обязательно гнать по частям наружу — контракт chat(): String).
  • Обработка ошибок: модель не загружена → понятное сообщение («локальная модель не скачана, скачайте в настройках»), инициализация упала → текст ошибки. НЕ падать.
  • Threading: Engine litertlm требует аккуратности с потоками — смотри пример автора (InferenceService.kt). Скорее всего вызовы идут из корутины — оберни в withContext(Dispatchers.IO) и соблюдай требования движка.
  • Время жизни: Engine создаётся один раз (как ассистент), переиспользуется; инициализация — лениво при первом вызове (модель может весить ~1-2 ГБ в RAM).

3. Переключатель (ручной, глобальный)

  • Состояние: LlmmodelChoice = REMOTE | LOCAL (или enum с именами). Хранить в SharedPreferences (переживает рестарт). Global: влияет на весь ассистент, не на сессию.
  • Фабрика: PhoneApp.ensureAssistant() выбирает реализацию по сохранённому выбору: REMOTE → RemoteLlmClient, LOCAL → LocalLlmClient. Переключение — пересоздать ассистента (или хотя бы llm-ссылку) при изменении выбора.
  • UI: на экране «Информация об очках» (GlassesInfoScreen, там уже есть InfoRow) — или лучше отдельный блок в GlassesScreen — добавить переключатель «Модель ассистента: Qwen (сервер) / Gemma (локально)». Компонент: два варианта (радио-кнопки или сегментированная кнопка), плюс статус: для LOCAL — «модель скачана/не скачана», кнопка «Скачать модель» (с прогрессом), для REMOTE — «сервер».
  • Индикатор активной модели — в чате (ChatScreen) мелкой подписью «модель: qwen/gemma» (не обязательно, но желательно — пользователь должен видеть, кто отвечает).

4. Проверка/тесты

  • Юнит-тесты (JVM, без Android — что можно):
    • Выбор реализации по настройке (фабрика/логика выбора — чистая часть).
    • Формирование промпта для litertlm (маппинг ChatMessage → initialMessages) — если вынести в чистую функцию.
    • SHA256-проверка файла (чистая функция).
  • Сборка: ./gradlew :app-phone:testDebugUnitTest и :app-phone:assembleDebug — зелёные (все старые 142 теста остаются зелёными).

Ограничения

  • Код только Kotlin
  • НЕ трогать механики тулсетов/скиллов (AgentToolkit/Skills/Toolsets), Assistant.kt — менять только точку создания LlmClient
  • НЕ ломать существующие экраны
  • НЕ коммитить

Проверка на железе (после твоей части)

  1. Сборка + установка на телефон (142, adb).
  2. Загрузка модели (нужен доступ к HF — проверь, скачай на машину, залей через adb, если HF с телефона недоступен).
  3. Переключение REMOTE ↔ LOCAL, общение с ассистентом на каждой.
  4. Оффлайн-тест: svc wifi disable → локальная модель отвечает, удалённая — понятная ошибка.
  5. Замер скорости локальной (ток/с из логов).

Ответ

Опиши: какие файлы создал/изменил, как устроен интерфейс, как инициализируется Engine, где переключатель, что осталось проверить на железе (особенно: работает ли litertlm на нашем устройстве, скачивается ли модель).