チュートリアル

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、Shortcuts、Siri、Apple Intelligence が同じカタログを共有します。ユーザーが「この請求書を支払済みにして」と言ったとき、システムは発話をあなたのMarkInvoicePaidにマッピングできなければなりません。モデルに UI をタップさせるのではありません。

接点は引数です。システム側はユーザーの発話を型付き値にまとめます:enum、日付、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 がフィールド、enum、ネストオブジェクトをデコード段階で固定します。Swift では型付きインスタンスを得ます。永続化、ログ、URLSession へ渡すなら、その時点で JSON にエンコードします。Apple は 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 に流すとコンテキストウィンドウをすぐ消費します。要約を返し、詳細が必要なら二つ目のツールで ID を取る方がよいです。

ツールはチェーンできます。モデルは最初のツールの出力を二つ目の入力に使い、フレームワークが順に実行します。ドメイン層で冪等性を保証してください:同じ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 は必要です:必須キー、enum 値、金額が 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。enum の raw 値、日付形式、キー戦略(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 検証 で末尾カンマと型エラーを捕捉し、Intent / Tool / API の共通フィールドを JSON Schema で固定し、次に JSON Diff で「モデル出力」と「実際のリクエスト本体」を比較。クラウドモデルの Structured Output は API が異なりますが、「契約を先に、解析は後」は同じです。サイト内の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 のどれに載せるか決めます。形が安定してから、プロンプトとマルチツール編成を足してください。