Apple Foundation Models: on-device LLM в iOS-приложении#
Зачем Apple вообще дала разработчикам LLM внутри iOS#
Год назад, на WWDC 2025, Apple открыла доступ к собственной on-device языковой модели — той же, что питает Apple Intelligence — через новый фреймворк Foundation Models. Никакого API-ключа, никакого счёта за токены, никакой отправки данных пользователя на сервер. Модель уже лежит на устройстве, и с iOS 26 / macOS 26 / iPadOS 26 / visionOS 26 к ней можно обратиться из своего кода парой строк на Swift.
Это не рекламный слоган — это реально изменило то, какие фичи стало осмысленно строить в приложении. Я практикующий iOS-разработчик, и в этой статье не будет пересказа keynote: только API, которые я проверял руками, ограничения, на которые реально натыкаешься, и честный разговор о том, когда on-device LLM — это правильный выбор, а когда нет.
К середине 2026 года framework прошёл ещё один цикл: на WWDC 2026 Apple показала третье поколение моделей Apple Foundation Models (AFM 3) и, отдельно, возможность подключать к тому же Swift API сторонние LLM-провайдеры — тема отдельной сессии «Bring an LLM provider to the Foundation Models framework». Это важный сигнал: Apple превращает Foundation Models не просто в обёртку над своей моделью, а в единый интерфейс для on-device и гибридного инференса.
Базовая on-device модель — плотная, около 3 миллиардов параметров (Apple называет её AFM 3 Core в текущем поколении). Она заточена под конкретный набор задач: суммаризация, извлечение сущностей, понимание и переработка текста, короткие диалоги, генерация коротких творческих текстов. Это не модель для чата "обо всём на свете" и не замена поисковику — Apple прямо пишет в документации, что она не предназначена быть источником общих фактических знаний о мире.
На части устройств (в актуальном поколении — там, где есть 12 ГБ оперативной памяти, то есть на топовых iPhone) доступна расширенная модель — AFM 3 Core Advanced, около 20 млрд параметров с разреженной архитектурой, которая активирует лишь 1–4 млрд параметров на запрос. Для разработчика это означает: одна и та же строчка кода на разных устройствах может выполняться заметно разным «мозгом», и рассчитывать на идентичное качество ответа между iPhone 15 Pro и iPhone 17 Pro не стоит.
Работа с моделью строится вокруг двух типов:
SystemLanguageModel— точка входа к модели, её доступности и специализированным режимам (useCase, например.contentTaggingдля тегирования и извлечения сущностей "из коробки").LanguageModelSession— сессия диалога: именно через неё отправляются запросы, поддерживается история (transcript), инструкции и инструменты.
Availability-гейтинг: три причины «не работает» — и это не одна проблема#
Самая частая ошибка в статьях про Foundation Models — трактовать недоступность модели как одно состояние. На практике SystemLanguageModel.default.availability возвращает одну из трёх принципиально разных причин, и путать их — гарантированный способ сделать фичу, которая ощущается как сломанная:
import FoundationModels
func checkFoundationModelsAvailability() -> String {
switch SystemLanguageModel.default.availability {
case .available:
return "✅ Модель готова к использованию"
case .unavailable(.deviceNotEligible):
// A13 или старше — фича на этом устройстве не появится никогда
return "❌ Устройство не поддерживает Apple Intelligence"
case .unavailable(.appleIntelligenceNotEnabled):
// Совместимое устройство, но Apple Intelligence выключен в настройках
return "⚠️ Включите Apple Intelligence в настройках"
case .unavailable(.modelNotReady):
// Модель ещё скачивается — состояние временное
return "⏳ Модель загружается, попробуйте позже"
case .unavailable(let reason):
return "❓ Недоступно: \(reason)"
}
}deviceNotEligible — постоянное состояние: старое железо, фичу нужно скрыть и не напоминать о ней. appleIntelligenceNotEnabled — управляемая пользователем настройка, один аккуратный призыв её включить уместен. modelNotReady — временное состояние загрузки модели, здесь нужен retry, а не сообщение об ошибке. Совмещение этих трёх сценариев в один общий "фича недоступна" — самый частый источник жалоб пользователей на "сломанный AI".
Поддерживаемые платформы на момент написания: iOS, iPadOS, macOS, visionOS. watchOS и tvOS в фреймворк не входят.
Структурированный вывод: @Generable вместо парсинга JSON руками#
До Foundation Models структурированный ответ от LLM означал: просить модель вернуть JSON, парсить его руками и молиться, что она не добавит лишний текст до или после скобок. Макрос @Generable убирает этот класс багов: он генерирует схему на этапе компиляции, и модель гарантированно возвращает значение нужного типа Swift.
import FoundationModels
@Generable
struct TripSummary {
@Guide(description: "Короткий заголовок поездки, до 6 слов")
var title: String
@Guide(description: "Основные моменты поездки", .count(3))
var highlights: [String]
@Guide(description: "Общая оценка настроения от 1 до 5")
var moodScore: Int
}
func summarizeTrip(notes: String) async throws -> TripSummary {
let session = LanguageModelSession(
instructions: "Ты помощник, который кратко резюмирует заметки о путешествиях."
)
let response = try await session.respond(
to: "Заметки пользователя: \(notes)",
generating: TripSummary.self
)
return response.content
}@Guide добавляет к полю не только текстовое описание для модели, но и программные ограничения — .count(), .maximumCount() и другие, которые реально сужают пространство генерации, а не просто "просят вежливо".
Стриминг и вызов инструментов#
Для отзывчивого UI структурированный вывод можно получать частями — тип, сгенерированный @Generable, автоматически получает "частичную" версию с опциональными полями:
func streamTripSummary(notes: String) async throws {
let session = LanguageModelSession()
let stream = session.streamResponse(
to: "Составь резюме поездки по заметкам: \(notes)",
generating: TripSummary.self
)
for try await partial in stream {
// partial: TripSummary.PartiallyGenerated — поля опциональны,
// заполняются постепенно, удобно для live-обновления UI
print(partial)
}
}А если модели нужны данные, которых нет в промпте — например, текущий город пользователя, — она может сама вызвать инструмент, который вы описали через протокол Tool:
struct CurrentLocationTool: Tool {
let name = "getCurrentLocation"
let description = "Возвращает текущий город пользователя"
@Generable
struct Arguments {
@Guide(description: "Нужна ли точность до района города")
var preciseArea: Bool
}
func call(arguments: Arguments) async throws -> ToolOutput {
let city = arguments.preciseArea ? "Лиссабон, Алфама" : "Лиссабон"
return ToolOutput(city)
}
}
let session = LanguageModelSession(
tools: [CurrentLocationTool()],
instructions: "Используй getCurrentLocation, если нужен город пользователя."
)Модель сама решает, вызывать ли инструмент, на основе описания — это принципиально иная модель мышления по сравнению с ручным роутингом intent'ов, к которому многие привыкли по классическим NLP-пайплайнам.
Где проходит граница: контекст, задачи и железо#
Главное практическое ограничение — размер контекстного окна сессии. Согласно техническому примечанию Apple TN3193 о работе с контекстным окном on-device модели, лимит session составляет порядка 4096 токенов на весь диалог, включая инструкции, историю и место под ответ. Это на порядки меньше, чем у облачных моделей вроде GPT или Claude, и означает, что длинные документы, большую историю чата или объёмный few-shot промпт в сессию просто не поместить — нужно резюмировать, обрезать или создавать новую сессию.
Если приблизиться к лимиту, модель может начать ошибаться ещё до формального превышения — ей физически не хватает места сгенерировать ответ. Практическое правило: закладывайте запас под сам ответ модели, не используйте всё окно под входные данные.
Второе ограничение — качество. Это не универсальный чат-бот и не источник фактов о мире: 3-миллиардная (или даже 20-миллиардная разреженная) модель хороша в сужении и трансформации уже имеющегося текста, но не в открытых вопросах "расскажи мне про...". Третье — фрагментация железа: расширенная модель AFM 3 Core Advanced доступна не на всех устройствах с Apple Intelligence, а только там, где хватает оперативной памяти. Проектируйте UX так, чтобы деградация до базовой модели не ломала сценарий.
On-device LLM или облако: таблица для честного выбора#
| Критерий | On-device (Foundation Models) | Облачные LLM (API) |
|---|---|---|
| Приватность данных | Данные не покидают устройство | Данные уходят на сервер провайдера |
| Стоимость | Бесплатно, нет счёта за токены | Оплата за токены/запросы |
| Офлайн-работа | Работает без сети | Требует подключения |
| Задержка | Низкая, без сетевого round-trip | Зависит от сети и очереди на сервере |
| Качество на сложных задачах | Ограничено (суммаризация, извлечение, короткие тексты) | Существенно выше на рассуждениях и знаниях |
| Контекстное окно | ~4096 токенов на сессию | Десятки-сотни тысяч токенов |
| Доступность | Только на Apple Intelligence-совместимых устройствах | Любое устройство с интернетом |
| Кастомизация модели | Нет тонкой настройки весов | Fine-tuning, system prompts, выбор модели |
Практический вывод из таблицы: on-device LLM — это инструмент для конкретных, ограниченных по объёму задач с высокими требованиями к приватности и нулевой стоимости, а не универсальная замена облачному API.
Не каждой задаче нужен LLM — и чек-лист перед стартом#
Работая над MeteoHealth (/projects/meteohealth/) — приложением, которое связывает погодные условия с самочувствием, — я сознательно выбрал классический статистический движок на устройстве вместо какой-либо ML или LLM-модели. Корреляции между давлением, влажностью и симптомами пользователя вычисляются детерминированными статистическими методами: результат воспроизводим, объясним и не требует ни токена генеративного текста.
Это не компромисс из-за нехватки ресурсов — это осознанный выбор инструмента под задачу. LLM, даже on-device, добавляет непредсказуемость там, где нужна прозрачная логика "если давление упало на X гПа — риск мигрени вырос". Foundation Models отлично подходит для задач, где вход — неструктурированный текст, а выход — структурированный или тоже текстовый результат: резюмирование заметок, извлечение сущностей, генерация заголовков, короткие диалоговые ассистенты внутри приложения. Для детерминированных вычислений над числовыми рядами он не нужен, и добавление LLM туда, где хватает арифметики, — это сложность без пользы.
Чек-лист перед тем, как добавлять Foundation Models#
- Проверяйте все три причины
unavailableпо отдельности — это разный UX, а не один экран ошибки. - Используйте
@Generable/@Guideвместо ручного парсинга JSON — это устраняет целый класс багов. - Бюджетируйте контекстное окно (~4096 токенов) заранее, а не когда сессия начнёт падать с ошибками.
- Не ждите от 3B-модели энциклопедических знаний — это инструмент для трансформации текста, а не для чата обо всём.
- Прежде чем тянуть LLM в фичу, спросите: а не решает ли задачу обычный код без генеративной модели? Иногда статистика лучше генерации.
Foundation Models — это не хайп ради хайпа: бесплатный, приватный, офлайн-способный LLM в паре строк Swift-кода — то, чего разработчики ждали годами. Но именно поэтому важно использовать его точечно, там, где он действительно превосходит альтернативы, а не потому что "у нас тоже есть AI".
Полезные ссылки:



