将 LLM 提供平台引入 Foundation Models 框架
Bring an LLM provider to the Foundation Models framework
2026年6月10日
一句话判断
✅ “Apple 把 Foundation Models 变成了 iOS 界的 Vercel AI SDK,通过 LanguageModelExecutor 协议彻底抹平了本地和云端大模型的调用差异。“
这场 Session 讲了什么
这场 Session 最值得关注的一件事是,Apple 终于把 Foundation Models 框架开放给第三方大模型了。以前我们在 iOS 里接 Claude、Gemini 或者跑本地 MLX 模型,每个都要写一套网络请求、流式解析、上下文管理的样板代码,换个模型就得重构一半逻辑。
现在情况变了。Apple 推出了 LanguageModel 和 LanguageModelExecutor 两个协议。只要模型提供商(或者你自己)实现这两个协议,把底层推理引擎包装起来,App 开发者就能用同一套 LanguageModelSession 和 respond(to:) API 无缝切换模型。从设备端的 System Language Model,到云端的 Private Cloud Compute (PCC),再到第三方的 Anthropic 和 Google,调用方式完全一样。
这对 AI 应用开发者是巨大利好,换模型就像换皮肤一样简单。对模型提供商也是个明确的信号:赶紧出个 Swift Package 抢占生态,谁先接入谁就能被开发者无痛集成。
值得深挖的点
Executor 缓存与 KV Cache 复用机制
Apple 在设计 LanguageModelExecutor 时,用了一个很聪明的机制:基于 Configuration 的 Hash 值来缓存 Executor 实例。当你在 LanguageModelSession 里发起多次请求时,只要配置没变,框架就会复用同一个 Executor。
这个设计的关键是状态复用。对于本地模型,这意味着权重只需要加载一次;对于云端模型,这意味着你可以跨请求保留 KV 缓存(Key-Value Cache)或持久化连接,大幅降低网络开销和首字延迟。不过这里有个坑:框架每次都会把完整的 Transcript(对话记录)传给你,如果你的 App 为了省 token 裁剪了历史消息,Executor 需要自己做 diff 对比。如果发现上下文被删改,你得自己处理状态回滚,Apple 并没有帮你做这层脏活。
流式输出的“元数据优先”握手协议
用过第三方大模型 SDK 的人都知道,流式输出时最怕的就是“不知道这次请求花了多少钱”或者“不知道底层到底路由到了哪个模型”。Apple 在 LanguageModelExecutorGenerationChannel 里强制规定了一个发送顺序:先发 metadata(模型和请求 ID),再发 usage(token 消耗),最后才是 text delta(文本增量)。
这个设计非常老道。它让开发者可以在流式输出刚开始时,就拿到计费和路由信息,提前做拦截或 UI 展示,不用傻等到整个流结束。如果你自己写 Executor,千万别一上来就发文本,严格遵守这个顺序,否则上层的 LanguageModelSession 状态机会乱套。
代码片段
1. 无缝切换底层模型
场景:展示如何用同一套 API 切换设备端模型和第三方开源模型。
import FoundationModels
import MLXFoundationModels
// 使用苹果设备端模型
let model = SystemLanguageModel()
// 或者无缝切换到 HuggingFace 上的 MLX 开源模型
// let model = MLXLanguageModel(modelID: "mlx-community/my-model")
// 创建会话并发起请求,API 完全一致
let session = LanguageModelSession(model: model)
let response = try await session.respond(to: "解释一下 Swift 的 actor 机制")
print(response.content)
坑:切换模型时,如果新模型不支持旧模型配置的某些 GenerationOptions(比如特定的 sampling strategy),Executor 可能会直接抛出 LanguageModelError,记得做好 fallback。
2. 实现标准的流式输出顺序
场景:在自己的 LanguageModelExecutor 中,按 Apple 推荐的顺序向 channel 发送事件。
func respond(
to request: LanguageModelExecutorGenerationRequest,
model: MyLanguageModel,
streamingInto channel: LanguageModelExecutorGenerationChannel
) async throws {
// 先发送元数据,让开发者知道请求 ID 和模型版本
await channel.send(.response(action: .updateMetadata([
"modelID": "my-model-v2",
"requestID": request.id.uuidString
])))
// 接着发送 token 消耗,方便提前计费或限流
await channel.send(.response(action: .updateUsage(
input: .init(totalTokenCount: 150, cachedTokenCount: 50),
output: .init(totalTokenCount: 0)
)))
// 最后才开始流式发送文本增量
for try await token in myInferenceEngine.stream(request) {
await channel.send(.response(action: .appendTextDelta(token)))
}
}
坑:updateUsage 里的 cachedTokenCount 对于支持 Prompt Caching 的模型(如 Claude)非常重要,漏传会导致开发者无法准确计算成本。
最佳实践
已有项目如果已经自己封装了各大模型的 SDK,建议逐步将网络层和流式解析层抽离,包装成独立的 Swift Package 并实现 LanguageModelExecutor。这样业务层就可以统一迁移到 LanguageModelSession,彻底消灭各种自定义的 Message 和 ChatStream 结构体。
新项目强烈建议直接基于 Foundation Models 框架开发。不要自己去写 SSE (Server-Sent Events) 解析或者 WebSocket 状态管理,Apple 的框架已经处理好了断线重连和内存释放。
实战中最容易踩的坑是凭证管理。官方明确反对在初始化器里直接接收 String 类型的 API Key。一定要用 Keychain 存储 token,如果是云端服务,尽量结合 App Attest 做设备认证,防止你的 API 被脚本刷爆。
还有什么值得关注
- PCC 模型开放:Private Cloud Compute 模型现在支持 32K 上下文窗口,并且可以直接在 Foundation Models 里调用,隐私级别最高。
- 自定义 Segment 类型:协议允许定义全新的输入输出模态,以后在 Transcript 里直接塞音频或视频流将成为可能。
- 官方生态扩充:Anthropic 和 Google 即将推出官方的 Swift Package,Claude 和 Gemini 将作为一等公民接入该框架。