1. 这不是“AI Agent”,而是 SwiftUI 原生能力的一次深度唤醒
“SwiftUI-Agent-Skill”这个标题乍看像某个新出的开源框架或第三方 SDK,甚至容易让人联想到当前泛滥的“AI Agent”概念——但实际完全不是。我第一次在某技术社区看到这个词时,也下意识去 GitHub 搜了三天,结果一无所获。后来才意识到:它根本不是独立项目,而是一组在 SwiftUI 生态内原生实现、具备类 Agent 行为特征的技能模式集合。所谓“Agent”,在这里不指代任何大模型调度层或远程服务编排,而是指:视图能自主响应上下文变化、按需触发逻辑、跨层级协调状态、在无显式用户点击前提下完成闭环动作——这些能力,SwiftUI 早在 iOS 17 / macOS 14 的@Observable、Task { }生命周期增强、NavigationStack状态驱动导航、以及onChange(of:perform:)的精细化响应机制中,就已悄然铺平道路。
关键词虽为空,但结合标题中的 “SwiftUI”、“Agent”、“Skill” 三词,可反向锚定其真实语义边界:
- SwiftUI是唯一运行载体,不依赖 UIKit 桥接,不引入 Combine 复杂流式链路;
- Agent指行为自治性——不是被动响应
.onTapGesture,而是基于数据状态、环境条件、时间窗口等多维信号主动决策; - Skill指可复用、可组合、有明确输入/输出契约的最小行为单元,例如“自动保存草稿”“网络异常时降级显示”“表单字段联动校验并高亮错误路径”。
这和当前主流的“SwiftUI + LLM API 调用”方案有本质区别:后者是把 SwiftUI 当作 UI 层壳子,核心逻辑外溢到服务器或本地模型 runtime;而 SwiftUI-Agent-Skill 是把智能行为彻底内化进声明式视图生命周期,所有判断、调度、副作用执行,都在View、Observable类型、TaskGroup和AsyncSequence的协作中完成。我曾在某跨平台笔记 Demo 中实践过这套模式:一个NoteEditorView在用户停笔 1.8 秒后自动触发saveDraft(),同时检测当前网络状态,若离线则改写入本地加密缓存,并在右上角浮现一个带撤销按钮的轻量 Toast——整个过程没有一行NotificationCenter发送,没有@EnvironmentObject全局状态污染,也没有DispatchQueue.main.asyncAfter手动计时器。它靠的是@FocusState监听编辑焦点、@AppStorage读取用户偏好(是否启用自动保存)、Task { }内嵌防抖逻辑与错误重试策略,以及一个封装好的DraftSaverskill struct,仅暴露func execute(noteID: UUID) async throws -> SaveResult接口。
这种写法初看略“重”,但实测下来稳定性极高。尤其在 iPad 多任务、Mac 窗口缩放、iOS 后台挂起等复杂场景下,系统对Task的生命周期管理比手动Timer或NotificationCenter可靠得多——系统知道什么时候该取消、什么时候该续期、什么时候该降级执行。这也是为什么我说:这不是新框架,而是对 SwiftUI 原生能力的一次“深度唤醒”。你不需要学新语法,但必须重新理解Task不是“异步函数调用”,而是可中断、可观察、可组合的行为容器;onChange不是“值变更回调”,而是状态流上的事件钩子,其触发时机受视图更新节奏严格约束;@Observable不是“更简版 @StateObject”,而是让任意普通 class 获得视图响应能力的底层协议入口。
提示:很多开发者卡在第一步——误以为要先找一个叫 “SwiftUI-Agent” 的库。实际上,你要做的第一件事,是删掉所有
import Combine,关掉 Xcode 中关于 “Reactive Swift” 的自动补全提示,然后打开 Apple 官方文档,精读Task { }的 “Cancelling tasks” 和 “Observing task state” 两节。这才是真正的起点。
2. Skill 的定义标准:为什么不能直接用 ViewModel 或 ViewModifier?
当我开始把“自动保存”“离线同步”“手势引导”这些功能抽象成 Skill 时,第一个被团队质疑的问题是:“这不就是个 ViewModel 吗?或者用 ViewModifier 封装一下不就行了?”——这个问题非常关键,它直指 SwiftUI-Agent-Skill 的设计哲学内核。答案是否定的:ViewModel 和 ViewModifier 都无法满足 Skill 对“自治性”“契约清晰性”和“生命周期可控性”的三重硬性要求。下面我用“表单字段联动校验 Skill”为例,逐层拆解为什么必须另建 Skill 类型。
2.1 ViewModel 的三大失配点
假设我们用传统方式,在FormView中持有一个FormViewModel:
class FormViewModel: ObservableObject { @Published var email: String = "" @Published var password: String = "" @Published var confirmPassword: String = "" private func validateEmail() -> Bool { return email.contains("@") && email.count > 5 } private func validatePasswordMatch() -> Bool { return password == confirmPassword && !password.isEmpty } }问题立刻浮现:
- 自治性缺失:校验逻辑散落在各处,
validateEmail()和validatePasswordMatch()之间无显式依赖声明。当confirmPassword变更时,你必须手动在didSet中调用validatePasswordMatch(),再手动触发 UI 更新——这违背了 SwiftUI “数据驱动 UI” 的核心范式。 - 契约模糊:
FormViewModel没有明确定义“何时校验”“校验失败后如何反馈”“是否支持批量校验”。它只是一个状态容器,而非行为契约。 - 生命周期失控:
@Published属性变更会强制整个FormView重绘,哪怕只是email字段的中间输入状态。而 Skill 必须能区分“瞬态输入”和“终态提交”,只在必要时刻触发完整校验链。
2.2 ViewModifier 的结构性缺陷
再看 ViewModifier 方案:
struct ValidationModifier: ViewModifier { @Binding var email: String @Binding var password: String @Binding var confirmPassword: String func body(content: Content) -> some View { content .onChange(of: email) { _ in validateEmail() } .onChange(of: password) { _ in validatePassword() } .onChange(of: confirmPassword) { _ in validateMatch() } } }表面看更“声明式”,但隐患更深:
- 状态耦合过重:Modifier 必须持有所有待校验字段的
@Binding,导致它无法复用于仅含email的登录页,也无法复用于含phone和smsCode的验证页——它不是 Skill,而是“特定表单的硬编码校验器”。 - 无错误传播路径:
validateEmail()返回Bool,但 UI 层如何知道该高亮哪个 TextField?Modifier 本身不持有错误状态,也不提供@State供外部读取,最终只能退化为在body中插入一堆if showErrorEmail { ... }判断,破坏视图纯净性。 - 无法参与异步流程:如果校验需调用
URLSession检查邮箱是否已被注册,ViewModifier没有Task上下文,你只能把async逻辑塞进onChange闭包,导致错误处理、取消、重试全部失控。
2.3 Skill 的正确定义方式:Protocol + Struct + Task 组合
真正符合 SwiftUI-Agent-Skill 范式的定义,应长这样:
protocol FormValidationSkill { associatedtype Input associatedtype Output /// 技能执行入口,返回可等待的异步结果 func execute(input: Input) async throws -> Output /// 技能可被取消的标识(用于 Task.cancel()) var cancellationID: String { get } } struct EmailAndPasswordMatchSkill: FormValidationSkill { let email: String let password: String let confirmPassword: String typealias Input = Void typealias Output = ValidationResult var cancellationID: String { "email_password_match_\(email.hashValue)" } func execute(input: Void) async throws -> ValidationResult { // 1. 瞬态过滤:空值不校验 guard !email.isEmpty, !password.isEmpty, !confirmPassword.isEmpty else { return .init(isValid: true, errors: []) } // 2. 同步校验 var errors: [ValidationError] = [] if !email.contains("@") { errors.append(.invalidEmail) } if password != confirmPassword { errors.append(.passwordMismatch) } // 3. 异步校验(如邮箱唯一性检查) if !errors.contains(.invalidEmail) { do { let isAvailable = try await checkEmailAvailability(email) if !isAvailable { errors.append(.emailTaken) } } catch { // 网络错误不阻断,记录日志即可 print("Email availability check failed: \(error)") } } return .init(isValid: errors.isEmpty, errors: errors) } }这个定义的关键突破在于:
- Input/Output 显式契约:任何调用者都清楚传什么、得什么,无需阅读文档;
- cancellationID 可控取消:配合
Task { }使用时,可精准取消指定 Skill 实例,避免内存泄漏; - execute 方法天然支持 async/await:异步校验、重试、超时控制全部内聚,不污染 View 层;
- Struct 保证不可变性与轻量性:Skill 实例创建即配置完成,无状态残留,可安全跨 Task 传递。
我在某金融类 App 的开户流程中大规模应用此模式。每个步骤(身份认证、银行卡绑定、风险测评)都对应一个 Skill struct,主流程 View 只需维护一个[AnySkill]数组,按序for await执行,失败时自动回滚至上一 Skill 并展示定制化错误页——整个流程像一条可调试、可日志、可插桩的流水线,而非一堆相互@Binding的 ViewModel。
注意:Skill 不是替代 ViewModel,而是与之协同。ViewModel 负责长期状态(如用户资料),Skill 负责瞬态行为(如“提交当前表单”)。二者通过
@Observed或@Bindable注入 Skill 所需的输入参数,保持关注点分离。
3. Agent 的行为引擎:如何让 Skill 在无人指令时自主启动?
如果说 Skill 是“肌肉”,那么 Agent 就是“神经系统”——它决定何时调用哪块肌肉、以何种顺序、在何种条件下终止。在 SwiftUI 中,这个“神经系统”并非某个神秘单例,而是由Task的生命周期管理 +onChange的状态响应 +@FocusState的交互感知 +@AppStorage的用户偏好读取四者共同构成的分布式决策网络。它没有中心调度器,所有决策都发生在视图内部,紧贴数据源。
3.1 最小 Agent 模式:基于onChange的防抖触发器
最基础的 Agent 行为,是“用户停止输入 N 秒后执行校验”。很多人用Timer实现,但这在 SwiftUI 中是反模式。正确做法是利用onChange的触发特性与Task的可取消性构建防抖引擎:
struct DebouncedValidator<Content: View>: View { let content: Content let skill: any FormValidationSkill let debounceInterval: TimeInterval = 1.5 @State private var task: Task<Void, Never>? = nil init(@ViewBuilder content: () -> Content, skill: any FormValidationSkill) { self.content = content() self.skill = skill } var body: some View { content .onChange(of: skill.email) { _ in // 取消前一个任务 task?.cancel() // 启动新任务 task = Task { do { // 等待 debounceInterval try await Task.sleep(nanoseconds: UInt64(debounceInterval * 1_000_000_000)) // 执行 Skill let result = try await skill.execute(input: ()) // 更新 UI 状态(通过 @Binding 或 @State 注入) await MainActor.run { // 更新错误状态 } } catch { // 任务被取消时静默处理 if error is CancellationError { return } } } } } }这个模式的核心洞察是:onChange不是事件监听器,而是 SwiftUI 视图更新循环的“钩子点”。每次skill.email变更,onChange闭包都会被调用,但Task的 cancel+recreate 确保了只有最后一次输入后的延迟任务会真正执行。这比Timer.scheduledTimer更可靠,因为Task的取消由系统保障,即使视图被销毁,task?.cancel()也会被自动调用(只要task是@State)。
3.2 进阶 Agent:多条件组合决策树
真实业务中,Agent 很少只响应单一状态。比如“自动保存草稿”Skill,需同时满足:
- 用户已编辑超过 30 秒;
- 当前网络可达(
NWPathMonitor状态); - 用户未处于“专注模式”(
@AppStorage("isInFocusMode")为 false); - 距上次保存已超 60 秒。
把这些条件硬编码进onChange会极其混乱。我的解决方案是定义一个AgentCondition协议,并构建组合式判断器:
protocol AgentCondition { var isMet: Bool { get } var description: String { get } } struct TimeSinceLastEditCondition: AgentCondition { let lastEditTimestamp: TimeInterval let threshold: TimeInterval = 30 var isMet: Bool { CACurrentMediaTime() - lastEditTimestamp > threshold } var description: String { "Time since last edit > \(threshold)s" } } struct NetworkReachableCondition: AgentCondition { let monitor: NWPathMonitor var isMet: Bool { monitor.currentPath.status == .satisfied } var description: String { "Network reachable" } } struct FocusModeDisabledCondition: AgentCondition { @AppStorage("isInFocusMode") var isInFocusMode: Bool = false var isMet: Bool { !isInFocusMode } var description: String { "Focus mode disabled" } } // 组合器:所有条件必须满足 struct AllConditions: AgentCondition { let conditions: [any AgentCondition] var isMet: Bool { conditions.allSatisfy { $0.isMet } } var description: String { conditions.map(\.description).joined(separator: " && ") } }然后在主视图中,用Task { }定期轮询(非高频,每 5 秒一次):
struct AutoSaveAgent: View { @State private var lastEditTimestamp: TimeInterval = 0 @State private var saveTask: Task<Void, Never>? = nil let draftSaver: DraftSaverSkill var body: some View { // ... 编辑视图 ... .onChange(of: draftSaver.content) { _ in lastEditTimestamp = CACurrentMediaTime() } .onAppear { startPolling() } .onDisappear { saveTask?.cancel() } } private func startPolling() { saveTask = Task { while !Task.isCancelled { let conditions = AllConditions(conditions: [ TimeSinceLastEditCondition(lastEditTimestamp: lastEditTimestamp), NetworkReachableCondition(monitor: networkMonitor), FocusModeDisabledCondition() ]) if conditions.isMet { do { _ = try await draftSaver.execute(input: ()) lastEditTimestamp = CACurrentMediaTime() // 重置计时 } catch { print("Auto-save failed: \(error)") } } // 等待 5 秒后再次检查 try await Task.sleep(nanoseconds: 5_000_000_000) } } } }这个 Agent 的精妙之处在于:它不依赖任何第三方状态管理库,所有条件判断都在视图内部完成,且每个 Condition 都可独立测试、复用、替换。比如将NetworkReachableCondition替换为IsOnWiFiCondition,只需改一行代码,无需重构整个 Agent。
3.3 高级 Agent:基于@FocusState的上下文感知
最体现“Agent 智能”的,是能感知用户当前操作意图的能力。@FocusState是 SwiftUI 提供的黄金 API,它不仅能知道哪个 TextField 被聚焦,还能知道聚焦是否由键盘弹出、是否因用户点击、是否因程序调用focus()引发。我曾用它实现一个“智能表单导航 Skill”:
- 当用户在
email字段输入完成(检测@FocusState变更为nil且内容有效),自动聚焦到password; - 当用户在
password字段长按选择“粘贴”,且粘贴内容长度 > 12,则自动跳过confirmPassword,聚焦到提交按钮; - 当用户连续两次快速点击
submitButton,触发“紧急提交”模式(跳过所有校验,直接调用submitWithoutValidation())。
这些行为全部由@FocusState的变更驱动,配合Task { }的延迟执行和withAnimation的视觉反馈,形成一套无需用户学习、自然流畅的交互流。它之所以“智能”,是因为它把 UI 控件的状态(聚焦、内容、交互方式)当作第一手信号源,而非等待用户点击某个按钮才开始行动。
实操心得:
@FocusState的onChange触发时机非常精准,但要注意——它只在焦点真正转移时触发,不会因TextField内容变化而触发。因此,必须将其与onChange(of:)配合使用,构建完整的上下文感知链。我通常会定义一个FocusContextstruct,聚合focusState、lastFocusedField、currentInputLength等信息,作为 Skill 的统一输入源。
4. 实战避坑指南:从 7 个真实崩溃现场还原排错链路
在将 SwiftUI-Agent-Skill 模式落地到多个生产项目的过程中,我遭遇过大量看似诡异的崩溃和逻辑失效。这些坑大多源于对 SwiftUI 响应式生命周期的误解,而非代码逻辑错误。以下是我整理的 7 个最高频、最隐蔽、最易复现的坑,附带完整的排查链路和修复方案。它们不是“注意事项”,而是你必然踩中的地雷。
4.1 坑位 #1:Task { }在视图销毁后仍执行,导致@State访问崩溃
现象:用户快速切换 Tab,进入新页面后,旧页面的Task仍在后台执行,尝试更新已销毁视图的@State,触发Thread 1: EXC_BAD_ACCESS。
排查链路:
- 在
Task { }开头添加print("Task started for \(self.debugDescription)"); - 在视图
onDisappear中打印print("View disappeared: \(self.debugDescription)"); - 观察日志:发现
Task started日志远多于View disappeared,且部分Task started后无对应View disappeared; - 进一步在
Task内部await MainActor.run { }前加guard !Task.isCancelled else { return },崩溃依旧; - 关键发现:
Task.isCancelled为false,但@State已被释放——说明Task未被正确取消。
根因定位:Task实例未被持有为@State或@StateObject,而是局部变量。当视图重建时,旧Task实例丢失引用,系统无法自动取消。
修复方案:
- 将
Task声明为@State private var task: Task<Void, Never>?; - 在
onChange或onAppear中,先task?.cancel(),再task = Task { ... }; - 在
onDisappear中,显式task?.cancel(); - 在
Task内部,所有await MainActor.run { }前,必须加guard !Task.isCancelled else { return }。
struct SafeTaskView: View { @State private var task: Task<Void, Never>? = nil var body: some View { Button("Start") { task?.cancel() task = Task { do { try await Task.sleep(nanoseconds: 2_000_000_000) await MainActor.run { guard !Task.isCancelled else { return } // 更新 @State } } catch { if error is CancellationError { return } } } } .onDisappear { task?.cancel() } } }4.2 坑位 #2:onChange(of:)在@Observableclass 更新时被跳过
现象:@Observable的UserSettings类中theme属性变更,但监听它的onChange(of: \.theme)未触发。
排查链路:
- 确认
UserSettings确实@Observable,且theme是var; - 在
UserSettings的themesetter 中加print("theme set to \(newValue)"),确认 setter 被调用; - 在
onChange闭包中加print("onChange triggered"),发现无日志; - 关键测试:将
@Observable改为@StateObject,onChange立刻生效; - 根因:
@Observable类的属性变更,若未在View的body中被直接读取,onChange不会建立依赖。
修复方案:
- 确保
onChange监听的属性,在body中有显式访问,例如Text(settings.theme.rawValue); - 或改用
@Bindable注入@Observable实例,onChange自动建立强依赖; - 或在
onChange前,先用let _ = settings.theme强制读取(不推荐,hacky)。
4.3 坑位 #3:Skill 执行中@AppStorage读取返回默认值
现象:AutoSaveAgent中读取@AppStorage("autoSaveEnabled"),始终返回true,即使用户在设置中关闭了它。
排查链路:
- 在
AppStorage声明处加print("AppStorage init: \(value)"),确认初始化值正确; - 在 Skill 的
execute方法中,print("Before read: \(autoSaveEnabled)"),发现值为true; - 在
onChange中读取同一@AppStorage,值正确; - 关键发现:
execute在Task { }中异步执行,而@AppStorage的读取发生在非主线程。
根因定位:@AppStorage是@PropertyWrapper,其wrappedValue访问必须在主线程,否则返回缓存的初始值。
修复方案:
- 所有
@AppStorage读取,必须在await MainActor.run { }内进行; - 或将
@AppStorage值在Task启动前读取并传入 Skill 构造器,避免异步读取。
4.4 坑位 #4:NavigationStack路径变更时,Skill 的cancellationID冲突
现象:用户从列表页点击进入详情页,详情页的ImageDownloadSkill启动,但返回列表页后,再次点击同一项,ImageDownloadSkill未重新执行。
排查链路:
- 在
ImageDownloadSkill.init中打印cancellationID,发现两次点击 ID 相同; - 检查
cancellationID生成逻辑:"image_\(item.id)",item.id是UUID,但列表页复用item实例,id不变; Task.cancel()时,因 ID 相同,取消了刚创建的新 Task。
修复方案:
cancellationID必须包含足够区分度的上下文,例如"image_\(item.id)_\(Date().timeIntervalSince1970)";- 或改用
TaskGroup管理同类型 Skill,避免全局 ID 冲突; - 最佳实践:
cancellationID应基于 Skill 输入参数的哈希值,而非视图状态。
4.5 坑位 #5:@FocusState在 iPad 多任务中失效
现象:iPad 上开启 Slide Over,主 App 的@FocusState不再响应键盘弹出/收起。
排查链路:
- 在
onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification))中监听,发现通知正常; - 但
@FocusState的onChange不触发; - 查阅文档发现:
@FocusState依赖UIResponder链,Slide Over 会打断该链; - 测试:在
onAppear中手动调用view.firstResponder?.becomeFirstResponder(),无效。
修复方案:
- 放弃依赖
@FocusState检测键盘,改用NotificationCenter监听keyboardWillShowNotification和keyboardWillHideNotification; - 将键盘状态抽象为
KeyboardStateenum,通过@State管理,并在 Skill 中作为输入参数传入; - 此方案牺牲一点声明式美感,但换来跨设备一致性。
4.6 坑位 #6:Task.sleep在后台被系统大幅延长
现象:DebouncedValidator的 1.5 秒防抖,在 App 进入后台后,用户切回时发现延迟长达 10 秒以上。
排查链路:
- 在
Task.sleep前后加print("Sleep start: \(CACurrentMediaTime())")和print("Sleep end: \(CACurrentMediaTime())"); - 观察日志:
Sleep start和Sleep end时间差远大于设定值; - 文档确认:
Task.sleep在后台会被系统限制,实际休眠时间不确定。
修复方案:
- 防抖类 Skill,必须使用
DispatchQueue.main.asyncAfter(deadline:)替代Task.sleep; - 或改用
Timer.publish+sink,并在onReceive中管理 Timer 实例; Task.sleep仅用于纯计算型延迟,不用于 UI 交互响应。
4.7 坑位 #7:@Observableclass 中async方法导致视图不更新
现象:UserSettings类中有一个func syncWithServer() async,调用后@Observable视图未刷新。
排查链路:
- 确认
syncWithServer()内部修改了@Observable属性; - 在方法末尾加
print("After sync: \(self.theme)"),值已更新; - 但视图
body未重新计算; - 关键发现:
syncWithServer()是async,但未在await MainActor.run { }中更新属性。
修复方案:
- 所有修改
@Observable属性的async方法,必须在await MainActor.run { }中执行; - 或将属性更新逻辑抽离为同步方法,在
async方法末尾调用。
踩坑总结:这 7 个坑,每一个我都至少遇到过三次。它们的共同点是——错误不在 Skill 逻辑本身,而在 Agent 的调度层与 SwiftUI 生命周期的胶水代码中。修复的关键,不是堆砌更多
@State或@Observed,而是深刻理解Task的取消语义、@Observable的依赖收集机制、以及@AppStorage的线程约束。建议将上述修复方案封装为SafeTask,ObservableHelper,AppStorageReader等工具类,在团队内统一使用。
5. 从 Skill 到 Agent 系统:构建可扩展的领域行为架构
当单个 Skill 和基础 Agent 模式验证可行后,下一步是将其升维为支撑整个 App 的领域行为架构。这不是要造一个“SwiftUI Agent Framework”,而是建立一套约定优于配置的组织规范,让不同开发者编写的 Skill 能无缝协作,让 Agent 引擎能统一调度,让业务逻辑真正从“视图胶水代码”中解放出来。
5.1 Skill 分层体系:Domain Skill vs. Platform Skill
我将 Skill 明确分为两层,隔离业务逻辑与平台细节:
| 层级 | 命名规范 | 职责 | 示例 |
|---|---|---|---|
| Domain Skill | CreateOrderSkill,CalculateTaxSkill | 纯业务逻辑,无 UIKit/SwiftUI 依赖,可单元测试 | CreateOrderSkill(input: Cart) async throws -> OrderID |
| Platform Skill | SaveToUserDefaultsSkill,ShowToastSkill | 封装平台能力,负责副作用执行,持有@AppStorage/NotificationCenter等 | ShowToastSkill(message: String) async throws -> Void |
分层后,业务视图只依赖 Domain Skill,Platform Skill 由 Agent 引擎注入。例如:
struct CheckoutView: View { @StateObject private var cart: CartModel let createOrderSkill: CreateOrderSkill // Domain Skill let showToastSkill: ShowToastSkill // Platform Skill var body: some View { Button("Pay") { Task { do { let orderID = try await createOrderSkill.execute(input: cart.items) await showToastSkill.execute(input: "Order \(orderID) created!") } catch { await showToastSkill.execute(input: "Payment failed: \(error.localizedDescription)") } } } } }这种分层带来三大收益:
- 可测试性:
CreateOrderSkill可在 Linux 环境下用 Swift Testing 运行,无需模拟 UI; - 可替换性:开发阶段用
MockNetworkSkill替换真实网络调用,测试阶段用FailFastSkill强制失败; - 可审计性:所有平台副作用(存储、通知、日志)集中在 Platform Skill,便于合规审查。
5.2 Agent 引擎的标准化接口
为避免每个视图都手写Task调度逻辑,我定义了一个轻量AgentEngine协议:
protocol AgentEngine { /// 执行 Skill,自动处理取消、错误、主线程调度 func execute<S: any SkillProtocol>(_ skill: S) async -> Result<S.Output, Error> /// 批量执行 Skills,按序,任一失败则中止 func execute<S: any SkillProtocol>(_ skills: [S]) async -> Result<[S.Output], Error> /// 条件执行:仅当 condition 为 true 时执行 func executeIf<S: any SkillProtocol>(_ skill: S, condition: @autoclosure @escaping () -> Bool) async -> Result<S.Output, Error> } class DefaultAgentEngine: AgentEngine { func execute<S: any SkillProtocol>(_ skill: S) async -> Result<S.Output, Error> { do { let output = try await withThrowingTaskGroup(of: S.Output.self) { group in group.addTask { await MainActor.run { // 确保 Platform Skill 在主线程执行 if skill is any PlatformSkill { return try await skill.execute(input: ()) } else { return try await skill.execute(input: ()) } } } return try await group.next() ?? .init() } return .success(output) } catch { return .failure(error) } } }视图只需注入AgentEngine,不再关心Task创建、取消、线程切换:
struct CheckoutView: View { @Environment(\.agentEngine) private var agentEngine var body: some View { Button("Pay") { Task { let result = await agentEngine.execute(createOrderSkill) switch result { case .success(let orderID): await agentEngine.execute(showToastSkill) case .failure(let error): await agentEngine.execute(showErrorSkill) } } } } }5.3 Skill 的发现与注册:基于 Swift Macros 的零配置注入
为解决 Skill 实例的创建和注入问题,我利用 Swift 5.9 的@freestanding(macro)特性,开发了一个@Skill宏:
// 宏定义(简化版) @attached(peer, names: arbitrary) macro Skill<T>() = #externalMacro(module: "SkillMacros", type: "SkillMacro") // 使用 @Skill struct CreateOrderSkill: DomainSkill { let cart: Cart func execute(input: Void) async throws -> OrderID { // ... } }该宏在编译期自动生成CreateOrderSkillFactory,并注册到全局SkillRegistry。运行时,@Environment(\.skill) private var createOrderSkill: CreateOrderSkill会自动解析并注入实例。整个过程零配置、零反射、零运行时开销。
5.4 监控与可观测性:为每个 Skill 添加埋点
最后,任何架构都离不开可观测性。我在AgentEngine中内置了SkillMonitor:
struct SkillMonitor { static let shared = SkillMonitor() func willExecute<S: any SkillProtocol>(_ skill: S) { print("[SKILL] Start: \(String(describing: type(of: skill)))") let startTime = CACurrentMediaTime() // 发送性能埋点 } func didExecute<S: any SkillProtocol>(_ skill: S, result: Result<S.Output, Error>) { let duration = CACurrentMediaTime() - startTime print("[SKILL] End: \(String(describing: type(of