0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Foundation Models】WWDCから体系的に理解していきたいメモ

0
Posted at

本記事の概要

Appleの「Foundation Models」フレームワークについて、WWDC25・WWDC26の主要セッションや関連動画の内容を包括的にまとめたメモです。

※個人学習用にAIを活用して整理したもので、実際の開発ノウハウというよりはセッション内容の包括的な記録・まとめたものになります。
※ボリュームがありますので、目次から気になるセッションをピックアップしてお読みください。

Foundation Models フレームワークの詳細 (WWDC25)

AppleのオンデバイスAIコア技術であるFoundation Modelsフレームワークを利用すると、macOS、iPadOS、iOS、visionOS上で完全にローカルで動作するLLMをSwift API経由で直接操作できます。外部サーバに依存しないため、プライバシーに配慮したインタラクティブな体験を低遅延で実現可能です。


1. セッションの基本とテキスト生成 (LanguageModelSession)

テキスト生成の基本単位となるのが LanguageModelSession です。

基本的な実装パターン

セッション初期化時に instructions(役割や行動の定義)を与え、respond(to:) でユーザーからのプロンプト(プロンプト入力)を渡します。

import FoundationModels

func respond(userInput: String) async throws -> String {
    let session = LanguageModelSession(
        instructions: """
        You are a friendly barista in a world full of pixels.
        Respond to the player’s question.
        """
    )
    let response = try await session.respond(to: userInput)
    return response.content
}

生成の内部メカニズムとコスト

  1. トークン化: 入力された指示やプロンプトはトークン(数文字程度の小さな単位)に分解されます。
  2. トークンコスト: モデルが応答生成を開始するには、まず全ての入力トークンを処理(プレフィル)する必要があります。入力・出力トークン数が増えるほどレイテンシーおよび計算コストが増加します。

2. コンテキストウィンドウとエラーハンドリング

LanguageModelSession はステートフル(状態保持)であり、全会話履歴は transcript(トランスクリプト)に記録されます。

コンテキスト上限と復旧処理

長時間の会話によりコンテキスト制限を超えると、exceededContextWindowSize エラーがスローされます。このエラーは捕捉して適切に処理する必要があります。

① 会話をリセットするシンプルな方法

var session = LanguageModelSession()

do {
    let answer = try await session.respond(to: prompt)
    print(answer.content)
} catch LanguageModelSession.GenerationError.exceededContextWindowSize {
    // 履歴を持たない新しいセッションを開始
    session = LanguageModelSession()
}

② 重要なコンテキスト(指示+最新応答)を引き継ぐ方法

キャラクターの記憶を完全に途絶えさせないよう、トランスクリプトの「最初の指示」と「直近の応答」を抽出して要約した新しいセッションを作成します。

var session = LanguageModelSession()

do {
    let answer = try me.respond(to: prompt)
    print(answer.content)
} catch LanguageModelSession.GenerationError.exceededContextWindowSize {
    // 過去セッションのトランスクリプトを圧縮して引き継ぐ
    session = newSession(previousSession: session)
}

private func newSession(previousSession: LanguageModelSession) -> LanguageModelSession {
    let allEntries = previousSession.transcript.entries
    var condensedEntries = [Transcript.Entry]()
    
    // 重要なポイント:必ず最初の「指示(instructions)」を含める
    if let firstEntry = allEntries.first {
        condensedEntries.append(firstEntry)
        if allEntries.count > 1, let lastEntry = allEntries.last {
            condensedEntries.append(lastEntry)
        }
    }
    
    let condensedTranscript = Transcript(entries: condensedEntries)
    return LanguageModelSession(transcript: condensedTranscript)
}

※より長い文脈を扱う場合、外部ライブラリやLLM自体を用いてトランスクリプトを「要約」してから引き継ぐアプローチも有効です。


3. サンプリング制御と言語サポート

サンプリング手法の制御 (GenerationOptions)

デフォルトではランダムサンプリングにより多様な応答を出力します。決定論的(確実的)な応答を得たい場合や創造性を調整したい場合は GenerationOptions を使用します。

// 決定論的出力(デモや再現性が必要な用途向け)
let response = try await session.respond(
    to: prompt,
    options: GenerationOptions(sampling: .greedy)
)

// 低バリアンス出力(応答のブレを抑える)
let response = try await session.respond(
    to: prompt,
    options: GenerationOptions(temperature: 0.5)
)

// 高バリアンス出力(多様でクリエイティブな応答)
let response = try await session.respond(
    to: prompt,
    options: GenerationOptions(temperature: 2.0)
)

注意: greedy サンプリングを指定しても、OSアップデート等でオンデバイスのモデルバージョンが更新された場合は出力結果が変わる可能性があります。

サポート対象外言語のハンドリング

ユーザーの言語設定にモデルが未対応の場合は、unsupportedLanguageOrLocale エラーを検出するか事前にシステム言語設定をチェックできます。

var session = LanguageModelSession()

do {
    let answer = try await session.respond(to: userInput)
    print(answer.content)
} catch LanguageModelSession.GenerationError.unsupportedLanguageOrLocale {
    // 未対応言語の場合のUI表示処理など
}

// 事前チェック
let supportedLanguages = SystemLanguageModel.default.supportedLanguages
guard supportedLanguages.contains(Locale.current.language) else {
    // 未対応言語の旨を表示
    return
}


4. コンパイル時構造化出力 (@Generable と ガイド)

大規模言語モデルからの出力をそのまま文字列として解釈(パース)するのはエラーの元です。Foundation Modelsでは、Swiftの型安全性を保ったまま構造化データを得る @Generable マクロが提供されています。

制約付きデコーディング (Constrained Decoding) の仕組み

マクロによって作られたスキーマを基に、モデルのデコードループ段階で無効なトークン(例:存在しないプロパティ名など)を除外(マスク)します。これにより手動パース作業を行うことなく100%型安全なデータ構造が得られます。

基本的な @Generable 構造体と列挙型

Struct や Enum(関連値つき)に対して付与できます。

@Generable
struct NPC {
    let name: String
    let encounter: Encounter

    @Generable
    enum Encounter {
        case orderCoffee(String)
        case wantToTalkToManager(complaint: String)
    }
}

func makeNPC() async throws -> NPC {
    let session = LanguageModelSession(instructions: "Generate fun NPCs")
    
    // generating引数にSwift型を渡す
    let response = try await session.respond(generating: NPC.self) {
        "Generate a character that orders a coffee."
    }
    return response.content // NPC型のインスタンスが得られる
}

プロパティの生成順序に関する重要ルール

@Generable 型のプロパティはソースコード上に記述された「定義順」に生成されます。
前述の例では name → encounter の順に生成されます。文脈が後のプロパティに影響する場合は定義順に配慮してください。また、プロパティごとのストリーミング受信にも活用できます。


5. ガイド (@Guide) による詳細生成制御

プロパティに対して生成制約(範囲・個数・正規表現・自然言語プロンプト)を付与できます。

@Guide 一覧と使用例

ガイドの指定方法 対象型 説明・用途
@Guide(description: "...") 全般 自然言語によるプロパティ個別のアノテーション
@Guide(.range(1...10)) 整数等 (Int) 生成範囲の上限・下限の限定
@Guide(.count(3)) 配列 (Array) 配列要素の固定数の指定
@Guide(Regex { ... }) 文字列 (String) 正規表現パターンによるフォーマット制御
@Generable
struct NPC {
    // 自然言語プロンプトの指定
    @Guide(description: "A full name")
    let name: String

    // 数値範囲の制限 (1〜10)
    @Guide(.range(1...10))
    let level: Int

    // 配列要素数の制限 (ちょうど3個)
    @Guide(.count(3))
    let attributes: [Attribute]

    let encounter: Encounter

    @Generable
    enum Attribute {
        case sassy
        case tired
        case hungry
    }

    @Generable
    enum Encounter {
        case orderCoffee(String)
        case wantToTalkToManager(complaint: String)
    }
}

Regex(正規表現)ガイド

正規表現ビルダー構文(Swift Regex Builder)を使用して出力フォーマットを強力に制約できます。

@Generable
struct NPC {
    // "Mr. " または "Mrs. " から始まる名前に制約
    @Guide(Regex {
        Capture {
            ChoiceOf {
                "Mr"
                "Mrs"
            }
        }
        ". "
        OneOrMore(.word)
    })
    let name: String
}

let response = try await session.respond(to: "Generate a fun NPC", generating: NPC.self)
// 例: { name: "Mrs. Brewster" }


6. 実行時の動的構造化出力 (DynamicGenerationSchema)

ユーザー入力やゲームレベルエディタなど、コンイル時に型が決定しない動的なデータ構造が必要な場合は DynamicGenerationSchema を使用します。

動的スキーマ構築・検証手順

  1. DynamicGenerationSchema でプロパティや配列要素を組み上げる。
  2. 他の動的スキーマの参照には referenceTo: を使用。
  3. 推論実行前に GenerationSchema(root:dependencies:) で検証(Validation)を実施。
struct LevelObjectCreator {
    var name: String
    var properties: [DynamicGenerationSchema.Property] = []

    mutating func addStringProperty(name: String) {
        let property = DynamicGenerationSchema.Property(
            name: name,
            schema: DynamicGenerationSchema(type: String.self)
        )
        properties.append(property)
    }

    mutating func addArrayProperty(name: String, customType: String) {
        let property = DynamicGenerationSchema.Property(
            name: name,
            schema: DynamicGenerationSchema(
                arrayOf: DynamicGenerationSchema(referenceTo: customType)
            )
        )
        properties.append(property)
    }
    
    var root: DynamicGenerationSchema {
        DynamicGenerationSchema(name: name, properties: properties)
    }
}

// 1. 動的スキーマの定義 (例: Riddle構造)
var riddleBuilder = LevelObjectCreator(name: "Riddle")
riddleBuilder.addStringProperty(name: "question")
riddleBuilder.addArrayProperty(name: "answers", customType: "Answer")

var answerBuilder = LevelObjectCreator(name: "Answer")
answerBuilder.addStringProperty(name: "text")
// (※ addBoolPropertyでisCorrect等を追加)

// 2. スキーマの検証と構築
let riddleDynamicSchema = riddleBuilder.root
let answerDynamicSchema = answerBuilder.root

let schema = try GenerationSchema(
    root: riddleDynamicSchema,
    dependencies: [answerDynamicSchema]
)

// 3. 生成と動的プロパティ取得
let session = LanguageModelSession()
let response = try await session.respond(
    to: "Generate a fun riddle about coffee",
    schema: schema
)

let generatedContent = response.content
let question = try generatedContent.value(String.self, forProperty: "question")
let answers = try generatedContent.value([GeneratedContent].self, forProperty: "answers")


7. ツール呼び出し (Tool Calling)

ツール呼び出しを利用することで、オンデバイスモデルが必要に応じてアプリローカルのコード(Contacts、EventKit、外部データ取得処理など)を自律的に呼び出せます。全ての処理がオンデバイスで行われるためプライバシーが担保されます。

ツールのベストプラクティス

  • 名前: 短くわかりやすい英語表記の動詞にする(例:findContact)。略語は避ける。
  • 説明: 1文程度でシンプルに記述(プロンプトに直挿入されるため、長文になるとトークンコストと遅延が増加する)。
  • 入力引数: Arguments 構造体を @Generable にして制約をかける。

ステートレスなツール実装

import FoundationModels
import Contacts

struct FindContactTool: Tool {
    let name = "findContact"
    let description = "Finds a contact from a specified age generation."
        
    @Generable
    struct Arguments {
        let generation: Generation
            
        @Generable
        enum Generation {
            case babyBoomers
            case genX
            case millennial
            case genZ            
        }
    }
    
    func call(arguments: Arguments) async throws -> ToolOutput {
        let store = CNContactStore()
        let keysToFetch = [CNContactGivenNameKey, CNContactBirthdayKey] as [CNKeyDescriptor]
        let request = CNContactFetchRequest(keysToFetch: keysToFetch)

        var contacts: [CNContact] = []
        try store.enumerateContacts(with: request) { contact, _ in
            if let year = contact.birthday?.year, arguments.generation.yearRange.contains(year) {
                contacts.append(contact)
            }
        }
        guard let pickedContact = contacts.randomElement() else {
            return ToolOutput("Could not find a contact.")
        }
        return ToolOutput(pickedContact.givenName)
    }
}

// セッションへの登録
let session = LanguageModelSession(
    tools: [FindContactTool()],
    instructions: "Generate fun NPCs"
)

ステートフル(状態保持)なツール実装

ツール呼び出しの間で状態(過去に選んだ連絡先の記録など)を保持・管理したい場合は、Tool プロトコルを class(参照型)に適用します。

import FoundationModels
import Contacts

class FindContactTool: Tool {
    let name = "findContact"
    let description = "Finds a contact from a specified age generation."
   
    // 選択済みの連絡先を追跡する内部状態
    var pickedContacts = Set<String>()
    
    // ... Argumentsの定義 ...

    func call(arguments: Arguments) async throws -> ToolOutput {
        // 重複を除外するロジック
        contacts.removeAll(where: { pickedContacts.contains($0.givenName) })
        guard let pickedContact = contacts.randomElement() else {
            return ToolOutput("Could not find a contact.")
        }
        pickedContacts.insert(pickedContact.givenName)
        return ToolOutput(pickedContact.givenName)
    }
}

イベント取得ツールの例 (GetContactEventTool)

import FoundationModels
import EventKit

struct GetContactEventTool: Tool {
    let name = "getContactEvent"
    let description = "Get an event with a contact."

    let contactName: String
        
    @Generable
    struct Arguments {
        let day: Int
        let month: Int
        let year: Int
    }
        
    func call(arguments: Arguments) async throws -> ToolOutput {
        // EventKitから指定日時のイベントを取得してToolOutputで返す処理
        // ...
    }
}

ツール実行のタイムラインと注意点

  1. コンテキスト推論: モデルはユーザープロンプトと instructions(例:現在の日時など)を照らし合わせ、ツールを呼ぶべきか・どんな引数にするか(例: "Tomorrow" から日付を計算)を自律判定します。
  2. 同期・並列処理: ツール実行中、セッションはツールからのレスポンスを待機します。1つのリクエストで複数のツールが要求された場合、並列で実行されるため、可変状態を扱う際はスレッドセーフに配慮が必要です。
  3. トランスクリプト追加: ツールの出力結果はトランスクリプトに記録され、それを基にモデルが最終的な回答を生成します。

8. 全体まとめ・機能比較

機能 コンパイル時 / 実行時 主な用途・特徴
LanguageModelSession - オンデバイスLLMとの会話セッション管理。transcriptで履歴保持
@Generable コンパイル時 Swiftの struct / enum を型安全に出力。制約付きデコーディングで完全にパースレス
@Guide コンパイル時 数値範囲、要素数制限、正規表現、自然言語記述によるデータ制約
DynamicGenerationSchema 実行時 ユーザー定義のデータ構造や動的UI/パズルのための可変スキーマ生成
Tool Calling 実行時 オンデバイスでローカルデータ(連絡先・カレンダー等)をモデルから自律利用

Foundation Models フレームワークの新機能 (WWDC26)

Foundation Modelsフレームワークは、OS間・モデル間・プラットフォーム間の連携を推し進め、オープンソース化を含めたメジャーアップデートを遂げました。

本記事では、新登場したオンデバイスモデルからプライベートクラウドコンピューティング(PCC)、Vision機能、サードパーティ製LLMの統合、Dynamic Profileによるエージェント構築、Evaluation(評価)フレームワーク、各種CLI・Python SDKツールまでを完全網羅して解説します。


1. オンデバイスモデルの進化 & トークン管理機能

オンデバイスで動作するシステム言語モデル(SystemLanguageModel)がゼロから再構築され、論理的思考能力(ロジック)やツール呼び出しの精度、ガードレール(安全性の向上・誤検知の削減)が大幅に向上しました。

コンテキストサイズとトークン数の取得

モデルのコンテキスト制限(サイズ)を取得するプロパティや、プロンプト・テキストのトークン数を事前に測定するAPIが追加されました。これにより実行環境に応じた最適な処理調整が可能になります。

let model = SystemLanguageModel()

// モデルがサポートするコンテキストサイズ(例: 8192トークン)を取得
print(model.contextSize) // 8192

// 指定したプロンプトのトークン数を計算
let count = try await model.tokenCount(for: "What are the Japanese characters for origami?")
print(count)


2. Vision(画像理解)機能のネイティブサポート

オンデバイスモデルで画像を直接理解できるマルチモダリティ機能が追加されました。

画像の添付方法

プロンプトビルダー(Prompt Builder)の拡張により、Attachment を使用してテキストと共に画像を直感的に渡すことができます。モデルは入力画像の比率やサイズをそのまま解釈可能です(事前トリミングやパディングは不要)。

// さまざまな画像型(UIImage, NSImage, CGImage, CIImage, CVPixelBuffer, ファイルURLなど)を添付可能
let response = try await session.respond {
    "What animal is this?"
    Attachment(UIImage(named: "origami_craft.jpg")!)
}

パフォーマンス上の注意点: 画像は解像度に応じたトークンを消費します。大きな画像ほど入力トークン数が増え、レイテンシー(応答時間)が大きくなるため適度なサイズ調整が推奨されます。


3. Private Cloud Compute (PCC) と手首でのAI体験

より高度な知能や長文コンテキストが必要な場合、Appleの最上位サーバーモデルを利用できる PrivateCloudComputeLanguageModel が提供されます。

特徴とスペック

  • コンテキストウィンドウ: 32,000トークン
  • 高度な推理(Reasoning): 応答出力前に思考・計画を深める専用モデルを搭載
  • シームレス&プライベート: APIキーやアカウント設定、認証処理が一切不要。プロンプトは一切保存されず独立した第三者機関によりプライバシーが検証済み
  • watchOS対応: watchOS からもPCC経由で最先端AI機能を利用可能
  • 無償アクセス枠: 初回ダウンロード200万件未満のアプリデベロッパに対してAPIコストなしで提供(iCloud+加入ユーザーは利用上限の優遇あり)

実装と ReasoningLevel の指定

import FoundationModels

// PCCモデルの初期化
let pccModel = PrivateCloudComputeLanguageModel()

let session = LanguageModelSession(model: pccModel)

// reasoningLevel で思考の深さをコントロール (.light / .deep など)
let response = try await session.respond(
    to: "Plan a complex crafting workshop schedule.",
    contextOptions: ContextOptions(reasoningLevel: .deep)
)


4. モデル抽象化レイヤー と サードパーティ / オープンソースモデルの統合

新しい LanguageModel プロトコルを中心に抽象化レイヤーが整理され、ローカル/クラウド問わず多種多様なモデルを LanguageModelSession のバックエンドとして透過的に扱えるようになります。

サードパーティモデルの利用

AnthropicやGoogleなどのパートナーが公開するSwiftパッケージや、MacのApple Neural Engine / GPU上でローカル動作するオープンソースパッケージ(CoreAILanguageModel や MLXLanguageModel)を簡単にセッションへ組み込めます。

import FoundationModels
import ThirdPartyModelProvider // 例: Anthropic / Google パッケージなど

let customModel = ThirdPartyLanguageModel(apiKey: "...") // セキュアに保存したKey/Tokenを渡す
let session = LanguageModelSession(model: customModel)

let response = try await session.respond(to: "Hello!")

トークン使用量(Usage)の可視化

サードパーティ製モデルやPCC利用時のコスト追跡のために、セッションや応答結果(response.usage)から入力・出力のトークン詳細を取得できます。

let response = try await session.respond(
    to: "Recommend a craft that doesn't require scissors.",
    contextOptions: ContextOptions(reasoningLevel: .light)
)

// 入力トークン詳細
print("Input Total Token Count: \(response.usage.input.totalTokenCount)")
print("Input Cached Token Count: \(response.usage.input.cachedTokenCount)")

// 出力トークン詳細
print("Output Total Token Count: \(response.usage.output.totalTokenCount)")
print("Output Reasoning Token Count: \(response.usage.output.reasoningTokenCount)")


5. 新しいシステムツール (Vision & Spotlight)

標準で提供されるネイティブツールにより、モデルの推論能力とプラットフォーム機能を強力に連携させることができます。

  1. BarcodeReaderTool: 画像からバーコード/QRコード情報を読み取る。
  2. OCRTool: Visionフレームワークを用いて画像内の構造化テキストを抽出する。
  3. Spotlight検索ツール (RAG): アプリローカルの個別情報やドメイン知識をモデルが探索できるようにする、完全オンデバイスな Retrieval-Augmented Generation (RAG) を実現。

6. エージェントアプリ構築のための Dynamic Profile

従来、アプリのモード(「分析モード」と「アイデア発想モード」など)に応じて指示(Instructions)やツール(Tools)、さらには使用するモデル自体を切り替えるには、膨大なボイラープレートコードや手動の会話履歴(Transcript)の引き継ぎ管理が必要でした。

従来の命令型アプローチの問題点(ボイラープレート)

@Observable
final class AppStates {
    var mode: Mode
}

let appStates: AppStates
var session: LanguageModelSession?

func updateSession() {
    // 指示の重複を防ぎつつ会話履歴を抽出
    let originalTranscript = session?.transcript.dropFirstInstructions() ?? Transcript()

    // モード変更のたびにセッションを手動で再構築
    switch appStates.mode {
    case .craftAnalysis:
        session = LanguageModelSession(
            tools: [
                RecordImageAnalysisTool(),
                SwitchModeTool(states: appStates)
            ],
            instructions: "Analyze the user's craft project...",
            transcript: originalTranscript
        )
    case .brainstorm:
        session = LanguageModelSession(
            tools: [
                RecordBrainstormTool(),
            ],
            instructions: "Brainstorm some ideas...",
            transcript: originalTranscript
        )
    }
}

// モード切り替えを自動検知して再構築
withObservationTracking {
    appStates.mode
} onChange: {
    updateSession()
}

宣言的アプローチ:DynamicProfile の活用

新しく導入された DynamicProfile を使用すると、SwiftUIのような宣言的記法でアプリの状態に応じて「指示・ツール・モデル・思考レベル」を自動的かつ動的に切り替えることができます。会話履歴(コンテキスト)の維持やセッションの移行処理はフレームワークが透過的に処理します。

import FoundationModels

struct CraftProfile: LanguageModelSession.DynamicProfile {
    let states: CraftProjectStates

    var body: some DynamicProfile {
        switch states.mode {
        case .craftAnalysis:
            // 高速なオンデバイスモデルで画像を解析
            Profile {
                Instructions {
                    """
                    You are an expert crafting assistant.
                    Record craft project image analyses using the recordImageAnalysis tool.
                    """
                }
                RecordImageAnalysisTool()
                SwitchModeTool(states: states)
            }
            
        case .brainstorm:
            // 深い推論が必要な発想フェーズでは PCC (Private Cloud Compute) を使用
            Profile {
                Instructions {
                    "Brainstorm creative project ideas using the knowledge learned so far."
                }
                BrainstormRecordTool()
            }
            .model(states.privateCloudCompute)  // タスクに合わせてモデルを動的に変更
            .reasoningLevel(.deep)              // 思考レベルを指定
        }
    }
}

// 初期化は DynamicProfile を渡すだけ
let session = LanguageModelSession(
    profile: CraftProfile(states: appStates)
)


7. 開発・評価(Evaluation)ツールとエコシステム

① Evaluation フレームワーク

確率的で非決定論的なモデル出力に対して、プロンプトやパラメータ調整が精度に与える影響を統計的に追跡・計測するための新しい評価フレームワークが追加されました。

② CLI ツール (fm)

macOS 27 から標準コマンドラインツール fm が提供されます。ターミナル上で直接オンデバイスモデルやPCCを操作し、スクリプトへの組み込みやプロンプト開発が行えます。

  • チャットセッション:
fm chat

  • ワンライナーでの処理(パイプライン連携):
    画像を内容に沿ったファイル名へリネームするスクリプト等に応用可能です。

③ Foundation Models SDK for Python

データサイエンティストや研究者向けに、Swift版と同一のオンデバイスモデルへアクセスできるPython SDKが提供されます。

import apple_fm_sdk as fm

model = fm.SystemLanguageModel()

# モデルの利用可能性を確認
is_available, reason = model.is_available()

if is_available:
    # セッションの作成
    session = fm.LanguageModelSession(model=model)

    # 非同期で応答の生成
    response = await session.respond(prompt="Hello!")
    print(response)

④ オープンソース化と Foundation Models Framework Utilities

  • フレームワーク本体のオープンソース化: Swiftが動作するあらゆる環境(Linuxサーバーなど)で機能します。
  • FoundationModelsUtilities パッケージ: トランスクリプト管理、プロファイルモディファイア、手続き型知識読み込み(Skill API)、標準的なChat Completionエンドポイント経由でのサーバー連携機能など、新興のLLM開発プラクティスが実験的コンポーネントとしてOSS公開されています。

8. まとめ・機能マップ

機能 / API 概要・対象プラットフォーム ユースケース
新 SystemLanguageModel オンデバイス (iOS/macOS/iPadOS/visionOS) 高速・プライベートなテキスト生成・Vision画像解析
PrivateCloudComputeLanguageModel PCC クラウド / watchOS対応 32kコンテキスト、高度な推論 (Reasoning) が必要なタスク
LanguageModel Protocol オープン抽象化レイヤー Anthropic, Google, MLX, CoreAI等のモデル統合
システムツール (Vision / Spotlight) ネイティブツール機能 OCR, バーコード読み取り, 完全ローカルRAG検索
Dynamic Profile 宣言的セッション構築API モード切替やモデル変更を伴う複雑なエージェントアプリ開発
Evaluation Framework 評価・ベンチマークツール プロンプト改善と応答精度の統計的測定
fm CLI / Python SDK macOS / Python 開発環境 シェルスクリプト連携、データサイエンスワークフロー

Foundation Modelsフレームワークによるエージェント活用アプリの構築 (WWDC26)

AppleのFoundation Modelsフレームワークでは、DynamicProfile や DynamicInstructions といった新しい宣言的プリミティブが提供され、複雑なエージェント型ワークフロー(Agentic Workflows)、マルチモデル間のコンテキスト制御、履歴の最適化が容易に行えるようになりました。

本記事では、セッションの基本設計思想から、宣言的Profileの設計、コンテキスト管理・要約手法、エージェント・オーケストレーションパターン(Baton-pass / Phone-a-friend / Skills)、ツール呼び出し制御、そしてKVキャッシュなどのパフォーマンス考慮事項までを徹底解説します。


1. エージェント設計の基礎課題と「FoundationModelsUtilities」

エージェント型アプリの構築における課題は、主に以下の2点に集約されます。

  1. コンテキスト管理: 長時間のセッションでコンテキストウィンドウの上限(Context Window Limit)を超えないよう、トランスクリプトを適切にトリミング・要約・フィルタリングする必要がある。
  2. モデル境界とコスト: タスクの難易度や要求される知能、レイテンシー、コスト(プライバシー境界含む)に応じて、オンデバイスモデルとサーバーモデル(PCCなど)を適切に使い分ける必要がある。

これらを解決するために提供されているのが FoundationModelsUtilities(オープンソースのSwiftパッケージ)です。このユーティリティはOSリリースの合間にも更新され、新興のLLM開発プラクティスや実験的コンポーネントを提供します。


2. 宣言的 API: DynamicInstructions と DynamicProfile

UIにおけるSwiftUIのように、モデルの指示(Instructions)やツール(Tools)、モディファイアを宣言的に組み立てる構造が導入されました。

① 再利用可能な指示コンポーネント (DynamicInstructions)

指示とツールを1つの再利用可能なコンポーネントとしてグループ化できます。条件付きロジック(if 文)やネストにも対応します。

import FoundationModels

struct BrainstormFacilitator: DynamicInstructions {
    var orchestrator: CraftOrchestrator

    var body: some DynamicInstructions {
        Instructions {
            "You are a warm and friendly expert crafting brainstorm facilitator."
        }
        
        // ツールの登録
        GenerateProjectTitle()
        
        // 条件付きでドメイン知識やツールを注入 (例: 折り紙)
        if orchestrator.techniques.contains(.origami) {
            OrigamiExpert()
        }
    }
}

② モード別のProfile宣言とモデル切り替え (DynamicProfile)

アプリの状態(例:orchestrator.mode)に応じて、自動的に適用する指示・ツール・モデル・パラメータを安全に切り替えます。

struct CraftProfile: LanguageModelSession.DynamicProfile {
    var orchestrator: CraftOrchestrator

    var body: some DynamicProfile {
        switch orchestrator.mode {
        case .brainstorming:
            // 創造的なアイデア出しには PCC (Private Cloud Compute) を使用し、temperatureを高く設定
            Profile { BrainstormFacilitator(orchestrator: orchestrator) }
                .model(orchestrator.pccLanguageModel)
                .temperature(1)

        case .planning:
            // 詳細なチュートリアル作成には PCC の深い推論 (deep) を適用
            Profile { TutorialAuthor(orchestrator: orchestrator) }
                .model(orchestrator.pccLanguageModel)
                .reasoningLevel(.deep)

        case .reviewing:
            // オンデバイスモデル(SystemLanguageModel)で高速かつオフラインでアドバイス
            Profile { CraftCoach() }
                .model(orchestrator.systemLanguageModel)
        }
    }
}

// セッションの初期化
let session = LanguageModelSession(
    profile: CraftProfile(orchestrator: orchestrator)
)

ポイント: モデルへプロンプトが送信されるたびに DynamicProfile の body が再評価され、帽子を掛け替えるようにモデルのペルソナ(およびモデル自体のバックエンド)が切り替わります。


3. コンテキスト管理とトランスクリプト変形 (Transcript Management)

モデルごとにコンテキストサイズが異なったり、サーバーモデルからオンデバイスモデルへ渡す際に個人情報や不要なツールログを削る必要が生じます。

① historyTransform(非破壊的なローカル変換)

モデルにプロンプトを送信する直前にのみ履歴(history)を変換します。セッション全体の永続的なトランスクリプトは破壊されません。

struct CraftProfile: LanguageModelSession.DynamicProfile {
    var orchestrator: CraftOrchestrator

    var body: some DynamicProfile {
        switch orchestrator.mode {
        case .reviewing:
            Profile { CraftCoach() }
                .model(orchestrator.systemLanguageModel)
                .historyTransform { history in
                    // 最新応答直前までの完了済みツール呼び出しログを削除して軽量化
                    guard let latestResponseIndex = lastResponseEntryIndex(history) else {
                        return history
                    }
                    let filteredHistory = history[0..<latestResponseIndex].filter { entry in
                        isToolCallsOrToolOutput(entry)
                    }
                    return filteredHistory + history[latestResponseIndex...]
                }
        }
    }
}

② カスタムモディファイア (DynamicProfileModifier) の作成

historyTransform などの複雑な変換ロジックは、カスタムモディファイアとしてカプセル化し、宣言的に再利用できます。

// カスタム修飾子の定義
struct DroppingToolCallsProfileModifier: LanguageModelSession.DynamicProfileModifier {
    func body(content: Content) -> some DynamicProfile {
        content.historyTransform { history in
            guard let latestResponseIndex = lastResponseEntryIndex(history) else {
                return history
            }
            let filteredHistory = history[0..<latestResponseIndex].filter { entry in
                isToolCallsOrToolOutput(entry)
            }
            return filteredHistory + history[latestResponseIndex...]
        }
    }
}

extension LanguageModelSession.DynamicProfile {
    func droppingCompletedToolCalls() -> some DynamicProfile {
        self.modifier(DroppingToolCallsProfileModifier())
    }
}

// 使用例 (FoundationModelsUtilities で提供される .rollingWindow 等との組み合わせ)
Profile { CraftCoach() }
    .rollingWindow(size: .entries(10)) // 直近10件のみ保持
    .droppingCompletedToolCalls()


4. ライフサイクルモディファイア と セッションプロパティ (Session Properties)

セッション全体で共有したい状態(要約テキストなど)や応答後の破壊的な履歴更新には、ライフサイクルモディファイア と セッションプロパティ を使用します。

① カスタムセッションプロパティの宣言

@SessionPropertyEntry マクロを使用して、ProfileやToolからアクセス可能なグローバル状態を定義します。

extension SessionPropertyValues {
    @SessionPropertyEntry var summary: String?
}

② onResponse による状態更新と破壊的履歴削減

historyTransform(非破壊的)とは異なり、@SessionProperty(\.history) や @SessionProperty(\.summary) への書き込みはセッション全体に永続的に反映されます。

struct CraftProfile: LanguageModelSession.DynamicProfile {
    @SessionProperty(\.history) var history
    @SessionProperty(\.summary) var summary
    var orchestrator: CraftOrchestrator

    var body: some DynamicProfile {
        switch orchestrator.mode {
        case .planning:
            Profile {
                TutorialAuthor(orchestrator: orchestrator)
                // 過去に要約されたテキストが存在すれば指示に含める
                if let summary {
                    Instructions { "Summary: \(summary)" }
                }
            }
            .onResponse {
                // 会話が50件を超えたら古い履歴を要約して永続的に切り詰める
                if history.count > 50, let responseIndex = lastResponse(history.prefix(40)) {
                    summary = try await summarize(history[0..<responseIndex])
                    history = history[responseIndex...] // 永続的書き換え
                }
            }
        }
    }
}


5. エージェント・オーケストレーション パターン

複数のモデルや専門タスクを協調動作させるための代表的なパターンです。

パターン 1: バトンパス (Baton-Pass)

  • 特徴: 1つのメインセッションのまま会話履歴(Transcript)を共有しつつ、ツールの実行を契機に次のProfile(およびモデル)へ主導権(バトン)を渡すコラボレーション型。
  • メリット: 両方のProfileから過去の会話文脈がすべて見える。
struct CraftProfile: LanguageModelSession.DynamicProfile {
    var orchestrator: CraftOrchestrator

    var body: some DynamicProfile {
        switch orchestrator.mode {
        case .brainstorm:
            Profile {
                BrainstormInstructions()
                BatonPassTool()
            }
            .onToolCall { orchestrator.mode = .tutorial } // モード変更により次のProfileへ遷移
            .model(orchestrator.serverModel)

        case .tutorial:
            Profile {
                TutorialInstructions()
                BatonPassTool()
            }
            .onToolCall { orchestrator.mode = .brainstorm }
            .model(orchestrator.systemModel)
        }
    }
}

パターン 2: 友人への電話 (Phone-a-Friend)

  • 特徴: メインセッションとは独立した短命(Ephemeral)なサブセッションをツール内部で立ち上げ、専門タスクの問い合わせ(コンサルテーション)を行うパターン。
  • メリット: コンテキスト(履歴)が完全に分離・カプセル化され、親セッションのコンテキストを汚さない。
struct PhoneFriendTool<P: LanguageModelSession.DynamicProfile>: Tool {
    let name: String
    let description: String
    let profile: () -> P

    func call(arguments: GeneratedContent) async throws -> String {
        // 短命な独立セッションを作成して実行
        let session = LanguageModelSession(profile: profile())
        let response = try await session.respond(to: arguments)
        return response.content // 結果のみを親セッションのツール出力として返す
    }
}

// 親Profileでの組み込み
Profile {
    BrainstormInstructions()
    PhoneFriendTool(
        name: "generate_title",
        description: "Generate a creative project title",
        profile: { TitleProfile() }
    )
}

パターン 3: スキルパターン (Skills Pattern)

手続き型の知識(Procedural Knowledge)やコンテキストを動的に読み込むためのコンポーネントです(FoundationModelsUtilities パッケージに含まれます)。

struct CraftingSkills: LanguageModelSession.DynamicInstructions {
    var activations: SkillActivations

    var body: some DynamicInstructions {
        Skills(activations: activations) {
            Skill(
                name: "origami_folds",
                description: "Details about specific types of folds",
                prompt: """
                    Valley Fold: Paper is folded toward you, creating a V-shaped crease.
                    Mountain Fold: Paper is folded away from you, creating an inverted V.
                    """
            )
        }
    }
}


6. ツール呼び出しモード (Tool Calling Mode) と エラーハンドリング

ツール呼び出しモードの指定

モデルがツールを起動する挙動をコントロールできます。

モード 動作 主な用途
.allowed (デフォルト) ツール呼び出しまたは直接テキスト回答のいずれかを自律選択 通常の会話
.disallowed ツール呼び出しを禁止し、テキスト回答のみを強制 ツール適用外のフェーズ
.required 応答において必ずツールを呼び出すことを強制 エージェント駆動の自動処理
// 修飾子による指定
Profile { ... }
    .toolCallingMode(.required)

// respond(to:) のオプションでの指定
let response = try await session.respond(
    to: "Write out instructions",
    options: GenerationOptions(toolCallingMode: .required)
)

.required の無限ループ脱出テクニック

モデルが .required モードの時、実質的に While ループ状態でツールを呼び続けようとします。脱出するには以下の方法があります。

① 条件付きモード切り替え

Profile {
    Instructions("Answer questions about origami")
    QueryOrigamiDatabaseTool()
}
// ツール呼び出し完了フラグを見てモードを .disallowed に戻す
.toolCallingMode(state.queriedDatabase ? .disallowed : .required)
.onToolCall { state.queriedDatabase = true }

② キャンセルエラーをスローする終了ツールの活用

struct FinalAnswerTool: Tool {
    @Generable struct Arguments { var answer: String }

    func call(arguments: Arguments) async throws -> Never {
        // 制御フローを中断して即座に脱出
        throw CancellationError()
    }
}

トランスクリプトのエラー処理ポリシー (TranscriptErrorHandlingPolicy)

ツール実行時のエラーやキャンセル時、通常はトランスクリプトがエラー前の状態にロールバックされます。エラー後の状態を維持したい場合は .preserveTranscript を設定します。

// ProfileまたはSessionに対して設定
Profile { ... }
    .transcriptErrorHandlingPolicy(.preserveTranscript)

// セッションからの手動トランスクリプト操作(isRespondingがfalse時のみ変更可能)
let session = LanguageModelSession()
session.transcriptErrorHandlingPolicy = .preserveTranscript

if !session.isResponding {
    session.transcript = updatedTranscript // 手動で更新可能
}


7. パフォーマンス・精度・評価(KVキャッシュの影響)

LLMでは、入力文脈の計算結果を保持する KVキャッシュ(Key-Value Cache) が重要な性能指標となります。

  1. 追記 (Append) は高速: 履歴の末尾にテキストを追加するだけであれば、KVキャッシュが維持され TTFT(Time To First Token: 最初のトークンが出力されるまでの時間) が最小化されます。
  2. 履歴削除・指示変更はキャッシュ無効化(Invalidation): historyTransform による履歴削除やツール変更、instructions の書き換えを行うとキャッシュが破棄され、遅延(レイテンシー)が増加します。
  3. 計測と評価:
  • Xcode の Foundation Models Instrument を使用して、キャッシュ無効化の発生をデバッグ・計測します。
  • 履歴を切り落とすと「ツールなしで応答した過去の履歴を見てモデルが誤解する」などの精度低下(モデルの混乱)が発生することがあります。コンテキスト削減を行う際は Evaluations フレームワーク でテストセットを作成し、精度の影響を定量的(データドリブン)に測定してください。

8. セッション全体のまとめ・比較

概念 / 機能 概要 適用範囲
DynamicInstructions 指示とツールをまとめた再利用可能なコンポーネント 構造化された指示設計
DynamicProfile 状態に応じた指示・ツール・モデル・パラメータの宣言的切り替え マルチモデル / エージェント設計
historyTransform プロンプト送信直前にのみ適用される非破壊的トランスクリプト変換 コンテキスト制限対応・プライバシー削除
onResponse / SessionProperty セッション全体で永続共有される状態更新・履歴の要約・削除 セッション全体の文脈圧縮
Baton-Pass 単一トランスクリプトを共有したまま主導権を渡すオーケストレーション シームレスなフェーズ遷移
Phone-a-Friend 独立した短命セッションを作成して特定タスクを委譲 カプセル化された問い合わせ
ToolCallingMode .allowed / .disallowed / .required によるツール発動の絶対制御 エージェントのループ制御

プライベートクラウドコンピューティング(PCC)による Apple Foundation Models の活用 (WWDC26)

Appleの Private Cloud Compute (PCC) を活用すると、高度なプライバシー保護を維持したまま、オンデバイスモデルの限界を超える最先端のサーバー大規模言語モデル(LLM)へアクセスできます。

本記事では、PCCのアーキテクチャやオンデバイスモデルとの比較、推論レベル(Reasoning Level)、利用制限(Quota)を考慮したUI設計、Xcodeでのシミュレーション・デバッグ手法までを包括的に解説します。


1. Private Cloud Compute (PCC) の概要と特徴

PCCは、Apple Intelligenceの複雑なタスク処理を支えるバックエンドインフラです。アプリデベロッパに対しても同一のサーバーLLMへのアクセスが提供されます。

PCCの主なメリット

  • エンドツーエンドのプライバシー: データはリクエストの処理にのみ使用され、サーバー上に永続保存・ログ収集・モデル学習されることは一切ありません(独立した第三者研究者による検証済み)。
  • 認証・APIキー不要: iCloudおよびOSレベルで完全に統合されているため、開発者はAPIキーの管理やアクセストークンの取得、独自認証システムの実装が一切不要です。
  • トークンコスト無償: 開発者側でのAPI利用料・トークン費用は発生しません。
  • 利用要件:
    • デバイス要件: Apple Intelligence 対応デバイス(iOS / iPadOS / macOS / watchOS 27〜)
    • アプリ要件: 初回ダウンロードの総数が200万件未満のデベロッパ(Apple Developer Webサイトからの申請が必要)

2. PCCモデルとオンデバイスモデルの比較

モデルの選定にあたっては、アプリのユースケース(インターネット非依存性、コンテキスト長、複雑なロジック処理など)に応じて適切なモデルを選択します。

仕様比較表

項目 オンデバイスモデル (SystemLanguageModel) PCC サーバーモデル (PrivateCloudComputeLanguageModel)
プライバシー 100% ローカル完結 ログ保存なし・プライベート空間で実行
オフライン動作 対応(ネット接続不要) 不可(インターネット接続が必須)
利用上限 (Quota) なし(無制限) ユーザーごとに1日の利用制限あり(iCloud+で拡張可)
コンテキストサイズ 4,096(旧OS)〜 8,192 トークン 32,768 (32K) トークン
推論機能 (Reasoning) 非対応 対応(.light, .moderate, .deep)
対応プラットフォーム iOS / iPadOS / macOS / visionOS 上記 + watchOS

3. 実装:1行の変更でサーバーモデルへ切り替え

Foundation Modelフレームワークの統一API設計により、コードの大部分をそのまま維持したままモデルを切り替えることができます。

① オンデバイスモデルのコード

import FoundationModels

let session = LanguageModelSession()
let response = try await session.respond(to: "Summarize this article: \(article)")

② PCC サーバーモデルへの切り替え(たった1行変更)

LanguageModelSession の初期化時に PrivateCloudComputeLanguageModel() を渡すだけで切り替わります。

import FoundationModels

let session = LanguageModelSession(
    model: PrivateCloudComputeLanguageModel() // モデルインスタンスを指定
)
let response = try await session.respond(to: "Summarize this article: \(article)")

③ 構造化出力 (@Generable) や ツール呼び出し (Tool) の共通利用

@Generable による型安全なデコードやカスタムツールの呼び出しは、PCCモデルでもオンデバイスモデルと全く同様に動作します。

import FoundationModels

@Generable
struct ArticleSummary {
    let oneLineSummary: String
    let keyPoints: [String]
}

struct FindRelatedArticlesTool: Tool {
    // ツールの定義
}

// ツールとPCCモデルを組み合わせて構造化出力を取得
let session = LanguageModelSession(
    model: PrivateCloudComputeLanguageModel(),
    tools: [FindRelatedArticlesTool.self]
)

let response = try await session.respond(
    to: "Summarize this article: \(article)",
    generating: ArticleSummary.self
)


4. 利用可否チェックとコンテキストサイズの取得

① デバイスの対応状況チェック (isAvailable)

Apple Intelligence 非対応デバイスや機能が無効化されている環境に備え、事前に利用可否を確認してグレースフルにフォールバック(画面切り替えや代替機能の提示)を行います。

import FoundationModels
import SwiftUI

struct ArticleSummarizationView: View {
    private var model = PrivateCloudComputeLanguageModel()

    var body: some View {
        if model.isAvailable {
            // PCCを利用したリクエスト画面を表示
        } else {
            // オンデバイスモデルへの切り替えや非対応メッセージを表示するフォールバックUI
        }
    }
}

② プログラムによるコンテキストサイズの参照 (contextSize)

contextSize プロパティを使用して、現在の実行モデルが扱える上限トークン数をプログラム上で取得できます。

// オンデバイスモデルのコンテキストサイズ取得 (8192など)
let systemContextSize = SystemLanguageModel().contextSize

// PCCモデルのコンテキストサイズ取得 (32768)
let pccContextSize = PrivateCloudComputeLanguageModel().contextSize


5. 推論レベル(Reasoning Level)と注意点

PCCモデルは、最終的な応答を出力する前にモデル自身が内部で思考プロセスを展開するリーズニング機能を備えています。

リーズニングレベルの種類

let response = try await session.respond(
    to: prompt,
    contextOptions: ContextOptions(reasoningLevel: .light)
)

  • .light: 応答生成前にわずかな追加文脈・検証ステップを収集。
  • .moderate: バランスの取れた思考プロセスを展開。
  • .deep: 複雑なロジック・多段階ステップに対して深く考察。思考テキストが実際の最終応答文より長くなる場合があります。

コンテキスト制限に関する重要な注意点:
リーズニングによってモデルが生成した内部の思考テキスト(リーズニングトークン)もトークンとして消費されます。これらは32Kトークンのコンテキストウィンドウ上限にカウントされるため、.deep を指定する場合はコンテキスト溢れに配慮が必要です。トランスクリプトの監視を行うことで、思考中の進捗ステータスをUIに反映できます。


6. 利用制限(Quota / Limit)に対するベストプラクティスとUI設計

PCCはユーザーのiCloudアカウントごとに1日の利用上限が設定されています。リクエスト上限に近づいた場合や達した場合のUX(ユーザー体験)設計が非常に重要です。

ベストプラクティス原則

  1. 突然のアラートモーダルを避ける: 突然のダイアログ(Alert)表示は避け、画面上の永続的UIの一部として組み込む。
  2. 送信ボタンの適切な制御: 上限到達時は生成ボタンを無効化(disable)し、ボタン下部に控えめな説明テキストと解決アクション(アップグレード誘導)を提示する。
  3. 事前警告: 上限に近づいている(isApproachingLimit)場合、事前に警告を表示してユーザーが重要なリクエストを選択できるようにする。

利用制限処理の実装コード (quotaUsage)

import FoundationModels
import SwiftUI

struct ArticleSummarizationView: View {
    private var model = PrivateCloudComputeLanguageModel()

    var body: some View {
        VStack {
            // 1. 上限に近づいている場合の警告表示
            if case .belowLimit(let info) = model.quotaUsage.status {
                if info.isApproachingLimit {
                    Text("Nearing daily usage limit.")
                        .foregroundStyle(Color.orange)
                }
            }
            
            // 2. 上限に達した場合のメッセージ表示
            if model.quotaUsage.isLimitReached {
                Text("Daily usage limit exceeded.")
                    .foregroundStyle(Color.red)
            }
            
            // 3. 利用上限を引き上げるための提案(iCloud+等の設定誘導ボタン)
            if let suggestion = model.quotaUsage.limitIncreaseSuggestion {
                Button("Show options to increase limit") {
                    suggestion.show()
                }
            }
        }
    }
}


7. Xcode での利用制限シミュレーション・デバッグ

利用制限到達時(Limit Reached)や上限接近時(Below Limit / Approaching)のUI挙動をテストするために、Xcodeに専用のデバッグ機能が用意されています。

設定手順

  1. Xcodeのスキーム編集画面を開く(Product > Scheme > Edit Scheme...)。
  2. Run 設定の Options タブを選択する。
  3. "Simulate Apple Foundation Models Availability" オプションを有効化する。
  4. シミュレートするステートを選択:
  • Limit Reached: 利用制限超過時のUIとエラーハンドリングのテスト
  • Near Limit: 上限接近時の警告ステータス(isApproachingLimit)のテスト

8. 全体のまとめ・開発のロードマップ

評価ステップ / 開発フェーズ 推奨アプローチ・使用ツール
1. モデル選定と精度検証 Evaluations フレームワーク でオンデバイスとPCCの精度・レイテンシーを定量的テスト
2. アーキテクチャ構築 DynamicProfile を使用し、軽い処理はオンデバイス、思考・長文処理はPCCへルーティング
3. エラー・制限ハンドリング quotaUsage によるUI更新 & XcodeのAvailabilityシミュレーションで上限状態のテスト
4. プロファイルと最適化 Instruments (Foundation Models) でトークン消費量と遅延をデバッグ

オンデバイス基盤モデルのためのプロンプト設計と安全性 (WWDC25)

Apple Foundation Modelフレームワークの登場により、iOS / iPadOS / macOS / visionOS アプリ上で動作するオンデバイス大規模言語モデル(LLM)を活用したAI体験が手軽に構築できるようになりました。

本記事では、オンデバイスモデル特有の設計戦略、プロンプトエンジニアリングのベストプラクティス、AIセーフティ(安全性)の多層防御、および評価・テスト運用について詳しく解説します。


1. オンデバイス LLM の特徴と設計戦略

Apple Intelligenceを支えるオンデバイスLLMは、約30億(3B)パラメータで構成され、モバイル端末のメモリや電力効率に最適化されています。クラウド上の超大規模モデル(数千億パラメータクラス)とは異なる特性を理解し、適切なタスクを割り当てることが重要です。

① オンデバイスLLMが得意なタスク

  • 文章の要約(Summarization)
  • テキストの分類(Classification)
  • マルチターン会話(Multi-turn Conversation)
  • 文章の推敲・構成(Text Formatting / Composition)
  • タグやキーワードの抽出・生成

② オンデバイスLLMで避けるべきタスクと対策

避けるべきタスク 理由 推奨される代替アプローチ
高度で複雑な推論 モデルサイズによる推論能力の限界 タスクをシンプルで単一のサブ手順に分割して与える
数値演算・計算 確実性・再現性に乏しい AIではなく通常のSwiftコードで演算処理を行う
コード生成 コード専用に最適化されていない 他のコード生成専用モデルやツールを利用する
最新の事実や事実確認 学習データ以降のリアルタイム知識を持たない 検証済みの正しい情報・コンテキストをプロンプト内に注入する

ハルシネーション(Factuality)への対応:
オンデバイスモデルは知識量が限られており、事実に基づかない嘘を出力(ハルシネーション)するリスクがあります。百科事典のような事実生成には使わず、ゲームのNPCの会話生成など、多少の逸脱がエンタメとして許容できるユースケースで活用するか、**ガイド付き生成(@Generable)**で出力フォーマットを厳密に制御します。


2. プロンプトエンジニアリングのベストプラクティス

明確で具体的な指示を与えることで、モデルの応答精度やスタイルを自在にコントロールできます。

① 生成量の制御

  • 短くまとめる: "In one paragraph"(1段落で)、"In three sentences"(3文で)、"In a few words"(数単語で)
  • 詳細にする: "In detail"(詳しく)

② ペルソナ・役柄(Role)の指定

モデルに特定の文体や話し方を演じさせることができます(例:"You are a fox speaking in Shakespearean English")。

③ 命令と例示のテクニック

  • 単一の明確なコマンド: 1度のプロンプトで与えるタスクは、具体的に1つに絞り込みます。
  • Few-shot Prompting: 望ましい出力例をプロンプト内に4つ以下提示することで、出力品質が著しく向上します。
  • ネガティブコマンド: 出力してほしくない挙動には "DO NOT" と大文字で厳密に指示します。

④ 指示(Instructions)とプロンプト(Prompt)の役割分担

LanguageModelSession の初期化時に設定する instructions は、その後の全てのプロンプトに対して優先適用される基本原則・キャラクター設定として機能します。

import FoundationModels

// セッション全体に「ティーン向けの怖い話を生成する」という基本指示を設定
let session = LanguageModelSession(
    instructions: "You are a helpful assistant that generates scary bedtime stories for teens."
)

// この後の一般的なプロンプトもすべて「怖い話」の文脈で生成される
let response = try await session.respond(to: "Write a poem about bagels.")

⑤ Xcode #Playground によるプロンプト試行

Xcodeのコードファイル内に #Playground を追加することで、SwiftUI プレビューのように即座にモデルの応答結果をキャンバス上でテストできます。


3. AI セーフティとガードレール設計

生成AI機能を提供するにあたり、有害コンテンツの出力や意図しない動作を防ぐ安全対策(Safety)が不可欠です。

① Appleのビルトイン・ガードレール

Foundation Modelフレームワークには、モデルの入力(命令・プロンプト・ツール呼び出し)と出力の双方に対して自動的に作用する安全性ガードレールが組み込まれています。入力ガードレールをすり抜けるような攻撃(プロンプトインジェクション等)が発生しても、出力ガードレールが有害出力を確実にブロックします。

② ガードレールエラーの捕獲とUIデザイン

安全ガードレールによって生成が拒否された場合、Swiftコード上でエラー(LanguageModelSession.GenerationError)として検出されます。

do {
    let response = try await session.respond(to: userInput)
    print(response.content)
} catch let error as LanguageModelSession.GenerationError {
    // 安全性ガードレール等に触れた場合の対応
    handleSafetyError(error)
}

UIフィードバックのガイドライン

  • バックグラウンド処理: ユーザーが意識していない内部処理であれば、無駄にアラートで割り込まず静かに無視・リトライします。
  • ユーザー主導処理: チャットや画像生成などユーザーが結果を待っている場合は、「リクエストを処理できませんでした」といった適切なUIフィードバックや、プロンプト取消・代替案の選択肢を提示します(例: Image Playground風のUI)。

4. 多層防御(スイスチーズ・モデル)によるリスク軽減

単一の防御機構に頼るのではなく、複数のレイヤーを組み合わせることでアプリ全体の安全性を担保します。

 [ Layer 1: フレームワークのガードレール (Apple標準機能) ]
                        ↓
 [ Layer 2: システム命令への安全性指示 (Instructions)  ]
                        ↓
 [ Layer 3: ユーザー入力の制限・アプローチ設計          ]
                        ↓
 [ Layer 4: アプリ固有の緩和策・UIによるケア            ]

ユーザー入力パターンの安全レベル比較

  1. 直接入力パターン(高リスク・高自由度):
    ユーザーのフリーテキストをそのままプロンプトとしてモデルに渡す方式。命令(Instructions)側で「不適切な発言には共感しつつ健全に応答すること」等のルールを厳格に記述する必要があります。
  2. テンプレート結合パターン(中リスク・中自由度):
    アプリ側で用意した固定プロンプト枠にユーザー入力を埋め込んで渡す方式。
  3. 選択肢提示パターン(低リスク・制御容易):
    アプリ側で厳選した組み込みプロンプトのリストをユーザーに選ばせる方式。確実で安全な応答が得られます。

アプリ固有のUI/UX緩和策(ユースケース別例)

  • アレルギー・健康に関するリスク: 食事やレシピを生成するアプリでは、ハルシネーションに備えて画面上にアレルギー警告を表示したり、食事制限フィルターのUIを設ける。
  • 物議を醸すトピックのリスク: クイズ・トリビア生成アプリでは、プロンプト命令でNGワードを指定するか、ML分類器(Classifier)を導入して事前チェックを行う。

5. 評価とテスト(Evaluations & Testing)

モデルの更新やプロンプトの修正によって期待通りの出力が得られるか、安全性が保たれているかを継続的に検証する運用体制が重要です。

評価プロセスの推奨ステップ

  1. テストデータセットの策定: 主要な成功パターンだけでなく、安全性エラーを引き起こしそうなエッジケースを含めたプロンプト集を用意する。
  2. 自動化パイプラインの構築: CLIツールやUIテストアプリを作成し、プロンプト変更時の結果を自動実行する。
  3. 自動採点(LLM-as-a-Judge): データセットが大規模な場合は、別のLLMを用いて応答品質や安全性を自動スコアリングする。
  4. 安全側動作のテスト: ガードレールエラーが発生した際に、アプリUIが正しく復旧・ハンドリングできるかをテストする。

6. まとめ・開発チェックリスト

アプリにオンデバイスFoundation Modelを組み込む際は、以下のチェックリストを活用してください。

  • エラー処理: ガードレールエラー(GenerationError)を検出した際のUI表示・フォールバックが準備されているか?
  • システム命令: Instructions に安全な応答ポリシーやペルソナが明記されているか?
  • 入力バランス: ユーザーからの直接入力をそのまま渡さず、テンプレート化や安全指示と組み合わされているか?
  • ドメイン固有の緩和策: レシピ・トリビア・医療など、ユースケースに応じた警告表示や制限ルールがアプリUI側にあるか?
  • 継続的評価: プロンプト変更時に品質と安全性を自動検証するテストデータセットが存在するか?
  • フィードバック収集: 安全上の問題が発生した際に、Feedback Assistantやアプリ内UIを通じてAppleや開発チームへ報告できる仕組みがあるか?

Instrumentsによるエージェント活用アプリのデバッグとプロファイリング (WWDC26)

Foundation Models API や DynamicProfile / DynamicInstructions を活用したエージェント型アプリケーション開発では、条件に応じてモデルの指示・ツール・バックエンドが動的に変化します。この柔軟性は魅力的なユーザー体験を生み出す一方で、非決定論的な挙動やマルチモデル間の通信による「複雑な不具合の特定」 を困難にします。

本記事では、Xcode 27 で強化された Foundation Models Instrument を使用し、アプリの制御フロー可視化、サイレント障害のデバッグ、および主要パフォーマンスメトリクス(TTFT / TPS / Total Latency)の計測・最適化手法について包括的に解説します。


1. 従来開発と LLM アプリ開発の考え方の違い

LLMアプリケーションをプロファイリング・デバッグするにあたり、従来のソフトウェア開発とは異なる3つの主要課題を理解しておく必要があります。

 [ 従来のコード ] : 入力 A ───(決定論的関数)───> 常に同じ出力 B (ユニットテストで完全一致を検証)
 [ LLMコード ]   : 入力 A ───(確率的モデル)  ───> 毎回異なる出力 B', B'' (応答の「品質と意図」を評価)

  1. 確率的(Stochastic)な出力:
    同じプロンプトであっても実行ごとに異なる応答が生成されるため、従来の「期待値の文字列と完全一致するか」を確かめるユニットテストが機能しにくくなります。
  2. モデル間・ツール間のオーケストレーション:
    画像の食材認識からレシピ作成へ繋ぐようなマルチモデル構成では、コンテキストの受け渡しや状態遷移の段階で障害が発生しやすくなります。
  3. 可視性(Observability)の欠如:
    モデル内部でどのような推論・ツール呼び出しが行われ、なぜ特定の応答に至ったかの推論プロセスが外部から見えにくいため、障害の発生源特定が困難になります。

2. Xcode Instruments (Foundation Models テンプレート) の立ち上げと注意点

プロファイリングの起動手順

  1. Xcode でプロジェクトを開き、Product > Profile (⌘+I) を選択します。
  2. テンプレートチェッカーから Foundation Models テンプレートを選択して起動します。
  3. 赤い「録音(Record)」ボタンをクリックしてアプリのトレースを開始します。

プライバシーに関する重要事項:
Foundation Models Instrument は、デバッグ実行中に送受信される生のプロンプトや応答データ、ツール呼び出し引数をそのままログ記録します。製品(本番)環境ではログ記録は自動的に無効化されますが、開発中のトレースファイル(.trace)にはユーザーの個人情報や機密情報が含まれる可能性があるため、保管・取扱には十分注意してください。


3. Foundation Models Instrument UI の構造とナビゲーション

プロファイラー画面は、上部のタイムライン(レーン)、下部のツリー詳細ビュー(Tree Detail View)、右側のインスペクタ(Inspector)の3つの領域で構成されています。

+-----------------------------------------------------------------------------------+
| タイムライン (レーン表示):                                                           |
|  - Instructions レーン  : [ アクティブな指示・ツールセットの適用期間 ]             |
|  - Model Inference レーン : [ 入力処理 (黄) ] [ 応答生成 (オレンジ) ]              |
+-----------------------------------------------------------------------------------+
| ツリー詳細ビュー (階層構造):                                                        |
|  Session ─── Request ─── Model Inference ─── Instructions / Prompt / Response      |
+-----------------------------------------------------------------------------------+
| インスペクタ (右パネル):                                                            |
|  選択したノードの詳細テキスト、トークン消費量、各種レイテンシーメトリクス           |
+-----------------------------------------------------------------------------------+

タイムラインの主要レーン

  • Instructions レーン: どの指示(Instructions)とツールのセットがアクティブであったかを時系列で表示。
  • Model Inference レーン:
  • 黄色バー: システムが入力を処理(トークナイズ / プレフィル)している時間。
  • オレンジバー: モデルが実際に応答トークンを逐次生成(デコード)している時間。

4. 実践デバッグ:サイレント障害(Silent Failure)の特定と修正

セッションの事例(Craft アプリ)では、ユーザーが「アイディア出しモード」から特定のクラフト(ペーパーバタフライ)を選択した際、本来なら「チュートリアル作成モード」に切り替わるべきところ、エラーも出ずに再びアイディア提案を繰り返すという不具合が発生しました。

① 原因の特定(Instruments での分析)

  1. Instructions レーンのチェック:
    セッション全体を通じて指示セットが「1つ」しか表示されておらず、チュートリアル用の指示セットへモード切り替え(ハンドオフ)が行われていないことが判明。
  2. ツリー詳細ビューとインスペクタの確認:
    プロンプト内では switchToTutorialMode ツールへの言及があるにもかかわらず、該当の Instructions ノードのインスペクタを確認すると、使用可能な toolset に GenerateCraftIdeasTool しか登録されていませんでした。
  3. サイレント障害の怖さ:
    モデルはツールが存在しないため呼び出せず、代わりに通常の会話を継続してしまったため、ログや実行時例外(Exception)は一切スローされず正常終了していました。

② コードの修正

BrainstormDynamicInstructions の定義において、プロンプト内で言及していたツールを toolset に追記します。

// 修正前の問題のあるコード
struct BrainstormDynamicInstructions: DynamicInstructions {
    var body: some DynamicInstructions {
        Instructions {
            "Brainstorm craft ideas. Use switchToTutorialMode when decided."
        }
        GenerateCraftIdeasTool() // ← SwitchToTutorialModeTool が漏れていた!
    }
}

// 修正後のコード
struct BrainstormDynamicInstructions: DynamicInstructions {
    var body: some DynamicInstructions {
        Instructions {
            "Brainstorm craft ideas. Use switchToTutorialMode when decided."
        }
        GenerateCraftIdeasTool()
        SwitchToTutorialModeTool() // 追加してツールを有効化
    }
}

③ 修正結果の再トレース検証

再実行してトレースをとると、Instructions レーンに「ブレインストーミング指示」と「チュートリアル指示」の2つのバーが順に表示され、ツリー上でも switchToTutorialMode ツール呼び出しをトリガーに正常にモードが切り替わったことが確認できます。


5. パフォーマンスメトリクスと最適化指針

プロファイラーのインスペクタには、モデル推論ノードごとに以下の3つの決定的なパフォーマンス指標が表示されます。

パフォーマンス指標 説明 主な改善アプローチ
Time to First Token (TTFT) リクエスト送信から最初の1トークン目が出力されるまでの時間(プレフィル時間) プロンプトや指示文を短縮する、不要なスキーマ定義を除外する、session.prewarm() を呼ぶ
Tokens per Second (TPS) 応答生成の全体的な出力速度(トークン/秒) プロンプトや設定(Temperature / Sampling)変更時のベンチマーク比較・リグレッション検出
Total Latency プロンプト送信から最終的な応答完了までの全所要時間 ストリーミングAPI (streamResponse) を使用して部分結果を即座にUIへ描画する

6. 全体まとめと開発ロードマップ

  1. 可視化から始める: 非決定論的なバグや応答の遅れに直面したら、まず Xcode 27 の Foundation Models Instrument でトレースを記録する。
  2. 2つのレーンを監視: Instructions レーンでモード遷移を確かめ、Model Inference レーン(黄/オレンジ)で入力処理と生成時間の比率を把握する。
  3. 指標に基づき最適化: TTFT(最初のトークンまでの時間)が高すぎる場合はプロンプトの削減や事前ウォームアップを行い、画面の停滞感はストリーミングで解消する。
  4. Evaluations で定着させる: 不具合を修正した後は Evaluations Framework で評価データセットを作成し、継続的な品質評価・回帰テストを行う。

Evaluationsフレームワークについて (WWDC26)

従来のソフトウェア開発では、特定の入力に対して常に同じ出力が得られるため、ユニットテスト(XCTest や Swift Testing)で期待値を検証できました。しかし、大規模言語モデル(LLM)をはじめとする生成AI機能は確率的(非決定論的)に動作し、同じ入力であっても実行ごとに異なる結果を出力します。

この課題を解決するために登場したのが Evaluations フレームワーク です。本記事では、デモアプリ「Book Tracker」のタグ自動生成機能を例に、定量的な評価(Evaluator / Metric)から定性的な評価(Model Judge / Score Dimension)、そして評価主導開発のプラクティスまでを包括的に解説します。


1. 生成AI時代のテスト課題と Evaluations の思想

従来のテストと生成AI機能のテストの根本的な違いは以下の通りです。

 [ 従来のテスト ]     : 入力 A ───(決定論的コード)───> 常に同じ出力 B (ユニットテストで100%一致を比較)
 [ インテリジェントテスト ]: 入力 A ───(確率的モデル)  ───> 毎回異なる出力 B', B'' (統計的な合格率や傾向を評価)

評価の目的

単一のテストで成功/失敗を決めるのではなく、「アプリが期待通りの結果を生成する頻度(割合)」や「安全・高品質な結果が得られる確率」をデータドリブンに可視化・追跡(ヒルクライミング)することが目的となります。

対応範囲: Evaluations フレームワークは LLM だけでなく、線形回帰、分類器、各種機械学習モデルなど、確率的な挙動を示すあらゆるシステムの評価に適用可能です。


2. 評価の基本構造(5つのステップ)

評価を構築・実行するには、主に5つのステップを踏みます。

  1. 対象コードの定義 (subject): 測定対象となるサービスやモデル呼び出しを記述。
  2. データセットの用意 (ModelSample): 評価に渡す入力データと期待される理想解をセット。
  3. メトリクスと評価者の定義 (Metric & Evaluator): 合格基準(1回の実行レベル)を設定。
  4. 結果の集計 (aggregateMetrics): データセット全体の平均値や標準偏差を算出。
  5. テストスイートでの実行 (Swift Testing): @Test(.evaluates(...)) で実行しレポートを出力。

3. 実装:初めての評価(定量的メトリクス)

Book Tracker アプリの「レビューから本にタグを付与する機能 (BookTaggingService)」を評価するコード例です。

① 評価構造体の定義 (Evaluation プロトコル)

import Evaluations

struct BookTaggingEvaluation: Evaluation {
    // 1. 評価用メトリクスの宣言
    let tagCount = Metric("TagCount")
    let tagTotal = Metric("TagTotal")
    let wordCount = Metric("WordCount")
    let hasGenreTag = Metric("HasGenreTag")

    // 2. データセットの準備 (ModelSample)
    var dataset = ArrayLoader(samples: [
        ModelSample(
            prompt: "okay I am OBSESSED and I need everyone to read this RIGHT NOW...",
            expected: BookTags(tags: ["classic", "romance", "wit", "regency"])
        ),
        ModelSample(
            prompt: "Read this in one sitting between midnight and 4am and I cannot...",
            expected: BookTags(tags: ["classic", "gothic", "horror", "vampire", "suspense"])
        )
    ])

    // 3. 評価対象 (Subject) の実行
    func subject(from input: ModelSample<String>) async throws -> BookTags {
        try await BookTaggingService.generateTags(for: input.prompt)
    }

    // 4. 評価ロジック (Evaluators)
    var evaluators: Evaluators {
        // タグ数が 3〜8 個の範囲内かを判定
        Evaluator { _, subject in 
            let count = subject.value.tags.count
            if (count >= 3 && count <= 8) {
                return tagCount.passing(rationale: "\(count) tags generated")
            } 
            return tagCount.failing(rationale: "Got \(count) tags, expected 3–8")
        }

        // 単語数チェック (1単語またはハイフン繋ぎか)
        Evaluator { _, subject in
            for tag in subject.value.tags {
                if tag.contains(" ") {
                    return wordCount.failing(rationale: "Tag '\(tag)' contains multiple words")
                }
            }
            return wordCount.passing()
        }

        // 既知の文学ジャンルが含まれているか
        Evaluator { _, subject in
            let tags = subject.value.tags.map { $0.lowercased() }
            let knownGenres = await BookTaggingService.knownGenres
            for tag in tags {
                if knownGenres.contains(tag) {
                    return hasGenreTag.passing(rationale: "Matched genre \(tag)")
                }
            }
            return hasGenreTag.failing(rationale: "No known genre tag found") 
        }
    }

    // 5. 統計の集計方法
    func aggregateMetrics(using aggregator: inout MetricsAggregator) {
        aggregator.computeMean(of: tagCount)
        aggregator.computeMean(of: wordCount)
        aggregator.computeMean(of: hasGenreTag)
        
        aggregator.group("Distribution of Tag Totals") { aggregator in
            aggregator.computeMean(of: tagTotal)
            aggregator.computeStandardDeviation(of: tagTotal)
            aggregator.computeVariance(of: tagTotal)
        }
    }
}

② Swift Testing によるテスト実行と最適化目標(Optimization Target)

Swift Testing の @Test(.evaluates(...)) トレイトを使用して、テスト実行とレポート生成を行います。

import Testing
import Evaluations

@Suite("Book Tagging Tests")
struct BookTagEvaluationTests {
    static let evaluation = BookTaggingEvaluation()
    static let evaluationInfo = ["Version": "1.0", "Model": "SystemLanguageModel"]

    @Test("Book Tag Evaluations", .evaluates(evaluation, info: evaluationInfo))
    func evaluateBookTagging() async throws {
        let result = EvaluationContext.current.result
        let tagCountMetric = Self.evaluation.tagCount

        // 「正しい数のタグが80%以上の確率で生成されること」を合格条件(最適化目標)とする
        #expect(result.aggregateValue(.mean(of: tagCountMetric)) >= 0.8)
    }
}


4. データセットの拡張と合成データ生成 (SampleGenerator)

2〜3件のサンプルでは統計的意図を抽出できません。実用的な評価には数千件の多様なサンプルが必要です。

データの多様性

  • ジャンルの幅: フィクション、ノンフィクション、エッセイなど。
  • レビュー長: 1文の短文から、数段落におよぶ長文まで。
  • 雑音・個人的感情: 読者の個人的感想(「最高だった」「途中で飽きた」等)が含まれるプロンプト。

SampleGenerator による合成サンプル(Synthetic Data)生成

人間によるサンプル作成コストを削減するため、大規模モデルを使ってテストサンプルを合成生成できます。

// 数行のシードデータから 1,000 件の多様なサンプルを自動生成
let samples: [ModelSample<String>] = [
    ModelSample(prompt: "The largest planet in our solar system...", expected: "Jupiter."),
    ModelSample(prompt: "The capital of Thailand...", expected: "Bangkok.")
]

for try await sample in samples.makeSamples(
    """
    Generate diverse sentence completions about the listed topics:
    - The Solar System
    - World Capitals
    """,
    targetCount: 1000
) {
    samples.append(sample)
}


5. 定性評価:モデルジャッジ (Model Judge) とスコアディメンション

数値や文字数などの「コードで判断できる定量的メトリクス」だけでは、「そのタグが本当に本の内容を表していて閲覧に役立つか」といった主観的・定性的な品質を測れません。

ここで登場するのが モデルジャッジ(Model Judge) です。評価対象のモデルと同等以上の知能を持つモデル(例: Private Cloud Compute / PCC)にジャッジを行わせます。

① シンプルなモデルジャッジの構築

ModelJudgeEvaluator(
    "TagQuality",
    scale: .numeric([
        4: "Tags are relevant and helpful for browsing",
        3: "Mostly relevant, one tag too vague or generic",
        2: "Several tags are wrong or generic",
        1: "Unhelpful or irrelevant"
    ]),   
    judge: PrivateCloudComputeLanguageModel() // オンデバイスより高性能なPCCを使用
)

② 評価の精緻化:スコアディメンション (ScoreDimension) の分割

質問範囲が広すぎると、ジャッジの解釈と開発者の意図にズレが生じます。「関連性(Relevance)」と「有用性(Usefulness)」のように評価軸を明確に分割します。

// 1. 関連性 (Relevance) の評価軸
let relevance = ScoreDimension(
    "Relevance",
    description: """
        Whether each tag describes a quality, theme, or tone
        of the book itself rather than incidental details or
        the reader's personal reactions.
        """,
    scale: .numeric([
        4: "Every tag describes the book itself",
        3: "Most tags describe the book",
        2: "Some tags describe personal reactions",
        1: "Tags don't meaningfully describe the book"
    ])    
)

// 2. 有用性 (Usefulness) の評価軸
let usefulness = ScoreDimension(
    "Usefulness",
    description: "Whether the tags help a user filter and find books in a personal library.",
    scale: .numeric([
        4: "Highly distinctive and useful for filtering",
        3: "Moderately useful",
        2: "Too broad or generic to be useful",
        1: "Completely useless for filtering"
    ])
)

③ アプリ文脈の注入 (ModelJudgePrompt)

ジャッジに対して「どのようなアプリで利用されるデータなのか」の背景情報と、期待する参照値(expectedTags)を渡します。

ModelJudgeEvaluator(
    judge: PrivateCloudComputeLanguageModel(),
    dimensions: [relevance, usefulness],
    prompt: ModelJudgePrompt( 
        instructions: """
            You are evaluating tags generated for a personal book-tracking app where users
            organize their library by browsing and filtering tags.
            """,
        evaluationTarget: { value in
            "\(value.tags.count) Generated tags: " + value.tags.joined(separator: ", ")
        },
        reference: { input, _ in 
            let expectedTags = input.expected?.tags.joined(separator: ", ")
            return ["Expected Tags": expectedTags ?? "No expected tags defined"]
        }
    )
)


6. 評価駆動開発 (EDD) とヒルクライミング (Hill Climbing)

評価スイートを構築したら、プロンプトやモデル指示(Instructions)を改善し、評価を再実行してスコアの変化を観測するループを回します。これを 評価駆動開発 (Evaluation-Driven Development) と呼びます。

 [ 指示 / プロンプトの変更 ] ──> [ 評価の自動実行 (Test Run) ] ──> [ Xcode レポート確認 / 根拠分析 ]
        ^                                                                    │
        └────────────────── スコアが向上するまで繰り返し (ヒルクライミング) ─────────┘

コード側の改善例(@Guide や指示の修正)

// 1. データ構造側で制約を強化
@Generable
struct BookTags: Codable {
    @Guide(description: "Descriptive tags capturing themes, genres, moods", .count(3...8))
    var tags: [String]
}

// 2. サービス指示文のイテレーション
let instructions = Instructions {
    """
    You are a librarian and literary analyst. Given a reader's summary of a book:
    Rules:
    - Return between 3 and 8 tags.
    - Tags should be lowercase, concise (single word or hyphenated), and descriptive.
    - Tags should include the book's genre from the known genres list.
    - Avoid personal opinions (e.g. 'overrated') or specific book titles.
    """
}


7. ベストプラクティス・まとめ

  1. 小さく始める: 20〜30件のフォーカスされたデータセットからスタートする。
  2. 定量的 vs 定性的な使い分け:
  • 定量的 (Code-based): 文字数、正規表現、要素数、固定リストマッチングなど、Swiftコードで判定できるもの。
  • 定性的 (Model Judge): 関連性、トーン、有用性、自然さなど、言葉でしか説明できない主観的品質。
  1. ジャッジのスコア目盛りは偶数にする: 1〜4 などの4段階スケールにし、モデルが安易に中立(真ん中)を選択するのを防ぐ。
  2. 根拠(Rationale)を活用する: モデルジャッジが出力した理由・診断テキストを読み、プロンプト改善やスコアディメンションの分割(解像度向上)に役立てる。
  3. Xcode テストレポートの連携: 失敗したサンプルを Xcode のアシスタントエディタで開き、実際のプロンプトと応答差分を深く分析する。

Evaluationsを活用した山登り法(Hill-Climbing)によるプロンプトの改善 (WWDC26)

AI生成機能を搭載したアプリでは、「プロンプトを少し書き換えたら、別のケースで精度が落ちた」という問題が頻繁に発生します。こうした感覚的な試行錯誤から脱却し、科学的かつ体系的に品質を高めるプロセスが ヒルクライミング(山登り法) です。

本記事では、デモアプリ「Book Tracker」のタグ自動生成機能を題材に、人間の評価とAIジャッジのズレ(ドリフト)を コーエンのカッパ係数(Cohen's Kappa) で解消する「キャリブレーション手法」、2つの評価結果を並べて比較する「実験手法」、そしてツールを追加したシステム全体の進化プロセスを解説します。


1. ヒルクライミング(山登り法)の基本ループと「科学的アプローチ」

ヒルクライミングとは、Evaluations のスコアを指針(ガイド)として、反復的に機能を改善していくプロセスです。

 [ 開発 (Dev) ] ───> [ 評価実行 (Eval) ] ───> [ 分析 (Analyze) ] ───> [ 比較 (Compare) ]
       ^                                                                       │
       └───────────────────────── スコアが目標に達するまで反復 ─────────────────┘

  1. 開発 (Development): プロンプトや指示、ツール、あるいはデータセットに変更を加える。
  2. 評価実行 (Evaluation): 自動評価パイプラインを実行してテスト結果を得る。
  3. 分析 (Analysis): 単純な合格/不合格だけでなく、個々のサンプル差分や不一致の理由を調べる。
  4. 比較 (Comparison): 変更前のベースライン(コントロール群)と変更後の実験群(実験グループ)の差分を科学的に検証する。

2. 定性評価の最大課題:「ドリフト」とコーエンのカッパ係数 (Cohen's Kappa)

人間の専門家評価とAIモデルジャッジの評価が食い違う現象を ドリフト(Drift) と呼びます。

なぜ単なる正解率(Accuracy)では不十分なのか?

高得点のサンプルが多いデータセットでは、ジャッジが適当に「高スコア」をつけても正解率(Accuracy)が高く見えてしまいます。データセットの偏りや「偶然の一致」を排除し、人間の専門家とAIジャッジがどれだけ真に一致しているかを測定するのが コーエンのカッパ係数 ($\kappa$) です。

$$\kappa = \frac{P_o - P_e}{1 - P_e}$$

  • $P_o$ : 観察された一致度(Accuracy)
  • $P_e$ : 偶然の一致確率(Chance Agreement)

合格ラインの目安: 統計学において、カッパ係数 $\kappa > 0.6$ は「意味のある高度な一致(Substantial Agreement)」を示します。


3. モデルジャッジの校正(キャリブレーション)実装

AIジャッジ自身を人間の評価基準に合わせる(校正する)ための Evaluation を構築します。

① 校正用データセットのロードと Calibration Evaluation

事前に抽出した「レスポンスと人間の専門家スコア」を含むJSONデータを評価入力とします。

import Evaluations
import FoundationModels

// 1. モデルジャッジ校正用 Evaluation
struct BookTagJudgmentCalibration: Evaluation {
    static let samples: [ModelSample<BookTagJudgmentValue>] = {
        guard let url = Bundle.main.url(forResource: "BookTaggingEvaluation-extracted", withExtension: "json"),
              let data = try? Data(contentsOf: url) else { return [] }
        // エキスパートのスコア付きサンプルをロード
        return (try? JSONDecoder().decode([ModelSample<BookTagJudgmentValue>].self, from: data)) ?? []
    }()

    var dataset: some Loader { ArrayLoader(samples: Self.samples) }

    // 生成済みの結果をそのまま渡す
    func subject(from sample: ModelSample<BookTagJudgmentValue>) async throws -> ModelSubject<BookTagJudgmentValue> {
        ModelSubject(value: sample.expected ?? BookTagJudgmentValue(tags: [], expertRelevanceScore: 0, expertUsefulnessScore: 0))
    }

    // 評価者には本番と同じ ModelJudgeEvaluator を使用
    var evaluators: Evaluators {
        ModelJudgeEvaluator(
            judge: .default,
            dimensions: [relevance, usefulness],
            prompt: ModelJudgePrompt(
                instructions: "You are evaluating automatically generated tags for Book Tracker...",
                evaluationTarget: { output in output.tags.joined(separator: ", ") },
                reference: { input, _ in ["Expected Tags": input.expected?.tags.joined(separator: ", ") ?? ""] }
            )
        )
    }

    // 2. コーエンのカッパ係数による集計
    func aggregateMetrics(using aggregator: inout MetricsAggregator) {
        let expertRelevance = Self.samples.map { Double($0.expected?.expertRelevanceScore ?? 0) }
        let expertUsefulness = Self.samples.map { Double($0.expected?.expertUsefulnessScore ?? 0) }

        aggregator.group("Relevance") { group in
            group.custom(of: relevance.metric, label: "Relevance Alignment Score") { judgeRatings in
                cohensKappa(ratings1: expertRelevance, ratings2: judgeRatings) ?? 0
            }
        }
        aggregator.group("Usefulness") { group in
            group.custom(of: usefulness.metric, label: "Usefulness Alignment Score") { judgeRatings in
                cohensKappa(ratings1: expertUsefulness, ratings2: judgeRatings) ?? 0
            }
        }
    }
}

② 校正テストの実行($\kappa > 0.6$ の検証)

@Suite("Book Tag Judge Calibration")
struct BookTagJudgmentCalibrationTests {
    static let evaluation = BookTagJudgmentCalibration()

    @Test("Judge Calibration", .evaluates(evaluation))
    func evaluateJudgeCalibration() async throws {
        let result = EvaluationContext.current.result

        // 人間とAIジャッジの一致度が 0.6 を超えているかテスト
        #expect(result.aggregateValue(.custom(label: "Relevance Alignment Score")) > 0.6)
        #expect(result.aggregateValue(.custom(label: "Usefulness Alignment Score")) > 0.6)
    }
}


4. プロンプト改善のテクニック:Few-shot 例の組み込み

モデルジャッジのスコア定義(ScoreDimension)を詳細化しても一致度が届かない場合、ジャッジ側のプロンプトに Few-shot 例(正解・不正解の評価実例) を数件提示します。

struct ExperimentalBookTagJudgmentCalibration: Evaluation {
    var evaluators: Evaluators {
        ModelJudgeEvaluator(
            judge: SystemLanguageModel(),
            dimensions: [relevance, usefulness],
            prompt: ModelJudgePrompt(
                instructions: """
                    You are calibrating with an expert librarian who scores
                    automatically generated tags for Book Tracker... Use the worked examples to calibrate.

                    ## Worked examples
                    ### Example A — clean fit (Pride and Prejudice)
                    Tags: romance, historical-fiction, love, redemption
                    Librarian: Relevance 4, Usefulness 4

                    ### Example E — flat genre contradiction (Frankenstein)
                    Tags: horror, science-fiction, self-help, self-improvement
                    Librarian: Relevance 2, Usefulness 3
                    """,
                evaluationTarget: { output in output.tags.joined(separator: ", ") },
                reference: { input, _ in ["Book Review": input.promptDescription] }
            )
        )
    }
}

注意点: 提示する例が多すぎると特定のケースに過剰適合(Overfitting)し、未見のデータに対する判定精度が低下します。実例は典型的な数件(A〜F程度)にとどめるのがベストプラクティスです。


5. プロンプトを超えた改善:ツール(Tool Calling)の追加

テキストの生成プロンプトを工夫するだけでは限界がある場合、外部情報検索ツール(Tool)を追加してモデルにコンテキストを補完させるアプローチが有効です。

① 本の詳細検索ツール (BookLookupTool)

レビュー内の登場人物名や引用句から、正式な「本タイトル」と「著者名」を検索してモデルへ供給するツールです。

struct BookLookupTool: Tool {
    let name = "lookupBook"
    let description = "Looks up the title and author of a book given distinguishing details from a review."

    @Generable
    struct Arguments {
        @Guide(description: "Distinguishing details from the review like character names or settings.")
        var details: String
    }

    @Generable
    struct Output {
        var title: String
        var author: String
    }

    func call(arguments: Arguments) async throws -> Output {
        // キーワードにマッチする本を検索して返すロジック
        // ...
        return Output(title: "Frankenstein", author: "Mary Shelley")
    }
}

② ツール付きサービスとツールなしサービスの比較テスト

同一のテストスイート内で「ツールなし」と「ツールあり」の評価を並行実行し、統計的効果を測定します。

@Suite("Book Tag Evaluations")
struct BookTagEvaluationTests {
    static let evaluation = BookTaggingEvaluation()
    static let lookupEvaluation = BookTaggingWithLookupEvaluation()

    // 1. ツールなし(コントロール群)
    @Test("Book Tag Evaluations (No Tools)", .evaluates(evaluation))
    func evaluateBookTagging() async throws {
        let result = EvaluationContext.current.result
        #expect(result.aggregateValue(.mean(of: BookTagEvaluationTests.evaluation.tagCount)) >= 0.8)
    }

    // 2. ツールあり(実験群)
    @Test("Book Tag Evaluations (with BookLookupTool)", .evaluates(lookupEvaluation))
    func evaluateBookTaggingWithLookup() async throws {
        let result = EvaluationContext.current.result
        #expect(result.aggregateValue(.mean(of: BookTaggingWithLookupEvaluation.lookupEvaluation.tagCount)) >= 0.8)
    }
}


6. まとめ・ヒルクライミングの鉄則

原則 内容
変数は1度に1つ 科学実験と同じく、プロンプト・指示・ツールの変更は1度につき1箇所に留め、何がスコア変動を起こしたかを分離して特定する。
ドリフト(Drift)を監視 人間とAIジャッジの評価軸の不一致を Cohen's kappa ($\kappa > 0.6$) で定期的に校正する。
失敗した実験も貴重なデータ スコアが下がった実験は「何がモデルを混乱させるか」を示す重要なフィードバックとして活用する。
創造的に変更候補を試す 指示(Instructions)、例示(Few-shot)、ツール(Tools)、サンプリング(Temperature)、モデル切り替え(オンデバイス $\leftrightarrow$ PCC)など、変更できる要素を広く検討する。

エージェント型アプリに対する信頼性の高い評価プロセスの確立 (WWDC26)

AIを活用したエージェント型アプリ開発において、手作業で作成した十数件程度のテストデータ(サンプル)では、多様な実世界の利用シナリオを網羅できません。また、エージェントが最終的に正しい回答を出力していても、「途中のツール呼び出し順序や引数が間違っていた」 という潜在的な不具合(バグ)を見落とす危険があります。

本記事では、Evaluations フレームワークの高度な機能を使い、合成データ生成(makeSamples / SampleGenerator)によるデータ拡張と自動バリデーション、および TrajectoryExpectation を用いたツール呼び出し履歴の完全検証手法を解説します。


1. データセットの拡張:合成データ生成 (makeSamples)

評価結果の品質は、テストデータの「量」よりも「カバレッジ(カバーする多様性)」に依存します。少数(例:13件)のサンプルでスコアが良く見えても、実環境では全く通用しないケースがあります。

① makeSamples によるシンプルな自動拡張

初期のサンプル群(dataset)をシード(種)として渡し、指定した目標件数(targetCount)まで多様なテスト用プロンプトと期待値を自動合成します。

import Evaluations
import FoundationModels

let prompt = Prompt("""
    Generate diverse range of book reviews and corresponding tags.
    Cover a wide range of genres, time periods, cultures, and reader personas.
    Do not repeat books already in the dataset.
    """)

let dataset = Book.sampleBooks.map { book in
    ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))
}

let targetCount = 100
var expandedDataset = dataset

// 非同期ストリームで合成サンプルを取得
for try await sample in dataset.makeSamples(prompt, targetCount: targetCount) {
    expandedDataset.append(sample)
    print("Generated \(expandedDataset.count) samples so far.")
}


2. 高度な合成データ生成とバリデーション (SampleGenerator)

生成モデルの変更、コンテキスト管理、およびルール違反サンプルの自動除外を行うには SampleGenerator を使用します。

① SampleGenerator の構築

コンテキスト容量が大きい Private Cloud Compute (PCC) を生成用モデル(sessionProvider)として指定できます。

let generator = SampleGenerator<ModelSample<BookTags>>(
    prompt,
    samples: dataset,
    targetCount: 100,
    sessionProvider: {
        // 合成データ作成には高性能な PCC モデルを使用
        LanguageModelSession( 
            model: PrivateCloudComputeLanguageModel(),
            instructions: """
                You are a synthetic data generator for a book-tracking app's evaluation suite.
                Rules:
                - Review must be at least 100 characters long.
                - Review should cover a mix of genre, mood/tone, and themes.
                - Create between 3 and 8 tags (must be lowercase).
                """ 
        )
    },
    // サンプリング戦略: ランダム選出 (.random) または 順次選出 (.slidingWindow)
    samplingStrategy: .random,
    
    // ② 合成サンプルの自動バリデーション (validator)
    validator: { sample in
        guard let book = sample.expected else { return false }

        // レビューは100文字以上
        guard sample.promptDescription.count >= 100 else { return false }

        // タグ数は 3〜8 個
        guard (3...8).contains(book.tags.count) else { return false }

        // タグはすべて小文字
        guard book.tags.allSatisfy({ $0 == $0.lowercased() }) else { return false }

        return true
    }
)

② 有効/無効サンプルの抽出

実行後、条件に合格したサンプル(samples)と却下されたサンプル(invalidSamples)を区別して取り出せます。

for try await sample in generator.run() {
    expandedDataset.append(sample)
}

// フィルタリング結果の取得
let validSamples = await generator.samples
let invalidSamples = await generator.invalidSamples

print("Valid: \(validSamples.count), Invalid: \(invalidSamples.count)")


3. エージェントの軌跡評価:TrajectoryExpectation によるツール検証

エージェント型アプリの評価では、「最終出力が合っているか」だけでなく、「正しいツールを、正しい引数で、正しい順序で呼び出したか」(実行軌跡 = Trajectory)を検証する必要があります。

① 代表的な引数マッチャー(Argument Matchers)

// 1. 完全一致 (.exact)
ToolExpectation("searchBooks", arguments: [.exact(argumentName: "tag", value: .string("gothic"))])

// 2. 意図・自然言語マッチ (.naturalLanguage)
ToolExpectation("searchBooks", arguments: [
    .naturalLanguage(
        argumentName: "mood",
        criteria: "Should relate to uplifting, hopeful, or positive feelings"
    )
])

// 3. 存在確認のみ (.keyOnly)
ToolExpectation("getBookDetails", arguments: [.keyOnly(argumentName: "bookId")])

  • その他マッチャー: .contains, .oneOf, .pattern, .range など

② 順序付き検証(ordered)と順不同検証(unordered)

マルチステップの処理(例:「検索」を行ってから「詳細取得」をする)では順序(ordered)が決定的に重要となります。

// 「ゴシック調の本を検索し、最初の1冊の詳細を取得する」軌跡期待値
let expectation = TrajectoryExpectation(
    ordered: [
        ToolExpectation(
            "searchBooks",
            arguments: [.exact(argumentName: "tag", value: .string("gothic"))]
        ),
        ToolExpectation(
            "getBookDetails",
            arguments: [.keyOnly(argumentName: "bookId")]
        )
    ]
)

③ 禁止ツール(disallowed)の明示

「余計な類似本検索を走らせない」といったネガティブケースをアサートできます。

let expectation = TrajectoryExpectation(
    unordered: [
        ToolExpectation("searchBooks", arguments: [.naturalLanguage(argumentName: "genre", criteria: "sci-fi")])
    ],
    // このツールが呼ばれたらテスト不合格 (Failure)
    disallowed: [
        ToolExpectation("findSimilarBooks")
    ]
)


4. ツール呼び出し評価スイート (ToolCallEvaluator) の構築

データセットと TrajectoryExpectation を組み合わせ、ToolCallEvaluator でテストスイートを作成します。

import Evaluations

// 1. 期待されるツール呼び出し軌跡を含む ModelSample 群
let toolSamples = SampleArrayLoader(samples: [
    ModelSample(
        prompt: "Find all the books tagged with 'gothic' and show details for the first.",
        instructions: "Help the user explore their book collection.",
        expectations: TrajectoryExpectation(
            ordered: [
                ToolExpectation("searchBooks", arguments: [.exact(argumentName: "tag", value: .string("gothic"))]),
                ToolExpectation("getBookDetails", arguments: [.keyOnly(argumentName: "bookId")])
            ]
        )
    )
])

// 2. Evaluation 構造体の定義
struct BookLibraryToolCallEval: Evaluation {
    var dataset = toolSamples

    let pass = Metric("All Passed")
    let percent = Metric("Percentage Passed")

    // トランスクリプト全体を自動解析・検証する Evaluator
    var evaluators: Evaluators { 
        ToolCallEvaluator(allPass: pass, percentagePass: percent)
    }
}


5. ツール評価用サンプルの合成(構造化データの自動生成)

TrajectoryExpectation 自体も @Generable マクロに対応しているため、「プロンプト」と「期待されるツール呼び出し構成」のセット自体も合成生成パイプラインで自動量産できます。

let syntheticToolPrompt = Prompt("""
    Generate diverse user queries for a personal book library assistant.
    Each sample needs a prompt, and a trajectory expectation describing which tools should be called.
    """)

let generator = SampleGenerator<ModelSample<BookLibraryOutput>>(
    syntheticToolPrompt,
    samples: initialToolSamples,
    targetCount: 100,
    sessionProvider: {
        LanguageModelSession(
            model: PrivateCloudComputeLanguageModel(),
            instructions: """
                AVAILABLE TOOLS:
                - searchBooks(query?, tag?, mood?, genre?, limit?)
                - getBookDetails(bookId)
                - findSimilarBooks(bookId, maxResults?)

                ORDER REQUIREMENTS:
                searchBooks must come before getBookDetails or findSimilarBooks.
                """
        )
    },
    // 合成された TrajectoryExpectation の整合性をバリデーション
    validator: { sample in
        guard let expectations = sample.output.expectations else { return false }
        
        let total = expectations.ordered.count + expectations.unordered.count
        guard total > 0 else { return false }

        let validTools: Set<String> = ["searchBooks", "getBookDetails", "findSimilarBooks"]
        let allExpectations = expectations.ordered + expectations.unordered + expectations.disallowed
        
        // 存在しないツール名が生成されていないかチェック
        return allExpectations.allSatisfy { validTools.contains($0.name) }
    }
)


6. 全体まとめ・開発のロードマップ

評価アプローチ 概要 主なツール / API
データセット拡張 少量データから100〜1,000件の多様なテストケースを自動生成 makeSamples(), SampleGenerator
自動バリデーション ルール違反(文字数不足、形式エラー等)の合成データを自動除去 validator クロージャ
軌跡評価 (Trajectory) エージェントのツール呼び出し順序・引数を完全検証 TrajectoryExpectation, ToolExpectation
引数照合 (Matchers) 完全一致から自然言語による曖昧照合まで対応 .exact, .naturalLanguage, .keyOnly
エンドツーエンド検証 生成コンテンツ品質(評価者)と実行プロセス(軌跡)の両立 ToolCallEvaluator + ModelJudgeEvaluator

Code Along:Foundation Modelフレームワークによるハンズオン開発 (Meet with Apple)

Apple Intelligenceを支えるオンデバイスLLM(大規模言語モデル)をSwiftAPI経由でアプリに組み込む手法を、ハンズオン(実機コーディング)形式で解説します。静的な旅行アプリを、構造化出力・リアルタイムストリーミング・ツール呼び出し・パフォーマンス最適化を備えた動的な「AI旅行プランナー」へ段階的に進化させます。


開発環境とシステム要件

  • OS / IDE: macOS Tahoe 26 / Xcode 26
  • ハードウェア: Apple Intelligence 対応の Apple シリコン搭載 Mac(iOS 26 搭載 iPhone でも実行可)
  • 初期条件: macOS の設定で Apple Intelligence が有効(ON)になっていること

第1章:Foundation Modelフレームワークの基本

まずは Playgrounds で動作を確認し、モデルのセッション作成、指示(Instructions)による応答制御、および端末の利用可能性(Availability)の判定を実装します。

1.1 基本的なテキスト生成 (LanguageModelSession)

import FoundationModels
import Playgrounds

#Playground {
    // 言語モデルセッションの作成
    let session = LanguageModelSession()

    // 非同期でプロンプトに対する応答を生成
    let response = try await session.respond(to: "Generate a 3-day itinerary to Paris.")
    print(response.content)
}

1.2 指示 (Instructions) によるペルソナとルールの設定

指示はセッション全体に適用される永続ルール・ペルソナを定義します。プロンプトインジェクションに対する防壁としても機能します。

#Playground {
    let instructions = """
        Your job is to create an itinerary for the user.
        Each day needs an activity, hotel and restaurant.
        Always include a title, a short description, and a day-by-day plan.
        """

    let session = LanguageModelSession(instructions: instructions)
    let response = try await session.respond(to: "Generate a 3-day itinerary to Paris.")
}

1.3 モデルの可用性(Availability)のチェック

ユーザーの端末状況に応じて適切にフォールバック表示を行います。

let model = SystemLanguageModel.default

switch model.availability {
case .available:
    // 利用可能:生成UIを表示
    LandmarkTripView(landmark: landmark)
case .unavailable(.appleIntelligenceNotEnabled):
    // Apple Intelligenceがオフの場合
    MessageView(message: "Apple Intelligence has not been turned on.")
case .unavailable(.modelNotReady):
    // アセットのダウンロード中など一時的な不可状態
    MessageView(message: "The model is not ready yet. Try again later.")
case .unavailable(.deviceNotEligible):
    // 対象外端末
    MessageView(message: "The model is not available on this device.")
default:
    MessageView(message: "Trip Planner is unavailable.")
}


第2章:構造化出力の生成(ガイド付き生成)

手動の文字列解析コード(String Parsing)を排除し、@Generable マクロを用いて100%型安全な Swift 構造体で出力を受け取ります。

2.1 データモデルの定義 (@Generable と @Guide)

import FoundationModels

@Generable
struct Itinerary {
    @Guide(description: "An exciting name for the trip.")
    let title: String

    @Guide(description: "A short, engaging description of the trip.")
    let description: String

    let rationale: String
    let days: [DayPlan]
}

@Generable
struct DayPlan {
    let title: String
    let subtitle: String
    let destination: String
    let activities: [Activity]
}

@Generable
struct Activity {
    let type: ActivityType
    let title: String
    let description: String
}

@Generable
enum ActivityType: String {
    case sightseeing, foodAndDining, shopping, hotelAndLodging
}

2.2 ViewModel での構造化リクエスト

// ViewModelでの生成処理
let response = try await session.respond(
    to: "Generate a 3-day itinerary to \(landmark.name).",
    generating: Itinerary.self // 生成したい型を指定
)
self.itinerary = response.content // Itinerary型のインスタンスが得られる


第3章:プロンプティングのテクニック

より高品質で一貫性のある出力を得るため、PromptBuilder による動的プロンプト構築と ワンショット・プロンプティング(One-shot Prompting) を適用します。

3.1 PromptBuilder とワンショット例の注入

let prompt = Prompt {
    "Generate a \(dayCount)-day itinerary to \(landmark.name)."
    "Give it a fun title and description."
    "Here is an example of the desired format, but don't copy its content:"
    Itinerary.exampleTripToJapan // Swiftで定義した模範データ構造をそのまま埋め込む
}

let response = try await session.respond(
    to: prompt,
    generating: Itinerary.self
)


第4章:ストリーミング応答による動的 UI

全文生成を待たずに、生成過程のデータ(スナップショット)を部分的に取得してリアルタイムに UI を描画します。

4.1 ViewModel のストリーミング化 (streamResponse)

Itinerary.PartiallyGenerated(すべてのプロパティが Optional になった自動生成型)を使用します。

// ViewModelの保持プロパティを PartiallyGenerated に変更
private(set) var itinerary: Itinerary.PartiallyGenerated?

func generateItinerary() async {
    let stream = session.streamResponse(
        to: prompt,
        generating: Itinerary.self
    )
    
    // AsyncSequence で順次受託
    for try await partialResponse in stream {
        self.itinerary = partialResponse.content
    }
}

4.2 SwiftUI ビューでの安全なアンラップ (if let)

struct ItineraryView: View {
    let itinerary: Itinerary.PartiallyGenerated

    var body: some View {
        VStack(alignment: .leading) {
            if let title = itinerary.title {
                Text(title)
                    .font(.largeTitle)
                    .contentTransition(.opacity)
            }
            if let description = itinerary.description {
                Text(description)
                    .foregroundStyle(.secondary)
            }
            if let days = itinerary.days {
                ForEach(days, id: \.title) { plan in
                    DayView(plan: plan)
                }
            }
        }
        .animation(.easeOut, value: itinerary)
    }
}


第5章:ツールの呼び出し (Tool Calling)

モデルが自律的に外部 API やローカル Swift 関数を呼び出し、リアルタイムな名所情報(POI: Points of Interest)を取得して文脈に組み込めるようにします。

5.1 ツールの作成 (Tool プロトコル)

@Observable
final class FindPointsOfInterestTool: Tool {
    let name = "findPointsOfInterest"
    let description = "Finds points of interest for a landmark."
    let landmark: Landmark

    init(landmark: Landmark) {
        self.landmark = landmark
    }

    @Generable
    struct Arguments {
        @Guide(description: "This is the type of business to look up for.")
        let pointOfInterest: Category
    }

    @Generable
    enum Category: String, CaseIterable {
        case hotel, restaurant
    }

    func call(arguments: Arguments) async throws -> String {
        let results = await getSuggestions(category: arguments.pointOfInterest, landmark: landmark.name)
        return "There are these \(arguments.pointOfInterest) in \(landmark.name): \(results.joined(separator: ", "))"
    }
}

5.2 セッションへの登録と決定論的サンプリング (greedy)

テストや再現性の確保、およびツール発動の決定論化のために greedy サンプリングを使用します。

let poiTool = FindPointsOfInterestTool(landmark: landmark)

let instructions = Instructions {
    "Your job is to create an itinerary for the user."
    "Always use the 'findPointsOfInterest' tool to find hotels and restaurants in \(landmark.name)."
}

// ツールをセッションに登録
self.session = LanguageModelSession(
    tools: [poiTool],
    instructions: instructions
)

// greedyサンプリングで応答を要求
let stream = session.streamResponse(
    to: prompt,
    generating: Itinerary.self,
    options: GenerationOptions(sampling: .greedy)
)


第6章:パフォーマンスと最適化

Xcode Instruments(Foundation Models テンプレート)を使用してボトルネックを検知し、レイテンシー(TTFT: Time To First Token)を最小化します。

6.1 Instruments によるプロファイリング

  • Xcode で Product > Profile (⌘+I) を選択。
  • Foundation Models テンプレートを追加して記録実行。
  • ボトルネックの特定:
  1. 画面遷移後のモデル読み込み遅延(アセットのロードで約700ms消費)。
  2. 不要なスキーマ定義の重複による入力トークン数の増大。

6.2 対処法 ①:モデルの事前ウォームアップ (prewarm)

画面が表示されたタイミング(.task など)でモデルをあらかじめメモリにロードします。

// ViewModel
func prewarmModel() {
    session.prewarm(promptPrefix: "\(landmark.name)の3日間の旅程を生成して")
}

// View (.task モディファイア内)
.task {
    let generator = ItineraryGenerator(landmark: landmark)
    self.itineraryGenerator = generator
    generator.prewarmModel() // ボタンが押される前に事前読み込み
}

6.3 対処法 ②:プロンプトからの重複スキーマ除外 (includeSchemaInPrompt: false)

ワンショット例で既に型構造(スキーマ)が渡されている場合、重複するスキーマ定義出力をオフにして入力トークン数を大幅に削減します。

let stream = session.streamResponse(
    to: prompt,
    generating: Itinerary.self,
    includeSchemaInPrompt: false // プロンプト内の重複スキーマ出力を削除して高速化
)


全体まとめと学習ロードマップ

章 主な解説トピック 実装した主要 API / マクロ
第1章 テキスト生成基礎・可用性チェック LanguageModelSession, SystemLanguageModel.default.availability
第2章 構造化出力(型安全な生成) @Generable, @Guide, generating: Itinerary.self
第3章 プロンプティング技法 Prompt, Itinerary.exampleTripToJapan(One-shot)
第4章 リアルタイムストリーミング session.streamResponse, Itinerary.PartiallyGenerated
第5章 ツール呼び出し(外部連携) Tool プロトコル, GenerationOptions(sampling: .greedy)
第6章 遅延削減・トークン最適化 session.prewarm(), includeSchemaInPrompt: false, Instruments

「Apple Intelligence」グループラボ 質疑応答 (WWDC2026)

WWDCのオンライングループラボでは、Apple Intelligence、Foundation Models、App Intents、Siri、Evaluations Framework、Private Cloud Compute(PCC)を担当するエンジニアチームが、デベロッパからの実用的な疑問に直接回答しました。

本記事では、セッション内で交わされた全30以上の質疑応答をテーマ別に整理し、最新プラットフォームにおける設計のベストプラクティスと公式見解をわかりやすく網羅しています。


1. App Intents / App Schemas / Siri AI インテグラル

Q1. アプリの概念が既存の「App Schema」に完全には適合しない場合、一番近いスキーマを使うべきですか?

回答:
機能が既存の標準スキーマ(Timer, Calendar, Messagesなど)に合致する部分は、そのドメインのスキーマ(AppSchema)を採用してください。合致しない機能についてはカスタム App Intent を併用できます。

  • 新しい Siri AI エクスペリエンスにネイティブ統合するには、原則として App Schema の採用が必須です。
  • ドメインに合わない汎用的なアクションには、検索用の system.search(検索画面へのディープリンク)や開く操作用の system.open(Spotlight donate されたエンティティの起動)などのシステムドメイン・スキーマを活用できます。
  • 音声の固定フレーズで直接起動したいヒーロー機能には、従来の App Shortcuts(フレーズ定義)が引き続き強力です。

Q2. スキーマに合致しないコアエンティティを持つアプリは、SiriのAgentic(自律型)体験にどう備えるべきですか?

回答:
エンティティを IndexedEntity プロトコルに準拠させ、Spotlight(Semantic Index)へドネーション(Donate)してください。Siriはセマンティックインデックスを参照してエンティティの文脈を理解し、画面上のアノテーションAPI(view annotations)と組み合わせることで質問や操作に対応可能になります。

Q3. サードパーティアプリから他のアプリの Intent を直接呼び出す API はありますか?

回答:
ありません。 アプリ間のインテント連携やオーケストレーションは、プライバシーとセキュリティの観点から常に Siri / ショートカット(システムオーケストレーター) が仲介します。
他アプリとデータを安全にやり取りしたい場合は、双方向データ転送を安全に制御する Transferable API を採用してください。データ共有の境界と権限(サンドボックス)が明確化されます。

Q4. iOS 27 の新しい Siri で、サードパーティアプリはマルチターン(複数回の対話)会話に参加できますか?

回答:
参加可能です。 App Schemas を通じてアクションやエンティティを晒すことで、LLMが文脈(コンテキスト)を把握し、ユーザーの自然言語による追加の質問(フォローアップ)や連続したアクションの実行に対応できます。

Q5. 1つのアプリ内で、異なるドメインの複数の App Schema や非 Schema Intent を混ぜて使用(Mix & Match)しても問題ありませんか?

回答:
全く問題ありません(推奨されます)。 カレンダー機能とリマインダー機能を持つアプリであれば、両方のドメインから最適なスキーマを選択して組み合わせてください。さらに App Shortcuts や内部検索とも共存させることができ、これらが組み合わさることでアプリ全体の統合体験が構築されます。

Q6. App Schema を使用すると、Siri での認識(発見性)に「優遇(アドバンテージ)」がありますか?(例:Finance スキーマが存在しない件について)

回答:
優遇というより、「Appleが自然言語処理やモデル学習の重労働を肩代わりしている」 というのが正確です。App Schema を使うと、開発者側で膨大なフレーズ定義を書く必要がなくなり、1stパーティアプリと同等の一貫したUI/UXが得られます。金融などの専用スキーマがない場合は、system.search やカスタム App Intent + App Shortcuts を使用してください。

Q7. 定義済みのエンティティ(例:AppSchema.BooksEntity)と IndexedEntity の違いは何ですか?

回答:

  • App Schema(例:BooksEntity): データの「形状(プロパティ群)」を定義するもの。
  • IndexedEntity: そのコンテンツをシステムのセマンティックインデックスへ登録(Index)するためのプロトコル。
    両者は対立するものではなく、補完しあう関係です。App Schemaを適用しつつ IndexedEntity として登録することで、Siri AIが最もスマートに検索・操作できるようになります。

Q8. スキーマのエンティティは相互に変換可能(Interchangeable)ですか?(例: FileEntity を PhotoEntity に変換)

回答:
Transferable プロトコルを通じて可能です。アプリが複数の表現形式(テキスト、ファイル、写真など)への転送をサポートしていれば、ユーザーの要求に応じて適切な型(例:メッセージ添付の写真を写真アプリや他社画像アプリへ渡す)に自動変換されます。

Q9. 連絡先リストを管理するアプリで、完璧に一致しないものの .messages.messagePerson スキーマを使ってもいいですか?

回答:
問題ありません。 部分的な一致であってもメッセージ機能や人物に関する属性を晒すために採用できます。どうしても適したスキーマがない場合は system.search や App Shortcuts で補完してください。


2. Foundation Models Framework & AI パフォーマンス

Q10. バックグラウンド実行時、Foundation Models の実行時間や処理量の制限(Budget)はありますか?

回答:
フォアグラウンド実行時は一切のレート制限(Throttling)はありません。バックグラウンド実行時のみ、システム全体の高負荷時にスロットリングが適用されます。 APIがスロットリング(Rate Limit)エラーを返した場合は、エラーを捕捉(Catch)して後で再試行(Retry)する設計にしてください。

Q11. Throttling、システム高負荷、PCC利用上限(Quota)などを示すエラーコードはどのように分類されますか?

回答:
新しく導入された LanguageModel プロトコルのエラー体系(LanguageModelError)により厳密に判定できます。

  • **rateLimited / systemBusy**: リトライ可能(Retryable)な一時的エラー。
  • quotaExhausted (PCC): ユーザーの1日の利用上限に達した状態。model.quotaUsage プロパティで事前に上限への接近状況の確認も可能です。

Q12. 理由が思い当たらないのに guardrailViolation(ガードレール違反エラー)が頻発します。どう回避すればよいですか?

回答:
Evaluations Framework を導入し、その中の「モデルジャッジ(Model Judge)」機能や安全評価ルーブリックを使ってプロンプトの安全性を評価してください。オンデバイスモデルで過剰拒否が発生する場合は、よりサイズの大きい PCC サーバーモデル をジャッジ役に使い、プロンプトの文言や類義語のチューニング(Hill Climbing)を行うのが効果的です。

Q13. Foundation Models は動画(Video)の理解や動画モデルに対応していますか?それとも静止画のみですか?

回答:
システム標準の組み込みマルチモーダル入力は静止画(Still Images)のみです。
ただし、LanguageModel プロトコルは拡張可能(Extensible)に設計されており、カスタムセグメントAPIを用いて動画を扱えるモデルを自前で統合することは可能です。また、実用的なアプローチとして、Visionフレームワーク や音声テキスト化(Speech/Transcript)ツールを事前実行し、得られたテキスト表現をモデルに渡す構成も非常に強力です。

Q14. トークン制限(オンデバイスは4K)がある中で、画像の追加やプロンプト指示の制約はどう考えるべきですか?

回答:
画像1枚あたりのトークン消費量は概ね 200トークン前後(Instrumentsで測定可能)です。入力指示(instructions)やプロンプトが簡潔であるほど出力の余地が増えます。オンデバイスモデル(~3B)で精度の高い出力を得るコツは、指示の文言を精緻(Precise)にし、具体例(Few-shot Examples)を提示することです。

Q15. オンデバイスとサーバーモデル(PCC)間でコンテキストをやり取りする際、セマンティック・エントロピー(文脈の混乱・損失)をどう防ぎますか?

回答:
宣言的な DynamicProfile を使用し、プロファイル遷移時に historyTransform モディファイアを適用して不要なツールログ等を削除・整理します。また、オープンソースの FoundationModelsUtilities に含まれる要約・圧縮機能を活用することも推奨されます。


3. Evaluations Framework(評価・テスト)

Q16. Evaluations Framework は製品評価だけでなく、査読付き研究(Academic Research)にも使えますか?

回答:
十分に使用可能です。 本フレームワークは特定の LLM 評価だけでなく、あらゆる確率的システム(機械学習モデル、分類器、線形回帰など)のベンチマーク用に設計されています。評価レポートはJSON形式でエクスポートできるため、他の研究ワークフローや論文用データ生成にもシームレスに組み込めます。

Q17. Evaluations Framework で、自分のカスタムツール(Tool Calling)が正しく呼び出されたかテストできますか?

回答:
可能です。 LanguageModelSession のトランスクリプトを ToolEvaluator に渡すことで、「指定したツールが呼び出されたか」「呼び出し順序は正しいか」「引数(プロパティ)に期待通りの値が渡されたか」を厳格にアサーション(検証)できます。

Q18. Evaluations Framework は Foundation Models を使う場合、必ず使用しなければなりませんか?

回答:
必須ではありません。 ただし、ツール呼び出しの検証やプロンプト改善(Hill Climbing)を自動化・定量化する上で極めて有用です。


4. プラットフォーム / ハードウェア対応 (CarPlay, HomePod, watchOS, Mac)

Q19. Siri は CarPlay の画面コンテキストを理解し、「2行1列目の曲を再生して」のような画面操作ができますか?

回答:
CarPlayにおいても運転者の安全(ハンズフリー・道路への集中)が最優先となります。画面上の直接操作を要求するよりも、運転に支障をきたさない意図に基づく操作設計が基本となります。

Q20. HomePod / audioOS で新しい Siri AI や App Intents は使えますか?

回答:
新しい Siri AI エクスペリエンスは iPhone, iPad, Mac, visionOS で提供されます。HomePod では従来の App Shortcuts や基本的な音声操作が引き続き動作します。

Q21. watchOS における App Intents の応答は、iOS と1対1で完全に一致しますか?

回答:
基本的な動作モデルは同じですが、画面サイズや利用シーンが異なります。dialog API を使用して、AirPods(画面なし・詳細な音声応答)や Apple Watch(画面小・短文応答)、iPad(リッチなスニペット表示)ごとに応答文をカスタマイズするのがベストプラクティスです。


5. その他の機能 & 開発者エコシステム

Q22. アプリ内で Image Playground を使ってフォトリアルな画像を生成できますか?

回答:
可能です。 Image Playground API のスタイルパラメータに .all(または特定のリアル指向パラメータ)を指定することで生成可能です。

Q23. Image Playground の生成にはインターネット接続が必要ですか?

回答:
必要です。 Image Playground は高度な画像生成を行うため、バックエンドで Private Cloud Compute (PCC) を使用しています。オフライン(オンデバイス)へのフォールバックは行われません。

Q24. 同一開発者(App Group 共有)の複数アプリ間において、SpotlightSearchTool を使って互いのデータを取得できますか?

回答:
取得できません。 セマンティックインデックスの検索・参照は、セキュリティ上、データをドネーションした**該当アプリのサンドボックス内に厳格に隔離(Sandboxed)**されます。データ共有は App Group 上でのファイル共有や Transferable プロトコルを介して明示的に行ってください。

Q25. 開発者向けのおすすめ学習リソースは何ですか?

回答:

  1. WWDC セッション動画: App Intents(5セッション)、Foundation Models、Evaluations、PCC等の各種セッション。
  2. サンプルコード: ドキュメントページのサンプルコードセクション(例:UnicornChat アプリなど、実際に動作する統合実装例)。
  3. Apple Developer Forums: 実装上の具体的な疑問やコードに関するディスカッション。
  4. Feedback Assistant: バグ報告や機能リクエスト(Enhancement Request)。

Q&A セッション全体のまとめ

ドメイン 核心となる設計原則 最重要 API / ツール
Siri & App Intents 定義済み App Schema の活用による統合、IndexedEntity によるセマンティック検索 AppSchema, IndexedEntity, Transferable
Foundation Models オンデバイスと PCC の適切な使い分け、型安全な入出力とプロンプト精密化 LanguageModelSession, DynamicProfile, PCC
Evaluations & Testing 定量・定性的ベンチマークとツール呼び出しの自動検証 Evaluations Framework, ToolEvaluator, Xcode Instruments
プライバシー & セキュリティ 個人データのサンドボックス保護と、明示的なデータ共有境界の設定 Private Cloud Compute (PCC), App Groups, Sandboxing
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?