Реализация AI-ассистента для подбора тренировок в мобильном приложении
Подбор тренировок — это персонализация на основе данных о теле, цели, доступном оборудовании и истории занятий. Без этих данных AI даст стандартный план «3x10 приседаний», который не учитывает травму колена, тренировки дома без инвентаря и то, что вчера была ногодень.
Сбор данных для персонализации
Качество рекомендаций напрямую зависит от полноты профиля. Минимальный набор:
- Цель: похудение, набор массы, выносливость, реабилитация
- Уровень: начинающий, средний, продвинутый
- Доступное оборудование (мультивыбор: штанга, гантели, турник, TRX, только вес тела)
- Ограничения/травмы: конкретные зоны (поясница, колени, плечи)
- Доступное время на тренировку
- Частота занятий в неделю
- История тренировок: что делал последние 7 дней
Из нативных источников добавляем данные HealthKit (iOS) или Health Connect (Android 14+):
// iOS: загрузка тренировок за последнюю неделю из HealthKit
func fetchRecentWorkouts() async throws -> [HKWorkout] {
let workoutType = HKObjectType.workoutType()
let predicate = HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .day, value: -7, to: Date())!,
end: Date()
)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
return try await withCheckedThrowingContinuation { continuation in
let query = HKSampleQuery(
sampleType: workoutType,
predicate: predicate,
limit: 20,
sortDescriptors: [sortDescriptor]
) { _, samples, error in
if let error { continuation.resume(throwing: error); return }
continuation.resume(returning: (samples as? [HKWorkout]) ?? [])
}
healthStore.execute(query)
}
}
Генерация тренировочного плана
Промпт с полным контекстом:
func buildWorkoutPrompt(profile: UserProfile, recentWorkouts: [WorkoutSummary]) -> String {
let workoutHistory = recentWorkouts.map {
"\($0.date.formatted()): \($0.type), \($0.duration) min, \($0.muscleGroups.joined(separator: "+"))"
}.joined(separator: "; ")
return """
Create a workout plan for today.
User profile:
- Goal: \(profile.goal)
- Level: \(profile.level)
- Available time: \(profile.availableMinutes) minutes
- Equipment: \(profile.equipment.joined(separator: ", "))
- Limitations: \(profile.limitations.isEmpty ? "none" : profile.limitations.joined(separator: ", "))
Recent workouts (last 7 days): \(workoutHistory.isEmpty ? "none" : workoutHistory)
Rules:
- Avoid muscle groups trained in last 48 hours
- If limitation mentions specific area (knee, back), exclude exercises for that area
- Balance push/pull if goal is hypertrophy
Return JSON: {
name, totalMinutes, exercises: [{
name, sets, reps, restSeconds, muscleGroups: [], notes, videoSearchQuery
}]
}
"""
}
videoSearchQuery — ключевое поле. Им формируем поисковый запрос для YouTube/Vimeo, чтобы показать демо упражнения прямо в карточке.
Адаптация по ходу тренировки
Ассистент не должен молчать после выдачи плана. Три триггера для адаптации:
- Пользователь нажал «Слишком тяжело» → LLM заменяет упражнение на более лёгкое
- Прошло времени больше запланированного → предлагает сократить оставшиеся сеты
- Все упражнения выполнены за 15 минут раньше → добавляет бонусный блок
// Android - адаптация упражнения
suspend fun substituteExercise(
exercise: Exercise,
reason: SubstitutionReason,
availableEquipment: List<String>
): Exercise {
val prompt = """
The user cannot do "${exercise.name}".
Reason: ${reason.description}
Available equipment: ${availableEquipment.joinToString(", ")}
Target muscles: ${exercise.muscleGroups.joinToString(", ")}
Suggest ONE simpler/alternative exercise that targets the same muscles.
Return JSON: {name, sets, reps, restSeconds, muscleGroups: [], notes}
"""
val response = openAIClient.chat(
model = "gpt-4o-mini",
messages = listOf(Message("user", prompt)),
responseFormat = ResponseFormat.JsonObject
)
return json.decodeFromString(response.content)
}
Отдых и восстановление
AI-ассистент должен рекомендовать не тренироваться, когда это нужно. Если HealthKit показывает высокий HKQuantityType.heartRateVariability снижение или Sleep Analysis меньше 6 часов — план меняется на растяжку или активный отдых.
func shouldRecommendRestDay(healthData: HealthSnapshot) -> Bool {
return healthData.sleepHours < 6.0 ||
healthData.restingHeartRate > healthData.averageRestingHR * 1.1 ||
healthData.hrvTrend == .declining
}
Это не AI-запрос — простая эвристика по нативным данным. LLM здесь не нужен.
Ориентиры по срокам
Базовый генератор плана без HealthKit — 3–5 дней. Полноценный ассистент с HealthKit/Health Connect, адаптацией по ходу тренировки, таймером отдыха и историей занятий — 4–6 недель.







