这两年我一直在折腾一个方向:把 AI Agent 塞进 SwiftUI 应用里,让它不只是当个聊天机器人,而是真正能用"技能"去操作 App 里的功能。这个组合我管它叫 SwiftUI-Agent-Skill,也就是用 SwiftUI 做交互外壳,用 Agent 做决策大脑,再用一套 Skill 技能体系让 Agent 能安全地调用应用能力。很多人问我,直接嵌一个 SDK 不就行了吗?真不是。聊天框式的嵌入和你自己设计的技能化 Agent 是两码事,后者复杂得多,也实用得多。这篇文章我把整个设计思路、核心实现、踩过的坑一次写透,给正在做类似项目的朋友一个可参考的落地路径。
先说一下这套方案能解决什么问题、适合谁看。如果你正在做的产品需要让用户用自然语言触发某个操作——比如"帮我把这笔 88 块的餐费记到餐饮分类""提醒我明天上午九点给客户回电话"——那单纯的文本生成往往满足不了,因为你要的是让 AI 驱动 App 去执行动作,而不只是输出一段文字。这篇文章适合两类人:一类是已经在 SwiftUI 里做过基础功能、想接入 Agent 的开发者;另一类是做过服务端 Agent 但完全没碰过 iOS 端、不清楚端侧技能该怎么设计的开发者。无论哪类,我希望你看完后能直接用这套思路搭出自己的第一个技能化 Agent。
1. 标题拆解:SwiftUI、Agent、Skill 三者到底在配合什么
这个标题不是三个词拼在一起炫技,它其实定义了一套完整的分层架构。我先逐个拆开说,再讲它们是怎么咬合在一起的。
1.1 SwiftUI 侧:Agent 交互界面为什么必须靠状态驱动
做过 iOS 开发的朋友都知道,SwiftUI 和 UIKit 最大的差别在于:前者是状态驱动、声明式描述,后者是事件驱动、命令式操作。这个差别在做 Agent 交互时会被无限放大。因为 Agent 的执行过程天然是异步的、多阶段的,而且每个阶段用户都需要看到反馈——技能解析中、参数确认、执行中、执行成功、执行失败。
如果用 UIKit 硬写,你会有一堆 delegate、block、通知中心来回穿插,界面的每个分支状态都要手动维护,很容易漏掉某个异常路径。而 SwiftUI 里你只要定义好一个状态模型,视图会根据状态自动切换,这个特性和 Agent 的技能执行生命周期是完美对应的。你可以把 Agent 的每次技能调用看成一组状态机:空闲 -> 解析参数 -> 用户确认 -> 执行 -> 完成/失败。SwiftUI 的@ObservableObject或者 Swift 5.9 之后的@Observable就非常适合承载这套状态。
另一个 SwiftUI 留下的重要优势是跨平台复用。同一套技能注册逻辑和视图状态管理,可以同时跑在 iOS、iPadOS、macOS 甚至 watchOS 上。我在 mac 端和 iOS 端跑同一套核心代码,区别只是交互细节不同,这让维护成本下降很多。
1.2 Agent 侧:技能(Skill)和提示词的本质区别
很多刚接触这个概念的朋友会问:技能不就是写进 system prompt 的一段描述吗?区别非常大。system prompt 里的文字描述只是告诉模型"你可以做什么",而技能是一套可执行的、有边界的、带参数契约和返回结构的真实代码。它包含三层信息:
第一层是描述层,给 LLM 看。内容包括技能名称、功能说明、何时触发、参数说明。这一层决定了模型能不能在正确的场景下选中这个技能。
第二层是协议层,给开发者看。每个技能声明自己需要哪些输入参数,参数类型是什么,哪些必填哪些选填,返回什么结构。这一层决定了技能调用过程中不会传错参、不会崩。
第三层是执行层,给系统看。对应一个具体的 Swift 函数或闭包,完成实际的动作,比如写入数据库、调用系统能力、请求网络接口。
把这三层拆开之后你会发现,技能其实是一个工程产物,而不是一段提示词。它要有版本、要有测试、要有错误处理。我在实际项目里把这套三层结构封装成了一个协议,后面会给出具体代码。
1.3 三者咬合:一次技能调用的完整闭环
把三个概念串起来,一次完整的闭环长这样:用户在 SwiftUI 界面输入一句话 -> Agent 核心拿到这句话和技能注册表 -> LLM 决定调用哪个技能、提取出参数 -> 回到 SwiftUI 层,以表单形式让用户确认参数 -> 确认后执行技能 -> 执行结果返回给 LLM -> LLM 根据结果生成自然语言反馈 -> SwiftUI 展示反馈。这个闭环里有决策、有人机协同确认、有执行、有反馈,每个环节都有明确的属主。
这也是我为什么坚持不把 Agent 纯粹放在服务端的原因。虽然模型推理放在服务端更合理,但技能执行往往必须在端侧,因为它要操作本地数据和应用功能。如果全程走服务端,要么把技能也暴露成远程接口,要么就得一次次把数据传到服务端再传回来,不仅慢,而且有隐私焦虑。SwiftUI 端侧承接技能执行恰恰解决了这个问题,这是很多团队忽略的架构红利。
2. 技能系统先于界面:请求、技能、执行三角关系如何设计
我建议任何人在开始画 SwiftUI 界面之前,先花半天时间把技能系统设计好。因为界面是状态的投影,而状态来源是技能系统。技能系统设计错了,界面再漂亮也是空中楼阁。
2.1 技能注册表:让 Agent 知道你的应用能做什么
技能注册表是 Agent 和应用之间的桥梁。它本质上是一份清单,把当前应用所有可用的技能以及每个技能的完整描述提供给 LLM。每次请求发出时,注册表会被转换成模型可读的 JSON Schema,拼入请求消息中。
实际项目中我的注册表长这样:
struct SkillRegistry { var skills: [Skill] func schemaPrompt() -> String { skills.map { $0.jsonSchema() }.joined(separator: "\n") } } protocol Skill { var name: String { get } var description: String { get } var parameters: [SkillParameter] { get } func execute(arguments: [String: Any]) async throws -> SkillResult } struct SkillParameter { let name: String let type: SkillParameterType let required: Bool let description: String }这段代码里最值得关注的是execute(arguments:options:)这个函数。技能一旦被模型选中,最终一定会走到这个函数里。它接收的是经过解析的参数字典,返回的是统一的SkillResult。这个结构有两个关键点:参数必须强校验,返回结果必须结构化。
参数校验我在 2.3 展开,先强调一下结构化返回的意义。LLM 拿到技能执行结果后需要理解它才能生成自然语言反馈,如果返回的是非结构的杂乱文本,模型很容易理解偏差。所以我要求SkillResult至少包含两个字段:原始数据和给 LLM 看的摘要。摘要可以是纯文本但必须简洁,原始数据则留给界面层使用。
一个容易被忽略的任务是技能注册表的体积控制。如果你往系统里塞了三十个技能,每个技能描述都写一百多个字,那 prompt 会被撑得很长,既费 token 又稀释注意力。我的经验是,技能注册表需要做分级管理:默认只注入高频技能描述,低频技能可以靠 Agent 在需要时动态查询。SwiftUI 侧可以做一个技能中心页面,用户主动启用某个低频技能时才把它注入这些后续请求。
2.2 调度状态机:一次执行必须经过哪些阶段
技能执行不能是裸奔的,每个状态都要有明确的定义和转移条件。我在项目里定义的状态如下:
- idle:初始状态,没有进行中的技能调用
- parsing:正在把用户语句里的意图和参数提取出来
- waitingForConfirmation:参数已经提取完成,等待用户确认或修改
- executing:已确认,正在执行具体操作
- succeeded:执行成功,等待生成反馈
- failed:执行失败,需要向用户解释原因并给出补救建议
这七个状态覆盖了绝大多数技能调用场景。特别要强调waitingForConfirmation这个状态。有些开发者为了追求无感体验,让 Agent 拿到参数后直接执行,省掉确认步骤。个人建议不要这样,尤其在涉及资金操作、数据删除、发送消息这类不可逆场景时,确认机制是必要的安全兜底。SwiftUI 里实现确认界面也太简单了:把提取出的参数渲染成一个可编辑表单,用户点击确认后状态转移。
状态的转移最好集中在一个协调器里管理,而不是散落在各个视图中。我写了个AgentActionCoordinator,它持有当前状态,暴露方法如submit(userInput:)、confirmExecution()、cancelExecution()。这样写的好处是测试容易,还能在调试时观察每次状态变化的原因。状态机里的每个状态都要能处理"用户中途离开"的情况,比如 App 切到后台再回来,执行中的技能可能已经结束了,这时界面应该能恢复到新状态而不是自顾自转菊花。SwiftUI 的生命周期方法配合状态机的异步恢复可以解决这个问题,这一步我放在第 4 部分的 Demo 里演示。
2.3 参数契约与类型安全:让 LLM 的随机输出变成可控输入
LLM 再聪明,它返回的参数也可能出错。参数类型不规范、字段缺失、枚举值写错,这些都是我在真实项目里踩过的坑。参数契约层要担起责任,我之前试过手写 JSON 解析,代码又碎又难维护。后来换成 Codable 协议,效果好了不少。
拿 CD 播放器的技能举例子,我定义一个播放参数:
struct PlaybackArguments: Codable { let trackName: String let playlistName: String? let mode: PlaybackMode enum PlaybackMode: String, Codable { case single, loop, shuffle } }关键是解析失败时的处理路径。不要默默吞掉错误,也不要直接弹红屏。我常用的做法是:解析失败就把失败原因返回给 LLM,让它根据错误提示重新生成参数。比如"模式只支持 single/loop/shuffle 三个值,你给的 random 不在其中,请重新选择"。这相当于把参数校验变成了可反馈的对话环节,而不是冷冰冰的报错。实际体验中,模型第二次调用技能时几乎都能修正参数。这种"重试教育机制"很值得在协议层内置支持,不仅提升了宽容度,也减少了一线用户遇到错误时的困惑。
3. SwiftUI 侧的核心实现:状态承载、任务取消与界面联动
这一章是纯 SwiftUI 技术点,但我会结合 Agent 场景来讲,因为普通的网络请求切换和技能调用还是有些不同——技能调用往往更持久、更多阶段、还涉及用户确认插入。
3.1 状态中心:用 Observable 承载技能执行生命周期
SwiftUI 里的状态管理选择不少,ObservableObject、@Observable、@State、@Binding、环境对象,每个都有适合的场景。对于技能执行生命周期这种需要被多个视图共享且包含复杂逻辑的对象,我的推荐是 Swift 5.9 以后的@Observable宏配合@Environment注入。
@Observable的优点是不需要维护@Published属性包装,任何被观察的属性变化都能自动触发视图刷新。在处理我上面提到的状态机时,代码量能少三分之一。核心的状态类我大概长这样:
import Observation import SwiftUI @Observable final class AgentExecutionStore { var currentState: SkillExecutionState = .idle var currentSkillName: String? var currentArguments: [String: Any]? var errorMessage: String? var resultSummary: String? }这里值得注意的是:视图像观察一个普通数据模型一样观察 Agent 的执行状态,当状态从.executing变成.succeeded时,所有关注这个属性的视图都会自动刷新。这比手动发送通知或者用回调层层转手干净太多。还有一个好处是调试体验的提升。你可以开 SwiftUI 的 View Hierarchy 检查器,直接在同一帧看到状态值和视图分支,定位问题快很多。
状态中心不要搞成单例。有人为了方便,把这种 store 写成全局单例,短期看省事,但一旦 App 里有两个独立面板需要各自跑技能,单例就会互相干扰。我习惯在视图层级上用@Environment往下传给每个需要的地方。这样做还有一个附带好处:你可以非常轻松地创建多个互不干扰的 Agent 会话,比如一个负责主界面,一个负责今日建议小组件。
3.2 异步调用:Task、MainActor 与取消信号的正确配合
技能执行百分百是异步的。Swift 的 async/await 和 Task 是这里的主干,但细节决定体验。我见过太多人把所有异步代码一股脑丢到主线程里跑,或者完全忘掉 UI 更新的线程问题,导致卡顿或崩溃。
正确的分工是:调用 Agent API、解析 JSON、执行业务逻辑,这些放在后台执行;只有真正需要更新 SwiftUI 状态的代码才回主线程。现在 iOS 17 时代,很多场景可以用@MainActor修饰 store 的方法,让 UI 状态更新自动落在主线程。另一种做法是用.task修饰符在视图上启动异步工作,生命周期会自动绑定视图出现和消失,省去手动管理的麻烦。
.task { guard let skill = viewModel.selectedSkill else { return } let result = try await skill.execute(arguments: viewModel.skillArguments) await MainActor.run { viewModel.result = result viewModel.currentState = .succeeded } }如果你有网络请求,记得处理取消。URLSession的 async 版本会自动响应 Task 取消信号,你只需要在 catch 里判断CancellationError。用户可能在执行到一半时点了"取消"或者直接关闭了页面,此时界面不应该再弹出成功提示,更不能还去更新已被销毁的视图。NavigationStack的 pop 动作或者dismiss动作背后,SwiftUI 会自动给当前 task 发取消信号,但如果你用的是自己创建的结构体 Task,还得手动处理。我建议坚持用.task这个修饰符为主路径来绑定生命周期,省心不少。
3.3 用户确认表单:技能参数的最后一道人机护栏
SwiftUI 做确认表单可以用Form和@Bindable很轻松地组合出来。当 Agent 从用户语句中提取参数后,我们把这些参数填充到表单的可编辑字段里,用户可以直接修改再点确认。这一步的价值除了防呆之外,还有一个好处:它可以作为交互层面上的全责审计点,让用户清楚接下来要执行什么,避免"我根本没让他做这个"的纠纷。
我在表单里常做两个增强。第一,必填参数标星并在用户未填写时禁用确认按钮;第二,对高影响技能加一个红字风险提示。比如删除类技能我会在确认表单里用红色字体展示"此操作将永久删除 12 条记录,无法撤销",让用户在物理上也感知到风险。这个设计后来在真实试用中反馈非常好——用户不再随手点确定了,失误率显著下降。确认表单的样式上可以灵活选择sheet或inspector,看 App 的实际情况,重点不是形式,而是确保这个确认动作存在于关键技能的路径中。
4. 一个能跑通的最小案例:技能面板 Demo 的完整链路
讲完设计,来做一个能落地运行的最小 Demo。这个 Demo 模拟的场景是:iOS 应用里有一个技能面板,用户可以在面板上选择技能并触发执行。为了聚焦,我让 Demo 只包含两个技能:一个是天气查询(需要网络请求外部接口),一个是本地存储的备忘录添加(直接写本地数据库)。这两个技能分别代表远程型技能和本地型技能,能让架构得到比较充分的展示。
4.1 定义两个真实的技能:远程天气与本地备忘
先定义协议和注册表。这部分对应我前面讲的协议层。
protocol Skill { var name: String { get } var description: String { get } var parameterDescriptions: String { get } func run(arguments: [String: String]) async throws -> String } struct WeatherSkill: Skill { let name = "查询天气" let description = "根据城市名查询当天的天气情况。适合用户在问明日天气、要不要带伞等场景。" let parameterDescriptions = "输入参数包括:city(城市名,必填)" func run(arguments: [String: String]) async throws -> String { guard let city = arguments["city"], !city.isEmpty else { throw SkillError.missingArgument("city") } let weather = try await WeatherService.query(city: city) return "\(city) 当前天气:\(weather.condition),气温 \(weather.temperature)°C" } } struct AddMemoSkill: Skill { let name = "添加备忘录" let description = "向本地备忘录新增一条文本记录,适合帮用户记下待办事项。" let parameterDescriptions = "输入参数包括:content(备忘内容,必填)、time(提醒时间,选填)" func run(arguments: [String: String]) async throws -> String { guard let content = arguments["content"], !content.isEmpty else { throw SkillError.missingArgument("content") } let memo = Memo(content: content, remindAt: arguments["time"]) try MemoStore.shared.insert(memo) return "已添加备忘录:\(content)" } }WeatherService.query和MemoStore.shared.insert是模拟的底层服务,实际项目中它们会对接网络客户端和数据库。这里有一点提醒:技能函数要尽量做到幂等。同一个请求触发两次,不应该产生两条备忘或者重复扣款之类的问题。备忘录添加最好生成一个请求 ID 做去重,虽然 Demo 没体现,但真实项目建议加这一层防护。
我再补一个技能注册表。它的作用就是把所有技能集合起来,供 Agent 和大模型使用。
struct SkillRegistry { var skills: [Skill] = [WeatherSkill(), AddMemoSkill()] func descriptionsForPrompt() -> String { skills.map { "- \($0.name): \($0.description)。参数:\($0.parameterDescriptions)" }.joined(separator: "\n") } func findSkill(named name: String) -> Skill? { skills.first { $0.name == name } } }4.2 视图层:画面随执行阶段自动切换的完整代码
视图层我用一个SkillPanelView展示面板,里面包含技能列表、参数输入区域和执行结果区域。最核心的处理逻辑放在SkillPanelViewModel里,它负责生成 Agent 调用提示词、解析模型返回、执行技能、更新状态。
import SwiftUI struct SkillPanelView: View { @State private var viewModel = SkillPanelViewModel() var body: some View { NavigationStack { List { Section("选择技能") { ForEach(viewModel.registry.skills, id: \.name) { skill in Button(skill.name) { viewModel.select(skill) } .disabled(viewModel.state == .executing) } } if let skill = viewModel.selectedSkill { Section("参数输入") { ForEach(viewModel.parameterKeys, id: \.self) { key in TextField(key, text: $viewModel.parameterValues[key]) } Button("执行技能") { viewModel.executeSelectedSkill() } .disabled(viewModel.state == .executing) } } Section("结果") { switch viewModel.state { case .idle: Text("等待操作") case .executing: HStack { ProgressView() Text("正在执行...") } case .success(let message): Text(message) case .failure(let errorMessage): Text(errorMessage).foregroundStyle(.red) } } } .navigationTitle("技能面板") } } }对应的 ViewModel 负责把用户选择的技能和参数包装成一次调用。为了简洁,Demo 里没有接入真实的 LLM 接口,我直接用了一个本地模拟的 Agent 决策器,它根据用户输入的文本模式决定调用哪个技能。真实项目中,你会在这里替换成网络请求 LLM 的流程,用函数调用的结构化输出来决定技能和参数,整体的状态管理逻辑不需要大改。
import Observation @Observable final class SkillPanelViewModel { enum State { case idle case executing case success(String) case failure(String) } var registry = SkillRegistry() var selectedSkill: Skill? var state: State = .idle var parameterKeys: [String] = [] var parameterValues: [String: String] = [:] func select(_ skill: Skill) { selectedSkill = skill parameterKeys = skill.parameterDescriptions .split(whereSeparator: { $0 == "、" || $0 == ",".first }) .map(String.init) parameterValues = [:] state = .idle } func executeSelectedSkill() { guard let skill = selectedSkill else { return } state = .executing Task { do { let result = try await skill.run(arguments: parameterValues) self.state = .success(result) } catch { self.state = .failure(error.localizedDescription) } } } }这个 Demo 的作用是把前面讲的架构跑通。你可以把它扩展成任何真实技能的入口:预订会议室、查询航班、开关智能硬件、给用户发送日程提醒。只需替换 Skill 实现和对应服务,界面和状态机框架都是通用的。这就是把技能系统设计在界面之前的回报——换业务,不换架子。
5. 流式输出、上下文记忆与性能:进阶避坑实录
当你的技能系统不再只是 Demo,而是要在真实场景里长期运行,就会碰到一堆和性能、体验、上下文相关的问题。我挑几个最有代表性的展开讲,希望能让后来者少走弯路。
5.1 流式输出为什么卡 UI:主线程与渲染线程的边界
Agent 执行技能后的大模型回复,在真实产品里基本都要求流式输出。但流式输出和 SwiftUI 的组合有个经典的性能陷阱:如果你把每个 token 都立刻扔到主线程触发视图重绘,UI 会肉眼可见地卡顿。尤其当回复流较长时,每秒钟几十次更新的高频渲染足以把主线程占满。
我踩过这个坑之后采用的策略是:限频合并。收集到的 token 先存入 buffer,用定时器或者Task.sleep做节流,控制在每 100 毫秒左右刷新一次视图。用户感知不到这个延迟,界面却能保持流畅。原理上这有点像把瀑布流的水先接到水桶里,再按节奏倒进池子,避免管道瞬间过载。
还有一个容易被忽略的点是长文本视图的性能。SwiftUI 的Text在渲染非常长的字符串时,性能会随字符串长度劣化明显。我的做法是限制技能结果中直接渲染文本的长度上限,超过阈值就截断显示,完整内容放入一个可展开的独立页面。这样既保证了关键信息的可见性,又不伤性能。
5.2 上下文记忆的容量控制与淘汰策略
Agent 和用户的对话是有上下文的。技能执行结果、用户对结果的反馈、后续的追问,都会成为上下文的一部分。当上下文无限膨胀,token 成本、请求延迟和模型理解准确度都会一起变差。上下文管理必须显式设计,不能依赖模型自己取舍。
我在项目里常用的策略是三级管理:短期记忆保留最近 4 轮左右的完整对话;中长期记忆把较早的对话压缩成一句话摘要;长期记忆从用户的行为和偏好中提取结构化的画像数据。什么时候做压缩?当上下文 token 数超过预设阈值时触发一次摘要化重写,这个阈值要根据你用的模型上下文窗口和成本预算来定。
技能调用相关的数据要不要进上下文?要,但有讲究。每次技能调用的参数和结果是否完整保留,要看它对后续对话有没有影响。比如查天气的结果基本只影响当时的回复,下一轮就可以丢弃;但用户设置的偏好类操作("以后都默认用列表视图")就要持久化到长期记忆里。我为每一个技能定义了一个"是否写入长期记忆"的元数据,让技能开发者自己决定,这样设计意图明确,也方便不同技能做差异化管理。
5.3 实测表现:我在小流量试运行中观察到的真实表现
我把这套架构在两个真实场景里跑了几周的小流量测试:一个是面向 C 端用户的个人记账辅助,另一个是企业内部的日程助手。用户总数不算多,但反馈足够说明问题。
记账场景里,技能调用最频繁的是"记一笔账"和"查询某类开销"。这两个技能加起来占了总调用量的 76%,说明用户对确定性很强的操作更愿意用自然语言来触发。错误最多的是参数提取不准确,尤其当用户说的金额和描述比较口语化,比如"买了个东西大概四十多块",模型很难直接给出准确的结构化参数。这里我学到的一点是:技能参数 schema 的描述要尽可能往用户口语习惯上靠,多给模型一些"转换提示"。比如在金额字段的描述里写明"用户可能说'四十多块',请取正常近似值 45,并在备注里记录原话"。加了这个提示后,解析准确率变化显著。
企业日程场景暴露出的是权限边界问题。技能尝试读取同事的日程时,被权限层拦截的情况不少。早期我们的权限层设计得偏粗,一旦技能没有显式授权就拒绝执行,用户会觉得"AI 能力被阉割"。后来我加了一个"申请权限 -> 用户授予 -> 记住选择"的链路,把授权决策交给了用户而不是系统一刀切。这其实和我在 3.3 里讲的人工确认护栏是同一个思路,本质是让控制权始终保留在人这一侧。
5.4 技能编排与冲突仲裁:当多个技能同时匹配时怎么办
真实 Agent 并不总是一次只调用一个技能。用户可能一句话里包含多个请求,比如"先把会议室订了,再给我明天的日程发个提醒"。这种复合需求需要技能编排机制。如果你支持的技能数量少,可以让模型一次调用多个技能,按顺序执行;但当技能数量增多,冲突仲裁就变得不可避免。
我采用的策略是基于依赖关系的链式执行:技能 A 的输出如果正好是技能 B 的输入,就自动拼接成一条流水线。比如"帮我约个理发店,并把预约提醒放进日历"——第一个技能返回的预约时间可以作为第二个技能的输入。方案是提供一份依赖图谱,让 LLM 在规划时参考,把多个调用串联成一条路径。
冲突仲裁也很有讲究。假设"删除记录"和"归档记录"同时匹配了用户的意图,系统不能随机选一个执行。我的处理办法是给每个技能定义一个"默认识别优先级",当且仅当两个技能的模型置信度都高且优先级相差不大时,才把选择权抛给用户。这个设计能避免大多数误判,也让用户感觉到系统"心里有数"。最终目标不是消灭一切错误决策,而是让错误的代价变得足够低,同时让系统的每一次不确定都能显式地传达给用户。
这套 SwiftUI-Agent-Skill 的架构走到这里已经形成闭环:技能设计、状态管理、界面联动、上下文演进、冲突仲裁,每一层都有明确职责和边界。我在实现过程中的最大体验是,不要把 Agent 当作一个独立的"大脑"塞进应用就完事,而是要把它当作应用功能的一个新的表达入口,让旧的业务能力通过技能层自然地暴露给意图识别。这样改造后的应用,既保留了原生体验的流畅和确定,又拿到了自然语言交互的灵活和低门槛,两者不是谁替代谁,而是在技能系统里完成了合流。
如果你打算在自己的项目里试这套方案,我的建议是:先别急着接大模型,用一个写死的模拟决策器把技能系统、状态机和界面联动跑通,确认体验顺畅了再替换成真实模型也不迟。这样排查问题时不会模型、代码、界面三个变量搅在一起。后续你还可以往这个方向继续加东西,比如技能的自动生成、基于用户反馈的技能参数调优、跨设备技能同步等,那都是架构稳定之后的水到渠成,而不是推倒重来的负担。