Главное ограничение Foundation Models в iOS 26 было не в качестве модели, а в арифметике: 4096 токенов на всё — инструкции, определения инструментов и весь транскрипт диалога. В iOS 27 у той же сессии появился режим с 32K и заметно более сильным reasoning, и включается он одной строчкой. Плата за это — не деньги и не API-ключ, а кое-что менее привычное, о чём ниже.
Базовый гайд по фреймворку я писал в августе по iOS 26 — SystemLanguageModel, @Generable, tool calling. Здесь разбираю только дельту: что появилось к сентябрю 2026 и какие продуктовые решения это меняет.
Версии: всё ниже — iOS 27, iPadOS 27, macOS 27 и visionOS 27, если не сказано иначе. Отдельно отмечу: watchOS получил Foundation Models только в 27 — на 26 его там не было.
Фреймворк больше не про одну модель Apple#
Год назад документация описывала фреймворк как доступ к модели Apple Intelligence. Сейчас первая строка обзора звучит иначе: доступ к любой большой языковой модели — встроенной, серверной или вашей собственной. Появился протокол LanguageModel, и все три варианта теперь равноправные граждане одного API.
Практический смысл: LanguageModelSession, Instructions, Tool, @Generable, стриминг — всё это вы пишете один раз, а менять можно то, что подставляется в модель. Ниже — три модели, которые туда подставляются.
Private Cloud Compute: та же сессия, другой потолок#
Переключение выглядит так:
// Сессия на серверной модели.
let session = LanguageModelSession(model: PrivateCloudComputeLanguageModel())Всё остальное переносится без правок: respond, стриминг, инструменты, инструкции. И SystemLanguageModel, и PrivateCloudComputeLanguageModel соответствуют протоколу LanguageModel, поэтому инициализатор сессии один и тот же.
Что даёт: контекст 32K токенов и более сильный reasoning — для длинных документов и многоходовых диалогов. Чего не даёт: работы офлайн. PCC требует сети, и при сетевой ошибке Apple прямо советует повторить запрос на on-device модели — то есть fallback вы всё равно пишете.
Доступность проверяется отдельно и по своим причинам:
let model = PrivateCloudComputeLanguageModel()
switch model.availability {
case .available:
// Показываем AI-интерфейс.
case .unavailable(.deviceNotEligible):
// Показываем альтернативу.
case .unavailable(.systemNotReady):
// PCC пока не готов обслуживать запросы.
case .unavailable(let other):
// Недоступно по неизвестной причине.
}Плюс #available-проверка на iOS 27 / macOS 27 / watchOS 27 / visionOS 27 с откатом на on-device модель для более ранних версий.
И вот тот самый неочевидный платёж. Чтобы разрабатывать с PCC, нужно соответствовать критериям Apple и запросить managed entitlement com.apple.developer.private-cloud-compute. Это не чекбокс в Capabilities — это заявка. Планируя фичу на PCC, закладывайте в сроки получение доступа, иначе на демо у вас будет красивый код, который не запускается.
Квота принадлежит пользователю, а не вам#
Привычная модель работы с серверным LLM: вы платите за токены, пользователь не знает об их существовании. С PCC наоборот — аутентификации и ключей нет вообще, зато у каждого человека есть дневной лимит запросов, а расширяется он апгрейдом подписки iCloud+.
Для архитектуры это разворот. Вы не можете «докупить лимит» за пользователя и не можете предсказать, сколько у него осталось на момент открытия вашего экрана. Поэтому фреймворк отдаёт состояние квоты, и его надо показывать, а не прятать за алертом:
let model = PrivateCloudComputeLanguageModel()
if model.quotaUsage.isLimitReached {
Text("Дневной лимит исчерпан")
.foregroundStyle(Color.red)
} else if case .belowLimit(let info) = model.quotaUsage.status {
if info.isApproachingLimit {
Text("Лимит скоро закончится")
.foregroundStyle(Color.orange)
}
}
if let suggestion = model.quotaUsage.limitIncreaseSuggestion {
Button("Показать варианты") {
suggestion.show()
}
}limitIncreaseSuggestion.show() поднимает системный UI апгрейда — своего экрана с ценами писать не нужно и, судя по формулировкам Apple, не нужно принципиально.
Исчерпание квоты приходит отдельной ошибкой и отличается от rate limiting: при rate limiting человек ждёт, при исчерпанной квоте — ждёт до сброса или апгрейдится. Дату сброса фреймворк отдаёт, но она бывает пустой.
Тестировать это можно без реального выжигания лимита: Product > Scheme > Edit Scheme > Run > Options > Simulated Apple Foundation Models Availability, там есть «Approaching Quota Usage Limit» и «Quota Usage Limit Reached».
Reasoning стал ручкой, которую крутите вы#
let response = try await session.respond(
to: "Какие тут компромиссы в архитектуре?",
contextOptions: ContextOptions(reasoningLevel: .deep)
)Уровней три; крайние — .light и .deep. Чем глубже reasoning, тем больше времени тратит модель и тем больше контекстного окна уходит на служебный текст рассуждений. Второе на практике неприятнее первого: вы включили .deep ради качества, а получили exceededContextWindowSize на третьем ходу диалога.
Сами сегменты рассуждений в ответ не попадают — их видно в транскрипте, и это удобно, когда разбираешься, почему модель выдала странное.
Apple советует начинать с минимального уровня и повышать по результатам оценки. Совет скучный, но здесь он подкреплён: на фичу теперь есть чем мерить — в фреймворке появились инструменты оценки промптов и отдельный инструмент Foundation Models в Instruments, показывающий латентность, промпты, вывод модели, вызовы инструментов и расход токенов.
Картинки в промпте#
Мультимодальность пришла в обычный respond:
func compareImages(imageOne: CGImage, imageTwo: CGImage) async throws -> String {
let session = LanguageModelSession()
let response = try await session.respond {
"Сравни эти два изображения тремя пунктами:"
Attachment(imageOne)
// Если поворот к изображению не применён — например, кадр из
// AVFoundation — укажите ориентацию, фреймворк сам сделает transform.
Attachment(imageTwo, orientation: .right)
}
return response.content
}Масштабирование и конверсию форматов фреймворк берёт на себя — готовить изображение не нужно. Принимает CGImage, данные и URL файла (тип определяется по UTType).
Сочетание с @Generable здесь работает лучше, чем свободный текст. Классификация одним вызовом:
@Generable
enum ImageLabel {
case cat
case dog
case frog
case bird
}
func classifyImage(_ image: CGImage) async throws -> ImageLabel {
let session = LanguageModelSession()
let response = try await session.respond(
generating: ImageLabel.self,
options: GenerationOptions(samplingMode: .greedy)
) {
"Выбери метку, которая лучше всего описывает изображение:"
Attachment(image)
}
return response.content
}.greedy тут по делу: без него модель может выбрать метку «почти подходящую», что для классификатора — тихий баг.
Ещё есть готовые инструменты для картинок — чтение штрихкодов (BarcodeReaderTool) и распознавание текста. Когда инструментов несколько, изображению стоит давать метку — Attachment(image).label("barcode-image") — чтобы модель понимала, к чему применять инструмент.
Промптовая мелочь, которая сильно меняет результат: «Перечисли все продукты на этой фотографии» работает заметно лучше, чем «Что на этой картинке?».
Динамические профили: инструкции, которые пересобираются перед каждым запросом#
Раньше сессия фиксировала инструкции при создании. Для мастера из нескольких шагов — поиск рецепта, замена ингредиентов, проверка запасов, пошаговая готовка — это означало либо одну раздутую инструкцию на все случаи, либо пересоздание сессии с потерей истории.
Теперь тело DynamicInstructions пересчитывается перед каждым обращением к модели:
struct PresentationInstructions: DynamicInstructions {
var isEditingImage = true
var isEditingAnimation = false
var body: some DynamicInstructions {
// Общая часть для любого состояния.
Instructions {
"Помогай человеку улучшать презентацию."
}
ListPhotosTool()
AddPhotoTool()
// Специфика текущего состояния приложения.
if isEditingImage {
ImageEditingInstructions()
}
if isEditingAnimation {
AnimationEditingInstructions()
}
}
}
let session = LanguageModelSession(
dynamicInstructions: PresentationInstructions()
)Уровнем выше — Profile, который связывает инструкции с настройками сессии, и DynamicProfile, который переключает профили. Причём переключение проверяется компилятором: активным может быть ровно один профиль, поэтому ветки пишутся через if / else if / else, а не параллельными if.
Profile {
// Инструкции и инструменты для творческой задачи.
}
.model(pccModel)
.temperature(likesPoetry ? 0.8 : 0.1)
.reasoningLevel(likesAstronomy ? .deep : .light)Модификаторы разрешаются по трём уровням приоритета: опции, переданные прямо в respond(to:options:), бьют всё; модификатор у подпрофиля бьёт модификатор динамического профиля; сам динамический профиль задаёт умолчания.
Одно правило, без которого профили сделают хуже#
Порядок объявления внутри body влияет на производительность, и влияет сильно.
Сессия раскладывается в последовательность токенов: сначала инструкции, потом определения инструментов, потом транскрипт. KV-кэш провайдера действителен ровно до первого изменившегося токена — всё, что после, пересчитывается. Значит статические инструкции и инструменты идут вверх тела, условные блоки — вниз. Поставите условный блок первым — каждое переключение флага будет инвалидировать кэш всего диалога.
Отсюда же следствия, которые легко нарушить из лучших побуждений:
- Набор инструментов фиксируйте при создании сессии. Добавление инструмента посреди разговора не только рвёт кэш, но и работает плохо: модель уже усвоила шаблон предыдущих ходов.
- Удаляя инструмент, вычищайте из транскрипта и его вызовы — иначе модель видит ссылки на то, чего в определениях больше нет.
- Обрезайте транскрипт редко и крупно. Частая подрезка после каждого хода — это частая инвалидация; одна консолидация при подходе к пределу контекста дешевле.
- Переключение профиля меняет весь префикс, то есть обнуляет кэш целиком. Делайте это на естественных границах сценария, а не на каждом ходу.
Если момент использования известен заранее хотя бы за секунду-две, помогает prewarm(promptPrefix:) — он считает префикс в кэш до первого запроса.
Проверяется всё это Instruments: делите кэшированные входные токены на общее число входных. Низкое отношение между ходами означает, что кэш сбрасывается и модель каждый раз перемалывает префикс заново.
Что со всем этим делать#
Порядок действий, который я считаю правильным на сегодня: начать с on-device модели и оценить фичу количественно. Если упирается в 4096 токенов или в качество рассуждений — добавлять PCC, заранее запросив entitlement и нарисовав состояние квоты. Профили подключать тогда, когда в сценарии реально несколько режимов, а не потому что API новый.
Третий вариант — не Apple-модель вообще. Протокол LanguageModel открыт, и модель, экспортированную через Core AI, можно положить в ту же LanguageModelSession: это снимает привязку к Apple Intelligence и работает на устройствах, где его нет.



