ハイライト
AppleはEvaluationsフレームワークを導入しました。開発者はSwift Testingを使ってAI駆動機能を自動評価できます。MetricとEvaluatorを定義することでモデル出力の品質を定量化し、従来のユニットテストでは検証できなかった確率的なふるまいを扱えるようになります。
主要内容
AI時代には従来のテストが通用しなくなる
従来のコードでは、入力Aは常に出力Bを返します。XCTAssertEqualはこの決定的なふるまいを検証するのに最適です。しかしFoundation Modelsを組み込むと、同じ書評入力から、今回は3個のタグ、次回は9個のタグが生成されるかもしれません。ユニットテストの「入力-出力」契約が崩れます。
(01:03)
開発者は3つの問いに答える必要があります。自分のAppはどれくらいの頻度で予期しない結果を出すのか。Agentはどれくらいの頻度で例外的な経路を通るのか。どんな条件で安全でない結果を出すのか。ユニットテストではこれらに答えられません。
Evaluationsフレームワーク: 確率的なコードに「テスト」を書く
(02:08)
Evaluationsフレームワークは100%の完全一致を目指しません。MetricとEvaluatorを定義し、サンプルのバッチを実行し、集計統計を見るための仕組みです。たとえば「80%のケースでタグ数が3個から8個の間に収まる」といった評価を行えます。
このフレームワークはSwift Testingと直接結びついています。評価はAppのテストTarget内で実行され、Xcodeは専用のEvaluation Reportを生成します。そこには各サンプルの合否と、モデルの完全な応答が表示されます。
2つのサンプルから評価駆動開発へ
(04:54)
Sessionでは「Book Tracker」Appを例にしています。このAppにはBookTaggingServiceがあり、書評に自動でタグを付けます。開発者はまず手動テストで、「Pride & Prejudice」は9個のタグを生成して多すぎる一方、「Dracula」は7個で妥当だと気づきます。
次に、その人間の判断を自動化します。BookTaggingEvaluationを定義し、書評と期待タグをModelSampleで包み、Evaluatorでタグ数が3個から8個の範囲に入っているかをチェックします。テストを実行すると、レポートには合格率50%と表示されます。
開発者は問題が@Generable型の@Guideマクロにあると推測します。tagsプロパティに.count(3...8)制約を追加して再評価すると、合格率は100%になります。Sessionでは、この「制約を変える -> 評価を実行する -> 結果を見る」というループをhill climbingと呼んでいます。
(10:04)
ただし話はそこで終わりません。データセットを拡張すると、すべての書評がちょうど8個のタグを生成していることが分かりました。モデルが上限に張り付いているのです。これは、定量指標が「正しいか」は教えてくれても、「よいか」は教えてくれないことを示しています。
AIでAIをテストする: Model Judge
(16:07)
タグ数、単語数、既知のジャンルを含むかといった定量指標はコードで計算できます。では定性的な問いはどう扱えばよいのでしょうか。たとえば「Alice in Wonderland」のタグに「overrated」や「pretentious」が含まれていたとします。これらは読者の反応であって、本そのものの属性ではありません。
Evaluationsフレームワークの答えはModelJudgeEvaluatorです。別のモデルを審判として使い、評価対象モデルの出力に点数を付けさせます。審判モデルは、少なくとも評価対象モデルと同等以上に強力であるべきです。たとえば、端末上の小さなモデルをPrivate Cloud Computeで判定します。
(18:53)
重要な設計の1つは、1-4点のような偶数段階の評価尺度を使い、奇数段階を避けることです。奇数段階の尺度では、審判モデルが中間点に寄りがちです。偶数段階なら、より明確な判断を強制できます。
(22:17)
もう1つの重要な設計がScoreDimensionです。「品質」を1つの曖昧な指標として扱ってはいけません。タグが本そのものを説明しているかを見る「Relevance」、タグがブラウズに役立つかを見る「Usefulness」のように、具体的な次元へ分解します。各次元には独立した採点基準とrationale(判定理由)があり、問題の場所を精密に特定できます。
(23:17)
最後に、ModelJudgePromptで審判モデルにAppの文脈を補足します。たとえば「これは個人の本棚Appであり、書評プラットフォームではない」と伝えれば、読者の批判を本の有効な説明として扱わなくなります。
詳細
基本的なEvaluationを定義する
(04:54)
import Evaluations
struct BookTaggingEvaluation: Evaluation {
// 1. テスト対象コードを定義する: BookTaggingServiceを呼び出して出力を返す
func subject(from input: ModelSample<String, BookTags>) async throws -> BookTags {
return try await BookTaggingService.generateTags(for: input.prompt)
}
// 2. データセットを定義する
var dataset = ArrayLoader(samples: [
ModelSample(
prompt: "もう夢中で、今すぐ全員にこれを読んでほしい...",
expected: BookTags(tags: ["古典", "ロマンス", "機知", "摂政時代"])
),
ModelSample(
prompt: "真夜中から午前4時まで一気読みして、もう言葉にならない...",
expected: BookTags(tags: ["古典", "ゴシック", "ホラー", "吸血鬼", "サスペンス"])
),
])
// 3. 指標を定義する
let tagCount = Metric("TagCount")
// 4. Evaluatorを定義する
var evaluators: Evaluators {
Evaluator { _, subject in
let count = subject.value.tags.count
if count >= 3 && count <= 8 {
return tagCount.passing(rationale: "\(count) 個のタグ")
}
return tagCount.failing(rationale: "タグ数は \(count)。期待値は3-8です")
}
}
// 5. 統計を集計する
func aggregateMetrics(using aggregator: inout MetricsAggregator) {
aggregator.computeMean(of: tagCount)
}
}
重要な点:
subject(from:)はテスト対象コードの入口です。フレームワークはdataset内の各sampleに対して自動でこれを呼び出します。ModelSampleには入力であるpromptと、期待出力であるexpectedが含まれます。expectedは任意です。Evaluatorのクロージャは、テスト対象コードの出力であるsubjectを受け取り、passingまたはfailingを返します。rationaleパラメータは重要です。レポート内で失敗理由として表示されるからです。
Swift Testingで評価を実行する
(08:02)
@Suite("書籍タグ評価")
struct BookTagEvaluationTests {
let evaluation = BookTaggingEvaluation()
let evaluationInfo: [String: String] = [
"モデル": "on-device",
"データセット": "v1"
]
@Test("書籍タグ評価", .evaluates(evaluation, info: evaluationInfo))
func evaluateBookTagging() async throws {
let result = EvaluationContext.current.result
let rangeMetric = BookTagEvaluationTests.evaluation.tagCount
#expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)
}
}
重要な点:
@Testマクロは.evaluatestraitを通じて評価をSwift Testingへ登録します。EvaluationContext.current.resultには、この評価実行のすべての指標と集計結果が含まれます。aggregateValue(.mean(of:))は指定した指標の平均合格率を計算します。- 最適化目標は0.8、つまり80%に設定されています。確率モデルの揺らぎを許容するためです。
@Guideでモデル出力を制約する
(10:09)
@Generable
struct BookTags: Codable {
@Guide(
description: "要約からテーマ、ジャンル、雰囲気、トピックを捉える説明的なタグ",
.count(3...8)
)
var tags: [String]
}
重要な点:
@Guideの.count(3...8)は構造的な制約であり、Prompt内の自然言語より信頼できます。- これがhill climbingの中核です。制約を自然言語から型システムへ下ろします。
データセットを拡張し、合成サンプルを生成する
(11:15)
// 既存データから構築する
var dataset = ArrayLoader(samples:
Book.sampleBooks.map { book in
ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))
}
)
// またはSampleGeneratorでサンプルを自動合成する
var samples: [ModelSample<String>] = [
ModelSample(prompt: "太陽系で最も大きな惑星は...", expected: "木星。"),
ModelSample(prompt: "タイの首都は...", expected: "バンコク。"),
]
for try await sample in samples.makeSamples(
"""
次のトピックについて、多様な文の続きを生成してください:
- 太陽系
- 世界の首都
""",
targetCount: 1000
) {
samples.append(sample)
}
重要な点:
ArrayLoaderはサンプル配列を包み、遅延読み込みをサポートします。SampleGeneratorはモデルを使って追加サンプルを自動生成し、手作業データの拡張性問題を解決します。- よいデータセットには多様性が必要です。長さの違い、ジャンルの違い、個人的な意見の有無などです。
複数の定量Evaluator
(14:02)
let wordCount = Metric("WordCount")
let hasGenreTag = Metric("HasGenreTag")
var evaluators: Evaluators {
// タグに複数の単語が含まれていないか確認する
Evaluator { _, subject in
for tag in subject.value.tags {
if tag.contains(" ") {
return wordCount.failing(rationale: "タグ \(tag) に複数の単語が含まれています")
}
}
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: "\(tag) に一致")
}
}
return hasGenreTag.failing()
}
}
重要な点:
- 1つのEvaluationには複数のEvaluatorを含められます。
- 定量EvaluatorはSwiftのクロージャであり、
knownGenresのような非同期APIも呼び出せます。 - 各Evaluatorは独立してpass/failを返し、互いに影響しません。
さらに多くの統計指標を集計する
(14:27)
let tagCount = Metric("TagCount")
let tagTotal = Metric("TagTotal")
func aggregateMetrics(using aggregator: inout MetricsAggregator) {
aggregator.computeMean(of: tagCount)
aggregator.group("タグ総数の分布") { aggregator in
aggregator.computeStandardDeviation(of: tagTotal)
aggregator.computeMean(of: tagTotal)
aggregator.computeVariance(of: tagTotal)
}
}
重要な点:
computeMeanは平均、computeStandardDeviationは標準偏差、computeVarianceは分散を計算します。groupは関連する指標をまとめ、レポート内で一緒に表示します。- 標準偏差と分散は、モデル出力が安定しているかを発見する助けになります。
Model Judgeを構築する
(18:53)
ModelJudgeEvaluator(
"TagQuality",
scale: .numeric([
4: "タグは関連性があり、ブラウズに役立つ",
3: "ほぼ関連しているが、1つのタグが曖昧または一般的すぎる",
2: "複数のタグが間違っている、または一般的すぎる",
1: "役に立たない、または無関係"
]),
judge: PrivateCloudComputeLanguageModel()
)
重要な点:
scaleは1-4点の採点基準を定義し、各レベルに明確な説明を付けます。- 4段階のような偶数の段階数を使い、審判モデルが中間点を選ぶのを避けます。
judgeは審判モデルを指定します。ここではPrivate Cloud Computeのより強力なモデルを使っています。
Score Dimensionを分ける
(22:17)
let relevance = ScoreDimension(
"関連性",
description: """
各タグが、本そのものの性質、テーマ、雰囲気を説明しているか。
偶発的な詳細や読者個人の反応を説明していないか。
""",
scale: .numeric([
4: "すべてのタグが本そのものを説明している",
3: "ほとんどのタグが本を説明している",
2: "一部のタグが個人的な反応を説明している",
1: "タグが本を意味のある形で説明していない"
])
)
let usefulness = ScoreDimension(
"有用性",
description: """
タグが、ユーザーが個人ライブラリを効果的に
ブラウズし絞り込む助けになっているか。
""",
scale: .numeric([
4: "すべてのタグがブラウズに役立つ",
3: "ほとんどのタグがブラウズの助けになる",
2: "一部のタグが曖昧すぎて役に立ちにくい",
1: "タグがブラウズの助けにならない"
])
)
重要な点:
- 各
ScoreDimensionには名称、詳細な説明、採点尺度があります。 - 説明は、審判モデルがその基準で実行できる程度に具体的である必要があります。
- 「関連性」と「有用性」は独立した2つの次元です。分けて採点するからこそ、問題の場所を特定できます。
完全なModel Judgeを組み立てる
(22:32)
var evaluators: Evaluators {
// 定量Evaluator
Evaluator { _, subject in /* タグ数チェック */ }
Evaluator { _, subject in /* 単語数チェック */ }
Evaluator { _, subject in /* ジャンルチェック */ }
// 定性Evaluator
ModelJudgeEvaluator(
judge: PrivateCloudComputeLanguageModel(),
dimensions: [relevance, usefulness],
prompt: ModelJudgePrompt(
instructions: """
ユーザーがタグをブラウズしたり絞り込んだりして
ライブラリを整理する、個人向け読書記録Appのために
生成されたタグを評価してください。
""",
evaluationTarget: { value in
"\(value.tags.count) 個の生成タグ: " + value.tags.joined(separator: ", ")
},
reference: { input, _ in
let expectedTags = input.expected?.tags.joined(separator: ", ")
return ["期待タグ": expectedTags ?? "期待タグは定義されていません"]
}
)
)
}
重要な点:
- 定量EvaluatorとModelJudgeEvaluatorは混在できます。
ModelJudgePromptは3つの重要な文脈を提供します。instructions: 審判モデルに、何のAppを評価しているのかを伝えます。evaluationTarget: 評価対象の出力を、審判が確認しやすい形に整えます。reference: 期待出力を参考情報として提供します。
referenceは任意です。期待タグがあれば審判は比較できます。なければ独立して採点できます。
重要ポイント
1. 既存のAI機能に品質ガードレールを追加する
何をするか: すでにFoundation Modelsを使っている機能を1つ選び、20-30個の代表的なサンプルを持つEvaluationを書きます。
なぜ価値があるか: 多くのAI機能はリリース後に継続的な品質監視を持たず、開発者はユーザーのフィードバックで問題を知るしかありません。EvaluationならCIで回帰を捕まえられます。
どう始めるか: まずは最も単純な定量指標から始めます。出力長、形式チェック、キーワードの有無です。動いたらModel Judgeを追加します。
2. Evaluation ReportでPromptを反復する
何をするか: 「Promptを調整する」作業を、「Promptを変える -> Evaluationを実行する -> レポートを見る」という閉ループに変えます。
なぜ価値があるか: Sessionではhill climbingの威力が示されました。@Guide(.count(3...8))という1つの変更だけで、合格率が50%から100%に上がりました。レポート内のrationaleは、次にどこを直すべきかを直接教えてくれます。
どう始めるか: Xcodeで@Test(.evaluates)を実行し、Report Navigatorを開いてEvaluationsタブを確認します。失敗したsampleのrationaleを1行ずつ分析します。
3. SampleGeneratorで回帰テストセットを構築する
何をするか: 手書きのseed sampleにSampleGeneratorを組み合わせ、数千件のテストケースを自動生成します。
なぜ価値があるか: AIモデルのバージョンアップ、Promptの微調整、システム更新は、いずれも出力挙動を変える可能性があります。十分に大きな回帰データセットがあれば、リリース前に問題を発見できます。
どう始めるか: 異なる場面をカバーするseed sampleを10-20個書き、makeSamples(targetCount: 1000)でデータセットを拡張し、完全なEvaluationを実行します。
4. 曖昧な「品質」をScoreDimensionへ分解する
何をするか: 「このAI出力はよいか」を、独立して採点できる2-4個の次元へ分けます。
なぜ価値があるか: 漠然とした「品質」スコアからは、実行可能なフィードバックが得られません。次元を分けると、rationaleが「入力を理解していない」のか「理解したが出力形式が違う」のかを正確に示せます。
どう始めるか: 人間がReviewするときに見ている観点を列挙します。それぞれをScoreDimensionにし、1-4点の具体的な採点基準を付けます。
5. EvaluationをCI/CDへ組み込む
何をするか: EvaluationテストをXcode Cloudまたは他のCIフローに追加し、目標しきい値を設定します。
なぜ価値があるか: AI機能の「テスト」はローカルだけで実行していてはいけません。CIで自動評価すれば、チームメンバーの変更がモデル性能を下げていないことを確認できます。
どう始めるか: @Test内で>= 0.8のような妥当な#expectしきい値を設定し、テストTargetをCIビルドに追加します。
関連 Session
- Evaluationsでhill climbingしながらPromptを改善する - EvaluationsフレームワークでPromptを反復改善する方法を深く扱うSessionで、Session 298のhill-climbing方法論と直接つながっています
- agentic appのための堅牢なEvaluationsを作成する - agentic appにおけるEvaluationsフレームワークの高度な使い方を広げ、SampleGeneratorも詳しく扱います
- モデルをApple siliconへ持ち込む - Foundation Modelsフレームワークの基礎能力を学びます。Evaluationsフレームワークが評価対象にするものです
- MLXで機械学習を探る - MLX SwiftによるローカルAIモデルのデプロイ。Evaluationsと組み合わせてモデル性能を評価できます
- Swift Testing - EvaluationsフレームワークはSwift Testingと深く統合されています。テストマクロとtraitの完全な使い方を学びます
コメント
GitHub Issues · utterances