- BrainTools - https://www.braintools.ru -
Когда я готовился к внутреннему митапу по WWDC 2025 в нашей iOS-команде, нужно было сделать обзор сессий #360 (Discover ML & AI Frameworks) и #265 (Dive Deeper into Writing Tools). Доклад я уже провёл, но при подготовке набралось много заметок, которые в формат презентации не влезли: подводные камни, неочевидные решения, паттерны использования. Эта статья — попытка собрать всё это в одном месте.
Речь пойдёт о Foundation Models Framework: что это, как устроено внутри, как с этим работать в реальном приложении и где у этого фреймворка границы применимости. Я постарался не пересказывать документацию Apple, а сосредоточиться на тех моментах, которые не очевидны при первом знакомстве и о которых я бы хотел узнать раньше, чем начал писать первый прототип.
До 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 завершится ошибкой [1]. Об этом стоит помнить и закладывать в архитектуру fallback на облачный API или просто отключение AI-функций для несовместимых устройств.
Вернёмся к минимальному примеру и разберём каждую строку. Это полезно сделать, потому что каждая из них скрывает за собой выбор, который повлияет на архитектуру приложения.
Подключение фреймворка. Звучит банально, но стоит проверить совместимость на этапе сборки и в рантайме. На этапе сборки достаточно поднять минимальный target до iOS 26, иначе линкер просто не найдёт символы. В рантайме нужна проверка LanguageModelSession.isAvailable:
guard LanguageModelSession.isAvailable else {
// Модель недоступна на этом устройстве.
// Здесь должен быть fallback — например, скрыть AI-функцию
// или использовать облачный API.
return
}
Этот guard критичен, потому что приложение может оказаться на устройстве, которое формально поддерживает iOS 26, но не имеет достаточно мощного Neural Engine. В моём прототипе я сначала забыл про эту проверку и получил неприятный сюрприз при тестировании на iPhone 14 — приложение крашилось при первом обращении к модели.
LanguageModelSession() создаёт объект сессии. И вот здесь начинается самое интересное, потому что сессия — это не просто обёртка над запросом. Это полноценный stateful объект, который хранит историю всех ваших запросов и ответов модели.
Это важно по двум причинам. Во-первых, модель помнит контекст: если вы в первом запросе сказали «меня зовут Артём», а во втором спросили «как меня зовут», она ответит правильно. Это и есть та самая память [2] диалога, которая в облачных API обычно реализуется ручной передачей истории сообщений в каждый запрос.
Во-вторых — и это куда важнее с практической точки зрения [3] — создание сессии стоит дорого. Под капотом происходит загрузка весов модели в память 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 с частями ответа по мере генерации. Это полностью повторяет поведение [4] 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. Полезно для отладки и понимания, насколько большие у вас запросы.
Особое внимание [5] стоит уделить случаю finishReason == .length. Если он встречается часто, значит ваши запросы или ожидаемые ответы слишком длинные. Пользователь в этой ситуации видит ответ, оборванный на середине предложения, что выглядит как баг. Лучше детектировать такой случай и либо разбивать запрос на части, либо явно сообщать пользователю, что ответ обрезан.
Это, пожалуй, та часть Foundation Models, которая мне нравится больше всего — даже больше, чем сам факт on-device LLM. Потому что она решает реальную головную боль [6], с которой сталкивался каждый, кто работал с языковыми моделями.
Допустим, мы хотим автоматически анализировать отзывы пользователей в 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 — механизм, который решает эту проблему. Вы описываете функции (тулы), которые умеет вызвать ваш код, и модель сама решает, когда и с какими аргументами их использовать.
Рассмотрим на конкретном примере. Пишем тул для поиска ресторанов:
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:), получает результат, и формирует финальный ответ для пользователя.
Чем это принципиально отличается от классического подхода с интентами и регулярными выражениями. Раньше, чтобы реализовать такую функциональность, нужно было писать сложную логику [7] разбора намерений пользователя: парсить дату из «следующая пятница», извлекать тип кухни через 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. Это правильный инструмент именно для гибкой текстовой логики, которую нельзя свести к фиксированной системной функции.
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 — видишь, как меняется ответ. Без необходимости запускать симулятор и проходить путь до экрана с моделью.
Соберём всё рассмотренное в одну рабочую 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, скорость падает. Это аргумент за стриминг — он сглаживает восприятие [8] задержки, даже если общее время генерации не уменьшается.
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/ [9]
Сессия #265 — Dive Deeper into Writing Tools: developer.apple.com/videos/play/wwdc2025/265/ [10]
Сессия #286 — Meet the Foundation Models Framework: developer.apple.com/videos/play/wwdc2025/286/ [11]
Документация фреймворка: developer.apple.com/documentation/foundationmodels [12]
Если у вас уже есть опыт [13] работы с Foundation Models — поделитесь, на какие подводные камни наткнулись вы. Особенно интересно, как решаете проблему с переполнением контекстного окна в долгих диалогах: пробовал несколько подходов с автоматическим резюмированием, но ни один пока не дал стабильно хороший результат.
Автор: O4ErtO
Источник [14]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/30224
URLs in this post:
[1] ошибкой: http://www.braintools.ru/article/4192
[2] память: http://www.braintools.ru/article/4140
[3] зрения: http://www.braintools.ru/article/6238
[4] поведение: http://www.braintools.ru/article/9372
[5] внимание: http://www.braintools.ru/article/7595
[6] боль: http://www.braintools.ru/article/9901
[7] логику: http://www.braintools.ru/article/7640
[8] восприятие: http://www.braintools.ru/article/7534
[9] developer.apple.com/videos/play/wwdc2025/360/: https://developer.apple.com/videos/play/wwdc2025/360/
[10] developer.apple.com/videos/play/wwdc2025/265/: https://developer.apple.com/videos/play/wwdc2025/265/
[11] developer.apple.com/videos/play/wwdc2025/286/: https://developer.apple.com/videos/play/wwdc2025/286/
[12] developer.apple.com/documentation/foundationmodels: https://developer.apple.com/documentation/foundationmodels
[13] опыт: http://www.braintools.ru/article/6952
[14] Источник: https://habr.com/ru/articles/1035022/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1035022
Нажмите здесь для печати.