Когда я готовился к внутреннему митапу по WWDC 2025 в нашей iOS-команде, нужно было сделать обзор сессий #360 (Discover ML & AI Frameworks) и #265 (Dive Deeper into Writing Tools). Доклад я уже провёл, но при подготовке набралось много заметок, которые в формат презентации не влезли: подводные камни, неочевидные решения, паттерны использования. Эта статья — попытка собрать всё это в одном месте.
Речь пойдёт о Foundation Models Framework: что это, как устроено внутри, как с этим работать в реальном приложении и где у этого фреймворка границы применимости. Я постарался не пересказывать документацию Apple, а сосредоточиться на тех моментах, которые не очевидны при первом знакомстве и о которых я бы хотел узнать раньше, чем начал писать первый прототип.
Зачем вообще понадобился ещё один AI-фреймворк
До iOS 26 у iOS-разработчика, который хотел добавить интеллектуальные функции в приложение, было два рабочих пути. Первый — использовать облачный API: OpenAI, Anthropic, Google, что-то ещё. Это работает, но накладывает ряд ограничений: каждый запрос стоит денег, требуется стабильное интернет-соединение, данные пользователя физически уходят на чужой сервер. Последнее особенно неприятно, если речь о приложениях с медицинскими, финансовыми или просто личными данными. В Европе, например, это сразу же поднимает вопросы соответствия GDPR.
Второй путь — взять готовую модель в формате Core ML и запустить её на устройстве. Здесь приватность не страдает, но появляются другие сложности: нужно подобрать или обучить модель под свою задачу, конвертировать её в Core ML, оптимизировать под Neural Engine, тестировать на разных устройствах с разной производительностью. Для большинства команд без выделенного ML-инженера это слишком дорого.
Foundation Models — третий путь, который закрывает обе проблемы сразу. Apple даёт прямой программный доступ к той же языковой модели, которая уже работает внутри системных AI-функций: Writing Tools, Smart Reply в Сообщениях, генерация Genmoji. Раньше эта модель была закрыта; начиная с iOS 26 её можно использовать в своём приложении.
Принципиальное отличие от облачных API в том, что весь инференс происходит локально, на специализированном AI-чипе внутри устройства. Это меняет экономику фичи (нет цены за токен), её надёжность (работает в самолёте, в метро, в горах) и архитектуру (не нужно беспокоиться о retry-логике для сетевых ошибок).
Минимальный пример
Прежде чем разбирать архитектуру, посмотрим на самый минимальный рабочий пример. Этот код реально работает в продакшне:
import FoundationModels
let session = LanguageModelSession()
let response = try await session.respond(to: "Напиши changelog")
print(response.content)
Четыре содержательных строки. Никаких API-ключей, никакой инициализации сети, никакой подписки. Но за этой простотой стоит несколько важных нюансов, которые проявляются ровно тогда, когда вы пытаетесь использовать это в реальном приложении, а не в Playground. О них и пойдёт речь дальше.
Что происходит под капотом
Когда вы вызываете session.respond(to:), запрос проходит через несколько слоёв.
Сверху вниз: ваш Swift-код вызывает API фреймворка. Сам фреймворк FoundationModels отвечает за управление сессией, обработку Guided Generation (об этом ниже), вызовы тулов и стриминг. Ниже — собственно языковая модель Apple на ~3 миллиарда параметров. Она же используется и в системных AI-функциях, то есть это не урезанная демоверсия, а та же модель. И, наконец, вычисления выполняются на Neural Engine — отдельном сопроцессоре внутри Apple Silicon, заточенном под нейросетевые операции.
Важный момент про Neural Engine: это не CPU и не GPU. В A17 Pro (iPhone 15 Pro) это 16-ядерный блок производительностью около 18 TOPS — триллионов операций в секунду. Именно благодаря такому железу модель размером в 3B параметров работает на телефоне с приемлемой задержкой. На обычном CPU инференс был бы слишком медленным, на GPU — слишком прожорливым по батарее. Neural Engine — компромисс, оптимизированный именно под inference нейросетей.
Из этого, кстати, следует важное ограничение: фреймворк работает только на устройствах с достаточно современным Neural Engine. Конкретно: iPhone 15 Pro и новее (чип A17 Pro и выше), все Mac на Apple Silicon (M1 и новее), iPad с M-серией чипов. На iPhone 14 и старше API недоступен, и попытка создать LanguageModelSession завершится ошибкой. Об этом стоит помнить и закладывать в архитектуру fallback на облачный API или просто отключение AI-функций для несовместимых устройств.
Разбираем код построчно
Вернёмся к минимальному примеру и разберём каждую строку. Это полезно сделать, потому что каждая из них скрывает за собой выбор, который повлияет на архитектуру приложения.
Первая строка: import FoundationModels
Подключение фреймворка. Звучит банально, но стоит проверить совместимость на этапе сборки и в рантайме. На этапе сборки достаточно поднять минимальный target до iOS 26, иначе линкер просто не найдёт символы. В рантайме нужна проверка LanguageModelSession.isAvailable:
guard LanguageModelSession.isAvailable else {
// Модель недоступна на этом устройстве.
// Здесь должен быть fallback — например, скрыть AI-функцию
// или использовать облачный API.
return
}
Этот guard критичен, потому что приложение может оказаться на устройстве, которое формально поддерживает iOS 26, но не имеет достаточно мощного Neural Engine. В моём прототипе я сначала забыл про эту проверку и получил неприятный сюрприз при тестировании на iPhone 14 — приложение крашилось при первом обращении к модели.
Вторая строка: создаём сессию
LanguageModelSession() создаёт объект сессии. И вот здесь начинается самое интересное, потому что сессия — это не просто обёртка над запросом. Это полноценный stateful объект, который хранит историю всех ваших запросов и ответов модели.
Это важно по двум причинам. Во-первых, модель помнит контекст: если вы в первом запросе сказали «меня зовут Артём», а во втором спросили «как меня зовут», она ответит правильно. Это и есть та самая память диалога, которая в облачных API обычно реализуется ручной передачей истории сообщений в каждый запрос.
Во-вторых — и это куда важнее с практической точки зрения — создание сессии стоит дорого. Под капотом происходит загрузка весов модели в память Neural Engine и выделение буфера под контекстное окно. На моём iPhone 15 Pro это занимало 200–500 миллисекунд. Это значит, что сессию нужно создавать один раз и переиспользовать, а не пересоздавать на каждый запрос.
В прототипе я наступил на эти грабли почти сразу. Сначала функция выглядела так:
// Так делать НЕ нужно: сессия создаётся при каждом вызове
func sendMessage(_ text: String) async throws -> String {
let session = LanguageModelSession()
let response = try await session.respond(to: text)
return response.content
}
При интенсивном использовании это превращало приложение в кисель: каждое сообщение от пользователя приводило к перезагрузке модели в память. Правильный паттерн — держать сессию как свойство ViewModel или сервиса:
@Observable
final class ChatViewModel {
// Создаётся один раз при инициализации ViewModel
private let session = LanguageModelSession()
func sendMessage(_ text: String) async throws -> String {
// Сессия переиспользуется для всех запросов
// и помнит весь предыдущий контекст диалога
let response = try await session.respond(to: text)
return response.content
}
}
Дополнительный приятный момент: при создании сессии можно задать системный промпт через параметр instructions. Это аналог system message в ChatGPT API — инструкции, которые модель будет учитывать во всех запросах в рамках этой сессии:
let session = LanguageModelSession(
instructions: """
Ты ассистент для iOS-разработчиков.
Отвечай кратко, с примерами кода на Swift.
Не используй markdown-форматирование в ответах.
"""
)
В моём прототипе чат-ассистента такие инструкции значительно улучшили качество ответов: без них модель часто скатывалась в общие фразы, с ними — давала структурированные ответы со сниппетами кода.
Третья строка: запрос с обработкой ошибок
try await session.respond(to:) — асинхронный запрос к модели. await понятен: мы ждём, пока модель сгенерирует ответ. А вот try — это уже отдельная история. У этого вызова есть три типа ошибок, и каждую из них нужно обработать отдельно, иначе приложение начнёт падать в неожиданных местах.
Первая ошибка — unsupportedLanguage. Возникает, когда модель не может работать с языком запроса. На момент iOS 26 поддерживаются 15 языков, включая русский, но если пользователь, например, напишет на тайском, прилетит именно эта ошибка.
Вторая — contextWindowExceeded. Контекстное окно — это максимальный объём информации, который модель может одновременно держать в памяти при генерации ответа. Когда история сессии становится слишком длинной (например, после двадцатого-тридцатого обмена сообщениями), новый запрос может в это окно не уместиться. Нужно создавать новую сессию, желательно — с кратким резюме предыдущего разговора в instructions.
Третья — guardrailViolation. Внутри модели работает система безопасности, которая блокирует запросы и ответы, нарушающие определённые правила: предложения насилия, генерация запрещённого контента, попытки jailbreak. Это не баг, а намеренная фича: вы получаете built-in safety без необходимости писать собственные фильтры. Но обрабатывать эту ошибку нужно вежливо — пользователь не должен видеть техническое сообщение «guardrail violation», ему нужно мягкое «не могу помочь с этим запросом».
Полный обработчик выглядит так:
do {
let response = try await session.respond(to: userInput)
updateUI(with: response.content)
} catch LanguageModelError.unsupportedLanguage {
showError("Этот язык пока не поддерживается")
} catch LanguageModelError.contextWindowExceeded {
// Сессия переполнена — создаём новую с резюме старой
await resetSessionWithSummary()
} catch LanguageModelError.guardrailViolation {
showError("Не могу помочь с этим запросом, попробуйте переформулировать")
} catch {
// Любые другие непредвиденные ошибки
showError("Что-то пошло не так")
}
Отдельно стоит сказать про стриминг. Метод respond(to:) возвращает финальный ответ — то есть пользователь ждёт, пока модель полностью сгенерирует текст. На коротких запросах это незаметно, но если модель генерирует длинный ответ (например, объяснение какого-нибудь концепта), задержка в несколько секунд воспринимается болезненно.
Для таких случаев есть streamResponse(to:), который возвращает AsyncSequence с частями ответа по мере генерации. Это полностью повторяет поведение ChatGPT, где буквы появляются постепенно:
@Observable
final class StreamingViewModel {
private let session = LanguageModelSession()
var generatedText = ""
func generate(prompt: String) async {
generatedText = ""
do {
// Каждая итерация даёт следующий кусок ответа
for await partial in session.streamResponse(to: prompt) {
await MainActor.run {
// Обновляем UI по мере поступления
generatedText += partial.text
}
}
} catch {
// обработка ошибок
}
}
}
С точки зрения UX разница огромная. Без стриминга на запрос «объясни SwiftUI» пользователь видит спиннер 3–5 секунд, потом сразу всю простыню текста. Со стримингом — первые слова появляются примерно через 200 мс, и текст «печатается» на глазах. Воспринимается это как «модель думает вслух», а не «приложение висит».
Четвёртая строка: содержимое ответа
response.content — строка с ответом модели. Кроме content объект response содержит ещё несколько полезных полей. finishReason показывает, почему генерация остановилась: .complete означает нормальное завершение, .length — модель упёрлась в лимит токенов и текст обрезан, .stop — попался стоп-токен. usage содержит статистику по токенам: promptTokens, completionTokens и totalTokens. Полезно для отладки и понимания, насколько большие у вас запросы.
Особое внимание стоит уделить случаю finishReason == .length. Если он встречается часто, значит ваши запросы или ожидаемые ответы слишком длинные. Пользователь в этой ситуации видит ответ, оборванный на середине предложения, что выглядит как баг. Лучше детектировать такой случай и либо разбивать запрос на части, либо явно сообщать пользователю, что ответ обрезан.
Структурированный вывод через @Generable
Это, пожалуй, та часть Foundation Models, которая мне нравится больше всего — даже больше, чем сам факт on-device LLM. Потому что она решает реальную головную боль, с которой сталкивался каждый, кто работал с языковыми моделями.
Допустим, мы хотим автоматически анализировать отзывы пользователей в App Store. От модели нам нужны структурированные данные: тональность отзыва, оценка от 1 до 5, список конкретных проблем, краткое резюме. Классический подход — попросить модель вернуть JSON-строку, потом распарсить её на стороне приложения. Выглядит это примерно так:
// Хрупкий подход — просим JSON и парсим вручную
let response = try await session.respond(to: """
Проанализируй отзыв: (userReview)
Верни JSON: { "sentiment": "...", "rating": ..., "issues": [...] }
""")
let data = response.content.data(using: .utf8)!
let json = try JSONSerialization.jsonObject(with: data) // может упасть
// Дальше — приведение типов, проверка полей, обработка ошибок...
С такой схемой связан целый букет проблем. Модель может вернуть невалидный JSON — например, с одинарными кавычками вместо двойных или с trailing comma. Может забыть обязательное поле или, наоборот, добавить лишнее. Может перепутать тип значения: вернуть rating как строку, а не как число. Может добавить пояснительный текст вокруг JSON, который ломает парсинг. Любой из этих случаев становится багом в продакшне.
Foundation Models решает эту проблему через макрос @Generable и технику под названием constrained decoding.
Идея простая: описываем нужную структуру как обычный Swift-тип, помечаем @Generable, каждое поле аннотируем @Guide с описанием на естественном языке. Дальше передаём этот тип в respond(to:generating:), и модель сама заполняет его:
@Generable
struct AppReview {
@Guide("Тональность отзыва: positive, negative или neutral")
var sentiment: String
@Guide("Оценка от 1 до 5, где 5 — отлично")
var rating: Int
@Guide("Список конкретных проблем, упомянутых в отзыве. Пустой массив, если проблем нет.")
var issues: [String]
@Guide("Краткое резюме в одном предложении")
var summary: String
}
// Использование:
let review = try await session.respond(
to: "Проанализируй отзыв: (userReview)",
generating: AppReview.self
)
// review — уже готовый Swift-объект, никакого парсинга
print(review.sentiment) // "negative"
print(review.rating) // 2
print(review.issues) // ["вылетает при загрузке", "запутанная навигация"]
Что здесь происходит. Когда мы пишем generating: AppReview.self, компилятор Swift через макрос @Generable генерирует схему, описывающую структуру типа: какие у него поля, какие у них типы, какие есть ограничения. Эта схема передаётся в модель не как часть промпта, а как ограничение на процесс генерации.
И вот тут самое интересное: модель в процессе генерации ограничена только теми токенами, которые валидны для текущей позиции в схеме. Если она сейчас генерирует значение для поля rating: Int, она физически не может вернуть слово или дробное число — следующий токен может быть только цифрой. Это и называется constrained decoding.
У такого подхода два преимущества. Первое — гарантия валидности: модель никогда не вернёт «битый» результат, потому что невалидный токен ей просто недоступен. Второе, неожиданное — скорость: чем меньше пространство возможных токенов на каждом шаге, тем меньше вычислений нужно. В сессии #286 Apple это явно подчеркнули: Guided Generation одновременно повышает точность и ускоряет инференс.
С качеством формулировок в @Guide есть свой нюанс. Чем точнее описание, тем точнее результат. Сравните:
// Размытое описание
@Guide("оценка")
var rating: Int
// Конкретное описание
@Guide("Оценка от 1 до 5, где 1 — очень плохо, 5 — отлично. Учитывай общий тон отзыва.")
var rating: Int
В первом случае модель может вернуть оценку в любом диапазоне — 0, 100, что угодно. Во втором случае она получает контекст: 1 — плохо, 5 — отлично, нужно учитывать тон. По сути @Guide — это инструкция модели на естественном языке, привязанная к конкретному полю. Это то же самое, что промпт, только более точный и локальный.
Структуры можно вкладывать и комбинировать с enum:
@Generable
enum Severity {
case critical, high, medium, low
}
@Generable
struct BugReport {
@Guide("Краткое описание проблемы одним предложением")
var title: String
@Guide("Шаги для воспроизведения проблемы")
var reproSteps: [String]
@Guide("Критичность проблемы")
var severity: Severity
@Guide("Предполагаемая причина, если очевидна. nil если не очевидна.")
var probableCause: String?
}
В моём прототипе бага-трекера такая структура заменила штук пятьдесят строк парсинг-кода и обработки ошибок. И, что более важно, исчезли все баги вида «иногда краш на пустом массиве».
Tool Calling: подключаем модель к реальным данным
Языковая модель работает только с тем контекстом, который ей даёт промпт. Она не знает прогноз погоды на завтра, не знает курс рубля сейчас, не знает ваше расписание и не имеет доступа к вашей базе данных. Если попросить её ответить на такие вопросы напрямую, она либо честно скажет «я не знаю», либо начнёт галлюцинировать — выдавать правдоподобный, но выдуманный ответ.
Tool Calling — механизм, который решает эту проблему. Вы описываете функции (тулы), которые умеет вызвать ваш код, и модель сама решает, когда и с какими аргументами их использовать.
Рассмотрим на конкретном примере. Пишем тул для поиска ресторанов:
struct RestaurantSearchTool: Tool {
// Имя тула — модель использует его внутренне
let name = "searchRestaurants"
// Описание для модели: когда использовать этот тул
let description = "Ищет рестораны по параметрам и возвращает список подходящих"
// Аргументы описываются через @Generable
@Generable
struct Arguments {
@Guide("Тип кухни: italian, japanese, russian и т.д.")
var cuisine: String
@Guide("Количество гостей")
var guests: Int
@Guide("Дата в формате YYYY-MM-DD")
var date: String
@Guide("Минимальный рейтинг от 1 до 5, по умолчанию 4")
var minRating: Double?
}
// Сама функция: получает аргументы, возвращает результат
func call(arguments: Arguments) async throws -> ToolOutput {
let results = await RestaurantAPI.search(
cuisine: arguments.cuisine,
guests: arguments.guests,
date: arguments.date,
minRating: arguments.minRating ?? 4.0
)
let text = results.map { "($0.name) — ($0.rating)⭐" }
.joined(separator: "n")
return ToolOutput(text)
}
}
Подключаем тул к сессии и пишем человеческий запрос:
let session = LanguageModelSession(tools: [RestaurantSearchTool()])
let answer = try await session.respond(
to: "Найди итальянский ресторан на 4 человека на следующую пятницу"
)
print(answer.content)
Что происходит внутри. Модель получает запрос, видит, что у неё есть тул searchRestaurants, читает его описание, понимает, что это именно то, что нужно. Дальше она самостоятельно извлекает параметры из запроса пользователя: italian для кухни, 4 для количества гостей, и преобразует «следующую пятницу» в конкретную дату. Вызывает call(arguments:), получает результат, и формирует финальный ответ для пользователя.
Чем это принципиально отличается от классического подхода с интентами и регулярными выражениями. Раньше, чтобы реализовать такую функциональность, нужно было писать сложную логику разбора намерений пользователя: парсить дату из «следующая пятница», извлекать тип кухни через keyword matching или NLP, обрабатывать варианты формулировок. С Tool Calling вы просто описываете, что умеет ваше приложение, а модель сама разбирается, как интерпретировать запрос. Это куда декларативнее.
В одну сессию можно передать несколько тулов, и модель будет выбирать нужный в зависимости от контекста, или комбинировать их:
let session = LanguageModelSession(tools: [
RestaurantSearchTool(),
WeatherTool(),
CalendarTool(),
])
// На запрос "Найди ресторан на пятницу если погода будет хорошей"
// модель может сначала вызвать WeatherTool, проверить прогноз,
// потом CalendarTool для уточнения даты, потом RestaurantSearchTool
Дополнительная мелочь, которая мне понравилась: ToolOutput поддерживает указание источника данных через ToolOutput.Source. Если ваш тул возвращает данные из конкретного API, можно передать ссылку и название источника, и модель включит это в финальный ответ. Получается встроенный fact-checking — пользователь видит не просто «вот ресторан», а «вот ресторан, источник такой-то».
Когда использовать что: выбор фреймворка под задачу
Foundation Models — мощный инструмент, но не единственный AI-фреймворк в iOS. И это, пожалуй, самая важная мысль, которую я хочу донести в этой статье: не нужно тащить LLM туда, где задачу решает более простой и подходящий инструмент.
Принцип, которого я придерживаюсь: использовать наивысший по уровню абстракции API, который решает задачу. Чем выше уровень — тем меньше кода нужно написать, тем лучше интеграция с системой, тем меньше ML-ответственности на команде.
Если задача — позволить пользователю отредактировать текст с помощью AI (переписать, исправить ошибки, сократить), то самый правильный инструмент — Writing Tools. Это системная функция Apple, доступная через контекстное меню в любом текстовом поле. Если в вашем приложении используется стандартный UITextView с UITextInteraction, Writing Tools уже работают — без единой строки кода с вашей стороны. Apple сделала эту интеграцию автоматической.
Если у вас кастомный текстовый редактор (например, Markdown-редактор с собственным движком), для интеграции Writing Tools есть Coordinator API, добавленный в iOS 26. Через UIWritingToolsCoordinator можно получить полноценную интеграцию с анимацией переписывания, проверкой орфографии прямо в строке текста и follow-up запросами от пользователя:
class CustomEditor: UIView {
var writingToolsCoordinator: UIWritingToolsCoordinator?
override func awakeFromNib() {
super.awakeFromNib()
// Создаём координатор и привязываем к своему движку
let coordinator = UIWritingToolsCoordinator(delegate: self)
self.writingToolsCoordinator = coordinator
}
}
extension CustomEditor: UIWritingToolsCoordinatorDelegate {
func writingToolsCoordinator(
_ coordinator: UIWritingToolsCoordinator,
replace originalText: String,
with newText: String,
in range: NSRange
) {
// Применяем переписанный текст к своему движку.
// Анимация переписывания добавится автоматически.
myTextStorage.replaceCharacters(in: range, with: newText)
}
}
Если задача — предложить пользователю готовые варианты ответа в чате (как это сделано в Сообщениях), то это Smart Reply API. Модель анализирует контекст переписки и возвращает 3–5 подходящих коротких ответов.
Если нужно проанализировать изображение — распознать объекты, прочитать текст, найти таблицы — это Vision Framework. В iOS 26, кстати, в Vision появился важный обновлённый API: VNRecognizeDocumentRequest, который понимает структуру документа целиком — таблицы, списки, заголовки, параграфы — а не просто распознаёт отдельные строки текста, как делал старый OCR.
Если задача — распознавание речи, особенно длинных записей вроде митингов или лекций, в iOS 26 рекомендуется использовать новый SpeechAnalyzer вместо устаревшего SFSpeechRecognizer. Новый API оптимизирован именно под длинные аудио, поддерживает streaming через AsyncSequence и работает с distant speaker (когда говорящий находится не вплотную к микрофону).
И только если задача — что-то более кастомное и связанное с текстом: анализировать отзывы, извлекать структурированные данные из произвольного текста, генерировать персонализированный контент, реализовать собственного чат-ассистента — тогда Foundation Models. Это правильный инструмент именно для гибкой текстовой логики, которую нельзя свести к фиксированной системной функции.
Что не стоит ожидать от модели на 3B параметров
Foundation Models — это компактная модель. По размеру она примерно в 500 раз меньше GPT-4. И этот факт нужно учитывать при выборе задач, которые ей поручать.
Что эта модель делает хорошо: классифицирует короткий текст, извлекает структурированную информацию из произвольного текста, переписывает короткие фрагменты, делает выжимку (summarization), отвечает на простые вопросы по контексту, который ей дали в промпте. То есть всё, что я называю «текстовая логика среднего уровня».
Что она делает плохо или не делает совсем. Математические вычисления — даже простую арифметику она часто проваливает. Это в принципе известное ограничение всех языковых моделей, и Foundation Models здесь не исключение. Для вычислений используйте обычный код. Длинные многошаговые рассуждения — модель путается, теряет промежуточные результаты, контекстное окно переполняется. Лучше разбивать сложную задачу на цепочку коротких запросов, где каждый следующий получает результат предыдущего как контекст. Фактические вопросы о реальном мире, особенно о свежих событиях, — здесь модель будет галлюцинировать с уверенным видом. Для таких случаев нужен Tool Calling с подключением к надёжному источнику данных.
Под промптинг такой модели нужно адаптироваться. Несколько практических наблюдений из прототипа.
Первое — указывать ожидаемый объём ответа явно. «Объясни SwiftUI» даёт расплывчатый длинный текст. «Объясни SwiftUI в трёх пунктах, каждый не длиннее двух предложений» — конкретный и применимый ответ.
Второе — указывать формат, если нужен конкретный. «Опиши шаги» может дать что угодно. «Опиши шаги в виде нумерованного списка, не более 5 пунктов» — структурированный ответ. Хотя для жёстко структурированных задач, конечно, лучше использовать @Generable.
Третье — разбивать сложные задачи. Если хочется одним запросом сделать ревью кода, придумать рефакторинг, написать тесты и составить документацию, модель растеряется. Гораздо лучше работает цепочка: сначала спросить про баги, потом про рефакторинг с учётом найденных багов, потом про тесты под рефакторенный код.
Для итерации по промптам в Xcode 26 есть удобный инструмент — макрос #playground, который исполняет код прямо в IDE без перекомпиляции основного проекта:
#playground {
let session = LanguageModelSession()
let result = try await session.respond(to: "Твой промпт здесь")
print(result.content)
}
// Cmd+Return — запуск, результат виден сразу
Когда дорабатываешь промпт, такой workflow реально экономит время. Меняешь формулировку — нажимаешь Cmd+Return — видишь, как меняется ответ. Без необходимости запускать симулятор и проходить путь до экрана с моделью.
Пример полной интеграции в SwiftUI
Соберём всё рассмотренное в одну рабочую ViewModel. Это код, который можно скопировать в реальный проект и доработать под свою задачу:
@Observable
final class AIAssistantViewModel {
// Сессия создаётся один раз — при создании ViewModel
private let session = LanguageModelSession(
instructions: "Ты помощник для iOS-разработчиков. Отвечай кратко, с примерами кода."
)
var response = ""
var isLoading = false
var errorMessage: String?
func ask(_ prompt: String) async {
// Проверяем, что модель доступна на этом устройстве
guard LanguageModelSession.isAvailable else {
errorMessage = "AI недоступен на этом устройстве"
return
}
isLoading = true
errorMessage = nil
response = ""
do {
// Используем стриминг для лучшего UX
for await partial in session.streamResponse(to: prompt) {
await MainActor.run {
response += partial.text
isLoading = false // первый кусок пришёл — спиннер убираем
}
}
} catch LanguageModelError.unsupportedLanguage {
errorMessage = "Этот язык пока не поддерживается"
} catch LanguageModelError.contextWindowExceeded {
// В реальном приложении здесь стоит создать новую сессию
// с резюме предыдущего разговора в instructions
errorMessage = "Разговор слишком длинный, начни заново"
} catch LanguageModelError.guardrailViolation {
errorMessage = "Не могу помочь с этим запросом"
} catch {
errorMessage = "Что-то пошло не так. Попробуй ещё раз."
}
isLoading = false
}
}
SwiftUI-вью, которая использует эту ViewModel:
struct AIAssistantView: View {
@State private var viewModel = AIAssistantViewModel()
@State private var input = ""
var body: some View {
VStack(spacing: 16) {
ScrollView {
if viewModel.isLoading {
ProgressView()
.frame(maxWidth: .infinity)
}
Text(viewModel.response)
.frame(maxWidth: .infinity, alignment: .leading)
.animation(.easeInOut, value: viewModel.response)
}
if let error = viewModel.errorMessage {
Text(error)
.foregroundStyle(.red)
.font(.caption)
}
HStack {
TextField("Спроси что-нибудь...", text: $input)
.textFieldStyle(.roundedBorder)
Button("Отправить") {
let prompt = input
input = ""
Task { await viewModel.ask(prompt) }
}
.disabled(input.isEmpty || viewModel.isLoading)
}
}
.padding()
}
}
Этот код реализует базового чат-ассистента: вводишь сообщение, видишь стриминг ответа, ошибки обрабатываются и показываются пользователю в человеческом виде. От продакшн-готового решения его отделяет ещё несколько шагов: обработка длинных диалогов с автоматическим резюмированием, сохранение истории между запусками приложения, более тонкая работа с UI во время стриминга. Но базовая структура — именно такая.
Подводные камни, которые я нашёл в прототипе
Несколько наблюдений из практики, которые могут сэкономить кому-то время.
Контекстное окно конечно, и его исчерпание приходит неожиданно. На моих типичных коротких диалогах оно держалось 20–30 обменов сообщениями, дальше начинался contextWindowExceeded. Если приложение предполагает долгие разговоры, нужен механизм автоматического резюмирования старой части истории и создания новой сессии с этим резюме в instructions. Без такого механизма пользователь получит непонятную ошибку в середине разговора.
Скорость генерации зависит от устройства. На моём iPhone 15 Pro модель генерировала примерно 30–50 токенов в секунду на коротких ответах. На более новых устройствах должно быть быстрее, но числа сильно зависят от загруженности системы — если параллельно идёт другой Neural Engine workload, скорость падает. Это аргумент за стриминг — он сглаживает восприятие задержки, даже если общее время генерации не уменьшается.
guardrailViolation иногда срабатывает на удивительных вещах. Например, в моих тестах модель отказывалась обсуждать некоторые медицинские темы даже в общем образовательном контексте. С этим придётся жить — встроенные guardrails переопределить нельзя. Если ваш use case плотно завязан на пограничные темы, возможно, Foundation Models не подойдёт.
Производительность зависит от размера контекста. Чем больше история сессии — тем медленнее каждый следующий запрос, потому что модели нужно учитывать весь контекст. Это ещё один аргумент за периодическое «обнуление» сессии с сохранением только релевантного резюме.
Отладка ошибок неудобная. Если что-то идёт не так внутри модели, наружу прилетает довольно общее сообщение об ошибке. В iOS 26 нет отдельного debug-режима для модели, который показал бы, например, как именно модель интерпретировала промпт. Это, видимо, обратная сторона приватности — но при разработке иногда хочется большей наблюдаемости. Помогает только итерация через #playground.
Что в итоге
Foundation Models — это не «революция» и не «убийца ChatGPT», как иногда преподносят в новостях. Это рабочий инструмент в одной конкретной нише: задачи с текстовой логикой средней сложности, которые нужно решать локально, без облака, с гарантией приватности и без затрат на инфраструктуру.
Для таких задач это, пожалуй, лучшее, что есть на iOS сейчас. Три строки кода до базовой работы, @Generable для type-safe структурированного вывода без парсинга JSON, Tool Calling для подключения к реальным данным, грамотная обработка ошибок и safety из коробки. Всё на устройстве, всё бесплатно.
В моём прототипе чат-ассистента для iOS-разработчиков Foundation Models покрыл, по моим оценкам, процентов 80 от того, что я ожидал получить от облачного API. Остальные 20 — это случаи, требующие свежих фактических данных или более глубокого рассуждения, и для них пришлось бы либо подключать тулы с внешними API, либо переходить на гибридную архитектуру с облачным fallback.
Главный совет, который я бы дал себе перед началом работы: не пытайтесь использовать Foundation Models для всего подряд. Сначала посмотрите, не закрывается ли ваша задача более высокоуровневым API — Writing Tools, Smart Reply, Vision. Если закрывается — берите его, выйдет меньше кода и лучше системная интеграция. И только если действительно нужна гибкая текстовая логика — тогда Foundation Models с правильно подобранным @Generable и Tool Calling.
Полезные ссылки для тех, кто хочет углубиться:
-
Сессия #360 — Discover ML & AI Frameworks: developer.apple.com/videos/play/wwdc2025/360/
-
Сессия #265 — Dive Deeper into Writing Tools: developer.apple.com/videos/play/wwdc2025/265/
-
Сессия #286 — Meet the Foundation Models Framework: developer.apple.com/videos/play/wwdc2025/286/
-
Документация фреймворка: developer.apple.com/documentation/foundationmodels
Если у вас уже есть опыт работы с Foundation Models — поделитесь, на какие подводные камни наткнулись вы. Особенно интересно, как решаете проблему с переполнением контекстного окна в долгих диалогах: пробовал несколько подходов с автоматическим резюмированием, но ни один пока не дал стабильно хороший результат.
Автор: O4ErtO


