FoundationModels: オンデバイスLLM (iOS 26)
FoundationModelsフレームワークを使用して、Appleのオンデバイス言語モデルをアプリに統合するためのパターン。テキスト生成、@Generable による構造化出力、カスタムツール呼び出し、スナップショットストリーミングをカバーします。これらはすべてオンデバイスで実行されるため、プライバシーとオフラインサポートが保証されます。
アクティブ化のタイミング
- Apple Intelligence をオンデバイスで使用したAI駆動機能の構築
- クラウド依存なしでのテキスト生成または要約
- 自然言語入力からの構造化データ抽出
- ドメイン固有のAIアクションのためのカスタムツール呼び出しの実装
- リアルタイムUI更新のための構造化レスポンスのストリーミング
- プライバシーを重視したAI(データがデバイス外に出ない)が必要な場合
コアパターン — 利用可能性の確認
セッションを作成する前に、必ずモデルの利用可能性を確認します:
struct GenerativeView: View {
private var model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
ContentView()
case .unavailable(.deviceNotEligible):
Text("端末がApple Intelligenceの対象外です")
case .unavailable(.appleIntelligenceNotEnabled):
Text("設定でApple Intelligenceを有効にしてください")
case .unavailable(.modelNotReady):
Text("モデルをダウンロード中、または準備ができていません")
case .unavailable(let other):
Text("モデルが利用できません: \(other)")
}
}
}
コアパターン — 基本セッション
// シングルターン: リクエストごとに新しいセッションを作成
let session = LanguageModelSession()
let response = try await session.respond(to: "パリを訪れるのに良い月は?")
print(response.content)
// マルチターン: 会話のコンテキストを保持するためにセッションを再利用
let session = LanguageModelSession(instructions: """
あなたは料理アシスタントです。
材料に基づいてレシピを提案してください。
提案は簡潔で実用的にしてください。
""")
let first = try await session.respond(to: "鶏肉とご飯があります")
let followUp = try await session.respond(to: "ベジタリアン向けのオプションはありますか?")
指示(instructions)の重要なポイント:
- モデルの役割を定義する(「あなたはメンターです」)
- 何をすべきかを指定する(「カレンダーイベントの抽出を手伝ってください」)
- スタイルの好みを設定する(「できるだけ簡潔に答えてください」)
- 安全対策を追加する(「危険なリクエストに対しては「それにはお答えできません」と答えてください」)
コアパターン — @Generable によるガイド付き生成
生の文字列ではなく、構造化されたSwift型を生成します:
1. Generable 型を定義
@Generable(description: "猫の基本プロフィール情報")
struct CatProfile {
var name: String
@Guide(description: "猫の年齢", .range(0...20))
var age: Int
@Guide(description: "猫の性格を一文で表したプロフィール")
var profile: String
}
2. 構造化出力をリクエスト
let response = try await session.respond(
to: "かわいい保護猫を生成してください",
generating: CatProfile.self
)
// 構造化フィールドに直接アクセス
print("名前: \(response.content.name)")
print("年齢: \(response.content.age)")
print("プロフィール: \(response.content.profile)")
サポートされている @Guide 制約
.range(0...20)— 数値範囲.count(3)— 配列要素数description:— 生成のための意味的なガイダンス
コアパターン — ツール呼び出し
ドメイン固有のタスクのために、モデルがカスタムコードを呼び出せるようにします:
1. ツールを定義
struct RecipeSearchTool: Tool {
let name = "recipe_search"
let description = "指定された用語に一致するレシピを検索し、結果のリストを返します。"
@Generable
struct Arguments {
var searchTerm: String
var numberOfResults: Int
}
func call(arguments: Arguments) async throws -> ToolOutput {
let recipes = await searchRecipes(
term: arguments.searchTerm,
limit: arguments.numberOfResults
)
return .string(recipes.map { "- \($0.name): \($0.description)" }.joined(separator: "\n"))
}
}
2. ツールを使用してセッションを作成
let session = LanguageModelSession(tools: [RecipeSearchTool()])
let response = try await session.respond(to: "パスタのレシピをいくつか見つけてください")
3. ツールエラーの処理
do {
let answer = try await session.respond(to: "トマトスープのレシピを見つけてください。")
} catch let error as LanguageModelSession.ToolCallError {
print(error.tool.name)
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
// 特定のツールエラーを処理
}
}
コアパターン — スナップショットストリーミング
PartiallyGenerated 型を使用して、構造化されたレスポンスをリアルタイムUIにストリーミング:
@Generable
struct TripIdeas {
@Guide(description: "今後の旅行のアイデア")
var ideas: [String]
}
let stream = session.streamResponse(
to: "エキサイティングな旅行のアイデアはありますか?",
generating: TripIdeas.self
)
for try await partial in stream {
// partial: TripIdeas.PartiallyGenerated (すべてのプロパティは Optional)
print(partial)
}
SwiftUI との統合
@State private var partialResult: TripIdeas.PartiallyGenerated?
@State private var errorMessage: String?
var body: some View {
List {
ForEach(partialResult?.ideas ?? [], id: \.self) { idea in
Text(idea)
}
}
.overlay {
if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
}
.task {
do {
let stream = session.streamResponse(to: prompt, generating: TripIdeas.self)
for try await partial in stream {
partialResult = partial
}
} catch {
errorMessage = error.localizedDescription
}
}
}
主要な設計上の決定
| 決定 | 根拠 |
|---|---|
| オンデバイス実行 | プライバシー — データがデバイス外に出ない;オフラインでも動作 |
| 4,096 トークン制限 | オンデバイスモデルの制約;大きなデータはセッション間で分割 |
| スナップショットストリーミング(デルタではない) | 構造化出力に適している;各スナップショットは完全な部分状態 |
@Generable マクロ | 構造化生成のためのコンパイル時安全性;PartiallyGenerated 型を自動生成 |
| セッションあたり1リクエスト | isResponding が同時リクエストを防止;必要に応じて複数セッションを作成 |
response.content(.output ではない) | 正しいAPI — 常に .content プロパティを介して結果にアクセス |
ベストプラクティス
- セッションを作成する前に必ず
model.availabilityを確認 — すべての利用不可ケースを処理する instructionsを使用してモデルの動作をガイドする — これらはプロンプトよりも優先される- 新しいリクエストを送信する前に
isRespondingを確認 — セッションは一度に1つのリクエストを処理する - 結果には
response.contentを使用 —.outputは使用しない - 大きな入力はチャンクに分割 — 4,096トークン制限は instructions + prompt + output の合計に適用される
- 構造化出力には
@Generableを使用 — 生の文字列を解析するよりも強力な保証を提供 GenerationOptions(temperature:)を使用して創造性を調整(値が高いほど創造的)- Instruments で監視 — Xcode Instruments を使用してリクエストのパフォーマンスをプロファイリングする
避けるべきアンチパターン
model.availabilityを確認せずにセッションを作成する- 4,096トークンのコンテキストウィンドウを超える入力を送信する
- 単一のセッションで同時リクエストを試みる
- レスポンスデータにアクセスする際に
.outputではなく.contentを使用しない @Generableによる構造化出力で十分な場合に、生の文字列レスポンスを解析する- 単一のプロンプトで複雑なマルチステップロジックを構築する — 複数の焦点を絞ったプロンプトに分割する
- モデルが常に利用可能であると想定する — デバイスの適合性と設定は異なる
使用するタイミング
- プライバシー重視のアプリ向けオンデバイステキスト生成
- ユーザー入力(フォーム、自然言語コマンド)からの構造化データ抽出
- オフラインでも動作する必要があるAI支援機能
- 生成されたコンテンツを徐々に表示するストリーミングUI
- ツール呼び出しによるドメイン固有のAIアクション(検索、計算、ルックアップ)