
【iOS27】FoundationModels入門③(Dynamic Profile)
はじめに
こんにちは。リテールアプリ共創部のYahiroです。
9月も最終週となり、いよいよ10月が見えてきました。
夏が終わると急に年末まですぐに日々が過ぎてしまうような気がするのは私だけでしょうか。
さて、前回Toolについての記事を書かせていただきました。
今回は、Toolに加えてDynamicProfileについて検証していきたいと思います。
この記事でわかること
LanguageModelSessionのinstructionsとToolは、宣言時にしか渡せない- Dynamic Profileを使うと、アプリの状態に応じて構成を宣言的に切り替えられる
- 切り替えても、会話の履歴はそのまま維持される
- 状態を書き換えるToolを用意すると、モデル自身がモードを切り替えられる
- プロファイルごとに、使うモデルや推論レベルも変えられる
検証環境
この記事では、以下の環境で検証しています。
- Mac mini(2024)
- Apple M4
- メモリ 16GB
- macOS GoldenGate 27.0
- Xcode 27.0
AI処理で困ること
前回、在庫をチェックするツールを作りました。
内容としては、在庫をチェックするツールを作成して、モデルに持たせ、在庫について回答をしてもらうと言うシンプルな流れです。
しかし、実際の現場では以下のような流れになることもあるでしょう。
①モデルに在庫を聞く
②在庫が存在したら、その在庫についてコーディネートを提案してもらう
今回は、アパレルを例に行なっているため、このような形になっていますが、アパレルでなくてもAの結果を持ってBを聞きたいと言う場面が出てくると思います。
このとき、Aの処理で使いたいシステムプロンプトとTool、Bの処理で使いたいシステムプロンプトとToolが異なる場合があります。
その場合、当然会話の流れは引き継いで行いたいわけですが、LanguageModelSessionにtoolsとinstructionsを渡すタイミングは宣言時にしかないため、別のToolやシステムプロンプトを使いたいときは、もう一度LanguageModelSessionを宣言しなければなりません。
会話の引き継ぎ自体は不可能ではなく、LanguageModelSession(tools:transcript:)に前のセッションのTranscriptを渡せば引き継げます。ただしその移植は自分で書く必要があります。さらにこのinitにはinstructionsを渡す口がないため、新しい指示を入れたい場合はTranscriptの中の.instructionsエントリを自分で組み立てて差し込むことになります。
つまり「やればできるが、モードが増えるほど面倒が増えていく」状態になってしまいます。
前回の復習
先の章で例に出した、在庫を聞くということと、コーディネートを提案してもらうという機能は別になります。
それぞれToolにすると、以下のようになります。
struct SearchInventoryTool: Tool{
let description = "商品名から、商品の在庫数を調べる"
@Generable
struct Arguments {
@Guide(description: "検索する商品名")
let productName: String
}
func call(arguments: Arguments) async throws -> some PromptRepresentable {
let stock = InventoryMock(name: arguments.productName)
return "\(arguments.productName) の在庫は \(stock) 個です。"
}
}
struct CoordinateInventoryTool: Tool{
let description = "商品名から、コーディネートを提案する"
@Generable
struct Arguments {
@Guide(description: "提案する商品名")
let productName: String
}
func call(arguments: Arguments) async throws -> some PromptRepresentable {
return CoordinateMock(coordinateString: arguments.productName)
}
}
func InventoryMock(name: String) -> Int{
return 2
}
func CoordinateMock(coordinateString: String) -> String{
return "秋には〇〇色が合うでしょう"
}
これらのToolを、モデルに渡すときは、下記のような書き方になります。
import Playgrounds
import FoundationModels
#Playground {
let session = LanguageModelSession(
tools: [SearchInventoryTool()],
instructions: "店舗スタッフの在庫に関する質問に答えてください。"
)
let answer = try await session.respond(to: "トートバッグの在庫ある?")
print(answer.content)
}
import Playgrounds
import FoundationModels
#Playground {
let session = LanguageModelSession(
tools: [CoordinateInventoryTool()],
instructions: "店舗スタッフのコーディネートに関する質問に答えてください。"
)
let answer = try await session.respond(to: "コーディネートを提案して")
print(answer.content)
}
このように、それぞれでボイラープレートを書かねばならず、またこれらを動的に切り替えることができません。
このような状況の時、tools、instructionsは自動で切り替わって欲しいとなると思います。
Dynamic Profileを使って動的にプロファイルを切り替える
上記を解決してくれるのが、Dynamic Profileです。
Dynamic Profileでは、instructions、tools、モディファイアをプロファイルとしてまとめて定義できる機能です。詳しくみていきましょう。
Dynamic Profileの実装をする前に、まずはモード切り替えのコードの準備をしていきましょう。
まず、以下のように、モード切り替えのenumを定義していきます。
@Generable
enum StaffMode {
case inventory // 在庫を確認する
case styling // コーディネートを提案する
}
@Observable
final class InventoryStates: @unchecked Sendable{
var mode: StaffMode = .inventory
}
次に、モード切り替えのToolを用意しましょう。
struct SwitchModeTool: Tool {
let description = "提案モードを切り替える。商品検索ならinventory、コーディネートの提案ならstyling"
let states: InventoryStates
@Generable
struct Arguments {
@Guide(description: "切り替え先のモード")
let mode: StaffMode
}
func call(arguments: Arguments) async throws -> some PromptRepresentable {
states.mode = arguments.mode
return "モードを\(arguments.mode) に切り替えました。"
}
}
これで準備は完了です。いよいよDynamic Profileを用意していきます。
以下のようにDynamic Profileを定義します。
switchでモードを切り替えられるようにし、それぞれのプロファイルに適したInstructions、toolsを渡していきます。
そしてポイントは、両方のプロファイルにSwitchModeToolを渡すことです。
片方にしか渡していないと、行きは切り替わるのに帰ってこられなくなります。たとえば在庫モードにだけ渡した場合、コーディネート提案に移ったあと、そのプロファイルには切り替える手段が無いので、スタッフが「別の商品の在庫を見たい」と言っても在庫モードに戻れません。行き来させたいなら、行き先のプロファイルにも同じToolを持たせておく必要があります。
struct InventoryProfile: LanguageModelSession.DynamicProfile {
let states: InventoryStates
var body: some DynamicProfile {
switch states.mode {
case .inventory:
Profile {
Instructions {
"店舗スタッフの在庫に関する質問に答えてください。"
}
SearchInventoryTool()
SwitchModeTool(states: states)
}
case .styling:
Profile {
Instructions {
"店舗スタッフのコーディネートに関する質問に答えてください。"
}
CoordinateInventoryTool()
SwitchModeTool(states: states)
}
}
}
}
実行してみる
ここまでで準備は完了です。
以下のように、これまで通りの呼び出しに、profileを渡して実行します。
import Playgrounds
import FoundationModels
#Playground {
let states = InventoryStates()
let session = LanguageModelSession(
profile: InventoryProfile(states: states)
)
let answer = try await session.respond(to: "トートバッグの在庫ある?")
print(answer.content)
let answer2 = try await session.respond(to: "ではコーディネートを提案して")
print(answer2.content)
}
すると出力は以下のようになります。

良いですね。会話の中で動的にinstructionsとtoolsが切り替わっています。
モデルの切り替えもできる
今回の検証では、オンデバイスのAFM3の呼び出しのみを行なっていますが、Private Cloud Computeや別のモデルなど、プロファイルによってモデル自体を切り替えたい場合もあります。
このとき、以下のような書き方で、モデルの切り替えも行うことができます。
struct InventoryProfile: LanguageModelSession.DynamicProfile {
let states: InventoryStates
let pcc: PrivateCloudComputeLanguageModel
var body: some DynamicProfile {
switch states.mode {
case .inventory:
Profile {
Instructions {
"店舗スタッフの在庫に関する質問に答えてください。"
}
SearchInventoryTool()
SwitchModeTool(states: states)
}
case .styling:
Profile {
Instructions {
"店舗スタッフのコーディネートに関する質問に答えてください。"
}
CoordinateInventoryTool()
SwitchModeTool(states: states)
}
.model(pcc)
.reasoningLevel(.deep)
}
}
}
なおpccを受け取るようにしたので、呼び出し側も引数を渡す形に変わります。
let states = InventoryStates()
let session = LanguageModelSession(
profile: InventoryProfile(states: states, pcc: PrivateCloudComputeLanguageModel())
)
Private Cloud Compute、外部モデルの切り替えについては別途記事で扱う予定です。
まとめ
LanguageModelSessionはinstructionsとToolを宣言時にしか渡せないため、モードごとに使い分けようとすると同じようなコードが増えていく- Dynamic Profileは、
instructions・tools・モディファイアをプロファイルとしてまとめ、状態に応じて切り替えてくれる bodyの中でswitchを書くだけでよく、会話の履歴の引き継ぎはフレームワークがやってくれる- 切り替え用のToolを両方のプロファイルに持たせると、モデル自身が行き来できるようになる
.model()や.reasoningLevel()で、プロファイルごとにモデルや推論レベルも変えられる
さいごに
Dynamic Profileを使って、複雑な組み合わせを動的に切り替えることができるようになりました。
業界ごとの複雑なオペレーションにも対応できそうです。
次回は、Private Cloud Computeについて調べていきたいと思います。
ではまた。









