🍚

Foundation Models のきほんのレシピ

に公開

これは GENDA Advent Calendar 2025 シリーズ2 の19日目の記事です。

https://qiita.com/advent-calendar/2025/genda

iOS 26 から利用できる Foundation Models フレームワークを使って、LLM を利用した料理アシスタントアプリを試作しました。この事例を通じて、Foundation Models の特徴や使用感を紹介します。

憂鬱な一年

2025年はAIコーディングツールが急速に普及しました。日々せっせとプロンプトを書いて LLM にソフトウェア開発経験を積ませる作業に勤しんでいると、早晩自分の仕事は AI に取って変わられてしまうんでは…という安直な、でもたぶんリアルな近未来予測がずっと腹の底に渦巻いて、憂鬱な一年でした。

今後の行く末を考えると不安ばかりなので、過去の「古き良き時代」に想いを馳せました。コンピューターって人間の創造性を刺激したり支援するツールのはずだったよね…。そんなことを考えて iOSDC 2025 で LT しました:

https://fortee.jp/iosdc-japan-2025/proposal/edf39cc0-6ced-4ac2-8b66-6a7ab3eb9bdc

かすかな光

その iOSDC 2025 で、LLMを活用したトークをいくつか見て感銘を受けました。

https://fortee.jp/iosdc-japan-2025/proposal/40681d36-4abd-4e54-84c4-448fdea48009

https://fortee.jp/iosdc-japan-2025/proposal/695622d8-ad9e-4533-9237-cfa0be122eb0

「AI に脅かされる」みたいなことばかり考えていた私にとっては、アプリケーションの中に LLM を組み込んで利用する、という発想は新鮮でした。

レシート読み取り機能のトークの例は Foundation Models を使っています。Foundation Models は Swift コードで呼べる API で、オンデバイス、すなわち端末の中だけで完結して LLM を利用できます。

手軽に試せそうですし、これはひとつ真似してなにか作ってみよう、と思いました。ひさしぶりに、すこし前向きな気持ちになれました。

料理しよう

ところで、料理っていいですよね。私はさして料理が得意でも、マメにするほうでもないですが、机から離れて手を動かすのは憂鬱な日々の気晴らしになります。ソフトウェア開発と違って、同じソースコード(レシピ)でも何度もビルド(調理)することに意味があるし、まだ当面 AI に侵食されない分野ですよね…。

ここ最近、料理家・長谷川あかりさんのレシピがSNSでバズってます。長谷川さんのレシピは、比較的少ない材料・手順で意外にもおいしく作れて、私も以前から参考にしていました。彼女はよく、写真に手順のテキストを載せたレシピをXに投稿します。

https://x.com/akari_hasegawa/status/1613892330855223296

件のレシート読み取り機能のトークを見ていて、こういった画像からレシピ情報を抽出するアプリに転用できそう、と思いました。レシピ画像を OCR してテキスト抽出し、それを LLM に投げれば、整理された材料表と手順表を作ることができそうです。

アレンジレシピ1: 画像からレシピ情報を抽出するアプリ

ということで、こんなアプリができました:

レシピ画像を登録すると、画像中のレシピ情報を読み取って、レシピデータとして整理して画面表示します。

長谷川あかりさんのようなレシピ投稿画像でなくても、たとえばレシピ本や手書きのレシピをカメラ撮影したものでも、まあまあの精度で読み取れました。

※ちなみに、この動画で登録したレシピ画像は、Gemini を使って画像の雰囲気だけ長谷川あかりさんぽく錬成したものです。↓
レシピ画像

Foundation Models について

画像からテキストを抽出する処理には Vision フレームワークを利用しましたが、本稿では説明を割愛します。抽出したテキストを解釈してレシピデータに整形するのに使っているのは Foundation Models フレームワークです。

あらためて説明すると、Foundation Models フレームワークは、iOS 26 / iPad OS 26 / macOS 26 / visionOS 26 以降で使えるオンデバイスのLLMフレームワークです。以下の特徴があります:

  • オンデバイスで動作する
  • 利用量に応じた課金がない
  • Swiftから自然な形で呼び出せる
  • 生成結果をSwiftの型として受け取れる(guided generation)

なお、Foundation Models を利用するには、Apple Intelligence に対応した端末で Apple Intelligence を有効にした状態である必要があります。

レシピの分析

テキストを LLM に分析させるメソッドは、こんなに短く書けます:

func extractRecipe(from ocrText: String) async throws -> String {
  let session = LanguageModelSession(
    instructions: """
    あなたは料理の専門家です。OCR読み込みした料理レシピのテキストについて分析を依頼されるので、料理レシピの情報を正確に抽出してください。
    """
  )
  let response =
    try await session.respond(
      to: "以下のテキストからレシピ情報を抽出してください\n\(ocrText)",
      generating: String.self
    )
  return response.content
}

大事なのは、コードよりもプロンプト = 自然言語による質問です。ここで「レシピ情報を抽出してください」と指示しているのがプロンプトです。LanguageModelSessionの初期化時に渡す instructionsはプロンプトよりも優先される指示で、LLM の役割や従うべきルールなどを設定します。

たとえば、画像から OCR で抽出した時点ではこんなテキストです:

2人分の材料
スパゲティ 160g
玉ねぎ 1/2個
ピーマン 2個
ソーセージ2本
バター15g
ケチャップ大さじ6
ウスターソース 大さじ1
赤ワイン 大さじ1
牛乳大さじ6

バターで野菜と
ソーセージを炒める。
1
野菜とソーセージを
いったん皿に取り出す。
②
③
クスターソース、ケチャッフ
牛乳を煮詰める。
野菜とソーセージを
戻して合わせる。

スパゲティを茹でる。
1
②
茹だったら、ソースと和えて軽く炒める。

これを extractRecipe(from:) に投げると、このように整理したテキストを返します:

**材料**

*   スパゲティ 160g
*   玉ねぎ 1/2個
*   ピーマン 2個
*   ソーセージ2本
*   バター15g
*   ケチャップ大さじ6
*   ウスターソース 大さじ1
*   赤ワイン 大さじ1
*   牛乳大さじ6

**作り方**

1.  バターで野菜とソーセージを炒める。
2.  野菜とソーセージを一度皿に取り出す。
3.  クスターソース、ケチャップ、牛乳を煮詰める。
4.  野菜とソーセージを戻して合わせる。
5.  スパゲティを茹でる。
6.  茹でたら、ソースと和えて軽く炒める。

ガイド付き生成 (guided generation)

この出力されたテキストは、人間が読むぶんにはなかなか見やすくなっていますが、アプリ上で整形して見せるためには、構造化されたデータになっていてほしいです。そのために、プロンプトで JSON を吐くように指示してその出力をパースする、といったことも考えられますが、Foundation Models にはもっとよい方法が用意されています。

ガイド付き生成 (guided generation) は、LLM の回答を指定した形式で返させるしくみです。下記の例のように@Generable@Guideのアノテーションを付与した型を用意すると、LLM はその型に合わせてガイド通りの出力をしてくれます。

@Generable(description: "料理レシピ")
struct Recipe {
  // 基本的な項目にはガイドは不要
  var name: String
  @Guide(description: "料理レシピの材料。材料は名前と分量を正確に分離してください。")
  var ingredients: [Ingredient]
  @Guide(description: "料理レシピが何人分かどうか。不明な場合は2人分としてください。")
  var servings: Int
  @Guide(description: "料理の手順。順番通りに整理してください。")
  var steps: [String]
}

@Generable(description: "料理レシピの材料")
struct Ingredient {
  // 基本的な項目にはガイドは不要
  var name: String
  @Guide(description: "材料の量。200g、大さじ1など。")
  var amount: String
}

このRecipe型を使ってガイド付き生成をするには、出力する型を指定するだけ。

let response =
  try await session.respond(
    to: "以下のテキストからレシピ情報を抽出してください\n\(ocrText)",
    generating: Recipe.self
  )

そうすると、このようなRecipe型の出力を得られます:

Recipe(name: "スパゲッティ",
    ingredients: [
      Ingredient(name: "スパゲッティ", amount: "160g"),
      Ingredient(name: "玉ねぎ", amount: "1/2個"),
      Ingredient(name: "ピーマン", amount: "2個"),
      Ingredient(name: "ソーセージ", amount: "2本"),
      Ingredient(name: "バター", amount: "15g"),
      Ingredient(name: "ケチャップ", amount: "大さじ6"),
      Ingredient(name: "ウスターソース", amount: "大さじ1"),
      Ingredient(name: "赤ワイン", amount: "大さじ1"),
      Ingredient(name: "牛乳", amount: "大さじ6")],
    servings: 2,
    steps: [
      "バターで野菜とソーセージを炒める。",
      "野菜とソーセージを一度皿に取り出す。",
      "クスターソース、ケチャップ、牛乳を煮詰める。",
      "野菜とソーセージを戻して合わせる。",
      "スパゲッティを茹でる。",
      "茹でたら、ソースと和えて軽く炒める。"
      ])

再作成機能

Foundation Models のオンデバイス LLM は ChatGPT 等と比べて性能は高くないようです。実は、今まで挙げたレシピ抽出例は、うまくいったケースを見せてました。いくつかレシピ画像登録を試しましたが、いまいちな結果の場合も、まあまあありました。

LLM は確率的動作をするので、同じ入力に対しても異なる出力をします。一度試しておかしな出力になっても、何度か試すとまともなレシピデータになる、ということがあります。

そこで、このアプリではレシピの再生成機能を追加し、OCR で抽出した同じテキストから LLM によるレシピ分析を再実行できるようにしました。

再作成の確認ダイアログのスクリーンショット

チューニング

レシピ画像登録を何度か試してみると、OCR の読み取りが不正確なゆえの妙な出力にも遭遇しました。

  • 「何人分」の表示が「20人前」というありえない数値になる
  • 「1/2」が読み取れなくて「12」になる

OCR 処理の調整が必要そうですが、今回はLLMを使って手抜き調理してみました。instructionsに下記のような注意書きを付与しました:

"""
あなたは料理の専門家です。OCR読み込みした料理レシピのテキストについて分析を依頼されるので、料理レシピの情報を正確に抽出してください。
*注意*
- 「何人分」の値は10以上になることはほぼないです。10以上の桁の数値は読み取り間違いであることを疑ってください。
- 「1/2」が読み取り間違いにより「12」となることがあります。大きすぎる値であることを疑ってください。
"""

これは思いのほか効いて、体感分析精度がグッと上がりました。さらにレシピ画像登録の試行を繰り返して、より多くの分析 tips を見つければ、もっと精度を磨けそうです。

アレンジレシピ2: 料理について質問できるチャット機能

意外に手軽に仕上がったので、もう一品。せっかく LLM が使えるので、チャット機能を作ってみました。登録したレシピについてあれこれ日本語で質問できる機能です。

チャット機能のスクリーンショット

なかなかいい感じに動いています。LLM に質問を投げるあたりのコードはこんな感じ:

import FoundationModels

final class RecipeAssistantService {
  let recipe: Recipe
  let session: LanguageModelSession
    
  init(recipe: Recipe) {
    self.recipe = recipe
    self.session = LanguageModelSession(model: .default, tools: [], instructions: {"""
あなたは料理レシピのアシスタントです。ユーザーが特定のレシピについて質問しているので、そのレシピ情報を参照して的確に答えてください。
"""
      "対象のレシピはこれです:"
      recipe
      """
回答時の注意事項:
- 簡潔で分かりやすい回答を心がけてください
- 材料や手順について聞かれた場合は、レシピの情報を正確に伝えてください
- レシピ情報にない内容については、「このレシピには該当する情報がありません」と伝えてください
- 料理のコツやアレンジ方法を聞かれた場合は、一般的な知識から適切にアドバイスしてください
"""
    })
  }
    
  func answerQuery(_ query: String) async throws -> String {
    let response = try await session.respond(to: query)
    return response.content
  }
}

LanguageModelSessionのインスタンスをクラスで保持して、answerQuery(:)で何度も質問を投げられるようにしています。

ここでの肝は、LanguageModelSession の初期化時の引数instructions(trailing closure)にRecipe型を渡していること。@Generableアノテーションを付与したGenerable準拠型はinstructionsに渡すことができます。LLM 使って生成したレシピデータを、その構造そのままに LLM に渡して参照させることができるわけです。

Safety guardrails との向き合い方

チャットに質問を投げていると、以下のようなエラーが発生することがあります。

チャット画面でDetected content likely to be unsafeのエラー
ありがとうって伝えただけなのに

Foundation Models には有害またはセンシティブな内容が入出力に含まれないよう、ブロックするためのガードレールが備わっています。上記の例では明らかに有害 or センシティブな内容が含まれてなさそうですが、このガードレールが過剰反応してしまったのでしょうか。

前述のとおり LLM は確率的動作をするので、「この会話をしたら必ず違反する」というパターンを見つけづらいです。しかし生成時に「貪欲法」を採用してランダムさを無くし、同じ入力に対してつねに同じ出力をさせることもできます。

func answerQuery(_ query: String) async throws -> String {
  let response = try await
   session.respond(to: query,
     options: GenerationOptions(sampling: .greedy)) // 貪欲法を採用
  return response.content
}

これによって、ガードレールに抵触するパターンを探すことはできそうです。この生姜焼きの例では下記の会話パターンで必ずエラーになることがわかりました。

チャット画面でDetected content likely to be unsafeのエラー
しかし、なぜ…?

また、ガードレールをオフにすることもできます。permissiveContentTransformationsを設定したモデルをLanguageModelSessionの初期化時に渡すと、ガードレール抵触のエラーが返ることは無くなります。

let model = SystemLanguageModel(guardrails: .permissiveContentTransformations)
let session = LanguageModelSession(model: model)

ただしこのモードは、センシティブな話題を扱わざるを得ない場合のためのもので、むやみに使うべきではないでしょう。また、Foundation Models の安全性担保のレイヤーは2段階で、ガードレールがオフでも LLM がコンテンツに問題ありと判断した場合は、エラーは返りませんが出力の中で回答できない旨を伝えてくるようです。

余談: 音声アシスタントの夢

料理しながら使うなら、メッセージアプリ的なチャット機能ではなくて、ハンズフリーで使える音声入力/音声出力の機能のほうがよいはず。そう考えて App Intents を使って Siri 経由でチャット機能を呼び出す方法も試しましたが、コンテキストを保持してマルチターンの会話をするのが難しく、あまり実用的でないので頓挫しました。このへんの話は、また別の機会に。

さいごに

料理アシスタントアプリの試作を通じて、Foundation Models フレームワークを紹介しました。Swift から自然に呼び出せること、ガイド付き生成で型安全な出力を得られることなど、アプリの一部品として LLM を使うための環境が整えられており、工夫のしがいがありそうです。

では、よいお年を。

Discussion