iOS 26 时期 Foundation Models 真正的硬限制从来不是模型质量,而是算术:4096 个 token 要装下全部内容 —— 指令、工具定义,以及整段对话记录。到了 iOS 27,同一个 session 多了一种 32K 上下文、推理明显更强的模式,切换只要一行代码。代价既不是钱也不是 API key,而那部分恰恰是最陌生的。
这个框架的基础指南我在八月针对 iOS 26 写过 —— SystemLanguageModel、@Generable、工具调用。这篇只讲增量:到 2026 年 9 月为止新增的东西,以及它改变了哪些产品决策。
版本说明:下文若无特别标注,均指 iOS 27、iPadOS 27、macOS 27 和 visionOS 27。有一处值得单独标出:watchOS 到 27 才有 Foundation Models,26 上这个框架并不存在。
这个框架不再只关于苹果的那一个模型#
一年前文档把它描述为"访问 Apple Intelligence 的模型"。现在概述的第一行换了说法:访问任意大语言模型 —— 端侧的、服务端的,或者你自己带来的。有了 LanguageModel 协议,这三者在同一套 API 里都是一等公民。
落到实处就是:LanguageModelSession、Instructions、Tool、@Generable、流式输出 —— 这些只写一次,变的是插进去的那个模型。下面就看这三个被插进去的模型。
Private Cloud Compute:同一个 session,另一个天花板#
切换长这样:
// 用服务端模型创建 session。
let session = LanguageModelSession(model: PrivateCloudComputeLanguageModel())其余部分原样迁移:respond、流式、工具、指令。SystemLanguageModel 和 PrivateCloudComputeLanguageModel 都遵循 LanguageModel 协议,所以 session 的初始化方法是同一个。
得到的是:32K 上下文和更强的推理,适合长文档和多轮对话。失去的是:离线能力。PCC 需要网络,而当请求因连接问题失败时,苹果自己给的建议就是改用端侧模型重试 —— 也就是说,兜底逻辑你照样得写。
可用性要单独检查,而且理由自成一套:
let model = PrivateCloudComputeLanguageModel()
switch model.availability {
case .available:
// 展示 AI 功能界面。
case .unavailable(.deviceNotEligible):
// 展示替代界面。
case .unavailable(.systemNotReady):
// PCC 暂时还不能处理请求。
case .unavailable(let other):
// 因未知原因不可用。
}再加上针对 iOS 27 / macOS 27 / watchOS 27 / visionOS 27 的 #available 判断,更早的版本回落到端侧模型。
接下来就是那笔陌生的费用。要基于 PCC 开发,你必须满足苹果的资格要求,并申请 managed entitlement com.apple.developer.private-cloud-compute。这不是 Capabilities 里勾一个框,这是一份申请。如果在规划 PCC 功能,请把拿到权限的时间排进日程,否则演示那天你手上会有一段漂亮但跑不起来的代码。
配额属于用户,不属于你#
服务端 LLM 的熟悉模式:token 的钱你付,用户根本不知道有 token 这回事。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() 会拉起系统的升级界面 —— 你不用自己做定价页,而从苹果的措辞看,也不该做。
配额耗尽是一个独立的错误,和限流是两回事:限流是等一会儿再来,配额耗尽则是等到重置,或者升级。重置时间框架也给,但可能为空。
这套东西可以在不烧掉真实额度的情况下测试:Product > Scheme > Edit Scheme > Run > Options > Simulated Apple Foundation Models Availability,里面有 "Approaching Quota Usage Limit" 和 "Quota Usage Limit Reached"。
推理变成了你自己拧的旋钮#
let response = try await session.respond(
to: "这套架构里有哪些取舍?",
contextOptions: ContextOptions(reasoningLevel: .deep)
)一共三档,两端是 .light 和 .deep。推理越深,延迟越高,而且模型自己的推理文本会吃掉更多上下文窗口。实践中后者更难受:你为了质量开了 .deep,结果在对话第三轮收到 exceededContextWindowSize。
推理片段本身不会进入回复正文 —— 它们在 transcript 里可见,排查模型为什么给出奇怪答案时很有用。
苹果的建议是从最低档起步,按评估结果往上调。建议本身很乏味,但现在有支撑:框架新增了提示词评估工具,还有一个专门的 Foundation Models instrument,会显示延迟、提示词、模型输出、工具调用和 token 消耗。
把图片放进提示词#
多模态输入进了普通的 respond:
func compareImages(imageOne: CGImage, imageTwo: CGImage) async throws -> String {
let session = LanguageModelSession()
let response = try await session.respond {
"用三条要点比较这两张图片:"
Attachment(imageOne)
// 图片没有应用旋转时 —— 比如来自 AVFoundation 的帧 ——
// 传入 orientation,框架会先做变换。
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 是有作用的:不加它,模型可能挑一个"差不多对"的标签,这在分类器里就是个不出声的 bug。
还有现成的图像工具 —— 条码识别(BarcodeReaderTool)和文字提取。当场上有多个工具时,给图片加标签 —— Attachment(image).label("barcode-image") —— 好让模型知道哪个工具对应哪张图。
一个影响很不成比例的提示词细节:"列出这张照片里的所有食物"比"这张图里有什么?"效果好得多。
动态配置文件:每次请求前重新组装的指令#
以前 session 在创建时就把指令冻住了。对于多步流程 —— 找菜谱、替换食材、查库存、一步步指导烹饪 —— 这只剩两个选择:写一条塞满所有情况的臃肿指令,或者重建 session 并丢掉历史。
现在 DynamicInstructions 的 body 会在每次请求模型之前重新求值:
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,它把指令和 session 级配置绑在一起;以及 DynamicProfile,负责在 profile 之间切换。切换由编译器把关:同时只能有一个 profile 处于活跃状态,所以分支要写成 if / else if / else,而不是并列的 if。
Profile {
// 面向创作类任务的指令与工具。
}
.model(pccModel)
.temperature(likesPoetry ? 0.8 : 0.1)
.reasoningLevel(likesAstronomy ? .deep : .light)修饰符按三级优先级解析:直接传给 respond(to:options:) 的选项压过一切;子 profile 上的修饰符压过动态 profile 的;动态 profile 本身负责定默认值。
少了这一条,配置文件只会让事情更糟#
body 里的声明顺序会影响性能,而且影响很大。
一个 session 会摊平成 token 序列:先是指令,然后是工具定义,最后是 transcript。提供方的 KV 缓存只在第一个发生变化的 token 之前有效,之后的全部重算。所以静态指令和工具放在 body 的上面,条件块放在下面。把条件块放最前,那个标志每切换一次,整段对话的缓存就作废一次。
由此引出几条好心最容易违反的推论:
- 工具集合在创建 session 时就定下。对话中途加工具不仅毁缓存,效果也不好:模型已经学会了前几轮的模式。
- 移除工具时,把 transcript 里它的调用记录也一并清掉,否则模型会看到指向定义中已不存在之物的引用。
- 裁剪 transcript 要少而集中。每轮裁一次就是每轮作废一次;在接近上下文上限时一次性整理反而更便宜。
- 切换 profile 会重写整个前缀,等于把缓存清空。请在流程的自然边界上做,而不是每轮都做。
如果你知道距离下一次请求至少还有一两秒,prewarm(promptPrefix:) 可以在它到来之前先把前缀算进缓存。
这些都能在 Instruments 里量化:用缓存命中的输入 token 除以输入 token 总数。轮次之间这个比值偏低,说明缓存正在被丢弃,模型每次都在重嚼前缀。
这些该怎么用#
我目前会保持的顺序是:先用端侧模型,并对功能做量化评估。如果撞上 4096 token 的墙或者推理质量不够,再加 PCC —— 前提是 entitlement 早点申请,配额状态也设计进界面。至于 profile,等流程里真的存在多个模式再上,而不是因为 API 是新的。
还有第三个选项,压根不是苹果的模型。LanguageModel 协议是开放的,用 Core AI 导出的模型可以放进同一个 LanguageModelSession:这样就切断了对 Apple Intelligence 的依赖,在没有它的设备上照样能跑。



