WWDC Quick Look 💓 By SwiftGGTeam
Build agentic app experiences with the Foundation Models framework

Build agentic app experiences with the Foundation Models framework

观看原视频

Highlight

Apple 在 Foundation Models 框架中引入 DynamicProfile API,让同一个 LanguageModelSession 能在不同任务阶段动态切换模型、指令和工具配置,并配套开源了 FoundationModelsUtilities 工具包来管理对话历史裁剪与多 Agent 编排。

核心内容

一个折纸 App 的困境

想象你在做一个叫 Origami 的手工艺教学 App。用户上传一张照片,App 先帮用户头脑风暴创意,然后生成详细教程,最后用户在制作过程中还能上传进度照片获取技巧指导。

这三个阶段共享同一段对话上下文,但各自的需求完全不同。头脑风暴需要创造力,教程生成需要深度推理,技巧指导则需要快速响应。以前的做法是开三个独立的 LanguageModelSession,手动传递摘要,上下文频繁断裂。要么就是一个 Session 硬扛到底,Token 越积越多,模型越来越笨。

Apple 今年的解法叫 DynamicProfile

动态切换模型:像换帽子一样换 Agent

02:08DynamicProfile 让你在同一个 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:58DynamicInstructions 把相关的指令和工具打包成可复用组件,支持像 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:41DynamicProfilebody 根据外部状态返回不同的 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:33historyTransform 在每次请求前过滤历史条目,不影响 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 协议
  • 通过 extensionDynamicProfile 添加链式调用方法
  • 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:48onResponse 在每次模型响应后执行,适合更新状态或修改历史。

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(...)
        }
    }
}

关键点:

  • SkillsFoundationModelsUtilities 包中的类型
  • 每个 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 跟踪当前阶段,在 DynamicProfilebody 里用 switch 返回不同配置。扩写阶段用 .reasoningLevel(.deep),润色阶段用 .temperature(0.3)

2. 做一个带领域知识库的客服 Agent

做什么:一个电商客服 App,用户咨询时自动加载对应品类的知识 Skill,咨询结束后卸载以节省上下文。

为什么值得做Skills 模式让你把每个品类的知识封装成独立的 Skill,通过 SkillActivations 动态加载。不需要把所有知识塞进一个巨大的 System Prompt。

怎么开始:用 FoundationModelsUtilitiesSkills 类型定义各品类知识,在 DynamicInstructions 里根据用户当前浏览的类目决定激活哪些 Skill

3. 做一个无限长对话的 AI 伴侣

做什么:一个需要记住数月对话的 AI 伴侣 App,通过 onResponse 定期把历史摘要化,同时用 @SessionPropertyEntry 存储关键事实。

为什么值得做SessionProperty 让跨 Profile 的状态共享变得声明式。你可以把用户提到的生日、偏好等关键信息存为属性,即使历史被裁剪也不会丢失。

怎么开始:声明 @SessionPropertyEntry var keyFacts: [String],在 onResponse 里用一个小模型提取关键事实并追加。历史超过阈值时做摘要,把摘要注入到 Instructions 中。

4. 做一个端云混合的代码助手

做什么:一个编程助手,简单补全用端侧模型秒回,复杂重构任务自动升级到云端模型,完成后切回端侧。

为什么值得做DynamicProfilebaton-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

评论

GitHub Issues · utterances