Highlight
Apple 在 Foundation Models 框架中引入
DynamicProfileAPI,让同一个LanguageModelSession能在不同任务阶段动态切换模型、指令和工具配置,并配套开源了FoundationModelsUtilities工具包来管理对话历史裁剪与多 Agent 编排。
核心内容
一个折纸 App 的困境
想象你在做一个叫 Origami 的手工艺教学 App。用户上传一张照片,App 先帮用户头脑风暴创意,然后生成详细教程,最后用户在制作过程中还能上传进度照片获取技巧指导。
这三个阶段共享同一段对话上下文,但各自的需求完全不同。头脑风暴需要创造力,教程生成需要深度推理,技巧指导则需要快速响应。以前的做法是开三个独立的 LanguageModelSession,手动传递摘要,上下文频繁断裂。要么就是一个 Session 硬扛到底,Token 越积越多,模型越来越笨。
Apple 今年的解法叫 DynamicProfile。
动态切换模型:像换帽子一样换 Agent
(02:08)DynamicProfile 让你在同一个 Session 里,根据当前任务阶段切换底层模型、温度(temperature)、推理级别(reasoningLevel)等配置。每次调用模型前,body 属性会重新求值,Session 的”人格”随之改变。
折纸 App 的例子很典型:头脑风暴阶段用 PrivateCloudComputeLanguageModel,温度设为 1 以激发创意;教程规划阶段同样用云端模型,但开启深度推理;最后的审查指导阶段切换到 SystemLanguageModel 节省成本。
对话历史管理:剪枝但不伤根
(07:56)切换模型时,不同模型的上下文窗口大小不同。DynamicProfile 提供了两种历史管理方式。
第一种是 historyTransform,它在每次请求前对历史做局部转换,只影响发给模型的快照,不修改 Session 的真实历史。这种无状态设计避免了状态同步的麻烦。
第二种是通过 @SessionProperty 直接读写 history。这种方式是”有损”的,修改会反映到 Session 的所有 Profile 上。适合在响应边界做摘要和裁剪,比如在 onResponse 回调里把 50 条历史压缩成一段摘要。
两种编排模式:接力与求助
(12:49)当多个 Profile 需要协作时,Apple 推荐了两种模式。
Baton-pass(接力):多个 Profile 共享完整对话历史,通过一个 Tool 切换当前激活的 Profile。接手的 Profile 负责给出最终答案。适合需要上下文连贯的协作场景。
Phone-a-friend(求助):主 Profile 通过一个 Tool 派生一个短生命周期的子 Session,子 Session 独立处理子任务后返回结果,然后销毁。适合需要隔离上下文的咨询场景。
控制 Tool 调用的时机
(15:29)新引入的 ToolCallingMode 有三个选项:allowed(默认,模型自行决定)、disallowed(禁止调用 Tool)、required(必须调用 Tool)。
required 模式在 Agent 系统中很有用,但有个陷阱:模型会陷入无限循环调用 Tool。你必须提供退出条件,比如用一个状态变量条件化模式切换,或者提供一个抛出异常的”最终答案” Tool 来强制退出。
性能与准确性的权衡
(18:29)修改对话历史会影响底层 KV Cache(键值缓存)。追加历史通常能保留缓存,但删除、修改历史条目或更换工具会触发缓存失效,增加首 Token 延迟。Apple 没有帮你自动处理这个,而是升级了 Xcode 的 Foundation Models Instrument,让你自己测量和优化。
另一个风险是准确性。如果你给 Session 添加了新的 Tool,但历史记录里显示模型之前没有 Tool 也完成了类似任务,模型可能会困惑。Apple 的建议是:用 Evaluations 框架建立评估集,用数据驱动的方式验证你的上下文工程策略。
详细内容
声明 DynamicInstructions 组合指令
(04:58)DynamicInstructions 把相关的指令和工具打包成可复用组件,支持像 SwiftUI 视图一样嵌套组合。
struct BrainstormFacilitator: DynamicInstructions {
var orchestrator: CraftOrchestrator
var body: some DynamicInstructions {
Instructions {
"You are a warm and friendly expert crafting brainstorm facilitator."
}
GenerateProjectTitle()
if orchestrator.techniques.contains(.origami) {
OrigamiExpert()
}
}
}
关键点:
DynamicInstructions可以条件化包含,比如只在用户选择折纸时加载OrigamiExpert- 嵌套时指令和工具会自动拼接,不需要手动字符串拼接
- 适合把领域知识封装成可复用模块
用 DynamicProfile 声明多阶段配置
(06:41)DynamicProfile 的 body 根据外部状态返回不同的 Profile 配置。
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .brainstorming:
Profile { BrainstormFacilitator(orchestrator: orchestrator) }
.model(orchestrator.pccLanguageModel)
.temperature(1)
case .planning:
Profile { TutorialAuthor(orchestrator: orchestrator) }
.model(orchestrator.pccLanguageModel)
.reasoningLevel(.deep)
case .reviewing:
Profile { CraftCoach() }
.model(orchestrator.systemLanguageModel)
}
}
}
关键点:
body在每次 prompt 时重新求值,状态变化会自动反映到下一次模型调用.model()指定底层模型,.temperature()控制创意程度,.reasoningLevel(.deep)开启深度推理- 不需要创建多个 Session,切换
orchestrator.mode即可改变 Agent 行为
初始化 Session 时传入 DynamicProfile:
let session = LanguageModelSession(profile: CraftProfile(orchestrator: orchestrator))
用 historyTransform 裁剪历史
(08:33)historyTransform 在每次请求前过滤历史条目,不影响 Session 的真实 transcript。
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .reviewing:
Profile { CraftCoach() }
.model(orchestrator.systemLanguageModel)
.historyTransform { history in
guard let latestResponseIndex = lastResponseEntryIndex(history) else {
return history
}
let filteredHistory = history[0..<latestResponseIndex].filter { entry in
isToolCallsOrToolOutput(entry)
}
return filteredHistory + history[latestResponseIndex...]
}
}
}
}
关键点:
historyTransform只修改发给模型的快照,Session 的完整历史仍然保留- 适合针对特定 Profile 做无损转换
- 对于端侧模型,裁剪历史能防止超出上下文窗口
用自定义 Modifier 封装复用逻辑
(09:15)把复杂的 historyTransform 封装成可复用的 Modifier。
struct DroppingToolCallsProfileModifier: LanguageModelSession.DynamicProfileModifier {
func body(content: Content) -> some DynamicProfile {
content
.historyTransform { history in
guard let latestResponseIndex = lastResponseEntryIndex(history) else {
return history
}
let filteredHistory = history[0..<latestResponseIndex].filter { entry in
isToolCallsOrToolOutput(entry)
}
return filteredHistory + history[latestResponseIndex...]
}
}
}
extension LanguageModelSession.DynamicProfile {
func droppingCompletedToolCalls() -> some DynamicProfile {
self.modifier(DroppingToolCallsProfileModifier())
}
}
关键点:
- 自定义 Modifier 遵循
DynamicProfileModifier协议 - 通过
extension给DynamicProfile添加链式调用方法 FoundationModelsUtilities包已经内置了.rollingWindow和.droppingCompletedToolCalls等常用 Modifier
使用 FoundationModelsUtilities 管理历史窗口
(09:27)
import FoundationModelsUtilities
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .reviewing:
Profile { CraftCoach() }
.rollingWindow(size: .entries(10))
.droppingCompletedToolCalls()
}
}
}
关键点:
FoundationModelsUtilities是 Apple 开源的 Swift 包,在 OS 发布周期之间更新.rollingWindow(size: .entries(10))只保留最近 10 条历史.droppingCompletedToolCalls()丢弃已完成的 Tool 调用记录- Modifier 的调用顺序会影响最终效果
用 Lifecycle Modifier 在响应边界做摘要
(10:48)onResponse 在每次模型响应后执行,适合更新状态或修改历史。
struct CraftProfile: LanguageModelSession.DynamicProfile {
@SessionProperty(\.history) var history
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .planning:
Profile { TutorialAuthor(orchestrator: orchestrator) }
.model(orchestrator.pccLanguageModel)
.reasoningLevel(.deep)
.onResponse {
if history.count > 50, let responseIndex = lastResponseIndex(history) {
history = history[responseIndex...]
}
}
}
}
}
关键点:
@SessionProperty(\.history)访问 Session 的内置历史属性- 直接修改
history会影响 Session 的所有 Profile - 适合在响应边界做有损的摘要和裁剪
声明自定义 Session 属性
(11:40)
extension SessionPropertyValues {
@SessionPropertyEntry var summary: String?
}
在 Profile 中读写:
struct CraftProfile: LanguageModelSession.DynamicProfile {
@SessionProperty(\.history) var history
@SessionProperty(\.summary) var summary
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .planning:
Profile {
TutorialAuthor(orchestrator: orchestrator)
if let summary {
Instructions { "Summary: \(summary)" }
}
}
.onResponse {
if history.count > 50, let responseIndex = lastResponse(history.prefix(40)) {
summary = try await summarize(history[0..<responseIndex])
history = history[responseIndex...]
}
}
}
}
}
关键点:
- 用
@SessionPropertyEntry声明自定义属性,所有属性必须有初始值 summary在 Profile 之间共享, dropped 的历史内容可以通过摘要保留关键信息- 任何 Profile 都能读写,修改对所有组件可见
Baton-pass 编排模式
(13:02)多个 Profile 共享完整历史,通过 Tool 切换激活状态。
struct CraftProfile: LanguageModelSession.DynamicProfile {
var orchestrator: CraftOrchestrator
var body: some DynamicProfile {
switch orchestrator.mode {
case .brainstorm:
Profile {
BrainstormInstructions()
BatonPassTool()
}
.onToolCall { orchestrator.mode = .tutorial }
.model(orchestrator.serverModel)
case .tutorial:
Profile {
TutorialInstructions()
BatonPassTool()
}
.onToolCall { orchestrator.mode = .brainstorm }
.model(orchestrator.systemModel)
}
}
}
关键点:
.onToolCall在 Tool 被调用时执行副作用,比如切换模式- 两个 Profile 都能看到完整的对话历史
- 接手的 Profile 负责给出最终答案
Phone-a-friend 编排模式
(14:14)主 Profile 派生子 Session 处理子任务。
struct CraftProfile: LanguageModelSession.DynamicProfile {
var body: some DynamicProfile {
Profile {
BrainstormInstructions()
PhoneFriendTool(
name: "generate_title",
description: "Generate a creative project title",
profile: TitleProfile()
)
}
}
}
struct PhoneFriendTool<P: LanguageModelSession.DynamicProfile>: Tool {
func call(arguments: GeneratedContent) async throws -> String {
let session = LanguageModelSession(profile: profile())
let response = try await session.respond(to: arguments)
return response.content
}
}
关键点:
- 子 Session 有独立的 transcript,与父 Session 隔离
- 子 Session 处理完后返回结果即销毁
- 父 Profile 始终负责给出最终答案
Skills 模式加载领域知识
(15:15)
struct CraftingSkills: LanguageModelSession.DynamicInstructions {
var activations: SkillActivations
var body: some DynamicInstructions {
Skills(activations: activations) {
Skill(
name: "origami_folds",
description: "Details about specific types of folds",
prompt: """
Valley Fold: Paper is folded toward you, creating a V-shaped crease
Mountain Fold: Paper is folded away from you, creating an inverted V
...
"""
)
Skill(...)
Skill(...)
}
}
}
关键点:
Skills是FoundationModelsUtilities包中的类型- 每个
Skill包含名称、描述和详细 prompt activations控制哪些 Skill 被激活,避免一次性加载过多领域知识
Tool Calling Mode 控制
(15:31)
public struct ToolCallingMode: Sendable {
public static let allowed: ToolCallingMode
public static let disallowed: ToolCallingMode
public static let required: ToolCallingMode
}
struct OrigamiExpert: LanguageModelSession.DynamicProfile {
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions("You are an origami expert")
QueryOrigamiDatabaseTool()
ShowDirectionsTool()
}
.toolCallingMode(.required)
}
}
作为生成选项传入:
let response = try await session.respond(
to: "Write out the instructions for folding a paper crane.",
options: GenerationOptions(toolCallingMode: .required)
)
关键点:
allowed:模型自行决定,最常用disallowed:禁止调用 Tool,适合用户进入不相关页面时required:模型只能调用 Tool,必须配合退出条件使用
退出 Tool 调用循环
(16:47)条件化模式切换:
struct OrigamiExpert: LanguageModelSession.DynamicProfile {
let state: OrigamiAppState
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions("Answer questions about how to fold origami")
QueryOrigamiDatabaseTool()
}
.toolCallingMode(state.queriedDatabase ? .disallowed : .required)
.onToolCall { state.queriedDatabase = true }
}
}
用抛出异常的 Tool 强制退出:
struct FinalAnswerTool: Tool {
var output: String?
@Generable struct Arguments {
var answer: String
}
func call(arguments: Arguments) async throws -> Never {
output = arguments.answer
throw CancellationError()
}
}
关键点:
- 条件化
toolCallingMode是较温和的退出方式 - 抛出
CancellationError()会立即中断循环,把控制权交还给你 - 默认情况下 Tool 报错会导致 transcript 回滚
控制 Transcript 错误处理策略
(17:28)
struct OrigamiExpert: LanguageModelSession.DynamicProfile {
let state: OrigamiAppState
var body: some LanguageModelSession.DynamicProfile {
Profile {
Instructions("Answer questions about how to fold origami")
QueryOrigamiDatabaseTool()
}
.transcriptErrorHandlingPolicy(.preserveTranscript)
}
}
let session = LanguageModelSession()
session.transcriptErrorHandlingPolicy = .preserveTranscript
关键点:
.revertTranscript(默认):Tool 报错后回滚到之前状态.preserveTranscript:保留错误现场,允许你手动修复后继续- 选择
.preserveTranscript时,你需要自己确保 transcript 处于有效状态
手动修改 Transcript
(17:51)
public final class LanguageModelSession: Sendable {
public var transcriptErrorHandlingPolicy: TranscriptErrorHandlingPolicy { get set }
public var transcript: Transcript { get set }
public var isResponding: Bool { get }
}
关键点:
transcript现在是可变的,但只能在isResponding == false时修改- 响应期间修改 transcript 是编程错误,会导致崩溃
- 配合
.preserveTranscript策略,可以在 Tool 报错后手动修复历史
核心启发
1. 做一个多阶段 AI 写作助手
做什么:一个长文写作 App,分”头脑风暴大纲 -> 逐段扩写 -> 润色校对”三个阶段,每个阶段自动切换最适合的模型配置。
为什么值得做:DynamicProfile 让三阶段共享同一 Session,大纲阶段的上下文能自然传递到扩写阶段,不需要手动拼接 Prompt。头脑风暴用高温 PCC 模型激发创意,润色用端侧模型快速响应。
怎么开始:定义一个 WritingOrchestrator 跟踪当前阶段,在 DynamicProfile 的 body 里用 switch 返回不同配置。扩写阶段用 .reasoningLevel(.deep),润色阶段用 .temperature(0.3)。
2. 做一个带领域知识库的客服 Agent
做什么:一个电商客服 App,用户咨询时自动加载对应品类的知识 Skill,咨询结束后卸载以节省上下文。
为什么值得做:Skills 模式让你把每个品类的知识封装成独立的 Skill,通过 SkillActivations 动态加载。不需要把所有知识塞进一个巨大的 System Prompt。
怎么开始:用 FoundationModelsUtilities 的 Skills 类型定义各品类知识,在 DynamicInstructions 里根据用户当前浏览的类目决定激活哪些 Skill。
3. 做一个无限长对话的 AI 伴侣
做什么:一个需要记住数月对话的 AI 伴侣 App,通过 onResponse 定期把历史摘要化,同时用 @SessionPropertyEntry 存储关键事实。
为什么值得做:SessionProperty 让跨 Profile 的状态共享变得声明式。你可以把用户提到的生日、偏好等关键信息存为属性,即使历史被裁剪也不会丢失。
怎么开始:声明 @SessionPropertyEntry var keyFacts: [String],在 onResponse 里用一个小模型提取关键事实并追加。历史超过阈值时做摘要,把摘要注入到 Instructions 中。
4. 做一个端云混合的代码助手
做什么:一个编程助手,简单补全用端侧模型秒回,复杂重构任务自动升级到云端模型,完成后切回端侧。
为什么值得做:DynamicProfile 的 baton-pass 模式让端侧和云端模型能无缝接力。简单问题不花云端 Token,复杂问题不牺牲质量。
怎么开始:定义 .quickComplete 和 .deepRefactor 两个模式,前者用 SystemLanguageModel,后者用 PCCLanguageModel + .reasoningLevel(.deep)。用一个 Tool 让用户或模型触发模式切换。
5. 做一个多 Agent 协作的研究工具
做什么:一个研究助手,包含”搜索 Agent -> 分析 Agent -> 写作 Agent”三个角色,用 phone-a-friend 模式隔离各自上下文。
为什么值得做:搜索 Agent 的上下文充满杂乱的搜索结果,不应该污染分析 Agent 的推理过程。phone-a-friend 让每个 Agent 专注自己的任务,通过 Tool 传递精炼后的信息。
怎么开始:主 Session 用 BrainstormProfile,搜索 Tool 内部创建独立的 SearchProfile Session,返回结构化结果后销毁。分析阶段再创建 AnalysisProfile Session 处理搜索结果。
关联 Session
- Session 241 - Bring your model to Apple Intelligence — 了解 PrivateCloudComputeLanguageModel 的详细能力和接入方式
- Session 243 - Debug and profile your Foundation Models features — 学习用 Xcode Instrument 检测 KV Cache 失效和性能瓶颈
- Session 298 - Evaluate your AI features — 掌握用 Evaluations 框架量化上下文工程策略的效果
- Session 324 - Integrate Core AI into your app — 了解 Core AI 框架与 Foundation Models 的集成方式
- Session 347 - Build secure agentic experiences — 学习在多 Agent 系统中设置隐私边界和安全策略
评论
GitHub Issues · utterances