深入 Foundation Models 框架
Deep dive into the Foundation Models framework
2025年6月9日
一句话判断
Foundation Models 框架让你在设备端跑一个 LLM,用 Swift 原生 API 就能拿到结构化输出、调用工具——关键优势是隐私和零网络延迟,关键限制是模型大小和上下文窗口。
这场 Session 讲了什么
Foundation Models 是 Apple 在 WWDC25 推出的全新框架,直接在设备端运行大语言模型,提供了 Swift API 来做文本生成、结构化数据生成(Generable)和工具调用(Tool Calling)。Session 深入讲解了四个核心机制:Session 的工作原理和上下文管理、Generable 宏的约束解码原理、运行时动态 Schema,以及 Tool Calling 的完整生命周期。
这不是一个”Hello World”级别的介绍——它真的在讲底层原理。比如 Generable 为什么可靠(约束解码在 token 级别做掩码,不允许模型输出不符合 schema 的结构),Tool Calling 的并行执行模型,以及 session 超出上下文窗口时的恢复策略。
值得深挖的点
Generable 背后的约束解码
Generable 不是”提示工程”——它是约束解码(constrained decoding)。原理是:模型每生成一个 token,框架会根据你定义的 Swift 类型的 schema,在 token 分布上做掩码,只允许模型选择符合 schema 的 token。这意味着模型在结构层面不可能幻觉——它不可能生成一个不存在的字段名,也不可能在应该输出 Int 的地方输出 String。这比传统的”在 prompt 里描述格式然后解析”可靠了几个数量级。
Guide 宏进一步加强了这个能力。你可以对 Int 做范围约束(@Guide(.range(1...100))),对 Array 做数量约束(@Guide(.count(3))),对 String 做正则约束(@Guide(.regex(#/[A-Z][a-z]+/#)))。正则约束特别强——它不是用来匹配输出的,而是用来定义生成的格式的。
Session 的上下文窗口管理
LanguageModelSession 是有状态的,每次 respond(to:) 调用都会被记录到 transcript 中。transcript 包含指令、所有 prompt 和响应。当 session 超出上下文窗口时会抛 exceededContextWindowSize 错误。
恢复策略有两种:创建新 session(丢失全部历史)或从旧 transcript 中选取部分条目转移到新 session。推荐做法是保留初始指令 + 最近几次对话。如果你的场景需要长对话,可以考虑用 Foundation Models 本身来总结旧对话再转移。
Tool Calling 的执行模型
Tool 是同步执行的——模型决定要调用工具时,会先生成参数(通过 Generable 保证参数合法),然后调用你的 call() 方法,等待返回后才继续生成后续输出。但关键是:同一个 request 内工具可以被调用多次,而且多次调用是并行执行的。如果你的工具有副作用(比如写数据库),需要自己处理并发安全。Session 的初始化接收 tool 实例而非类型,所以你可以用类来保持状态。
采样控制
GenerationOptions 可以控制采样方式。greedy 模式下输出确定性的(相同 prompt + 相同 session 状态 = 相同输出),适合 demo 和测试。temperature 控制随机程度——0.5 输出变化小,更高值输出更发散。注意:greedy 模式只对同一个 OS 版本内的模型有效,系统更新后输出可能变化。
代码片段
1. Generable + Guide:生成带约束的游戏 NPC
场景:生成咖啡店 NPC,level 在 1-20 之间,正好三个属性。
@Generable
struct NPC {
@Guide(description: "a full name")
let name: String
@Guide(.range(1...20))
let level: Int
@Guide(.count(3))
let attributes: [String]
}
let session = LanguageModelSession(
instructions: "You are a game NPC generator for a coffee shop game."
)
let npc = try await session.respond(
to: "Generate a friendly barista NPC",
generating: NPC.self
)
2. Tool Calling:让模型自主调用联系人
场景:NPC 对话时从用户联系人中随机拉取名字,增加个性化体验。
class FindContactTool: Tool {
let name = "findContact"
let description = "Find a contact by age generation like millennials"
var usedContacts: Set<String> = []
@Generable
struct Arguments {
@Guide(.anyOf(["gen_z", "millennial", "gen_x"]))
let generation: String
}
func call(arguments: Arguments) async throws -> String {
// 查询联系人,跳过已用的
let contacts = try await fetchContacts(for: arguments.generation)
let unused = contacts.first { !usedContacts.contains($0) }
if let name = unused {
usedContacts.insert(name)
return name
}
return "A stranger"
}
}
let session = LanguageModelSession(
instructions: "Today's date is \(Date())",
tools: [FindContactTool()]
)
坑:tool 的 name 和 description 会被直接放入 prompt,太长会增加 token 数和延迟。name 用动词短语(findContact),description 一句话搞定。
最佳实践
Generable 是首选输出方式,比纯文本生成可靠得多。定义类型时注意属性声明顺序——模型按声明顺序生成属性,先声明的属性会影响后声明属性的值。如果需要某个属性的值受另一个影响,把被依赖的放在前面。
Tool 的 description 要简短精确。长 description 不仅增加 token,还会降低模型判断何时调用工具的准确度。多试几个版本看哪个效果最好。
上下文窗口超限时,优先保留指令(transcript 的第一个条目)和最近的响应。不要把整个 transcript 搬过去——你会立刻再次触发超限。
还有什么值得关注
- Dynamic Schema:如果你的数据结构在运行时才确定(比如用户自定义的关卡编辑器),用
DynamicGenerationSchema而不是 Generable。它给你运行时完全控制,但仍享受约束解码的安全性。 @Generable支持枚举和关联值:可以让模型在不同”遭遇类型”之间选择,每个类型有不同字段,非常适合游戏叙事。- 语言支持检查:用
LanguageModelSession.isSupported(language:)检查用户语言是否支持,不支持时显示降级提示。