iOS 26 Foundation Models 框架完全指南:设备端 LLM、Guided Generation 与 Tool Calling 实战

深入解析 iOS 26 Foundation Models 框架:从 LanguageModelSession 快速上手,到 @Generable 结构化输出、streamResponse 流式响应,再到 Tool Calling 让模型调用你的 Swift 函数。所有推理设备端完成,无需 API Key、无需联网。

更新时间:2026 年 7 月 20 日

iOS 26 的 Foundation Models 框架是 Apple 提供的设备端 LLM 编程接口,让 Swift 开发者可以直接调用为 Apple Intelligence 提供支持的约 30 亿参数语言模型。所有推理都在 Neural Engine 上跑完,不用 API Key,不用联网,也不用把用户数据上传到任何服务器。本指南从 LanguageModelSession 的最小示例开始,逐步深入到 @Generable 结构化输出、流式响应,以及 Tool Calling 的完整实战。文章里也会顺手分享一些我自己在真实 App 里踩过的坑。

  • Foundation Models 框架自 iOS 26、macOS 26、iPadOS 26 与 visionOS 26 起可用,需要 Xcode 26 与支持 Apple Intelligence 的设备。
  • 三行 Swift 代码就能完成一次设备端推理:let session = LanguageModelSession(); let reply = try await session.respond(to: prompt)
  • @Generable 宏通过约束解码(constrained decoding)在 token 级别强制模型产出符合 Swift 类型的结构化数据,彻底告别 JSON 解析。
  • streamResponse 返回 AsyncSequence<PartiallyGenerated>,跟 SwiftUI 的 .task 配合就能做出打字机式的渐进渲染。
  • Tool Calling 允许模型调用你在 Swift 里实现的 Tool 协议函数,把 WeatherKit、Core Location、SwiftData 查询变成模型的"外部感官"。
  • 务必先检查 SystemLanguageModel.default.availability,为不支持 Apple Intelligence 的设备提供降级路径。

Foundation Models 框架是什么?为什么它改变了 iOS 开发

Foundation Models 框架是 iOS 26 引入的一个原生 Swift API,它把 Apple Intelligence 背后那颗约 30 亿参数(3B)的设备端语言模型直接开放给第三方 App。整套推理在 Apple Silicon 的 Neural Engine 上完成,模型权重驻留在系统层面而不是随你的 App bundle 分发,因此不会把包体积撑大几个 GB,也不会消耗用户流量。

作为一个从 Objective-C 时代就在写 iOS 的老兵,我把 Foundation Models 视作近几年 Apple 平台最重要的一次 API 扩容。它把"接一个云端 LLM"这个几乎每个新项目都会经历的架构决策,彻底变成了系统能力。之前你要接 OpenAI、Anthropic 或者 Gemini,都得考虑账号、Key 管理、审计日志、GDPR、SOC 2、成本预算,还得为断网场景准备好降级 UI。而现在,只要用户设备支持 Apple Intelligence,你就能在飞行模式下召唤一个通用语言模型,同时向 App Review 和用户交出一份"你的数据不会离开设备"的承诺。

框架的定位也很明确:它不是一个万能的 GPT-4 竞品,而是专门为 focused tasks 打磨的工具。摘要、抽取、分类、标签生成、内容改写、结构化数据填充、简单对话,都是它的舒适区。你不该拿它去写论文或者做深度代码推理;但用它做智能搜索、给游戏 NPC 生成对白、把用户笔记切成待办事项、给日历事件自动打标签,这些场景它非常胜任。理解这个"能力边界"是设计得体 UX 的第一步。

环境准备:Xcode 26、iOS 26 与设备可用性检查

要动手写 Foundation Models 代码,你需要一台运行 macOS 26 (Tahoe) 的 Mac、Xcode 26,以及一台支持 Apple Intelligence 的真机(iPhone 15 Pro 及以上、任何 M 系列 iPad、任何 Apple Silicon Mac)。模拟器在 Xcode 26 里能跑,但模型推理速度取决于你 Mac 的 Neural Engine,因此调试延迟不代表真机表现,性能测试请务必回到真机。

在你要使用框架的 target 里,只需要 import FoundationModels,没有额外的 Framework 需要在 Xcode 中链接。框架本身就是一个纯 Swift 模块,也没有 Objective-C 桥接头。想跑一个 quick sanity check,Xcode 26 的 Playgrounds 支持内嵌 #Playground { ... } 宏,可以直接在源文件里执行一段 async 代码。

关键的一步是检查可用性。并不是所有装了 iOS 26 的设备都能跑 Apple Intelligence,用户也可能主动把它关掉。任何调用 LanguageModelSession 而不做前置检查的代码,都会在运行时抛出 generation error,这是我在测试组见过的头号崩溃。正确的做法是把可用性视作一个 feature flag:

import FoundationModels

@Observable
final class AIStatus {
    enum State { case ready, unavailable(reason: String) }
    var state: State = .unavailable(reason: "checking")

    func refresh() {
        switch SystemLanguageModel.default.availability {
        case .available:
            state = .ready
        case .unavailable(.deviceNotEligible):
            state = .unavailable(reason: "此设备不支持 Apple Intelligence")
        case .unavailable(.appleIntelligenceNotEnabled):
            state = .unavailable(reason: "请在系统设置中开启 Apple Intelligence")
        case .unavailable(.modelNotReady):
            state = .unavailable(reason: "模型正在下载或准备中")
        @unknown default:
            state = .unavailable(reason: "未知原因")
        }
    }
}

在 UI 上,我通常会根据 state 决定是否显示 AI 相关的按钮或菜单项,而不是等用户点了以后再弹错误弹窗。Apple 也在 WWDC26 关于 LLM Provider 的会议里强调,把不可用状态显式化是评审通过率的关键。

快速上手:从 LanguageModelSession 开始

LanguageModelSession 是整个框架的入口类型。一个 session 大致对应一次对话上下文,多轮对话会共享同一个 session,模型能记住之前说过什么;如果想让每次调用都独立,就每次新建一个。session 的实例化非常轻,真正的模型加载和 KV cache 分配是在第一次 respond 或者手动 prewarm() 时才发生的。

老实说,这个 API 上手比我预期还快。下面是一个完整的例子:给用户输入一段任意文本,返回三条精炼摘要。整个函数是一个 async throws,任何抛出的错误都会被 LanguageModelSession.GenerationError 包装:

import FoundationModels

func summarize(_ text: String) async throws -> String {
    let session = LanguageModelSession(
        instructions: """
        你是一名中文写作编辑,把用户提供的段落浓缩成三条要点。
        每条不超过 25 个字,使用第三人称。
        """
    )
    let response = try await session.respond(to: text)
    return response.content
}

几个值得注意的细节:

  • instructions 相当于 system prompt,只在 session 创建时设置一次;后续 respond 的调用不能覆盖它。
  • 返回值是 LanguageModelSession.Response<String>,包含 contenttranscriptEntries(完整对话历史)以及 rawContent
  • 如果 prompt 触发了模型的安全护栏(guardrails),会抛出 GenerationError.guardrailViolation,你需要单独处理,不能笼统 catch。

Guided Generation:用 @Generable 与 @Guide 生成结构化数据

如果你用过任何云端 LLM,一定体会过"让它输出 JSON 但它偶尔就是不听话"的痛苦。Foundation Models 的解法是约束解码:在采样每个 token 时直接过滤掉不符合 schema 的候选,因此模型不可能产出结构错误的内容。你要做的只是用 @Generable 描述你想要的 Swift 类型。

@Generable
struct RecipeCard {
    @Guide(description: "菜品的中文名称,不超过 12 个字")
    let title: String

    @Guide(description: "3 到 6 个字的一句宣传语")
    let tagline: String

    @Guide(.range(5...60))
    let cookMinutes: Int

    @Guide(.count(3...8))
    let ingredients: [String]

    @Guide(.anyOf(["简单", "中等", "较难"]))
    let difficulty: String
}

func makeRecipe(prompt: String) async throws -> RecipeCard {
    let session = LanguageModelSession(
        instructions: "你是一位家庭菜谱作者,回答一律用中文。"
    )
    let result = try await session.respond(
        to: prompt,
        generating: RecipeCard.self
    )
    return result.content
}

几个我在生产项目里踩出来的经验:

  1. 字段顺序影响生成质量。模型是按你在 struct 里声明的顺序逐个生成的,所以要把"依赖前面上下文的字段"放后面。比如 summary 或者 tagline,应该放在具体内容之后。
  2. @Guide(.range(...)).anyOf([...]).count(...),而不是在 prompt 里写"请返回 3-8 个"。约束由采样阶段强制执行,比 prompt 稳得多,也让你的 prompt 更短。
  3. 嵌套 Generable 类型是被支持的,但深层嵌套会拖慢速度并增加 hallucination 概率,尽量把层级压平。

关于什么类型可以做 Generable,官方在 Deep dive into the Foundation Models framework 会议里给出的清单是:StringIntDoubleBoolArray(元素必须是 Generable),以及你自己标注 @Generable 的 struct 与 enum。DateURL 目前不是原生 Generable,需要用字符串接收后再转换。

流式响应:在 SwiftUI 里实现打字机效果

把生成出的内容一整块吐给用户是很糟糕的体验,用户在等三到五秒后突然收到一堵墙。session.streamResponse(...) 返回一个 AsyncSequence,每个元素是当前时刻的完整快照,非常适合直接绑定到 SwiftUI 的 state。

更妙的是,@Generable 宏会自动为你的结构体合成一个 PartiallyGenerated 内嵌类型(所有字段都变成 optional),允许流式过程中还没生成的字段暂时为 nil。

import SwiftUI
import FoundationModels

struct RecipeStreamView: View {
    let prompt: String
    @State private var draft: RecipeCard.PartiallyGenerated?
    @State private var error: (any Error)?

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            if let title = draft?.title {
                Text(title).font(.title2.bold())
            }
            if let tagline = draft?.tagline {
                Text(tagline).foregroundStyle(.secondary)
            }
            if let ingredients = draft?.ingredients {
                ForEach(Array(ingredients.enumerated()), id: \.offset) { _, item in
                    Label(item, systemImage: "leaf")
                }
            }
        }
        .task(id: prompt) {
            do {
                let session = LanguageModelSession(
                    instructions: "你是一位家庭菜谱作者,回答一律用中文。"
                )
                let stream = session.streamResponse(
                    to: prompt,
                    generating: RecipeCard.self
                )
                for try await snapshot in stream {
                    draft = snapshot
                }
            } catch {
                self.error = error
            }
        }
    }
}

在这段代码里注意两件事。第一,ForEach 我们用了 enumerated() 作为 id,是因为流式过程中 ingredients 数组的最后一个元素可能会边生成边变化,如果用元素本身做 id,会导致 SwiftUI 频繁重建视图。第二,task(id: prompt) 会在 prompt 变化时自动取消上一个 Task。Swift 并发的结构化取消对流式 LLM 尤其重要,否则用户切换问题时你会同时跑两条推理,白白耗电。(这坑我在上一版 App 里就踩过,电量掉得让人怀疑人生。)

如果你还没适应 Swift 6.2 之后的并发默认隔离模型,可以先读一下我们之前那篇 Swift 6.2 Approachable Concurrency 完全指南,里面讲了 @MainActor 默认隔离怎么影响这里的 @StateTask 组合。

Tool Calling:让模型调用你的 Swift 函数

Tool Calling 是 Foundation Models 从"聪明的自动完成"跃升为"能真正做事的助手"的关键。没有它,模型只能靠训练数据里模糊的世界知识回答问题;有了它,模型可以主动调用 WeatherKit、Core Location、你的 REST 客户端、你的 SwiftData 查询函数,把这些结果编织回自己的回答里。

要实现一个 tool,你需要写一个符合 Tool 协议的类型。它有三个必需成员:namedescription(模型据此决定什么时候用你的工具),以及 call(arguments:)。参数类型必须是 @Generable,返回值必须是 ToolOutput。下面是一个把当前位置的天气告诉模型的示例:

import FoundationModels
import WeatherKit
import CoreLocation

struct CurrentWeatherTool: Tool {
    let name = "current_weather"
    let description = "获取给定城市当前的实时天气,包括温度与体感描述。"

    @Generable
    struct Arguments {
        @Guide(description: "城市名称,例如 '柏林' 或 'Tokyo'")
        let city: String
    }

    func call(arguments: Arguments) async throws -> ToolOutput {
        let geocoder = CLGeocoder()
        guard let placemark = try await geocoder
                .geocodeAddressString(arguments.city).first,
              let location = placemark.location else {
            return ToolOutput("找不到城市 \(arguments.city)")
        }
        let weather = try await WeatherService.shared.weather(for: location)
        let current = weather.currentWeather
        return ToolOutput("""
        \(arguments.city) 当前 \(Int(current.temperature.value))°C,\
        \(current.condition.description)。
        """)
    }
}

let session = LanguageModelSession(
    tools: [CurrentWeatherTool()],
    instructions: "你可以调用 current_weather 工具查看天气再回答。"
)
let reply = try await session.respond(to: "我今天在柏林要穿几件?")

模型收到用户问题后会自主决定是否调用 current_weather。如果它认为要,它会先生成一个符合 Arguments 的 JSON,框架把它转成 Swift 值传给你的 call,然后把 ToolOutput 作为一条内部消息注入回对话,让模型基于结果继续生成最终回答。整个过程对调用方是透明的,你只等 respond 返回。

与 SwiftData、SwiftUI Liquid Glass 的整合思路

Foundation Models 不是孤岛,它跟 iOS 26 里其它新东西天然咬合。最常见的组合是把 SwiftData 查询包装成 Tool,让模型可以按用户的自然语言在你自己的数据库里搜索:搜索用户的笔记、任务、通讯录、CloudKit 同步的私有记录。如果你还没上 SwiftData,可以先读 SwiftData 与 CloudKit 同步完全指南,那里讲了 iOS 26 的模型继承与关系迁移,之后回来把它接成一个 Tool。

在 UI 层,SwiftUI 的新 glassEffect 修饰符特别适合渲染 AI 生成的动态内容。半透明的 Liquid Glass 卡片配合流式打字动画,视觉上比传统的方块背景更有"活物感"。想在你的 AI 交互界面里用起来,可以参考 SwiftUI Liquid Glass 完全指南 里的组合技巧。

此外,Foundation Models 的输出流是原生的 AsyncSequence,天然兼容 Swift 6.2 的 Observations 类型。这意味着你完全不需要引入 Combine:一个纯粹用 @Observable + AsyncSequence 构建的 ViewModel 就能覆盖大多数场景。旧代码里如果还有 Publisher,可以借这次重构一起下掉。

性能与内存:会话生命周期、预热与限流

设备端 LLM 的性能瓶颈跟云端完全不一样。因为模型权重是共享的,你不会为多个 App 各自加载一份;但每个 LanguageModelSession 都会分配自己的 KV cache,随着上下文变长,内存会线性增长。我的经验法则:

  • 短对话(单轮问答、摘要):每次新建 session,用完 nil,让 ARC 立即释放 KV cache。
  • 长对话(聊天助手、多步 agent):把 session 作为 @Observable ViewModel 的属性,在 initawait session.prewarm();用户结束会话时手动 session = nil
  • 批处理(比如批量给 100 封邮件生成标签):不要并发多个 session,Neural Engine 是单队列的,串行反而更快,还能避免热节流。

关于响应速度,Apple 官方数据显示首 token 延迟在 iPhone 15 Pro 上约 300 ms,之后大约 30 tokens 每秒。这意味着一段 500 字的中文回答大约需要 5 到 8 秒。用户体验层面,一定要用 streamResponse 让用户第一时间看到内容开始出现,而不是等整段完成。这一点比模型本身的绝对速度更影响感知快慢。

常见错误、Guardrail 与调试技巧

下面是我在测试组里最常遇到的几类错误以及对策:

错误典型原因解决方案
GenerationError.unavailable没有先检查 SystemLanguageModel.availability把可用性提升为 App 的一等状态,UI 层隐藏 AI 入口
GenerationError.guardrailViolationPrompt 涉及自残、成人内容或提示注入捕获后向用户提示"该请求无法处理",不要把原文回显
GenerationError.exceededContextWindow对话历史 + 输入超过 ~4k tokens切分历史、只保留最近 N 轮,或者摘要历史后重开 session
输出偶尔为空Guardrail 过滤掉了整段响应检查 rawContent.transcript,改写 prompt 避免敏感触发
Tool 从未被调用description 太模糊或与 instructions 冲突把 tool 名字与场景写进 instructions,明确"何时该调"

调试时我强烈推荐打开 Xcode 26 新增的 Foundation Models Instrument。它会显示每次 session 的首 token 延迟、总 token 数、Neural Engine 占用,以及每次 Tool Call 的参数和返回。这个工具比自己塞 print 有用得多,尤其是在 tool chain 变复杂之后。

常见问题

Foundation Models 需要网络吗?

核心推理完全在设备上完成,不需要联网。飞行模式下你的 App 依然可以调用 LanguageModelSession。只有当你自己在 Tool 里调用云端 API(比如 WeatherKit)时才需要网络。

哪些设备支持 Foundation Models?

需要设备支持 Apple Intelligence,目前包括 iPhone 15 Pro/Pro Max、全部 iPhone 16 与 17 系列、任何 M 系列 iPad、任何 Apple Silicon Mac,以及 Apple Vision Pro。用户还必须在系统设置里开启 Apple Intelligence,否则可用性检查会返回 appleIntelligenceNotEnabled

@Generable 与手写 JSON 解析相比有什么优势?

@Generable 用约束解码在采样阶段就过滤非法 token,模型不可能产出结构错误的输出,你也不用写任何 JSONDecoder 或 fallback。同时 @Guide 让你可以声明字符串枚举、数值范围、数组元素数量等约束,全部在编译期生成 schema。

Foundation Models 能替代 OpenAI 或 Claude 吗?

不能完全替代。设备端模型约 30 亿参数,适合摘要、抽取、分类、简单对话等 focused tasks,但复杂推理、长文写作、多语种深度理解仍然远不及云端 GPT-4 或 Claude Sonnet 4.5。合理策略是把日常轻量任务放到设备端保护隐私,把重推理任务留给云端。

Tool Calling 的调用是同步的还是异步的?

你实现的 call(arguments:)async throws,模型在等待你的 Tool 返回期间会暂停生成。因此如果你的 Tool 里做了长耗时的网络请求,用户看到的整段响应也会被拖慢。尽可能让 Tool 立即返回,或者提前缓存需要的数据。

Lukas Müller
关于作者 Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.