Foundation Models — это Swift-фреймворк, дающий доступ к той самой on-device LLM, что обслуживает системные функции вроде Writing Tools, Genmoji и Smart Reply. В отличие от облачных провайдеров (OpenAI, Anthropic, Google), здесь:
- модель выполняется полностью на устройстве — данные физически не покидают iPhone или Mac;
- нет токенных биллингов, ключей и rate-limits;
- работает офлайн, в режиме полёта, в фоне;
- интегрирована в систему через стандартные Swift-типы и макросы.
За такую приватность приходится платить компромиссами. Модель меньше облачных аналогов, контекстное окно ограничено 4096 токенами, она не годится для код-генерации, сложных вычислений или фактологических вопросов о мире (cutoff обучения — октябрь 2023). Зато она отлично справляется с суммаризацией, извлечением сущностей, классификацией, переписыванием, генерацией коротких сценариев — теми задачами, ради которых раньше приходилось гонять данные в облако.
Честно говоря, после пары недель экспериментов отношение у меня поменялось: это не «GPT в кармане», а очень узкоспециализированный инструмент. Когда задача попадает в его нишу — он удивляет скоростью и приватностью. Когда не попадает — лучше даже не пробовать.
Требования и поддерживаемые устройства
Перед тем как писать код, проверьте, что у вас:
- Xcode 26 (или новее) и SDK iOS 26 / macOS 26 / iPadOS 26 / visionOS 26;
- устройство с поддержкой Apple Intelligence: iPhone 15 Pro / Pro Max или любой iPhone 16/17 на A17 Pro и выше; Mac на M1 и новее; iPad на M1+; Apple Vision Pro;
- в настройках устройства включён Apple Intelligence (модель скачивается отдельно, ~2–3 ГБ).
Перед использованием модели всегда проверяйте её доступность. Это первое правило production-кода — без вариантов.
import FoundationModels
let model = SystemLanguageModel.default
switch model.availability {
case .available:
// Готовы вызывать модель
break
case .unavailable(.deviceNotEligible):
// Старое устройство — нужен fallback
break
case .unavailable(.appleIntelligenceNotEnabled):
// Пользователь не включил Apple Intelligence в настройках
break
case .unavailable(.modelNotReady):
// Модель ещё скачивается
break
case .unavailable(let other):
print("Недоступно: \(other)")
}
Никогда не предполагайте, что модель доступна. Это не Foundation API уровня String — это сервис, который физически может отсутствовать на устройстве пользователя. И, судя по статистике распространения Apple Intelligence, в обозримом будущем не появится у заметной части аудитории.
Первый запрос: LanguageModelSession
Все взаимодействия идут через LanguageModelSession — объект, который хранит историю диалога (transcript) и опции генерации. Простейший «Hello, world»:
import FoundationModels
func askModel() async throws -> String {
let session = LanguageModelSession()
let response = try await session.respond(to: "Назови три преимущества SwiftUI перед UIKit одной строкой каждое.")
return response.content
}
Сессия запоминает предыдущие сообщения, поэтому повторный respond(to:) на той же сессии будет учитывать предыдущий контекст. Если хотите чистый запрос — создавайте новую сессию.
Системные инструкции
Чтобы задать модели роль и стиль, передавайте instructions при создании сессии:
let session = LanguageModelSession(instructions: """
Ты — лаконичный технический редактор. Отвечай на русском.
Никогда не используй маркетинговые штампы вроде «революционный» или «мощный».
""")
Системные инструкции эффективнее, чем повторение требований в каждом prompt. Они занимают часть контекста один раз, а не на каждый запрос — и при коротком окне в 4096 токенов это очень ощутимая экономия.
Структурированный вывод: @Generable и @Guide
Вот ради этого, по-моему, и стоит вообще смотреть в сторону Foundation Models. Главная фишка фреймворка — guided generation. Вместо того чтобы парсить «грязный» JSON из строки и молиться, чтобы модель не забыла закрыть кавычку, вы объявляете обычный Swift-тип, помечаете его @Generable — и получаете уже типизированный объект. Это работает не магией промптинга, а constrained decoding на уровне токенов: модель буквально не может сгенерировать токен, который сломал бы вашу схему.
import FoundationModels
@Generable
struct Recipe: Equatable {
@Guide(description: "Название блюда, не более 5 слов")
let title: String
@Guide(description: "Краткое описание в 1–2 предложения")
let summary: String
@Guide(.count(5))
let ingredients: [String]
@Guide(.range(1...60))
let prepMinutes: Int
}
func generateRecipe() async throws -> Recipe {
let session = LanguageModelSession()
let response = try await session.respond(
to: "Придумай простой рецепт пасты с кабачками для будней.",
generating: Recipe.self
)
return response.content
}
Что здесь происходит:
- Макрос
@Generable на этапе компиляции генерирует JSON-схему типа и инициализатор, способный её разобрать.
@Guide(description:) добавляет в схему пояснения для модели — фактически локализованные подсказки.
@Guide(.count(n)), @Guide(.range(...)), @Guide(.pattern(...)) и подобные генераторы накладывают жёсткие ограничения: длина массива, диапазон чисел, regex для строк.
- Все сохранённые свойства типа сами должны быть Generable. Базовые типы (
String, Int, Double, Bool, Array, Optional, перечисления без ассоциированных значений) — уже Generable.
Порядок свойств имеет значение
А вот это меня в первый раз застало врасплох. Модель генерирует поля последовательно, в порядке объявления. Это критично, когда одно поле логически зависит от другого. Допустим, вы хотите сначала список тезисов, а потом резюме на их основе:
@Generable
struct ArticleAnalysis {
@Guide(.count(3))
let keyPoints: [String] // Сначала генерируем тезисы
let summary: String // Потом резюме, опираясь на них
}
Если поменять поля местами, качество резюме упадёт — модель будет писать summary «вслепую», ещё не сгенерировав тезисы. Ошибка не выскочит, всё скомпилируется, просто на выходе будет каша. Имейте в виду.
Перечисления как ограниченный выбор
Generable-перечисления — мощный способ заставить модель выбирать только из заданного множества:
@Generable
enum Sentiment: String {
case positive, neutral, negative
}
@Generable
struct ReviewClassification {
let sentiment: Sentiment
@Guide(.range(1...5))
let stars: Int
let reasoning: String
}
Никакого «модель вернула positiv вместо positive» — на уровне декодирования таких токенов просто не существует. После многолетнего ковыряния регулярок по выхлопу LLM это, признаться, ощущается как чудо.
Стриминг: реактивный UI на async-последовательностях
Дожидаться полного ответа на длинной генерации — плохой UX, и точка. Foundation Models поддерживает стриминг через streamResponse, причём, в отличие от облачных API, он стримит не сырые токены, а частично собранные структурированные снимки: экземпляры специального типа T.PartiallyGenerated, у которых поля заполняются по мере генерации.
import SwiftUI
import FoundationModels
@Observable
final class RecipeViewModel {
var partial: Recipe.PartiallyGenerated?
var isGenerating = false
func generate(prompt: String) async {
isGenerating = true
defer { isGenerating = false }
let session = LanguageModelSession()
do {
let stream = session.streamResponse(
generating: Recipe.self,
prompt: { prompt }
)
for try await snapshot in stream {
partial = snapshot
}
} catch {
print("Ошибка генерации: \(error)")
}
}
}
struct RecipeView: View {
@State private var viewModel = RecipeViewModel()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if let title = viewModel.partial?.title {
Text(title).font(.title2.bold())
}
if let summary = viewModel.partial?.summary {
Text(summary).foregroundStyle(.secondary)
}
if let ingredients = viewModel.partial?.ingredients {
ForEach(ingredients, id: \.self) { Text("• \($0)") }
}
}
.animation(.snappy, value: viewModel.partial)
.task { await viewModel.generate(prompt: "Паста с кабачками для будней") }
}
}
SwiftUI сам анимирует появление полей по мере их прихода — пользователь видит «живую» генерацию, как в ChatGPT, но с типизированным выводом. Эффект, на мой вкус, даже приятнее: текст не сыпется буква за буквой, а проявляется блоками.
Tools: даём модели руки
Сама по себе LLM знает только то, что было в её обучающих данных (cutoff октябрь 2023). Чтобы модель могла обращаться к актуальным данным — Apple Health, Core Location, базе приложения, REST API — используйте tools. Это Swift-типы, реализующие протокол Tool; вы регистрируете их в сессии, и модель сама решает, когда их вызвать.
import FoundationModels
import CoreLocation
final class WeatherTool: Tool {
let name = "get_weather"
let description = "Возвращает текущую погоду по названию города на русском."
@Generable
struct Arguments {
@Guide(description: "Название города в именительном падеже")
let city: String
}
@Generable
struct Output {
let temperatureC: Double
let condition: String
}
func call(arguments: Arguments) async throws -> Output {
// Здесь — реальный вызов WeatherKit или вашего API
return Output(temperatureC: 12.4, condition: "облачно")
}
}
func askWithTools() async throws {
let session = LanguageModelSession(
tools: [WeatherTool()],
instructions: "Если нужна актуальная погода, всегда вызывай get_weather."
)
let response = try await session.respond(to: "Что мне надеть в Москве сегодня?")
print(response.content)
}
Несколько важных моментов:
- Имя инструмента (
name) и его description попадают в prompt автоматически. Потратьте время на их формулировку — относитесь к этому как к хорошему docstring.
Arguments и возвращаемый тип должны быть @Generable.
- Модель может вызвать инструмент несколько раз за один
respond(to:), если задача комплексная.
- Все вызовы инструментов фиксируются в
session.transcript — удобно для отладки и аналитики.
Управление контекстным окном (4096 токенов)
А вот тут начинается боль. Главное практическое ограничение модели — 4096 токенов на ввод и вывод вместе. Это примерно 3000 слов на английском, заметно меньше на русском (русские тексты дороже по токенам — кириллица режется на куски почти посимвольно). Превысите лимит — получите GenerationError.exceededContextWindowSize, и сессия будет «отравлена»: продолжить в ней нельзя, нужно создать новую.
В iOS 26.4 (март 2026) Apple наконец-то добавила два долгожданных API для проактивного управления контекстом:
let model = SystemLanguageModel.default
// Сколько токенов всего доступно (зависит от устройства и версии модели)
let total = model.contextSize
// Сколько токенов займёт строка — измеряем ДО отправки
let prompt = "Очень длинный текст для суммаризации..."
let tokens = try model.tokenCount(for: prompt)
if tokens > total - 512 { // Резервируем место под ответ
// Слишком длинно — режем или суммаризируем порциями
}
Стратегии работы с лимитом:
- Map-reduce-суммаризация: длинный текст режется на куски, каждый суммаризируется отдельной сессией, а потом результаты сводятся ещё одним проходом.
- Сжатие истории: в чат-сценариях периодически просите модель пересказать предыдущие N сообщений одним абзацем и стартуйте новую сессию с этим summary в инструкциях.
- Минимальные Generable-схемы: не включайте в один тип всё, что может понадобиться когда-либо. Для каждого экрана — свой компактный
@Generable-тип.
- Лимит на ответ: в
GenerationOptions можно задать maximumResponseTokens, чтобы модель не растягивала вывод.
let options = GenerationOptions(
temperature: 0.7,
maximumResponseTokens: 300
)
let response = try await session.respond(
to: "Суммаризируй в 3 предложения",
options: options
)
Обработка ошибок
Foundation Models бросает LanguageModelSession.GenerationError с богатым набором кейсов. Минимум, который должен ловить production-код:
do {
let response = try await session.respond(to: prompt, generating: Recipe.self)
handle(response.content)
} catch let error as LanguageModelSession.GenerationError {
switch error {
case .exceededContextWindowSize:
// Сессия отравлена — создаём новую
await restartSession()
case .assetsUnavailable:
// Модель ещё качается или удалена
showFallbackUI()
case .guardrailViolation:
// Сработал safety-фильтр Apple — переформулируйте prompt
showSafeAlternativePrompt()
case .unsupportedLanguageOrLocale:
// Модель не поддерживает выбранный язык
switchToEnglishOrFallback()
case .decodingFailure:
// Структурный вывод не удалось разобрать (редко при @Generable)
retryWithSimplerSchema()
case .rateLimited:
// Слишком частые запросы
try? await Task.sleep(for: .seconds(1))
@unknown default:
log(error)
}
} catch {
log(error)
}
Отдельно стоят ошибки инструментов — LanguageModelSession.ToolCallError. Они оборачивают ошибку, выброшенную из вашего Tool.call(arguments:), и доступны при итерации стрима транскрипта.
Безопасность и guardrails
Apple встроила в Foundation Models два слоя guardrails: один проверяет ввод (prompt injection, недопустимый контент), второй — вывод (опасные инструкции, медицинские/юридические утверждения). При срабатывании вы получаете .guardrailViolation.
Что это значит на практике:
- Никогда не вставляйте в prompt сырой пользовательский ввод без экранирования контекста — оборачивайте его в кавычки или префиксы и давайте модели чёткие инструкции, как с ним обращаться.
- Не пытайтесь обходить guardrails — это нарушает App Review Guidelines и приведёт к реджекту.
- Для UGC-сценариев показывайте пользователю понятное сообщение при срабатывании фильтра, а не технический stack trace.
Производительность и батарея
On-device — не значит бесплатно. Каждый вызов модели — это секунды на Neural Engine и заметный расход батареи. Из практики:
- Кешируйте результаты для одинаковых входов. Хеш prompt + версия модели — отличный ключ.
- Прогревайте сессию в фоне до того, как пользователь нажмёт кнопку:
session.prewarm() загружает модель в память и драматически ускоряет первый ответ.
- Не дёргайте модель в цикле на каждое нажатие клавиши — debounce минимум 300–500 мс.
- Используйте
maximumResponseTokens везде, где это уместно. Короче ответ — меньше вычислений.
- Для повторяющихся запросов с одинаковыми инструкциями переиспользуйте сессию — она держит KV-кэш в памяти.
final class AISummarizer {
private let session: LanguageModelSession
init() {
session = LanguageModelSession(instructions: "Суммаризуй в 2 предложения.")
Task { await session.prewarm() }
}
func summarize(_ text: String) async throws -> String {
try await session.respond(to: text).content
}
}
Когда не стоит использовать Foundation Models
Apple явно перечисляет сценарии, где локальная модель — плохой выбор:
- Кодогенерация. Модель меньше Copilot/Claude и не натренирована специально на код.
- Точные вычисления и математика. Используйте
Tool, который вызывает обычный Swift-расчёт.
- Фактологические Q&A о мире. Cutoff октябрь 2023; для актуальных фактов нужны tools, ходящие в API.
- Длинные контексты. 4096 токенов — это не «суммаризировать книгу за один проход».
- Тонкая работа с редкими языками. Качество вне «больших» языков заметно ниже.
Для всего этого правильнее использовать облачную LLM или специализированные модели, а Foundation Models оставить для приватных, лёгких, латентность-критичных задач. Не пытайтесь забить гвоздь микроскопом — он не сломается, но будет очень обидно.
FAQ
Какие устройства поддерживают Foundation Models?
Только устройства с поддержкой Apple Intelligence: iPhone 15 Pro/Pro Max, вся линейка iPhone 16/17, iPad на M1 и новее, Mac на M1+, Apple Vision Pro. На остальных SystemLanguageModel.default.availability вернёт .unavailable(.deviceNotEligible), и вам нужно реализовать fallback (например, облачную модель или просто отключение фичи).
Можно ли использовать Foundation Models бесплатно и без интернета?
Да. Модель выполняется локально, плата за токены отсутствует, интернет не требуется. Единственное условие — устройство пользователя должно поддерживать Apple Intelligence и иметь его включённым в настройках. После первого включения модель скачивается один раз (~2–3 ГБ) и далее работает офлайн.
Чем @Generable отличается от обычного парсинга JSON из ответа LLM?
@Generable — это constrained decoding на уровне токенов: фреймворк ограничивает множество допустимых следующих токенов схемой вашего типа, поэтому модель физически не может сгенерировать невалидный JSON. В отличие от подхода «попроси LLM вернуть JSON и парси», здесь не бывает обрезанных кавычек, лишних запятых или галлюцинированных полей. Декодирование в Swift-тип происходит автоматически — без JSONDecoder и опциональных полей-костылей.
Что делать, если контекст превысил 4096 токенов?
Сессия после GenerationError.exceededContextWindowSize восстановлению не подлежит — нужно создать новую LanguageModelSession. Чтобы предотвратить ошибку, в iOS 26.4 используйте SystemLanguageModel.default.tokenCount(for:) для измерения prompt'а заранее и стратегию map-reduce: режьте длинный вход на куски, суммаризуйте каждый отдельной сессией, а финальное резюме собирайте ещё одним проходом.
Как Foundation Models соотносится с App Intents и Apple Intelligence?
Это разные слои стека. App Intents описывают действия вашего приложения, чтобы Siri и Spotlight могли их вызывать. Foundation Models даёт прямой доступ к языковой модели внутри вашего приложения — для генерации текста, классификации, извлечения данных. Apple Intelligence — это зонтичный термин для всех системных AI-фич (Writing Tools, Genmoji, Image Playground и т.д.), которые внутри используют ту же модель. App Intents и Foundation Models часто комбинируют: интент ловит запрос пользователя, а Foundation Models внутри генерирует ответ.
Что дальше
Foundation Models — не «волшебная кнопка ИИ», а инструмент с конкретной нишей: приватные, быстрые, оффлайновые языковые задачи прямо на устройстве. Освоив @Generable, стриминг и tools, вы получаете слой, на котором можно построить умные ассистенты, генерацию контента, классификаторы и парсеры — без единого запроса в облако.
Следующий шаг простой: попробуйте переписать одну фичу в вашем приложении, которая раньше требовала запроса к облачной LLM, на локальную модель. Замерьте латентность, расход батареи и качество. В большинстве UI-сценариев результат вас приятно удивит — а заодно вы перестанете платить за токены там, где это никогда и не было нужно.