-
跟随编程之 Foundation Models 框架篇
获得使用 Foundation Models 框架访问 Apple 设备端 LLM 的实际操作体验。在这个在线讲座中,我们将在 Xcode 中实时演示如何将生成式 AI 功能融入示例 App 中,你可以跟随我们一起进行编程。我们将引导你实现基本文本生成等核心功能,并探讨一些进阶主题,包括结构化数据输出的引导式生成、动态 UI 更新的流式响应,以及用于检索数据或执行操作的工具调用。
欢迎大家参加讲座。如要跟随编程,你需要一台搭载 Apple 芯片的 Mac,它应该支持 Apple 智能,并且运行最新版本的 macOS Tahoe 26 和 Xcode 26。
活动语言为英语。章节
- 0:00:00 - 简介
- 0:03:33 - 资源和系统要求
- 0:05:04 - 先决条件和设置
- 0:06:30 - 初学者项目导览
- 0:10:32 - 第 1 章:Foundation Models 框架基础知识
- 0:11:51 - 第 1.1 章:提出你的第一个生成请求
- 0:15:50 - 第 1.2 章:使用指令引导模型
- 0:18:46 - 第 1.3 章:处理模型可用性
- 0:20:50 - 第 1.4 章:在视图中处理可用性
- 0:24:07 - 第 1.5 章:创建行程生成器
- 0:27:07 - 第 1.6 章:更新视图以显示文本输出
- 0:32:43 - 第 2 章:生成结构化输出
- 0:34:36 - 第 2.1 章:生成简单的结构化输出
- 0:37:52 - 第 2.2 章:生成嵌套的结构化输出
- 0:41:58 - 第 2.3 章:重构行程生成器
- 0:44:48 - 第 2.4 章:更新视图以显示结构化数据
- 0:49:22 - 第 3 章:提示词技巧
- 0:50:25 - 第 3.1 章:使用 PromptBuilder 构建提示词
- 0:53:23 - 第 3.2 章:一步到位的提示词
- 0:55:31 - 第 3.3 章:使用示例更新行程生成器
- 0:59:11 - 第 4 章:流式传输响应
- 0:59:51 - 第 4.1 章:更新行程生成器以进行流式传输
- 1:02:52 - 第 4.2 章:更新视图以呈现流式传输内容
- 1:08:16 - 第 5 章:工具调用
- 1:11:12 - 第 5.1 章:构建 FindPointsOfInterestTool
- 1:16:32 - 第 5.2 章:允许模型访问 FindPointsOfInterestTool
- 1:23:07 - 第 5.3 章:更新行程生成器以使用工具
- 1:27:39 - 第 6 章:性能和优化
- 1:34:20 - 第 6.1 章:预热模型
- 1:36:56 - 第 6.2 章:优化提示词
- 1:41:16 - 总结和后续步骤
资源
-
搜索此视频…
大家好 欢迎参加 Foundation Models 框架跟随编程 我叫 Shashank 我是 Apple 的技术布道师 今天很高兴能引导大家 将设备端生成式 AI 功能 直接集成到 App 中 我们将介绍从基本提示词 到生成结构化输出、 流式传输响应等各个方面的内容 在 Slido 上有我们专业的专家团队 如果你在任何时候有任何问题 请在那里提问 我们先快速概述一下 确保大家的理解是一致的 在 WWDC24 上 我们推出了 Apple 智能 它由内建于我们操作系统核心的 大型基础模型提供支持 这带来了写作工具 和智绘表情等系统级功能 你们中的许多人都 请求访问底层模型 为此 我们在 WWDC25 上 推出了 Foundation Models 框架 它让你能够直接访问 为 Apple 智能提供支持的 设备端大语言模型 这通过强大的 Swift API 来实现 对于开发者来说 这种设备端方法具有显著优势 用户数据能够保持私密 因为所有内容都在本地运行 你的功能在离线时完全能正常工作 而不需要设置账户或管理 API 密钥 对于你或使用 App 的用户来说 这类请求不会产生任何费用 由于它是操作系统的一部分 因此不会影响 App 大小
今天我们将一起构建一款 App 我们首先构建一个简单的静态 App 来列出地标 然后将它转换为 动态旅行规划器 你将了解如何生成丰富的结构化行程 以用于自定义 UI 并在创建结果的同时 实时流式传输结果 你还将了解如何允许模型访问 自定义工具以查找真正的兴趣点 最后 还将了解如何优化 App 性能
让我们快速了解一下 将构建的最终 App
这是在我的 Mac 上 运行的一个已完成的 App 也是今天的讲座结束时 你将获得的成果 我们将首先来查看 使用 SwiftUI 构建的 简洁且清晰的著名地标列表 让我们选择一个地标 Serengeti 怎么样?
当我们点按进入详情视图时 你会看到页眉图像和描述 底部是“Generate Itinerary”按钮 当我点按这个按钮时 App 将调用设备端模型 生成一个完整的三日旅行计划 请密切关注屏幕 看看发生了什么
UI 正在实时自行构建 首先是标题 接着是描述 然后是每日计划 这就是我们将在第四章中 引入的流式传输 API 它将打造出色的动态用户体验 这里不仅仅是一个代码块 它是丰富的结构化响应 我们将在第二章中了解相关知识 我们将在不同部分中显示每天的安排 每一部分都有标题、副标题和地图 请注意这里的 Hotel 1 和 Restaurant 1 等名称 这些不是随机生成的 App 使用工具调用来获取这些名称 我们将在第五章中介绍相关知识 借助 Foundation Models 框架 你可以打造 丰富的结构化智能体验 将这种体验无缝融入自己的 App 中 这就是我们今天要一起构建的内容 要充分从今天的跟随编程讲座中获益 你可以使用三个关键资源 首先是 Xcode 初学者项目 在其中所有样板 UI 和资源都已准备就绪 如果正在 developer.apple.com/cn 或 Developer App 上观看这个视频 你会在页面底部的 “资源”下找到这个项目 如果你正在 YouTube 上观看 项目已链接在视频描述中 第二个是网页上的分步指南 这是可信来源 其中包含所有说明和代码片段 你可以直接拷贝粘贴 这些内容以免输错 最后一个资源是直播中的我 幕后还有专家团队 为你解答疑问 在这里 我将与你一起构建这个项目 说明每项更改背后的原因 在我们进入设置 并设置项目之前 让我们快速了解一下 今天讲座的系统要求 欢迎大家观看并跟着操作 但是 如果你打算和我一起实时编程 则需要一台搭载 Apple 芯片且运行 macOS Tahoe 和 Xcode 26 的 Mac 你还需要确保在“设置”下 打开了 Apple 智能 今天我将直接在 Mac 上 构建并运行这个 App 但你也可以使用 Xcode 26 并将运行 iOS 26 的最新 iPhone 作为目标设备
接下来让我们按照 跟随编程指南中的 “先决条件”部分操作 下载和配置 我们的初学者项目
在我们的指南中 你将看到“先决条件”部分 首先 请点按链接下载项目文件
下载后 你将在这里 找到一个 zip 文件 macOS 可能会自动解压缩这个文件 在其中 你会找到一个名为 FoundationModelsCodeAlong 的文件夹 这是我们今天将使用的初学者项目 它包含所需的所有视图、模型 和占位符代码 让我们能够顺利开始 我已经打开了项目 可以开始了
我们首先要做的是 设置开发者团队 在项目导航器中选择项目文件
然后选择“Targets”
点按“Signing & Capabilities” 在“Team”下 选择下拉菜单并选择你的团队
为了确保一切都能正常工作 请在 Xcode 工具栏中 选择“My Mac”作为运行目标设备
然后点按运行按钮 这将构建并运行项目 或者 可以使用 Command + R
这里显示的是 我们将构建并在其中添加 生成式 AI 功能的 App 这是我们的着手点 在整个讲座中 我们将添加强大的功能 现在让我们快速了解一下初学者项目
首先有一个 Playground.swift 文件 它包含在 Playgrounds 文件夹下 在这里 我们将对提示词进行迭代 并单独测试 Foundation Models API 而无需构建并运行整个 App 对提示词感到满意后 我们将这段代码移到我们的 App 中
接下来是 ViewModels 文件夹 其中对我们最重要的文件是 ItineraryGenerator.swift 这个文件中包含用于创建 和管理基础模型会话、 调用框架 API 和处理结果 的所有核心逻辑 最后还有一个 Views 文件夹
我们的所有 SwiftUI 代码都在这里 在这个跟随编程讲座中 UI 大多是预先构建好的 以便我们能 专注于 Foundation Models 框架 你会注意到这里有几个文件 为了方便大家跟随编程 将编辑的关键文件都有编号
我们的任务是获取 行程生成器的输出 并将输出连接到这些视图 以打造丰富的交互式 UI 在 App 中呈现
当浏览这些文件时 你会注意到有一些特殊注释 格式如下 Mark Code-Along Chapter 后跟编号 这里的每个编号都直接对应于 跟随编程指南中 具有相同编号的章节 你可以使用 Xcode 查找导航器 来搜索章节编号 查看所有未完成的代码更改
在这里输入章节编号 你将看到所有代码更改 每完成一个步骤 我们便会删除相关注释 这样我们就能跟踪 跟随编程的进展情况
总之 我们将完成三个简单步骤 首先 在 Playground 中试验 其次 在视图模型中 实现核心逻辑 最后 在视图中显示结果 让我们详细了解一下每种视图
第一个屏幕是我们的着手点 显示地标主列表 它由 LandmarksView.swift 提供支持 今天我们不会修改这个文件 它已经设置好了 让我们可以浏览并选择目的地 当你轻点一个地标时 将进入详情屏幕 这个视图由 LandmarkDetailView.swift 文件控制 它的作用是检查 Foundation Models 框架 在设备上是否可用 并根据这一信息决定要显示的 UI
接下来是 LandmarkTripView 它的作用是显示 “Generate Itinerary”按钮 这也是一开始显示从模型获取的 原始非结构化文本的地方
最后是 ItineraryView 这是我们的目标视图 这个视图呈现 在跟随编程结束时 我们将拥有的丰富结构化行程数据
现在 我们可以深入探讨议程了 我们将跟随编程分为六章 我们将首先探讨最基础的内容 你将了解如何开始向模型 发送提示词以生成文本 然后 我们突破简单文本的局限 看看如何从模型获取 结构化的 Swift 类型 从而轻松地将模型输出 映射到你的自定义视图 然后 我们将深入探讨提示词技巧 这让你能够在提示词中 直接提供高质量的示例 以提高模型的准确性 接下来 我们将了解如何 流式传输模型的响应 以实时更新 UI 从而提供出色的用户体验 然后 我们将探讨工具调用 工具是有效的方式 让模型能够访问 你自己的自定义函数和数据 以扩展自身功能 最后 我们将介绍性能优化 以使生成式功能运行速度更快 响应更迅速 接下来 让我们深入探讨 Foundation Models 框架的基础知识 你可以使用 Foundation Models 框架 向简称 LLM 的 设备端大语言模型发送提示词 然后 LLM 可以根据提示词 进行推理并生成文本 例如 你可以让它生成 一份巴黎三日游行程 模型会提供一个详细的计划作为回应
要开始向模型发送提示词 你需要创建一个会话 这个框架围绕有状态的 语言模型会话这一概念构建 它会保留所有提示词 和响应的历史记录 在本章中我们 将熟悉基础模型 提示词和会话 首先 我们将在 Playground 中 熟悉一下这个 API 我们将创建一个语言模型会话 并从模型获取第一个响应 然后 我们将添加简明指令 以细化调整语气和内容 接下来 我们了解一下可用性 API 以妥善处理不同状态 熟悉这些操作后 我们将切换到 App 在视图模型中更新行程生成器 并在视图中显示原始文本输出 让我们前往跟随编程指南
在第一章中 我们的目标是向设备端语言模型 发出我们的第一个请求 我们将使用 Xcode Playground 发送简单的文本提示词 看看会发生什么 这有助于我们 了解模型的基本行为
直接将这个代码块拷贝并粘贴到 Xcode Playground.swift 文件中 可以使用右上角的 快捷按钮“Copy” 我将逐步添加这些代码行 并讲解各项操作 让我们前往 Xcode 打开 Playground.swift 文件 要向模型发送提示词 需要完成三个简单步骤 第一步是导入 Foundation Models 框架 我们已完成这一步 下一步是创建 Playground
使用 Playground 宏 创建 Playground 后 你会看到右侧显示了一个画布 如果没有看到 你可以随时 点按编辑器选项 确保“Canvas”旁边 有一个勾号 你可以点按刷新按钮 它的作用是运行包含在 Playground 代码块中的所有代码 现在你看不到任何输出 因为我们还没有添加任何代码 向模型发送提示词的 第二步是创建会话
在这里我们定义了 let 变量 session = LanguageModelSession 你会看到 Playground 画布中 自动显示 session 变量中的内容 你会看到 有一些工具 我们将在后面的章节中讨论 然后还有会话记录 其中包含你与模型的 所有对话
第三步是向模型发送提示词
我们定义 let response = try await session.respond to 并提供提示词 Generate a 3-day itinerary to Paris 这是一个异步请求 因此我们要等待才能收到响应
完成这一步后 在右侧的画布上 你会看到有一个 response 变量 其中包含一些属性 第一个是 prompt prompt 显示 Generate a 3-day itinerary to Paris 然后还有一个名为 content 的属性 类型为字符串 让我们点按这个属性 你会看到 有一份详细的畅游巴黎三日行程 这里有一份三天的巴黎探索计划 包含城市里最经典的景点和体验 你可以看到每日计划 其中包含 第一天、上午、下午的安排等信息
好极了 让我们返回指南 并讨论一个关键主题 当你第一次 调用 session.respond 时 你可能会注意到存在轻微的延迟 这是因为 在设备端语言模型 处理你的请求之前 需要先载入到内存中 我们的第一个请求会触发 系统载入模型操作 这会导致产生初始延迟 我们将在后面的章节中 了解如何解决这个问题 我们还看到输出是 非结构化的自然语言文本 这易于我们阅读 但难以在自定义 Swift UI 中使用 在下一章中 我们将了解如何使用 Swift 类型 而不是原始文本生成结构化输出 最后 务必注意 生成整个行程的过程中 没有任何数据离开你的设备 数据是完全私密的并且可以离线使用 恭喜你 你使用 Foundation Models 框架 成功向设备端基础模型 发送了提示词
哦 最后还有一点 让我返回 Playground
我们一直致力于改进模型 如果你想要提供反馈 可以随时使用 画布中的这些按钮 与我们分享你的反馈 让我们前往 跟随编程指南的 1.2 节 即“使用指令引导模型”
现在我们的目标是获得更一致、 更高质量的结果 我们可以通过向模型 提供指令来实现这一点 可以将指令 视为单个会话中 适用于整个对话的 永久规则或自影像 再次直接将这段代码 拷贝到 Playground 中并运行 我将添加这些指令
返回 Playground.swift 文件
我将添加一个新变量 命名为 instructions 我输入“你的任务是 为用户创建一份行程 每天都需要包含一个活动、 一家酒店和一家餐厅 并且一定要包含标题、简短说明 以及按日列出的行程规划” 我们可以使用 instructions 参数将这些指令 传递给 LanguageModelSession
传递时 画布会 自动检测到代码更改 并更新结果 现在我们看到 在 response 下有 content 属性 其中包含我们提出的请求 这包括 活动、酒店和餐厅 你可以在这里看到 Activity、Hotel 和 Restaurant 你可能有疑问 这些指令和提示词 有什么区别? 下面我们来看一看
指令可用于 定义自影像、设置规则和 指定所需的响应格式 这应该由开发者提供 另一方面 提示词可能 由使用 App 的用户提供 模型经过训练 会优先遵循指令而不是提示词 这有助于防范提示词注入攻击 在这类攻击中用户可能会 让模型忽略在提示词中 提供的指导说明 一般来说 指令应该是静态的 以免在其中插入用户输入
另请注意 指令在整个会话期间都会保留 每次交互都 记录在会话记录中 并且初始指令 始终是第一个条目
太好了 我们可以成功 向模型发送提示词并获得响应 但务必要考虑的一点是 我们的 App 可能会在 Apple 智能不可用的设备上运行 显示一项功能但功能却无法正常使用 可能会带来糟糕的用户体验 例如 设备可能 不支持 Apple 智能 或者设备可能支持 Apple 智能 但用户没有启用 或者模型资源仍在下载中 尚不可用 让我们详细了解一下 如何处理这些用例 接下来 我们返回跟随编程指南 现在我们进行到了 跟随编程指南的第 1.3 节 即“处理模型可用性”
这个模型提供了可用性 API
让我们前往 Xcode 详细了解 这个 switch 代码块中的每个用例 以及它们对 App 来说意味着什么
返回 Playground.swift 文件 Playground 有一项便捷功能 就是可以在同一个 Swift 文件中 添加多个这样的代码块
我在这里添加了一个新的 #Playground 块 其中包含可用性代码 好了
我们来看看这些 API 你还可以查看 多个 Playground 的输出 第二个 Playground 在画布上 显示为另一个标签页 你会看到我的 Mac 支持 Apple 智能 系统提示 Foundation Models 可用且处于就绪状态 现在让我们详细了解一下这些用例
第一个用例是可用 这意味着一切就绪 模型已载入 你可以发出生成请求
如果提示不可用且设备不符合条件 则表示这个模型 不支持 Apple 智能 你应该妥善隐藏生成式 UI 并显示备用体验
不可用 且 Apple 智能并未启用 这意味着设备符合条件 但 Apple 智能在“设置”中已关闭 你应该借此机会提示用户启用它
不可用且模型处于未就绪状态 这是一种临时状态 可能是因为模型资源 仍在下载中 最佳做法是告诉用户重试 现在我们可以 将这些功能添加到 App 中 让我们前往跟随编程指南
现在我们进行到了 第一章的 App 部分 在本节中我们 将更新 LandmarkDetailView.swift 以检查模型可用性 并在模型不可用时显示一条信息 直接拷贝这些代码块 你可以搜索这些标记的注释 以确切知道这些代码更改的插入位置 我将与大家实时进行操作 让我们前往 Xcode 项目
然后点按 Views 文件夹中的 LandmarkDetailView.swift 再次提醒一下 你可以随时使用查找导航器 来查找需要在本章中 进行的所有代码更改 好了 首先要做的是 添加模型实例
我们定义 private let model = SystemLanguageModel.default 这与我们在 Playground 中 使用的代码行完全相同 因此你应该会觉得很熟悉 由于我已添加这行代码 我将删除这条注释 这样 它就不会出现 在我们的查找导航器中了 我们需要进行的 下一项代码更改是删除 这里的这些占位符可用性代码 这些代码只是方便演示用的 所以我要删除 一旦删除 Xcode 会立即提醒我 还没有定义可用性 但这个问题很容易解决 因为我们现在有了模型 直接使用 model.availability 即可
我要将这行代码也删掉 好了 完成这项代码更改后 对这个特定文件的所有更改便完成了
现在我们已经添加了这些可用性检查 你应该很熟悉 因为我们 在 Playground 中 使用过相同的可用性检查方法 但如何测试它们呢? 你可能没有多台测试设备可用 值得庆幸的是 有一个简单的方法 在项目的方案设置中 有一个选项用于模拟不可用状态 下面我们来看一看 点按 FoundationModelsCodeAlong 点按“Edit Scheme” 向下滚动 你会看到一个 名为“Simulated Foundation Models Availability”的选项
如果你点按这里 会显示几个不同的选项 这些选项你应该很熟悉 因为这些是我们在 Playground 中 介绍过的用例 我将点按“Apple Intelligence Not Enabled” 点按“Close”
然后构建并运行 App
这里显示了我们的 App 我将选择“Sahara Desert” 啊哈 我在这里看到以下信息 “行程规划器无法使用 因为 Apple 智能 尚未开启。”
这与我们在不可用状态视图中 指定的信息相同
好极了 让我切换回来 以便我们可以在跟随编程 过程中继续添加其他功能
好了 让我们前往跟随编程 指南的第 1.5 节
现在 我们已准备好 更新 App 的行程生成器 以初始化语言模型会话并定义 名为 generateItinerary 的函数 以便从视图中调用模型 这些代码你应该同样觉得很熟悉 因为我们已经在跟随编程过程中 实现了这些代码 现在 我们将这些代码迁移到 App 中 让我们前往 Xcode 并打开 ItineraryGenerator.swift 文件 这个文件位于 ViewModels 文件夹中
我们将再次使用查找导航器 来查找需要进行和 跟踪进展的各项代码更改 好了 在 ItineraryGenerator.swift 文件中 我们需要进行的第一项更改是 添加一个 session 属性 我先完成这一步 我们定义了名为 session 的变量 来保存 LanguageModelSession
接下来 Xcode 将提醒我们 会话尚未初始化 我们将在这里的 init 函数中 初始化这个会话
好了 这就是我们添加的内容
我们添加了一个 instructions 变量 我们使用了 与 Playground 中相同的指令: “你的任务是 为用户创建一份行程 每天都需要包含一个活动、 一家酒店和一家餐厅 并且一定要包含标题、简短说明 以及按日列出的行程规划” 我定义了 session 变量 来保存 LanguageModelSession 并传入了指令 好了 我们需要进行的第三项也是 最后一项更改是 更新 generateItinerary 函数 这是为了发送提示词 并获取响应 我们将从视图中 调用的函数 我们来进行这项代码更改
好了 这是我们添加的内容 首先 我们定义 let prompt =
generate a dayCount-day itinerary to landmark.name 这里的 dayCount 默认为 3 landmark.name 是用户打开 App 后 点按的地标名称 我们收集此名称并将它传递给提示词 我们可以为这个 特定地标生成响应 接下来 我们定义 let Response = try await session.respond 并传入提示词 最后 response 变量 包含属性 content 你可能还记得我们在 Playground 画布中观察过这个属性 其中包含所有非结构化自然语言文本 它为字符串类型 我们将它赋值给 itineraryContent
它包含我们对视图模型 进行的所有代码更改 现在可以从视图中调用视图模型了 让我们返回 跟随编程指南的第 1.6 节 这是我们第一章的最后一节 现在 我们将更新 LandmarkTripView 以获取行程生成器的输出 并在 App 中显示 同样 直接按照这些注释 进行这些代码更改 我将前往 Xcode
点按“Views”
然后点按 LandmarkTripView 好了
好了 我们需要进行的 第一项代码更改是为视图模型 中的 ItineraryGenerator 类添加一个局部变量
好了 ItineraryGenerator 类型的 itineraryGenerator 已添加完成 我将删除这条注释
接下来我们需要 进行的代码更改是 在视图载入时 创建这个类的实例
这是我们在 .task 修饰符下 引入的更改 我们定义 let generator = ItineraryGenerator 其中 ItineraryGenerator 是视图模型类 我们传入 landmark 这样它就能获得有关 用户点按了哪个地标的信息 如果你还记得 我们将这一信息传递给了提示词 然后我们在这里 保留 itineraryGenerator 我将删除我们刚刚 所做代码更改的相关注释
我们需要进行的下一项代码更改是 更新视图本身 让我们详细了解一下视图 默认情况下 这里有一个布尔变量 名为 requestedItinerary 它设置为 false 由于它设置为 false 因此我们载入上方的第一个视图 这是一个文本字段 显示地标名称 我们获取 landmark.name 并使用 landmark.shortDescription 获取简短描述 这是当用户没有生成行程 或者没有要求模型生成行程时 显示的内容 当 requestedItinerary 设置为 false 时 我们需要载入一个新视图 我们可以在其中填充模型的输出 这正是我们现在 要实现的功能 在这里 我将删除 else 用例
并引入一个新的 else 用例 在其中我定义 if let content = itineraryGenerator.itineraryContent 如果你还记得 itineraryContent 是一个 字符串变量 用于保存模型的输出 然后我们只需获取 content 并更新到文本视图 由于完成了这项更改 我要将这条注释也删掉
我们就快完成了 在这个视图中 我们还要进行最后一项更改 如果你向下滚动到这里
就会看到我们定义了一个按钮 它将显示在屏幕底部 这个按钮目前处于隐藏状态 在这里我们需要进行 两项微小的代码更改 第一 我们希望显示这个按钮 所以可以注释掉这行代码 或者像我一样直接删除 然后我们需要在这里插入代码 以便在用户轻点按钮时 生成行程 让我们在这里添加相应代码
好了 我们定义了 await itineraryGenerator 并调用了 generateItinerary 函数 如果你还记得 这个函数的作用是接收提示词 然后传递给模型并获取输出 本章中的所有代码更改任务 到这里就全部完成了 好了 我们现在可以 构建并运行 App 了 好了 点按这里的运行按钮 这样将构建并运行 App
这是我们的 App
我将点按这里的 “Sahara Desert” 可以看到显示有 “Generate Itinerary”按钮 当我点按这个 “Generate Itinerary”按钮时 提示词和指令 将发送到 设备上的 LLM LLM 将逐个词元生成响应 这些都在设备上进行
如果你像我一样看到了这个行程 恭喜你 你使用 Foundation Models 框架 构建了第一个功能完备的 设备端生成式 AI 功能 只需几行 Swift 代码 你就能利用 Apple 智能的强大功能
这很棒 在这里我们看到的是一大段文字 如果我想提取酒店名称 并在地图上显示 该怎么做 这不是我们想要的丰富体验 我们将在第二章中 通过引导式生成来解决这个问题 我们将讨论如何使用 Swift 结构体 直接从模型获取输出 现在 让我们快速回顾一下第一章
在本章中 我们了解了如何创建会话 并向模型发送提示词 以获取基本文本响应 我们了解了如何提供指令 来引导模型生成输出 并探讨了如何使用 可用性 API 处理 不同的可用性状态 最后 我们通过更新视图模型和视图 将这些功能 整合到了我们的 App 中
第一章到此结束
现在我们可以生成原始文本了 接下来让我们看看如何 从模型获取结构化数据 以构建更丰富的 UI
首先谈谈使用 LLM 时 面临的一个基本挑战 它们默认会为我们提供非结构化文本 就像我们刚刚生成的行程一样 虽然人类可以阅读 但对于 App 开发者来说 处理起来却颇具挑战 例如 如何可靠地提取 第一天的酒店信息 以在地图上标注出来? 你不得不编写复杂的字符串解析代码 一旦模型输出发生变化 运行可能会中断 我们真正想要的是能够直接 映射到 App 逻辑的结构化数据
我们需要可以通过 Swift 结构体 实现的更高级嵌套结构 这个行程对象 应该包含一个对象数组 而这些对象又应该包含 一个活动对象数组 依此类推 这时候就需要用到引导式生成 Foundation Models 框架 提供的 API 让你能够 明确指定输出格式 如果你有一个 Swift 结构体 可以直接对结构体应用 @Generable 这样模型就能使用原生 Swift 类型 生成结构化数据
我们将在 Playground 中 开始本章的学习 我们将定义一个简单的结构体 以及应用于结构体的 Generable 宏 然后 我们将在此基础上 创建更复杂的嵌套数据结构 供模型生成 最后 我们将返回 App 重构行程生成器 以输出新的结构化行程类型 并更新视图 以在丰富的 UI 中显示输出
让我们返回跟随编程指南
我们现在进入第二章 即“生成结构化输出” 我们的目标是突破简单字符串的局限 直接从模型获取结构化类型 的安全 Swift 数据 这让我们能够构建丰富的自定义 UI 而无需进行任何 容易出现问题的字符串解析
再次直接将这段代码 拷贝到 Playground 中 并查看输出 我将在引入这个名为 SimpleItinerary 的新结构体时 讲解各项操作 让我们前往 Xcode Playground 文件 并完成代码更改
我将删除 我们刚刚添加的 第二个 Playground
在这里我将引入这个 名为 SimpleItinerary 的新结构体 我来逐步讲解一下这个结构体的定义 以及如何将结构体包含在 基础模型代码中 以生成所展示的输出 首先这个结构体 有几个不同的属性 一个字符串类型的 title 一个字符串类型的 description 还有一个字符串数组 days
我们希望模型生成这些字段 并且我们可以通过 Guide 来提供额外的信息 这个 Guide 有一个 description 参数 参数值为 “这趟旅程的一个有趣标题” 这告诉模型必须为这个变量 生成对应的标题 同样还有一个 description 参数 参数值为 “旅程的简短、有吸引力的说明” 同样还有表示天数的参数 我们现在可以 将这个结构体提供给模型 我们可以使用 generating 参数来实现这一点 之前我们定义了 session.respond 并提供了提示词 我将添加名为 generating 的新参数
并提供 SimpleItinerary.self
然后我们可以刷新画布 这将运行代码 我们可以查看输出
好了 这里显示了 response 让我们详细了解一下 这里的 content 属性 之前 content 为字符串类型 如果你仔细查看这里 会发现已标明 它是 SimpleItinerary 结构体 让我们打开这个属性 你会注意到输出 与我们刚刚在这里定义的 结构体一一对应 有标题 即“Parisian Bliss” 它对应此处的 title 属性 还有描述 它对应这里的属性 还有字符串数组 你会看到 days 它是一个 包含每日活动计划的字符串数组
太棒了 让我们返回跟随编程指南 的第 2.2 节 行程并非只能是 字符串或字符串数组 它也可以包含嵌套结构体 现在让我们看看 我们将在 App 中构建的 完整 Itinerary 结构体 我们将在这里 进行一项微小的代码更改 你要做的就是 将 SimpleItinerary 替换为 Itinerary.self 我们将进行这项代码更改 我将说明 这个 Itinerary 结构体的定义 返回 Xcode
我将删除这个 SimpleItinerary
并将 SimpleItinerary 替换为 Itinerary 好了 那么 Itinerary 的定义 是怎样的呢? 你可以按住 Command 键 点按它来打开定义 或者前往 Models 文件夹 点按 Itinerary.swift 文件 在这里 你会看到一个 名为 Itinerary 的新结构体 它包含的字段与在 SimpleItinerary 中我们看到的类似但数量更多 让我们详细了解一下 它也有一个字符串类型的 title 还有 description 和 rationale 如果你仔细查看 days 就会发现它不再是字符串数组 它实际上是一个 DayPlan 数组 而 DayPlan 本身也是结构体 它有自己的 title、subtitle、 destination 和 activities activities 是一个数组 由另一个 名为 Activity 的结构体组成 Activity 有 type、title、description 在这里 type 为枚举 也是可生成的 枚举是让模型生成 预定义的特定用例的绝佳方法 例如 在这里 type 只能是 sightseeing、 foodAndDining、 shopping、hotelAndLodging 如果一直滚动到顶部 你会看到还能通过另一种方法 来约束模型可以生成的内容 我们可以使用枚举 或者对于 destinationName 我们在这里定义了 Guide: anyOf ModelData.landMark 这告诉模型 它必须生成目的地名称 而目的地名称是我们 在打开 App 时看到的地标之一 这包括 Serengeti、Grand Canyon、 Sahara Desert 等 输出必须是这些地标之一 这就是 Itinerary 结构体的定义 这就是我们 在 App 中实际使用的代码 让我们返回 Swift Playground 如果你还记得 我们说过目的地名称 应该是列表中的名称之一 Paris 不在列表中 所以我要将它改成 实际存在于列表中的名称 Grand Canyon 怎么样?
画布将检测到这项代码更改 我们来看一下输出
这里显示了 response 它包含 content 同样 如果你仔细看一下 就会发现 它是 Itinerary 结构体 而不是 SimpleItinerary 结构体 因为我们进行了更新 让我们打开这个属性 你会看到它有 title、 destinationName、 description、rationale 和 days days 是一个 DayPlan 结构体数组 打开这个数组 你会发现 它包含多天的信息 activities 为 activity 结构体类型等等
在这里值得注意的要点是 当应用 @Generable 时 可任意组合 框架知道如何 自上而下构建 整个复杂对象 同时保证结构的正确性 现在让我们将它整合到 App 中 让我们前往跟随编程指南 现在我们进行到了 第二章的 App 部分 在这一部分中 我们将更新行程生成器 以使用刚刚在 Playground 中测试过 的可生成型 Itinerary 结构体
再次直接拷贝这些代码 我将与大家一起 完成这些代码更改 让我们前往 ViewModels 文件夹
下的 ItineraryGenerator
调出查找导航器并设置为第 2 章 这样我就可以查看将在本章中 进行的所有代码更改
我们需要进行的第一项代码更改是 我们必须更新 顶部的 itineraryContent 使其不再是字符串 而是 Itinerary 类型 让我们首先将这个变量的名称 更改为 itinerary 并将 String 更新为 Itinerary
我们可以删除这条注释 因为我们已完成这项代码更改
我们需要进行的下一项代码更改是 如果向下滚动至 generateItinerary 函数 你将看到 Xcode 会及时提醒我们 itineraryContent 已不存在 我们可以将 itineraryContent 更新为 itinerary 因为我们刚刚已添加 它之所以报错 是因为当前从 session.respond 输出的 content 为字符串
与上次在 Playground 中一样 我们将添加 generating 参数 并提供 Itinerary.self
模型现在可以输出 Itinerary 类型的值 由于我们已完成这项代码更改 我删除这里的这条注释
好了 我们需要进行的最后一项更改 是移除我们在 instructions 中 提供的其他结构性指导说明 请注意我们如何定义以下内容: “每天都需要包含一个活动、 一家酒店和一家餐厅 并且一定要包含标题、 简短说明 以及按日列出的行程规划” 但所有这些信息均已包含在 可生成型 Itinerary 结构体中 我们无需在 instructions 中 再次提供 所以使用 Generable 的 另一个好处是 可以让提示词更简洁 这也有助于提高性能 我将删除这条注释
本节中的所有代码更改任务 到这里就全部完成了 我们更新了 行程生成器视图模型 以便能够生成可生成型结构 让我们前往第 2.4 节 即“更新视图以显示结构化数据” 在本节中 我们将更新 LandmarkTripView 以生成行程视图 而不是我们在上一节中 看到的原始文本 这是一项非常简单的代码更改 让我们前往 LandmarkTripView
你可以在 Views 文件夹中找到它 它是第二个文件
我们需要在此处 进行代码更改
如果你还记得 我们之前在生成模型输出时 载入了这个视图 但我们不再生成字符串 因此我们不能再使用文本视图了 首先我们必须更新这里 然后我们还需要将这里更新为 另一个视图而不是文本视图 以便我们可以真正 从 itinerary 中提取字段 并填入丰富的 UI 中 让我将这个视图替换为更新的视图 我将说明效果是怎样的
好了 这就是我修改后的代码 你也可以从我们的指南中 拷贝并粘贴这段代码 让我们详细了解一下 我定义了 itinerary= itineraryGenerator.itinerary 我们将文本视图 替换成了 ItineraryView 它接收地标 和生成的行程 现在 ItineraryView 已存在于 Views 文件夹中 但我们尚未查看 因此让我们仔细查看一下 这应该是第三个文件 当然你也可以通过按住 Command 键 点按来打开这个文件 好了 在本章中我们不会 对这个文件进行任何代码更改 但你会在这里看到注释 这意味着我们可能会进行更改 或者说我们肯定会 在后面的章节中进行更改 这个视图的作用是 它接收 模型生成的 Itinerary 结构体、 提取字段并创建我们 在初始演示中看到的丰富 UI 如果仔细查看 body 部分 我们会看到 它可以提取行程的 title、 description 并填入界面中 如果向下滚动 你会看到当它提取每日活动时 会使用名为 DayView 的 专用视图来显示 我们使用 ForEach 来遍历这些项、 提取各项属性并设置布局 请注意 这比解析字符串 并更新界面简单得多
好了 让我们前往幻灯片 引导式生成的主要好处是 它能从根本上保证 结构的正确性 它使用名为约束解码的 技术来实现这一点 它的作用是让你能够控制 模型应该生成的输出 无论是字符串、数字、数组 还是你定义的自定义数据结构
这也意味着提示词 可以更简洁 进一步聚焦理想效果 而不是通过向模型发送提示词 来指定具体的输出格式 这往往也能提高模型准确性 以实现优化来加快推理速度 回顾一下 在本章中我们探讨了 如何通过模型获取结构化数据 我们使用 Generable 自行定义了 Swift 类型 并了解了如何通过嵌套它们 来创建复杂的数据结构
然后我们更新了 App 以便在丰富的用户界面中 生成和呈现这些结构化数据
让我们构建这个模型 看看我们做出的所有更改
这是我们的 App 让我们点按“Sahara Desert” 然后点按“Generate Itinerary” 与之前一样 它将接收提示词和指令 并发送到模型 现在它不再生成一大段文字 而是生成 Itinerary 类型 我们提取所有字段 然后使用新视图即 ItineraryView 填入我们的 App 中
好了 这一章到此结束
现在我们能够 获取结构化数据 作为模型输出 现在我们可以换个话题 重点探讨如何通过 其他提示词技巧 来提高输出的质量和一致性 虽然恰当的提示词能告诉模型怎么做 但有时示范更有效 我们可以直接在提示词中 包含高质量的示例 作为可生成型实例
这种方法很好 因为它让模型 能够更好地了解 我希望生成的响应类型 在本章中 我们将重点探讨 如何改进生成内容的质量 我们首先在 Playground 中 使用 Prompt Builder API 创建具备更强动态调整特性的提示词 然后我们将探索一步到位的提示词 为此 我们会在提示词中 提供高质量的示例 来提高模型的准确性 最后 我们将所学知识 运用于 App 的行程生成器
让我们前往跟随编程指南
我们现在进行到了第三章 即“提示词技巧” 现在我们的目标是提高 模型输出的质量和可靠性 首先 我们将探索 如何使用 Prompt Builder API 引入能够动态调整的提示词
让我们前往 Playground 看看如何实现 再次直接将这个代码块 拷贝到 Playground.swift 文件中
我们已打开 Xcode 前往 Playground.swift
好了 在这里我们将完成的关键代码更改 是使用 Prompt Builder API 引入提示词 如果你还记得 之前我们在 session.respond 下 提供了两个参数 其中一个是字符串格式的 “Generate a 3-day itinerary to Grand Canyon” 但我们可以 不将提示词定义为字符串 而是使用 Prompt Builder API 并将值传递给闭包 这样做的主要好处是它现在可以 包含 Swift 条件语句之类的内容 在顶部这里有一个 名为 kidFriendly 的布尔变量 它当前设置为 true 然后在 Prompt Builder API 中 我使用这个布尔变量 来有条件地更新提示词 若 kid-friendly 布尔变量为 true 那么我们会将这一额外信息 注入到提示词中 即行程必须适合儿童 我们可以更新 session.respond 调用以包含这个新提示词
并刷新画布
我们来看看具体输出
这里显示了 response 变量的 content 让我打开这里的 rationale 看一看 它指出:“这份行程为孩子们 提供了一个安全、富有趣味 且具有教育意义的体验 确保他们在欣赏大峡谷 自然之美的同时 也能获得符合年龄的 活动与住宿安排” 你会看到模型 满足了我们的请求 这是作为一个条件出现的 同样 这样做的好处是可以 让提示词能够实现快速、动态调整 用户在 App 中选择了某些内容 或者你作为开发者 从用户偏好中了解到了某些信息 并相应地更新了提示 这些都可以归为这一情况
太棒了 让我们返回 跟随编程指南的第 3.2 节 现在我们的目标是 使用更高级的提示词技巧 即一步到位的提示词 向模型准确展示 高质量的响应是什么样的 让我们前往 CodeAlong
在这里的 Prompt Builder API 中 我将在闭包内 添加另一行代码 这里已写明:“这是我们 预期的格式示例 但不要直接复制其中的内容” 我引入了一个示例 让我们详细了解一下 这个示例为 Itinerary.exampleTripToJapan 它的定义是怎样的? 你可以按住 Command 键点按它 或者前往 Models 文件夹 点按 Itinerary 并向下滚动 你将看到 exampleTripToJapan 在此处定义 你首先会注意到 这不是包含示例的一长串字符串 这实际上是 Itinerary 的 可生成型实例 所有属性都已填充 你会看到这里有 title、 destinationName、description、 rationale、days 所有属性 都已手动填充 我们可以返回 Playground 你会看到这里有一项输出 输出包含我们 为了给响应的语气和质量 定好基调 以一步到位的示例形式 提供的额外信息
最重要的是 我们直接将 itinerary.exampleTripToJapan 嵌入到了提示词中 这是一个范例 我们还明确告诉模型 不要拷贝示例的内容 我们希望模型学习示例的风格和结构 而不是直接照搬数据 让我们返回指南
现在我们进行到了 第三章的 App 部分 我们现在将这个一步到位的 提示词方法集成到 App 中
我们需要进行的 代码更改如下 在 ViewModels 文件夹的 ItineraryGenerator 中 更新 prompt 并将我们的 示例包含在其中 我们来进行这项代码更改 我们返回了 Xcode 我将依次点按 ViewModels 和 ItineraryGenerator 我将打开查找导航器 并点按 Chapter 3 在这里你可以看到 我们需要进行的代码更改
在 generateItinerary 函数中 显然我们在这里定义了 prompt 我们将替换这个 prompt
我将删除之前的 prompt
同样 与我们在 Playground 中 采用的做法一样 我们定义 let prompt =、指定使用 Prompt Builder API 并传递此闭包 它不仅包含 我们之前使用的字符串 还包含这些额外的信息 从而引入 Itinerary 类型的 Itinerary.exampleTripToJapan 所以它不仅仅包含所有指导说明 现在架构也包含在这个 prompt 中
由于完成了这项更改 我们可以删除这条注释 你会注意到 第 3 章中的 所有代码更改都已完成 这意味着我们可以 构建并运行这个 App 了 我们来看一下构建好的 App
我们可以选择这里的“Serenity” 点按“Generate Itinerary” 我们可以确保它接收提示词、 指令和额外的示例、 将这些传递给模型并生成最终输出 搞定
好了 我们的 App 运行良好 让我们关闭 App 并前往幻灯片 在本章中我们 重点探讨了提示词技巧 我们了解了如何使用提示词构建器 来动态构建提示词 还了解了如何使用一步到位的提示词 提高模型输出的 质量和一致性 然后我们运用了这些知识来更新 App 将一个详细示例包含在了提示词中 @Generable 强制生成定义的结构 而一步到位的示例 可以告诉模型 结构内部的关系和风格 模型还使用提供的示例 来获知应该采用的语气 确保生成的文本与 你希望 App 采用的语气一致
虽然输出之间 并非总是存在显著差异 但这是一种重要方法 能够显著提高生成内容的质量 有关提示词技巧的章节 就讲到这里
正好我们可以暂停一下 短暂休息 10 分钟 大家可以趁这段时间补一补代码、 冲杯咖啡或者起身活动一下腿脚 回来后我们接着 探讨一些精彩的主题 我们将通过流式传输实时更新 UI、 通过工具调用 来扩展模型的功能 最后优化一下性能 我们 10 分钟后回来 一会见
欢迎大家回来 希望大家得到了充分的放松 让我们继续 我们已经有了高质量的提示词 接下来让我们通过实时 流式传输响应来改善用户体验 在本章中 我们将重点探讨 如何重构行程生成器 以使用流式传输 API 通过流式传输模型响应 来改善用户体验 我们将了解如何 在模型生成响应时 处理 PartiallyGenerated 内容 然后我们将更新视图 以便在生成行程的同时实时呈现 让用户获得响应迅速的体验 让我们前往指南 现在我们进行到了第四章 即“流式传输响应” 本章的目标是 流式传输响应 并在生成行程的同时实时显示 从而显著改善 用户体验 我们首先来更新 ItineraryGenerator 文件 本节不涉及 Playground 组件 因为直接在 App 中 感受响应的流式传输 非常简单 让我们前往 Xcode 并打开 ItineraryGenerator
我们将再次使用查找导航器 切换至第四章 然后看一下我们需要进行的 所有代码更改 让我们从 itinerary 开始 我们需要进行的第一项代码更改是 将 itinerary 变量更新为 Itinerary.PartiallyGenerated 类型
那么什么是 PartiallyGenerated? 可以将它想象成结构体的镜像版本 其中每个属性都是可选的 它由 @Generable 自动定义 它非常适合呈现 随时间推移接收的数据 这是第一项代码更改 我将移除这里的注释
我们需要在这里 进行下一项代码更改 回想一下 generateItinerary 函数 包含对 session.respond 的 异步调用 我们传递了提示词 然后传递了可生成型对象 然后获得了输出 而我们希望实现的效果是 模型生成响应 并将响应流式传输给我们 我们要做的是将这段代码替换为名为 session.streamResponse 的新 API 下面我们来看一看
我们将 session.respond 替换成了 session.streamResponse 其余参数保持不变 你仍要传入 prompt 仍要提供 generating 参数 以及 Itinerary 但这里没有 await 这里使用的是 名为 stream 的异步序列 这意味着我们可以循环遍历它 并将所有输出赋值给 包含各种选项的 itinerary 我们定义“在流式处理中 try await partialResponse” 我们可以使用 partialResponse.content 提取响应 这样每次都能得到 特定时间点 所生成内容的快照 由于完成了这项代码更改 我要将这条注释也移除
好了 这就是我们需要 对行程生成器进行的所有代码更改 让我们前往 跟随编程指南的第 4.2 节 现在我们可以更新视图了 由于 PartiallyGenerated 字段是可选的 因此我们可以使用 if let 语句 安全地解包这些选项 这就是我们在本节中要完成的操作 我们将更新 ItineraryView 我们之前在前面的章节中 预览过这个视图 现在我们实际上要 对这个视图进行代码更改 让我们前往 Xcode
点按 Views 文件夹
然后点按 ItineraryView
好了 你会注意到 最顶部有 itinerary 所以我们也应该使用在视图模型中 定义的 PartiallyGenerated 类型 来更新这个变量 我们需要对这里的所有可生成型对象 进行这项代码更改 所以不仅要更改 itinerary 还要更改所有嵌套的可生成型对象 一直向下滚动 如果你还记得 这里定义了 DayView 它包含 DayPlan DayPlan 也应该为 PartiallyGenerated 类型 每次进行这项代码更改后 我都会移除这些注释 再往下 如果你还记得 这里有 activity 数组 我们将对这个数组进行同样的更改
好了 这就是对可生成型对象 进行的主要代码更改 让我们返回顶部 你会看到 Xcode 显示了 另外几项报错 接下来我们需要进行另一项代码更改 如果你还记得 我说过这些字段 是可选的 所以我们必须解包它们 我们来解包吧
这就是我修改后的代码 我定义了 if let title = itinerary.title if let 是处理 这些可选字段的绝佳方法 由于这里已有 title 因此无需从 itinerary 中提取 所以我要移除这个前缀 title 处理完成 现在我需要重复同一 步骤来处理 description
我使用了 if let 并更新了 文本视图以包含 description
然后我需要再次重复 这一步骤来处理 rationale
我需要再次重复这一步骤 来处理其他字段 即 days
好了 你应该已经明白了 我们必须继续对我们要访问的所有 itinerary 字段即属性采取这一步骤 以安全地解包它们 现在我要做的是拷贝代码 我一直在让大家 拷贝代码 让我们返回跟随编程指南 拷贝完全更新后的文件 并粘贴到这里 因为我们必须对每个属性 都采取这一步骤 如果你滚动到这里 则会看到第三步要求 对所有这些属性重复这一步骤 我们已经更改了 title、description、rationale 但你必须对 所有 DayPlan 和 Activity 视图 也采取这一步骤 因此在这次跟随编程中 我现在要做的是 点按“Show the Updated Views” 其中包含所有代码更改 我要做的是点按右上角的 “Copy”按钮、返回 Xcode ItineraryView.swift 文件 然后使用更新后的代码 替换所有代码 在查找导航器中你可以看到 没有其他注释了 因此我们已完成所有代码更改 我展示了我们需要进行的 几项不同代码更改 但你必须对每个属性 采取相同的操作 第 4 章的所有代码更改任务 到这里就全部完成了 快速回顾一下 我们讨论了 需要对视图模型进行的更改 即使用 PartiallyGenerated 并更新了视图以解包这些选项 我们现在可以运行 App 了 点按运行按钮 这样将构建并运行这个 App 这里显示了我们的 App 我将点按这里的“Sahara Desert” 然后点按“Generate Itinerary”
与之前的异步调用不同 现在我们可以在生成响应 的同时实时流式传输响应 这带来了卓越的用户体验 因为使用 App 的用户 在所有行程载入完毕之前 即可开始使用这些内容
太棒了 在这一章中 我们让用户体验有了显著提升 我们重构了 App 以使用流式传输 API 并了解了如何在视图模型中 处理 PartiallyGenerated 内容 最后 我们更新了视图 以在生成行程时 实时显示行程
有关流式传输响应的 第四章就讲到这里 现在我们的 App 已经非常出色了 但让我们通过工具调用 为模型提供新功能 以使 App 更加智能
首先让我介绍一下 工具调用的概念 除了你提供给提示词的信息 模型还自带来自训练数据的核心知识 但请记住 模型内建于操作系统中 它不会随时间推移 而积累更多知识 例如 如果你询问它 现在库比提诺的天气 它没有办法 知道这一信息 为了应对需要实时或 动态数据的用例 这个框架支持工具调用 它的工作原理是这样的 它自带会话记录 如果你为会话提供了工具 则会话会将工具定义 连同指令一起 提供给模型 在我们的示例中 提示词会告诉模型 我们想要前往哪个目的地
现在 如果模型确定调用工具 可以改进响应 它将生成一个或多个工具调用 在本例中 模型生成了两个工具调用 分别查询餐厅和酒店 在此阶段 Foundation Models 框架 会自动调用 你为这些工具编写的代码 然后框架会自动 将工具输出 插入到会话记录
最后 模型会 整合工具输出 和会话记录中的所有信息 生成最终响应
正如我们目前所看到的 模型可能具有很强的创造性 每次我们提出请求时 生成的行程往往略有不同 这种随机性对发挥创造性很有帮助 当我们需确保可预测性时则面临挑战 对于工具调用等高级功能 特别是在测试和调试时 我们需要确保 模型行为一致 我们需要保证 它按照我们的预期调用工具 为了实现这一点 我们将使用 GenerationOptions API 对我们的请求进行另一项微小的更改 以使用贪婪采样 贪婪采样告诉模型 停止发挥创造性 并始终选择最明显的下一个词元 这使得模型输出具有确定性 对于我们的 App 而言 这确保模型 每次都能可靠地调用我们的工具
在本章中 我们将探讨一个工具 它可用于查找兴趣点 然后我们将这个工具提供给 语言模型会话 并告知模型如何使用它 返回 App 我们将这个工具 集成到行程生成器中 以将现实世界的数据引入行程中 让我们前往跟随编程指南 现在我们进行到了第五章 即“工具调用”
我们的行程包含模型生成的 酒店和餐厅名称 这些可能不是最新的 我们的目标是为模型提供一个工具 这个工具可用于调用 Swift 代码 并获取我们提供的 酒店和餐厅名称
让我们完成这些代码更改 以便首先构建工具 然后在 App 中使用这个工具 我将前往 Xcode 然后点按 ViewModels 文件夹 在这里你会看到一个新文件 名为 FindPointsOfInterestTool
点按这个文件 这里有一个名为 FindPointsOfInterestTool 的类 它遵从工具协议 这意味着必须在这里定义一些属性 下面逐步讲解 让我们开始进行这些代码更改 我将说明各项操作 我们需要进行的第一项代码更改是 为工具添加名称和描述 我将在这里完成
我们为工具提供了名称即 findPointsOfInterestTool 还提供了说明 “为一个地标寻找兴趣点” 这对于模型判断 何时调用工具至关重要 它将使用 名称和描述来确定 何时调用这个工具 我们需要在这里进行下一项代码更改 让我打开查找导航器 这样我们就能看到需要进行的 所有代码更改
我们需要在这里进行的 下一项代码更改是定义 工具可以搜索的 兴趣点类别
为此 我们将引入 这个可生成型枚举
在这里 Category 是一个枚举 它包括酒店和餐馆 当然它还可以包括其他用例 如博物馆或露营地等 在下一项代码更改中更新参数时 我们会用到这个枚举
这里有一个 Arguments 结构体 让我们更新这个结构体 我会讲解它的作用
在这里 Arguments 结构体 有一个属性 它定义为 let pointofInterest 它的类型为 我们刚刚定义的 Category 所以 pointofInterest 可以是酒店或餐厅 我们还提供了 Guide Guide 的 description 为 “这是要查询的目的地类型” 这个 Arguments 是工具 与模型之间达成的约定 当模型想要调用工具时 它会将 Arguments 传递给工具 这样工具就能根据 它希望从工具获取的响应 是酒店还是餐厅 即它想要从工具获取的响应类别 来访问相应数据
我们已经更新了 Arguments 现在我们将更新 这里的 call 函数
这个函数是我们工具的核心 它接收参数、执行操作 并返回输出 输出会被添加到会话记录 以供模型查看和使用 让我们进行这项更改
好了 我将逐步讲解 各项操作
首先 我定义 let results = await getSuggestions 我们还没有定义 getSuggestions 我们稍后会进行定义 本质上 可以将它视为 为了获得这些特定的兴趣点 call 方法可以调用的函数 然后 results 是输出的一部分 正如你在 return 语句中看到的 我们可以将 results 以字符串输出的形式插入 以返回给模型 然后模型使用 这些信息以及提示词和指令 来生成最终响应 我们需要进行的最后一项代码更改 当然是定义这个函数 这里有一个占位符函数 名为 getSuggestions 让我们来更新这个函数
好了 在 getSuggestions 中 我定义了一个 switch 块 它接收 category 如果 category 为餐厅 它可以返回 Restaurant 1、 Restaurant 2 或 Restaurant 3 同样 如果 category 为酒店 它可以 返回 Hotel 1、Hotel 2 或 Hotel 3 在这个演示中 我们使用了硬编码数据 在实际 App 中 你会在这里调用 MapKit 或服务器端 API 等 API 来获取真实的实时数据
好了 我们对工具 进行了所有代码更改 这意味着我们已全面定义我们的工具 让我们返回跟随编程指南 并前往第 5.2 节
我们现在要做的是测试这个工具 我们将前往 Playground 并将这个工具提供给模型 看看结果如何 与之前一样 再次直接拷贝粘贴这段代码 我将逐步讲解每一行代码 并说明各项操作 返回 Xcode 我将切换到 Playground.swift 文件 在本节中 让我清理一下之前的代码并从头开始
好了 我们有了空白 Playground
首先 我要添加指令
在这里 Playground 有一项便捷功能 就是可以访问 Xcode 项目中的 所有数据结构 而无需构建 App 在这里 我要做的是 创建一个 landmark 变量 它可以访问在 Models 文件夹下的 ModelData.swift 中 定义的模型数据 我指定了 ModelData.landmark 0 这意味着我将前往 你可以看到的地标之一 具体而言 我们将前往第一个地标 如果你还记得 第一个是“Sahara Desert” 在这里使用的地标列表 与运行 App 时 看到的地标列表一样 我们获取地标信息 并且我们已经在 ViewModels 文件夹中定义了 FindPointsOfInterestTool 我们将创建这个工具的实例 我们可以将 landmark 传递给它 因为它会用到这一信息 最后 与之前一样 我们定义了指令 如果你仔细观察 会发现有两项微小的代码更改 第一 它不再是字符串 而是类似于提示词构建器 的指令构建器 在其中我们传入一个闭包 并提供我们的指令 第二项关键更改对工具调用非常重要 你会注意到我们指定了 “始终使用 findPointsOfInterest 工具 来查找该地标附近的酒店和餐厅” 现在 这条指令告诉模型 它必须调用这个工具 才能获得兴趣点并生成响应 现在我们将创建 LanguageModelSession 与之前的代码更改类似 我们定义了 LanguageModelSession 并传递了指令 但我们引入了 一个名为 tools 的新参数 tools 可以是一个工具数组 在这里只有一个工具 就是 pointOfInterestTool 由于它是一个数组 你可以提供多个工具 以便模型可以根据你的 提示词和指令进行推理 确定何时调用哪个工具并返回响应 我们已经将工具包含在了会话中 接下来我们定义提示词
在这里提示词本身没有变化 最后我们将调用模型
在这里代码基本没有变化 只是我们引入了之前在幻灯片中 简要讨论过的 options 这个 GenerationOptions 的 sampling 设置为 greedy 鉴于其余提示词和指令是一致的 因此这将确保我们 始终获得一致、可重复 且确定性的输出 好了 我们来看一下这里的画布 并了解一下输出 好了 response 已生成 这里还显示了 content
其中有 title、 description、rationale、days 让我选择其中一天 例如第 0 天 Arrival 让我看看其中的 activities
我将打开第 0 项、 第 1 项和第 2 项活动 现在 如果你仔细观察 就会看到 第 1 项活动下的 description 为 “在 Restaurant 1 享用传统摩洛哥晚餐” 你还会看到 title 为 “在 Restaurant 1 用餐” 同样地 你会看到第 2 项 活动的 title 为 “在 Hotel 1 住宿”、 description 为“在 Hotel 1 放松休息” 这是要插入到 模型输出中的 工具输出 模型接收提示词、指令和地标名称 调用工具 返回酒店和餐厅名称 将这一信息插入会话记录 然后生成响应 让我们来看看会话记录本身
在这里我要做的是 为会话本身 创建一个临时变量 并将其捕获到 inspectSession 中 我这样做的原因是 为了仔细查看 会话和会话记录 我们可以看到发生的工具调用 好了 我们刚刚创建了 inspectSession 现在我们来看看这些属性 你可以看到 tools 有一个我们提供的工具 如果看一下 transcript 你可以看到 entries 中有 6 个元素 这里是 instructions 它始终是 transcript 中的第一个条目 然后是 prompt 也就是我们的初始请求 然后是 toolCalls 模型自主决定 是否需要调用我们的工具 然后是 toolOutput 框架执行我们的工具 并将工具输出 插入到会话记录中 最后是 response 模型提炼原始提示词 工具输出数据 以生成最终响应 这里有两个工具调用 因为我们同时针对 餐厅和酒店提出了请求 你会在 toolCalls 下看到这一点 请求是同时针对餐厅和酒店提出的
太棒了 让我们返回跟随编程指南
现在我们知道了工具的工作原理 我们定义了工具 我们在 Playground 中测试工具 现在我们可以更新 ItineraryGenerator.swift 文件 将我们的工具整合到 App 中 这就是在第 5.3 节中 我们要完成的事项
我们对 ItineraryGenerator.swift 进行代码更改 直接将这段代码 拷贝并粘贴到你的文件中 如你所见 我们将进行的 关键更改是更新我们的指令 创建工具实例并 将其传递给 LanguageModelSession 让我们前往 Xcode 并打开 ItineraryGenerator.swift 我还会调出查找导航器 设置为第 5 章 然后开始进行代码更改
我们需要进行的第一项更改 当然是更新指令
我将删除之前的指令 因为我有了新的指令 其中包含我们定义的 pointOfInterestTool 以及附加文本 要求模型调用这个工具 来获取兴趣点 当然 我们还需要 使用 tools 参数 更新 LanguageModelSession
它是一个数组 因为它可以接受多个工具 我们将传入工具
好了 这就是我们需要在构造器中 进行的两项代码更改 更改完成后 我将删除这些注释 以便我们可以跟踪更改 好了 我们需要 在 generateItinerary 方法中进行最后一项更改
回想一下 我们提到过 如果要获得确定性输出 我们可以使用贪婪采样 默认进行的是随机采样 所以在这里 在 session.streamResponse 后面 在传递 prompt 之后 在传递 generating 参数之后 我们可以传递 options 我来整理一下 这样大家都能轻松阅读
好了 这里有 session.streamResponse、 有 prompt、有 generating 参数 最后还有 options options 中包含 GenerationOptions 我们将 sampling 设置为 greedy 好了 我们需要进行的所有代码更改 到这里就全部完成了 让我们确保删除这条注释
搞定 如果你在查找导航器中没有看到 第 5 章的任何相关内容 则意味着我们完成了所有代码更改 可以构建并运行这个 App 了
点按运行按钮 这样将构建并运行 App
这就是我们的 App 让我们来了解一下标准用户流 即点按“Sahara Desert” 我可以看到 “Generate Itinerary”按钮 我点按这个按钮 现在 既有流式传输 API 还有我们的工具 它接收我们的指令、提示词 并将这些连同工具定义 一起发送给模型 如你所见 你可以看到 “在 Hotel 1 住宿”和 “在 Restaurant 1 用餐” 这些是来自工具的响应 这些响应会 被插入到会话记录中 模型使用了来自指令、 提示词、工具调用 和工具响应 的所有信息 它将所有这些信息 进行整合并加以提炼 能够以可生成型 Itinerary 格式生成输出
太棒了! 好了 让我们回到幻灯片 并简单总结一下 在本章中 我们通过工具调用 提升模型的处理能力 我们讨论了一个自定义工具 它有自己的参数和调用函数 我们了解了如何将工具 提供给 LanguageModelSession 重要的是如何 就工具的使用时机和方式 向模型发送指令 最后 我们将工具集成到了 App 中 以获取兴趣点 并将其包含在生成的行程中 有关工具调用的第五章就讲到这里
在结束这个跟随编程讲座之前 我们来了解一些 用于优化性能和 让生成式功能响应更迅速 的关键技巧
让我们前往 跟随编程指南的第 6 章 即“性能和优化” 我们的 App 现在功能完备 但为了让 App 具备卓越性能 我们首先需要了解 瓶颈在哪里 无法衡量便无法优化 为此 我们将使用一个 强大的开发者工具即 Instruments
让我们前往 Xcode
这次的操作略有不同 如果长按这里的运行按钮 你会看到几个不同的选项 你可以看到 Run、Test、Profile 和 Analyze 我将点按 Profile 这样做会构建 App 然后启动 Xcode Instruments
让我们等待 App 构建完成 好了 这是 Xcode Instruments 我们将选择空白模板
打开 Instruments 后 我将点按这里的加号 并搜索 Foundation Models
好了 现在可以 对 App 进行性能分析了
我将点按录制按钮 这将启动我们的 App 我们将像往常一样使用这个 App 作为用户 “Sahara Desert”值得期待 我看了标题和描述 觉得不错 我点按“Generate Itinerary” 看到生成了精心安排的行程 结果正在以流式传输的方式发送给我 我可以阅读显示的内容 浏览规划的所有不同活动
好了 我将停止录制
现在 让我们详细了解一下 Instruments 提供的洞察
好了 这里有几个不同的跟踪轨道 我将说明每个跟踪轨道提供的洞察 以确定我们可以消除的 任何潜在瓶颈 第一个跟踪轨道是 Response 这里的蓝条代表整个会话 这是用户点按 “Generate Itinerary”、 开始创建会话、 模型接收指令和提示词 并生成输出的整个过程 所有这些活动都由这个蓝条表示
第二行是“Asset Loading” 如果你仔细观察 就会发现会话开始后 有短暂的延迟 然后模型也就是 模型资源开始载入 这意味着从会话开始 一直到模型载入结束 的这段时间 模型不会生成任何响应 这段时间大约 持续了 700 毫秒 差不多整整一秒钟 对吗? 如果看一下第三个跟踪轨道 你会看到生成 第一个词元的时间点 这意味着要等到 所有模型载入完毕 然后才会开始词元生成过程 这个过程从第一个词元开始 持续到生成所有响应 这里有提升 性能的机会 如果我们能提前载入这些资源 也许就能在会话开始后 立即开始生成过程 这是我们可以尝试解决的一个瓶颈 第二个瓶颈 看一下底部的这个位置 我将选择“Inference”部分 如果你仔细观察 就会发现这里显示了最大词元数 我们可以看到 目前这个数字是 1044 这个词元数包含了 我们添加到会话中的所有内容 这包括指令、 提示词和工具 还包括可生成型对象 以及行程等 由于它包含的内容繁杂 我们可以看看是否 有机会减少数量 因为词元数 会对模型的性能产生影响 这是第二个瓶颈 我们可以看看能否尝试解决一下 好了 如果你还记得 当我们调用 session.respond 时 如果模型不在内存中 操作系统会载入模型 预热可以在提出请求之前载入模型 从而让会话 提前做好准备 在我们的 App 中 当有人轻点地标时 他们很可能很快就会 提出请求 我们可以在他们按下 “Generate Itinerary”按钮之前 进行预热以主动载入模型 当他们读完描述时 我们的模型便可以使用了
我们再来了解一下另一项优化 它可以减少请求延迟 回想一下 提供给模型的 可生成型结构体 有助于生成结构化输出 但这样做的代价是 词元数增加 这会影响初始处理时间 同样回想一下 在第 3 章中 我们传递了一个示例行程 名为 exampleTripToJapan 由于指令包含 可生成型架构的这个完整示例 因此通常可以排除 前面的定义本身 这样可以节省空间 并加快模型响应速度
借助 Xcode Instruments 我们找出了 App 中的瓶颈 现在 我们将直接在 App 中 实现一些优化 首先 当用户轻点地标时 我们将通过调用 prewarm 方法 来预热会话 这使框架在用户请求提供行程之前 就开始载入模型 其次 由于一步到位 的示例非常详细 因此提示词中的 完整架构定义是多余的 我们可以将 includeSchemaInPrompt 设置为 false 来移除架构定义 我们将在 streamResponse 调用中 进行这项更改 这将大幅减少 输入词元数
让我们前往跟随编程指南 了解一下我们将进行的代码更改 现在我们进行到了 第 6 章的 App 部分 第一部分是“预热模型” 代码更改将反映在 ItineraryGenerator 中 我们将在其中添加一个函数来预热 在视图中也要进行同样的更改 这样我们就可以在载入视图时 调用 prewarm 方法 让我们在 ItineraryGenerator 和 LandmarkTripView 中进行这些更改 让我们前往 Xcode 我会让 Instruments 保持打开状态 因为我想检查这些优化的效果 我将前往 Xcode 然后点按 ItineraryGenerator 它已经打开 我将使用查找导航器 打开第 6 章
好了
为了实现预热 我们要进行的 第一项更改是在这里添加预热代码 我们定义了这个名为 prewarmModel 的占位符函数 在这里我要做的是 在会话中调用 prewarm 方法
就是这么简单
现在我们定义了一个函数 我们可以从视图中调用它 来对模型进行预热 如果你提前知道 提示词是什么 也可以使用 promptPrefix 来更新 prewarm 方法
在 session.prewarm 函数中 有一个可选参数 名为 promptPrefix 你可以在其中提供提示词 这样模型就能知道 用户可能提供的提示词 并使用提示词进行预热 在这里 我们将使用闭包传递提示词 “生成一个三天的行程 目的地为 landmark.name” 这可以进一步提升性能 好的 我们需要进行的下一项代码 更改应在 LandmarkTripView 中完成 在 Views 文件夹中 有一个 LandmarkTripView 文件 在这里 我们需要更新 task 以便在实际载入模型时 调用 prewarm 方法 让我们在这里进行更改
同样 这非常简单 只需调用我们刚刚定义的 generator.prewarmModel 函数即可 这就是为了预热模型 需要进行的所有代码更改 让我们返回跟随编程指南 了解一下我们讨论过的 第二项优化 即减少最大词元数 我们现在进行到了第 6.2 节 我们将优化提示词
同样需要在 ItineraryGenerator 中 进行这项代码更改 我们将添加这个额外的参数 参数名为 includeSchemaInPrompt 并且应设置为 false 让我们进行这项更改 同样 我会简要说明各项操作
返回 ItineraryGenerator
这里显示了 session.streamResponse 我们在其中传入了 prompt、 generating 和 options 我们还将添加新参数
参数名为 includeSchemaInPrompt 并且应设置为 false 这告诉模型 可以排除 我们传递的 Itinerary 的架构 因为我们已在指令中 传递 exampleTripToJapan 其中包含范例 以及结构 我们可以跳过添加架构操作 这有助于减少 最大词元数
由于完成了这项更改 我要将这条注释也删掉 好了 第 6 章中的所有 代码更改任务到这里就全部完成了 这意味着我们现在 可以再次对 App 进行性能分析 好的 让我们再进行一次性能分析 我将再次点按“Profile”选项 同样 这将构建 App 并立即启动我们的性能分析器
你会看到 Xcode 正在构建 性能分析器再次启动 现在当我录制时 App 将重新启动 我们将再次完成 相同的 App 使用流程
点按录制按钮
我的 App 在这里 我将完成完全相同的步骤 我点按“Sahara Desert” 我阅读标题 描述在我看来也不错 我希望生成行程 我看到行程正在生成 看起来规划周密 有每日计划、 吃饭的餐厅 还有入住的酒店等 让它执行完毕 我将停止性能分析
让我们按照之前的操作流程 来查看输出 看看优化对 App 产生了什么影响 你首先应该会注意到 得益于 prewarm 函数 资源载入 在会话开始之前便已顺利进行 所以当用户点按 详情视图时资源便已载入 我们在 task 添加了 prewarm 函数 来调用 prewarm 方法 这意味着当用户 按照习惯阅读标题和描述时 模型已经载入完毕并准备就绪 如果你仔细查看 会话开始时的数据 会发现输出几乎在会话开始后 立即开始生成 会话开始时 由于模型已经载入 它会立即开始准备词汇表、 开始生成词元 响应速度也会大幅提升 我们再来看看 我们进行的第二项优化 以及产生的影响 在“Inference”下 你会看到最大词元数 已降至 700 之前是 1000 也就是说 我们通过从提示词中排除架构 将最大词元数降至了 700 这也意味着模型 能够更快地处理初始词元 并开始更快地 生成响应
太棒了 在最后一章中 我们了解了性能 我们了解了如何预热模型 以使 App 响应更迅速 以及如何通过排除不需要的架构 来优化提示词 这是提升生成式功能性能 的两种简单 而有效的方法
最后我们再看一下 我们共同构建的这款 App
让我们返回 Xcode 然后构建并运行
好了 这对你而言应该很熟悉 因为这是在你的电脑上运行的 App 启动时首先会显示 这个简单的 Swift 地标列表 选择“Serengeti”时 会显示这个详情视图 让我们最后一次 轻点“Generate Itinerary”
UI 会实时自行构建 这是在第四章中使用 session.streamResponse 和 PartiallyGenerated 内容实现的流式传输 API 在第二章中 我们通过添加可生成型 对象获得了丰富的结构化响应 在第五章中 我们使用工具调用 来查找兴趣点 模型会智能地决定是否要调用工具 来获取这些数据
好了 今天我们讲解的内容很多 包括基本文本生成、引导式生成、 流式传输、工具调用 和性能优化 但还有更多内容等着我们去探索 我们没有时间讲解某些进阶主题 例如训练自定义模型适配器、 动态运行时架构 或深入研究防护机制和错误处理 如需进一步了解这些主题 强烈建议大家观看有关 Foundation Models 框架 的 WWDC25 视频
看看 Slido 大家提出了很多很棒的问题 如果我们没有回答到你的问题 你可以前往 developer.apple.com/forums 在开发者论坛上提问 在那里我们可以继续讨论 今天完成的示例项目 包括一些 附加功能 可以在 Foundation Models 框架 文档中下载 最后 今天晚些时候 你会收到一份调查问卷 希望你们喜欢这次讲座 也欢迎你们提供反馈 就到这里吧 感谢大家跟随我一起编程 我们很快就会再见 再见!
-