ブログ • AI / App Intents

iOS 27 で Siri の App Actions が増えたあと、JSON はどう流れる?音声指示から API 実行まで

Siri の指示が実行できるのは、先に App Intent のパラメータになり、AppIntent の perform() で端末内に留めるか自分の API を呼ぶかを決めるからだ。システムは音声をそのままサーバーへ送らない。

Siri が Agent かどうかは 前回の記事 に書いた。この記事は一つの指示だけを追う。フレーズが Intent に当たるまで、パラメータが JSON になるまで、検証のあとで API を呼ぶまで。

先に押さえる点:音声はリクエスト本体ではない。埋まった Intent のパラメータが本体だ。以下はフレーズ、パラメータの型、エンティティ id、自分の JSON、API の順。

一つの指示が通る五段階

公式の入口は App Intents。説明は App Intents 入門。Siri、ショートカット、アプリ内ボタンは同じ Intent を使う。違うのは誰がパラメータを埋めるかだ。

  1. 1
    フレーズが当たる

    口頭の句は AppShortcutsProvider に登録する。システムはその句で Intent を選ぶ。原文をモデルに自由に扱わせない。

  2. 2
    パラメータを埋める

    Intent の @Parameter は、必要な値と必須かどうかをシステムに伝える。欠けたらユーザーに聞き返す。推測して送らない。

  3. 3
    エンティティを解決する

    ユーザーデータは EntityQuery が id で解決する。パラメータに入るのはエンティティであり、それを説明する文章ではない。

  4. 4
    実行する

    解決済みの値を読むのは perform() だ。端末で終われる処理はここで終える。

  5. 5
    Siri に返す

    結果は IntentResult か対話で返す。失敗にも口に出せる理由が要る。サーバーログだけに残さない。

パラメータは三種類

型の説明は App Intent にパラメータを追加する。型を誤ると、その後の JSON が安定しない。

型向いているものJSON に入れるとき
文字列、数値、日付数量やメモのような単純な入力parameters にそのまま入れる
AppEnumコンパイル時に固定の選択肢。店頭受取か配送かなど列挙値を入れる。表示文を主キーにしない
AppEntity変わるユーザーデータ。ある商品など解決後の id だけを入れる

固定集合は AppEnum。ユーザーが増減させる一覧は AppEntity とクエリ。変わる一覧を列挙にすると、選択肢が古くなった瞬間にフレーズが当たらなくなる。

エンティティが残すのは id

システムが要るのは表示でき、問い合わせできるエンティティだ。データの源は自分のデータベースのまま。エンティティには少なくとも安定した id と人が読める名前が要る。

解決後に記録できるエンティティのフィールド。後続 API の鍵は id。
{
  "entity": "OrderItem",
  "id": "sku_42",
  "display": "Oat milk"
}

entities(for identifiers:) を実装し、id で対象を取る。見つからなければ id を作って続けない。

自分のパラメータ JSON

下の文書は、perform() の中で保存し、API を呼ぶか決める契約だ。Apple のリクエスト形式ではなく、システムもこのファイルを読まない。

解決済みの注文指示。resolved が true なのは、エンティティのクエリが成功したときだけ。
{
  "intent": "StartOrder",
  "source": "siri",
  "parameters": {
    "item_id": "sku_42",
    "quantity": 1
  },
  "resolved": true
}

いつ API を呼ぶか

  1. 1
    端末で終える

    端末の状態だけを変える処理は perform() で終える。通信は要らない。

  2. 2
    先に検証する

    サーバーが要るときは、先に Schema で JSON を検証する。item_id が無い、quantity が 1 未満なら端末で止める。

  3. 3
    それから通信する

    検証が通り resolved が true のときだけ parameters を自分の API に送る。サーバーは id で自分の記録を引く。表示名は信用しない。

  4. 4
    失敗は言葉で返す

    API が拒否するか時間切れなら、Intent の対話で未完了だと伝える。Siri に成功したと言わせない。

サードパーティの App が Intent をどう宣言し、失敗をどう返すかは次の記事。この記事はデータ経路で止める。

以前の記事との分担

Siri は Agent かこの記事
答える問いシステムが自分で計画してツールを呼ぶか一つの指示が API までにどの JSON を通るか
ここで探さないもの製品発表の記事をもう一篇音声原文を保存できるリクエスト本体とみなすこと

ハードウェアとシステム機能の製品の話は iPhone 18 がハードウェア以上である理由。あちらはパラメータのフィールドを分けない。

JSONNote でこの JSON を確認する

  1. 1
    整形する

    JSON 整形 でパラメータ記録を開き、末尾カンマが無いことを確認する。

  2. 2
    Schema で欠けたフィールドを止める

    下の Schema を JSON Schema に貼る。intent や item_id が無い、quantity が正の整数でないなら API に送らない。

  3. 3
    サンプルを一つ検証する

    JSON 検証 に実サンプルを貼る。見るのは item_id が空文字でないか。

パラメータ記録の JSON Schema。source は siri、shortcut、app だけ。
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["intent", "source", "parameters", "resolved"],
  "properties": {
    "intent": { "type": "string", "minLength": 1 },
    "source": { "enum": ["siri", "shortcut", "app"] },
    "resolved": { "type": "boolean" },
    "parameters": {
      "type": "object",
      "required": ["item_id", "quantity"],
      "properties": {
        "item_id": { "type": "string", "minLength": 1 },
        "quantity": { "type": "integer", "minimum": 1 }
      }
    }
  }
}

よくある質問

システムは音声全体を自分のサーバーに送りますか?

この経路では送らない。サーバーが受け取るのは、perform() の中で組み立てて検証したパラメータ JSON である。音声は端末側の認識に留まる。

パラメータに商品名を直接書かないのはなぜですか?

名前は変わり、重複する。AppEntity は安定した id を使い、クエリはその id で対象を取る。API も id だけを見る。

Schema を通れば API を呼べますか?

resolved も見る。形は正しくてもエンティティが未解決なら、ユーザーはまだ対象を選んでいない。ここで API を呼ぶと、確認の質問が誤った注文になる。

端末内の処理と通信する処理はどう分けますか?

端末内だけで終わる変更は perform() で終える。バックエンドが要るものは、パラメータ JSON を書いて検証してからリクエストする。どちらも IntentResult で結果を返す。

ショートカットで手入力したパラメータも同じ経路ですか?

同じ AppIntent だ。違うのは誰がパラメータを埋めるかだけだ。Siri はフレーズ、ショートカットはユーザーか前のアクション。エンティティ解決、JSON、API の門は同じ。

まとめ

一つの指示のデータ経路は、フレーズが Intent を選び、パラメータが型ごとに埋まり、エンティティが id になり、自分で JSON を書き、検証を通ってから API を呼び、対話で結果を Siri に返す。

結論:生の音声はアップロードしない。id が解決する前にリクエストしない。失敗はサーバーログだけでなく、ユーザーに言葉で返す。

次にすること:このパラメータ JSON を Schema で止めてから perform() に接続する。

ブログへ戻る