22 KiB
Дизайн: создание и корректировка программы тренировок с помощью LLM
Дата составления: 17.10.2026 Привязка к проекту: «Феникс 6X» (см. «Техническое задание.md»). Архитектура: телефон + часы Garmin Fenix 6 + личный сервер (FastAPI + PostgreSQL), данные из Garmin Connect в .fit.
1. Главная идея: LLM — планировщик, код — источник истины
Ключевое архитектурное решение: LLM никогда не является хранилищем программы и не имеет прямого доступа к данным.
- Программа живёт в БД сервера как версионированный JSON-документ строгой схемы.
- LLM только предлагает программу или изменение — в виде структурного JSON (structured output / function calling), не свободного текста.
- Детерминированный валидатор проверяет предложение по схеме и правилам спортивной методологии. Прошёл — новая версия программы. Не прошёл — повторный запрос LLM с текстом ошибки (ограниченное число попыток), затем fallback на правила.
Почему так:
| Риск «LLM всем управляет» | Как снимается |
|---|---|
| Галлюцинации (несуществующие упражнения, веса 500 кг) | Жёсткая JSON-схема + справочник ID упражнений + валидатор |
| Небезопасная прогрессия (резкий скачок нагрузки) | Детерминированные правила-ограничители (разд. 6) |
| Непредсказуемость, «откатить нельзя» | Неизменяемые версии программы + журнал причин изменений |
| Дорого и медленно при каждом чихе | Гибрид: микроправки после тренировки делает простой код, LLM вызывается редко (разд. 3) |
| Зависимость от интернета/провайдера | Офлайн-fallback: чисто правила (разд. 7) |
2. Представление программы: версионированный JSON
Формат хранения — свой JSON (внутренний формат из документа «Форматы_тренировок_и_рекомендации.md»). Иерархия:
Программа (Program, версия неизменяема)
└─ Неделя (Week / мезоцикл: целевой объём, тонус)
└─ Тренировка (Session: день, фокус — верх/низ/тяни-толкай)
└─ Упражнение (Exercise: ссылка на ID из библиотеки)
└─ Подход-план (SetPlan: вес, повторения, RPE-цель, отдых)
Пример фрагмента:
{
"program_id": "prg_2026_10",
"version": 3,
"based_on_version": 2,
"status": "active", // draft | active | archived
"author": "llm", // llm | rules | user
"created_at": "2026-10-17T18:00:00Z",
"weeks": [
{
"week": 1,
"goal": "гипертрофия, объёмная",
"sessions": [
{
"day": "monday",
"focus": "push",
"exercises": [
{
"exercise_id": "bench_press", // только ID из библиотеки!
"sets": [
{"weight_kg": 80, "reps": 8, "target_rpe": 7, "rest_sec": 150}
]
}
]
}
]
}
],
"rationale": "Увеличил вес в жиме на 2.5 кг: 3 тренировки подряд все подходы закрыты при RPE<=7. Объём недельный не менял."
}
Важные свойства:
- Упражнения — только по ID из библиотеки упражнений приложения (иначе сломаются голосовой ввод, статистика и сопоставление с .fit).
- Каждая версия неизменяема. Корректировка = создание новой версии + запись
program_changes(что, почему, решение валидатора, принял ли пользователь). - Выполненные тренировки ссылаются на конкретную версию — история не «поплывёт» после правок.
- Из этой же структуры при необходимости можно собрать экспорт (CSV/JSON/FIT workout).
3. Два контура и три частоты корректировки
Контур A — планирование (редко, LLM)
Когда: холодный старт, смена цели, начало нового мезоцикла (каждые 4–6 недель), явная просьба пользователя.
Что делает LLM: строит программу целиком (или следующий мезоцикл) из профиля, цели, инвентаря, ограничений (травмы) и сводки истории.
Контур B — корректировка (три частоты, гибрид «правила + LLM»)
| Частота | Кто решает | Что корректирует | Пример |
|---|---|---|---|
| После каждой тренировки | Код (правила), без LLM | Вес/повторы следующего занятия по прогрессии | «Все подходы закрыты при RPE≤7 → +2.5 кг»; «не дотянул 2 раза → −5%» |
| Раз в неделю | LLM | Следующая неделя: объём, порядок, замена упражнений, лёгкая неделя | «Сон плохой, пульс покоя +5 → снизить объём на 15%» |
| Раз в мезоцикл (4–6 нед.) | LLM | Перепланирование: делоад, новые упражнения, смена акцентов | «Плато в жиме 3 недели → сменить на жим гантелей, добавить паузы» |
Смысл гибрида: 90% решений — тривиальная прогрессия, которую надёжнее и дешевле считает обычный код. LLM подключается там, где нужна интерпретация нетривиальных сигналов (усталость, плато, пропуск тренировок), — редко, обдуманно и с полным контекстом недели.
4. Конвейер данных: из .fit — в признаки для LLM
LLM не видит сырые .fit и пульсотограммы. Сервер сам считает метрики и отдаёт компактную сводку.
Garmin Connect (.fit)
→ парсер (Session / Lap / Record / Set) [уже есть в ТЗ]
→ каноническое JSON-представление тренировки
→ MetricsEngine: детерминированный расчёт признаков
→ хранилище фактов (PostgreSQL)
→ сборка промпта для LLM (сводка недели/мезоцикла)
Метрики, которые считает MetricsEngine (детерминированно, без LLM):
- Выполнение плана: доля закрытых подходов/тренировок (adherence).
- Тоннаж по упражнению и по мышечным группам (план vs факт).
- Оценочный 1ПМ (формулы Эпли/Бжицки) и его динамика по неделям.
- Фактический RPE — из голосового ввода («Подход 120 на 8» → если план был 120×6 при RPE 8, фактически RPE ниже) и/или короткого опроса после тренировки.
- Кардионагрузка: средний/пиковый пульс за подход, время восстановления пульса между подходами (из Lap/Record .fit — это как раз данные часов).
- Сигналы усталости: падение рабочего веса при том же RPE, рост времени отдыха, падение числа повторений в последних подходах (кластеры «до отказа»).
- Субъективный фидбек из мини-опроса в приложении после тренировки (1 вопрос: «Как прошла? 1–10» + опционально сон/самочувствие) — стоит дёшево и резко повышает качество корректировок.
5. Контракт с LLM: промпт и структурный вывод
Состав запроса
- Системный промпт — роль и методология (правила прогрессии, пределы изменений, приоритет безопасности), требование вернуть строго JSON по схеме.
- Профиль: пол, возраст, вес, цель, стаж, инвентарь, травмы/ограничения.
- Текущая версия программы (JSON).
- Сводка фактов от MetricsEngine: последняя неделя (или мезоцикл) в сжатом виде — adherence, тоннаж по группам, динамика e1PM, пульс-метрики, субъективный фидбек. Сырую историю — не давать.
- Задача: «предложи план на следующую неделю/мезоцикл» или «создай программу с нуля».
Ответ
Только JSON по схеме: новая версия (или дельта-изменения) + обязательное поле rationale («почему») — оно попадает в журнал изменений и показывается пользователю («Феникс повысил вес: ты закрыл все подходы»).
Технически: API провайдера с structured output / function calling + JSON Schema; при неудаче валидации — повтор с текстом ошибки, максимум 2–3 попытки.
Дополнительно (при желании, этап 2)
Двухпроходная схема: первый вызов генерирует план, второй («ревьюер») проверяет его по чек-листу методологии и возвращает замечания. Дёшево и ловит логические ошибки, которые схема не видит (например, два тяжёлых дня на одну группу подряд).
6. Слой валидации и безопасности (детерминированный)
После LLM предложение проходит обязательные проверки. Это обычный код, не LLM:
Схема: все ID упражнений существуют в библиотеке; веса/повторы/отдых в допустимых диапазонах.
Методологические лимиты (пример, настраиваются):
- Рост рабочего веса ≤ +5% (или ≤ +2.5 кг на базовых) в неделю на упражнение.
- Изменение недельного объёма ≤ ±20% (кроме запланированного делаода).
- Не поднимать одновременно объём и интенсивность (одна переменная за раз).
- Делоад — не реже чем каждые 6 недель (объём −40–50%).
- Не более 2 «тяжёлых» (RPE≥8) тренировок на одну мышечную группу в неделю.
- Замена упражнения — только в пределах той же двигательной группы (жим → жим, тяга → тяга); список групп — атрибут библиотеки упражнений.
- Лимит общего числа подходов за тренировку (например, ≤ 25).
Человеко-машинный контур: предложение показывается в приложении в виде диффа («Вес в приседе: 100 → 102.5, потому что…») с кнопками «Принять / Изменить / Отклонить». По мере доверия настраивается «бюджет автобонуса»: например, микроправки весов ±5% применяются сами, смена упражнений и делоады — только по подтверждению.
7. Офлайн-fallback и отказоустойчивость
Требование автономности из ТЗ (разд. 5) распространяется и на планирование:
- Если LLM недоступен (нет сети, лимит API, ошибки) — работает контур правил: прогрессия по формулам + делоад по счётчику недель. Программа продолжает корректироваться безопасно, просто без «умных» интерпретаций.
- Правила-микроправки (после каждой тренировки) вообще не зависят от LLM — это базовый режим, LLM — надстройка.
- Корректировки — фоновая задача (cron после синхронизации тренировки / вечером воскресенья), не в реальном времени, поэтому очереди и ретраи из ТЗ подходят без изменений.
8. Компоненты сервера и схема данных
┌────────────┐ .fit ┌──────────────┐ факты ┌───────────────┐
│ Garmin │─────────▶│ FitParser │────────▶│ PostgreSQL │
│ Connect API│ │ (из ТЗ) │ │ факты, БД │
└────────────┘ └──────────────┘ └──────┬────────┘
│ сводка
┌───────────────┐ план/дельта │ ▲
│ LLM Planner │◀──────────────┘ │
│ (промпт+JSON) │ │
└──────┬────────┘ ┌──────┴────────┐
▼ │ MetricsEngine │
┌───────────────┐ │ (правила, │
│ Validator │──✗ ошибка→ретрай│ прогрессия) │
└──────┬────────┘ └───────────────┘
▼ версия
┌───────────────┐ дифф «принять?» ┌──────────┐
│ ProgramStore │────────────────────▶│ Телефон │
│ (версии JSON) │◀────────────────────│ (UI) │
└───────────────┘ подтверждение └──────────┘
Таблицы (минимум):
programs— id, версия, статус, author, created_at, JSON-тело.program_changes— from/to версия, rationale, отчёт валидатора, решение пользователя (auto/accepted/rejected/edited).workout_facts— факт тренировки из .fit + голосового ввода (уже следует из ТЗ).workout_metrics— посчитанные признаки (тоннаж, e1PM, adherence, пульс-метрики).athlete_state— профиль, цели, текущие веса, счётчики недель до делоада.llm_calls— журнал: входная сводка (хэш), ответ, результат валидации, принят ли. Нужен для анализа качества и отладки промптов.
9. Полный жизненный цикл (пример недели)
- Пн–Сб. Пользователь тренируется: часы пишут .fit, телефон голосом пишет подходы, всё синхронизируется на сервер.
- После каждой тренировки: MetricsEngine обновляет факты → правила двигают веса на следующее занятие (+2.5 кг / удержание / −5%). Без LLM, мгновенно.
- После тренировки — опрос 1–10 в приложении (5 секунд).
- Воскресенье, cron: сервер собирает сводку недели → LLM предлагает план следующей недели (или «без изменений») → валидатор проверяет → дифф в приложении → пользователь принимает (или авто в рамках бюджета доверия).
- Каждые 4–6 недель: LLM строит следующий мезоцикл (делоад/новая волна) на сводке всего цикла.
- Всё попадает в журнал → раз в месяц можно глазами оценить: растут ли e1PM, как часто LLM менял план и помогало ли это (сравнение трендов до/после изменений).
10. Риски и их обработка
| Риск | Обработка |
|---|---|
| LLM «перетренирует» пользователя | Жёсткие лимиты валидатора + делоад-счётчик в правилах (код, не LLM) |
| LLM занижает нагрузку, прогресс остановится | Метрика «плато» в MetricsEngine (нет роста e1PM N недель) триггерит обязательное предложение изменений |
| Пользователь не принимает правки | Бюджет автобонуса + показывать rationale простым языком; правки сохраняются как «edited» и учитываются в следующих промптах |
| Расходы на API | Вызовы редки (1 раз в неделю + мезоцикл), сводка компактная — копейки; микроправки бесплатны (правила) |
| Некачественный голосовой ввод портит факты | Сопоставление голосовых данных с .fit по времени (уже в ТЗ п. 5.1) — расхождения помечаются и не идут в метрики как истина |
| Субъективность RPE | Опрос после тренировки + расчёт по выполненным повторам; расхождения — сигнал, а не приговор |
11. Порядок внедрения (в развитие MVP из ТЗ)
- Шаг 1 (без LLM): ProgramStore с версиями JSON + правила прогрессии после тренировки + MetricsEngine (тоннаж, adherence, e1PM). Система уже полезна.
- Шаг 2: еженедельный вызов LLM (структурный вывод) + валидатор + дифф-подтверждение в приложении + журнал
llm_calls. - Шаг 3: мезоцикловое планирование, двухпроходная схема «генератор+ревьюер», опрос RPE, автоматизация бюджета доверия.
Такой порядок даёт работающую систему на каждом этапе и позволяет накапливать данные для LLM, а не проектировать его вслепую.