教程

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。形状稳定之后,再加提示词和多工具编排。