初心者同然の個人開発者が、WWDC25で発表されていたiOS26対応のFoundation Modelフレームワークについて勉強しながらAIApp開発にむけて勉強し記録として書いています。
間違いがあれば勉強になりますので是非ともコメントいただければ幸いです。
前回の内容:
引き続き勉強していきたいと思います。
大規模言語モデルから構造化データを取得するAPI
WWDC25でも紹介されているように、大規模言語モデルから構造化された出力を取得するのはとても困難で、必要なフィードを指定してプロンプトを作成しそれを抽出して解析する...
この方法は維持が難しく非常に脆弱という欠点がある。有効なキーが返されずメソッド全体が失敗する可能性すらある。
従来の書き方の一例としてはこんな感じ↓
"""
In a world full pixels...
"""
}
let response = try session.respond("""
Generate a character that orders a coffee.
The output should be JSON, in the following structure:
{
"name",
"order"
}
ONLY output JSON, without ```.
"""
}
let rawOutput = response.content
let data = rawOutput.data(using: .utf8)!
let decoder = JSONDecoder()
let npc = try decoder.decode(NPC.self, from: data)
return npc
)
問題点:
- 常に正しい形式で出力される保証がない
- 手動でのJSONパースが必要
- フィールド名の間違いでパースが失敗
- 維持とデバッグが困難
そこで、SwiftUI には Foundation Models Framework ではこれを解決する方法として Generable API がある。
-使用するAPI-
Generable API
公式ドキュメント
@Generable
struct NPC {
let name: String
let coffeeOrder: String
}
構造体全体に @Generable マクロを適用させることで、コンパイル時に以下のようなスキーマを生成し、モデルはそれを使用して期待される構造を生成するらしい。このマクロをイニシャライザーも生成する。
// Macro-generated
static var schema: GenerationSchema {
GenerationSchema(type: NPC.self, properties: [
GenerationSchema.Property(name: "name", type: String.self),
GenerationSchema.Property(name: "coffeeOrder", type: String.self)
])
}
// Macro-generated
extension NPC: Generable {
init(_ content: GeneratedContent) throws {
self.name = try content.value(forProperty: "name")
self.order = try content.value(forProperty: "coffeeOrder")
}
}
コンパイル時に以下が自動生成される:
- JSON スキーマ
- 制約付きデコーディング用の設定
- 型安全なイニシャライザ
これらをセッションで応答メソッドを呼び出す。
以下は generating 引数を渡して生成する方をモデルに指定する例:
func makeNPC() async throws -> NPC {
let session = LanguageModelSession(instructions: ...)
let response = try await session.respond(generating: NPC.self) {
"Generate a character that orders a coffee."
}
return response.content
}
こうすることで、Generable 型に含まれるフィードをモデルに伝えることなく言語モデルからの構造体データの取得を行うことができる。
制約付きデコーディング
LLM はトークンを作成しテキストに変換する。Generable を使うとそのテキストは型安全な方法で自動的に解析される。
制約付きデーコーディングがなければモデルは誤って間違ったフィールド名を生成する可能性がある。例えば "name" ではなく "firstName" など。その結果解析に失敗する。
そこで制約付きでコーディングを使ってモデルがこのような構造的ミスを防ぐ。
制約付きデコーディングでは無効なトークンを除外することで機能するらしい。任意のトークンを選択するのではなくスキーマに従って有効なトークンのみを選択する。
Guide マクロ
そこで使うのが @Guide マクロ。これを使って制約の範囲などを指定すことができる。
-@Guide の内部動作メカニズム-
- コンパイル時スキーマ生成
@Guide の情報がスキーマに組み込まれる。制約情報がモデルに伝達される。 - 制約付きデコーディング
無効なトークンが除外される。有効な選択肢のみが候補になる。 - 確率分布の調整
ガイドに従った確率分布が作成される。より適切な値が高確率で選択される。
例えばキャラクターのレベルを設けてそのレベルの範囲を指定したいときは、
@Guide(.range(1...10))
let level: Int
このように書く。
さらにキャラクターの名前についての細かく指定をすることができる。
この場合は、
@Guide(description: "A full name")
let name: String
このように書きことができる。
さらに言えば、このような場合にはより具体的な指示に方がより望んだイメージのトークンを生成しテキストとして返すことができるみたい。
これらを踏まえてキャラクターの特性(疲れている・お腹の空いた)なども合わせて待たせるようするためのコードを書いてみた↓
import FoundationModels
import Foundation
@available(iOS 26.0, *)
@Generable
struct NPC {
@Guide(description: "A full name")
let name: String
@Guide(.range(1...10))
let level: Int
@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)
}
}
@available(iOS 26.0, *)
func makeNPC() async throws -> NPC {
let session = LanguageModelSession(instructions: "")
// 型安全な生成 - パースエラーなし
let response = try await session.respond(generating: NPC.self) {
"Generate a character that orders a coffee."
}
return response.content // 直接NPC型で返される
}
またこれらの Generable 型のプロパティはソースコードで宣言されている順列で生成されます。この書き方だと名前 → レベル -> 属性 -> エンカウント です。
そのため、プロパティが別のプロパティの影響を受けることを想定している場合には注意が必要です。
動的スキーマについて
@Generable はコンパイル時に構造が確定している場合に最適です。しかし、実行時(アプリ動作中)に構造を決める必要がある場合があります。ユーザーがクイズの質問数を選択したり、ユーザーが選択肢の数を設定したり、ユーザーが難易度を決定する場合です。
これらは実行時まで分からないため、@Generable では対応できません。
そこで役立つのが DynamicGenerationSchema
DynamicGenerationSchema は、Foundation Models Framework の柔軟性を最大限活用できる高度な機能らしい。
実行時の要求に応じて構造化データの形式を決められるため、より動的でインタラクティブなアプリケーションの構築が可能になるとのこと。
例えば クイズを例にすると,
@Generable で書くとこんな感じ。
@Generable
struct Riddle {
let question: String
let answers: [Answer]
@Generable
struct Answer {
let text: String
let isCorrect: Bool
}
}
これだと、固定構造になり必ず question と answers を持つ。Answer は必ず text と isCorrect を持つという構造になる。
構造は固定・内容は動的生成する感じで、クイズは常に「1問、複数選択肢」の形式になる。
これを DynamicGenerationSchema を使って可変構造で書いておくならば↓
struct LevelObjectCreator {
// 動的に追加されるプロパティのリスト
var properties: [DynamicGenerationSchema.Property] = []
// このスキーマの名前
var name: String
// 文字列型のプロパティを追加するメソッド
mutating func addStringProperty(name: String) {
let property = DynamicGenerationSchema.Property(
name: name,
schema: DynamicGenerationSchema(type: String.self) // String型であることを指定
)
properties.append(property) // プロパティリストの追加
}
// 配列型のプロパティを追加するメソッド
mutating func addArrayProperty(name: String, customType: String) {
let property = DynamicGenerationSchema.Property(
name: name,
schema: DynamicGenerationSchema(
arrayOf: DynamicGenerationSchema(referenceTo: customType))
// 他のスキーマ(customType)への参照を持つ配列
)
properties.append(property)
}
// Bool型のプロパティを追加するメソッド
mutating func addBoolProperty(name: String) {
let property = DynamicGenerationSchema.Property(
name: name,
schema: DynamicGenerationSchema(type: Bool.self)
)
properties.append(property)
}
// 完成したスキーマを返すプロパティ
var root: DynamicGenerationSchema {
DynamicGenerationSchema(
name: name, // このスキーマの名前
properties: properties // 追加された全プロパティ
)
}
}
この場合、ユーザーが「3問出して」「5択にして」と設定可能になるが。レベルエディター機能が必要。
動的スキーマは他の動的スキーマの参照を持つことができる。
このスキーマを使ってユーザーの入力から指定したプロパティを持つスキーマを作成できるようになる。
スキーマ作成のコード例:
func generateDynamicRiddle(prompt: String = "Generate a fun riddle about coffee") async throws -> RiddleData {
// クイズ本体のスキーマ構築
var riddleBuilder = LevelObjectCreator(name: "Riddle")
riddleBuilder.addStringProperty(name: "question")
riddleBuilder.addArrayProperty(name: "answers", customType: "Answer")
// 答えのスキーマ構造
var answerbuilder = LevelObjectCreator(name: "Answer")
answerbuilder.addStringProperty(name: "text")
answerbuilder.addBoolProperty(name: "isCorrect")
// スキーマの完成と検証
let riddleDynamicSchema = riddleBuilder.root
let answerDynamicSchema = answerbuilder.root
let schema = try GenerationSchema(
root: riddleDynamicSchema,
dependencies: [answerDynamicSchema]
)
// AI生成実行
let session = LanguageModelSession()
let response = try await session.respond(to: prompt, schema: schema)
let generatedContent = response.content
// データ抽出
let question = try generatedContent.value(String.self, forProperty: "question")
let answers = try generatedContent.value([GeneratedContent].self, forProperty: "answers")
// 答えデータの詳細抽出
var answerData: [AnswerData] = []
for answerContent in answers {
let text = try answerContent.value(String.self, forProperty: "text")
let isCorrect = try answerContent.value(Bool.self, forProperty: "isCorrect")
answerData.append(AnswerData(text: text, isCorrect: isCorrect))
}
return RiddleData(question: question, answers: answerData)
}
// 戻り値用の構造体
struct RiddleData {
let question: String
let answers: [AnswerData]
}
struct AnswerData {
let text: String
let isCorrect: Bool
}
このような場合でも Foundation Models Framework はガイド付き生成を使用して出力がスキーマと一致するかを確認し想定外のフールドが生成される心配はないようで、たとえ動的であっても出力を手動で解析する必要は無いみたい。
@Generable はコンパイル時に定義するのに対して、DynamicGenerationSchema は実行時。
柔軟性は高いがパフォーマンスに多少影響するのと型の安全性が部分的になるという点はきになる。
とりあえず今回はここまで。
書き方・解釈に間違いがあれば勉強になりますのでぜひコメントお願いします!
引き続きAIApp開発に向けて勉強した内容をまとめつつ記録していきます。