教程
Apple AI Agent 如何连接 App、API 和工具?JSON 扮演什么角色?
系统 Agent 走 App Intents,App 内模型走 Foundation Models 的 Tool。JSON 不是装饰,而是参数契约、结构化输出和 HTTP 载荷共用的形状。
「Apple AI Agent」经常被当成一个产品名。实际落地时,你至少会碰到两条完全不同的调用链:一条是系统级 Apple Intelligence 替用户选 App、选动作;另一条是你的 App 自己跑 on-device 模型,再由模型决定调用哪些工具。两条链都能碰到 App、API 和工具,但谁拥有会话、谁生成参数、JSON 出现在哪一层,并不一样。把这三层分清,后面的 Schema、日志和排障才有落点。
先分清谁在跑模型
先问一句:这次推理是谁发起的?如果用户对 Siri 或系统级 Apple Intelligence 说话,模型在系统侧(设备上或 Private Cloud Compute),你的 App 只是被路由到的能力提供方。这时你暴露的是 App Intents:类型化的动作、实体和参数。系统 Agent 决定何时调用,你的代码负责执行。官方入口见App Intents 文档。
如果用户已经在你的 App 里,你用 Foundation Models 起了一个LanguageModelSession,那是另一条链:模型由你的进程驱动,工具由你注入。系统不会替你选「打开哪个 App」,因为会话本来就在 App 内。工具可以查通讯录、读日历、打你自己的网络 API,再把结果写回 transcript。框架说明见Foundation Models,工具调用的会话级讲解见 WWDC25 的Meet the Foundation Models framework。
第三种情况越来越常见:外部主机(Claude、ChatGPT、自建 Agent)通过 MCP 或普通 HTTPS 调你的服务。模型不在 Apple 栈里跑,但载荷几乎一定是 JSON。同一套领域函数可以同时服务 App Intents、Foundation Models Tool 和 HTTP,差别只在适配层。
| 调用链 | 谁跑模型 | App 提供什么 |
|---|---|---|
| 系统 Agent | Apple Intelligence | App Intents / App Entity |
| App 内 Agent | 你的 LanguageModelSession | Tool 协议与 @Generable 参数 |
| 外部 Agent | 第三方主机 | JSON-RPC 或 REST JSON |
不要把三条链画成一张「万能 Agent 图」。排障时先定位是哪一条:系统路由失败,查 Intent 声明和参数类型;App 内工具乱调,查 name、description 和 Arguments 的可生成形状;外部 API 对不上,查 HTTP JSON 与 Schema。混在一起只会觉得「模型胡来」。
App Intents:系统 Agent 如何连上你的 App
对系统 Agent 来说,App 不是「打开一下看看」,而是一份可发现的能力目录。你用 AppIntent 声明动作名称、自然语言描述、参数类型和结果。Spotlight、快捷指令、Siri 和 Apple Intelligence 共用这套目录。用户说「把这张发票标成已付」,系统要能把话映射到你的MarkInvoicePaid,而不是让模型在界面上乱点。
参数才是连接点。系统侧会把用户话语收成类型化参数:枚举、日期、AppEntity 引用。你在 Swift 里看到的是结构体,不是一段散文。跨进程或跨框架时,这套结构仍然要有一份可序列化的形状——调试、日志、服务端回放,最后都会落到 JSON 或等价的属性列表。把 Intent 参数设计成「能写成 JSON 对象」的字段集,后面接 API 会省很多适配。
常见取舍:Intent 要不要直接打网络?短动作可以在 perform 里完成。一旦涉及鉴权、分页、幂等,perform 里只做「校验参数 + 调用你的领域服务」,领域服务再发 JSON 请求。系统 Agent 不需要知道你的 REST 路径,它只需要 Intent 成功或失败的语义。你需要知道路径,因为账单、审计和重试都在 API 这一层。
Foundation Models:App 内模型如何调用工具
App 内 Agent 的连接方式更像「给模型一份函数说明书」。你实现Tool:给name、description,再给带@Generable的Arguments。框架把这些写进 prompt,模型决定何时调用;调用时先生成参数,框架再执行call(arguments:),把返回值(通常是 String 或可生成类型)塞回 transcript。模型据此写最终答复。这不是让模型自己拼 URL,而是让它在你允许的工具集合里选。
参数生成走的是结构化输出,不是「请用 JSON 回答」。@Generable与动态 Schema 把字段、枚举、嵌套对象钉在解码阶段。你在 Swift 里拿到的是类型实例;若要落盘、打日志、转给 URLSession,再编码成 JSON。官方把这类能力概括为 language understanding、structured output 和 tool calling,见 Foundation Models 概览。
struct FindOrders: Tool {
let name = "findOrders"
let description = "Find recent orders by customer email and status."
@Generable
struct Arguments {
var email: String
var status: String
var limit: Int
}
func call(arguments: Arguments) async throws -> String {
// Call your domain API, then return a compact summary.
return "3 orders, latest is paid"
}
}
上面这段里,模型不会「随便发明」字段名。它必须填email、status、limit。description 决定会不会被选中:写得太宽,模型遇事都调;写得太窄,该调时不调。工具输出也要克制:把整份订单 JSON 倒进 transcript 会迅速吃掉上下文窗口。更好的做法是返回摘要,需要细节时再调第二个工具拿单号。
工具可以链式调用。模型用第一个工具的输出当作第二个工具的输入,框架按顺序执行。你要在领域层保证幂等:同一orderId标两次已付,不能扣两次款。Agent 循环看不见你的数据库约束,JSON 参数对了不等于业务对了。
JSON 在其中扮演什么角色
Swift 类型是编译期契约,JSON 是运行期契约。Agent 一旦跨出当前进程——打后端、写文件、给另一个模型看、给测试夹具回放——形状就要变成语言无关的文本。JSON 在这条链上至少扮演三层角色,混用其中两层就会出现「能 parse 但字段全错」。
1. 工具参数的交换格式
Foundation Models 在内部用 GeneratedContent 表示结构化值。你调试时,最有用的视图往往是「这份 Arguments 若编码成 JSON 长什么样」。字段名、可选性、数组还是对象,都必须稳定。把参数日志打成 JSON,才能和线上 API 访问日志对齐:同一customerId到底有没有传到服务端。
{
"email": "ada@example.com",
"status": "paid",
"limit": 5
}
2. 结构化输出的 Schema
不想走工具、只想让模型吐一份发票摘要时,仍然需要 Schema:哪些键必填、枚举有哪些值、金额是 number 还是 string。Apple 侧用@Generable;云端模型常用 JSON Schema。两套声明应对同一领域对象时,应用同一份字段表,避免 App 内叫totalCents、API 叫amount。写 Schema 的共用规则可对照Understanding JSON Schema。
3. App 到 API 的载荷
工具的call里一旦访问网络,JSON 就是 HTTP 的正文。Agent 并不替代 API 设计:鉴权头、幂等键、错误对象仍然要你定义。模型只负责填「业务参数」;传输层、分页、限流仍是普通后端问题。把错误也设计成稳定 JSON(code、message、retryable),模型才可能决定是换工具还是向用户解释失败。
三层可以共用一份 Schema 文档:Intent 参数 ⊂ 工具 Arguments ⊂ HTTP body。子集关系让测试简单:用同一份合法 JSON 夹具,先测 API,再测 Tool,再测 Intent 适配。超集关系(HTTP 比工具多三个内部字段)也可以,但不要让模型看见内部字段,否则它会开始「帮忙填写」你并不想公开的键。
接到 HTTP API 与 MCP
App 内工具打 REST 时,建议显式编码,不要把 GeneratedContent 直接当 Data 发出去。先映射到你的 Codable 模型,再JSONEncoder。这样枚举原始值、日期格式、键策略(snake_case)都由你控制。模型生成的是领域值,编码器负责协议细节。
{
"tool": "markInvoicePaid",
"arguments": {
"invoiceId": "inv_9f2",
"paidAt": "2026-08-20T09:00:00Z"
}
}
MCP 把「工具名 + 参数对象」变成 JSON-RPC。对 Apple 开发者来说,这只是第三种适配:同一markInvoicePaid(invoiceId:paidAt:),App Intent 走 perform,Foundation Models 走 Tool.call,MCP 走 tools/call。不要为 MCP 另写一套业务逻辑。外部 Agent 更容易把参数发错类型(数字变成字符串),所以服务端仍要校验,不能因为「已经是 JSON」就信任。
隐私边界也要写进工具描述。On-device 模型可以读日历,不代表你该把日历事件 POST 到自己的服务器。工具输出给模型看可以是本地摘要;只有用户明确要同步时,才构造上传用的 JSON。系统 Agent 与 App 内 Agent 的数据出境策略应分开评审,日志里也要能区分来源。
落地时怎么看 JSON
联调 Agent 时,最有效的不是再写一句提示词,而是把三次快照放在一起看:模型生成的参数 JSON、你发出的 HTTP JSON、服务端回的 JSON。三次形状不一致,问题几乎总在映射层,而不是「模型不够聪明」。本地把它们格式化、校验、对比,比在 Xcode 控制台里盯 Optional 更快。
一份参数可以先在浏览器里走一遍:JSON 格式化确认能 parse;JSON 校验抓住尾逗号和错误类型;用JSON Schema把 Intent / Tool / API 的共用字段钉死;再用JSON Diff对比「模型输出」和「实际请求体」。云端模型的 Structured Output 做法不同,但「先契约后解析」是一样的,可对照站内Gemini API 如何输出 JSON。
建议固定三个夹具文件:tool-args.valid.json、http-body.valid.json、http-error.json。CI 里用同一份 Schema 校验前两份;错误夹具用来测 Agent 是否向用户说出可行动的失败原因,而不是吞掉retryable: true。
常见问题 FAQ
App Intents 和 Foundation Models 的 Tool 能共用同一套参数吗?
可以共用领域模型,但不要假设系统能直接跑你的 Tool 协议。Intent 面向系统发现与权限;Tool 面向当前会话的 prompt。中间做一层映射,JSON 夹具测的是领域模型,不是某一个框架类型。
为什么不让模型直接输出完整 HTTP 请求?
URL、Header、签名不该由模型编造。模型只填业务字段,客户端按固定模板发请求。否则一次幻觉就会打到错误环境或带上错误鉴权。
JSON 和 @Generable 冲突吗?
不冲突。@Generable 是 Swift 侧的生成与解码;JSON 是跨语言、跨网络的形状。先有稳定字段表,再分别生成 Swift 宏和 JSON Schema。
工具返回值一定要是 JSON 吗?
不一定。给模型看的可以是短文本摘要,给服务端的必须是 JSON。两套输出不要混在同一个字符串里硬 parse。
总结与下一步
Apple 栈上的 Agent 不是一个插座,而是三条可组合的线:系统 Agent 用 App Intents 发现并调用 App;App 内模型用 Tool 调用你的代码;外部主机用 JSON 调用同一领域服务。JSON 负责让参数、Schema 和 API 载荷说同一种形状。类型安全解决编译期,Schema 解决运行期,业务校验解决对错。
下一步:列出三到五个领域动作,为每个动作写一份最小 JSON 对象和一份 Schema,再决定它出现在 Intent、Tool 还是 HTTP。形状稳定之后,再加提示词和多工具编排。