Highlight
Apple 推出 Evaluations 框架,让开发者可以用 Swift Testing 对 AI 驱动的功能进行自动化评估,通过定义 Metric 和 Evaluator 量化模型输出的质量,解决了传统单元测试无法验证概率性行为的难题。
核心内容
传统测试在 AI 时代失效了
写传统代码时,输入 A 永远输出 B。XCTAssertEqual 能完美验证这种确定性行为。但接入 Foundation Models 后,同样的书评输入,模型这次可能生成 3 个标签,下次可能生成 9 个。单元测试的”输入-输出”契约被打破了。
(01:03)
开发者需要回答三个问题:我的 App 多久产生一次意外结果?Agent 多久走一次异常路径?在什么情况下会产生不安全的结果?这些问题单元测试回答不了。
Evaluations 框架:给概率性代码写”测试”
(02:08)
Evaluations 框架不追求 100% 匹配。它让你定义指标(Metric)和评估器(Evaluator),跑一批样本,然后看聚合统计。比如”80% 的情况下标签数量在 3-8 个之间”。
这个框架直接与 Swift Testing 绑定。评估跑在 App 的测试 Target 里,Xcode 会生成专属的 Evaluation Report,显示每个样本的通过情况和模型完整响应。
从两个样本到评估驱动开发
(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-4 分),不用奇数。奇数量表会让裁判模型倾向于选中间分”和稀泥”,偶数则强迫它做出明确判断。
(22:17)
另一个关键设计:ScoreDimension。不要把”质量”作为一个笼统指标,要拆成具体维度,比如”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: "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. 定义指标
let tagCount = Metric("TagCount")
// 4. 定义评估器
var evaluators: Evaluators {
Evaluator { _, subject in
let count = subject.value.tags.count
if count >= 3 && count <= 8 {
return tagCount.passing(rationale: "\(count) tags")
}
return tagCount.failing(rationale: "Got \(count) tags, expected 3-8")
}
}
// 5. 聚合统计
func aggregateMetrics(using aggregator: inout MetricsAggregator) {
aggregator.computeMean(of: tagCount)
}
}
关键点:
subject(from:)是被测代码的入口,框架会自动用 dataset 中的每个 sample 调用它ModelSample包含prompt(输入)和expected(期望输出),expected是可选的Evaluator的闭包接收subject,即被测代码的输出,返回passing或failingrationale参数很重要,它在报告中显示失败原因
用 Swift Testing 运行评估
(08:02)
@Suite("Book Tag Evaluations")
struct BookTagEvaluationTests {
let evaluation = BookTaggingEvaluation()
let evaluationInfo: [String: String] = [
"Model": "on-device",
"Dataset": "v1"
]
@Test("Book Tag Evaluations", .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 TestingEvaluationContext.current.result包含本次评估的所有指标和聚合结果aggregateValue(.mean(of:))计算指定指标的平均通过率- 优化目标设为 0.8(80%),给概率模型留有余地
用 @Guide 约束模型输出
(10:09)
@Generable
struct BookTags: Codable {
@Guide(
description: "Descriptive tags capturing themes, genres, moods, and topics from the summary",
.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: "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)
}
关键点:
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 \(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 \(tag)")
}
}
return hasGenreTag.failing()
}
}
关键点:
- 一个 Evaluation 可以包含多个 Evaluator
- 定量 Evaluator 就是 Swift 闭包,可以调用异步 API(如
knownGenres) - 每个 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("Distribution of Tag Totals") { 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: "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()
)
关键点:
scale定义 1-4 分的评分标准,每级有明确描述- 用偶数等级(4 级)避免裁判模型选中间分
judge指定裁判模型,这里用 Private Cloud Compute 的更强模型
拆分 Score Dimension
(22:17)
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"
])
)
let usefulness = ScoreDimension(
"Usefulness",
description: """
Whether the tags help users browse and filter
their personal library effectively.
""",
scale: .numeric([
4: "All tags are useful for browsing",
3: "Most tags help with browsing",
2: "Some tags are too vague to be useful",
1: "Tags don't help with browsing"
])
)
关键点:
- 每个
ScoreDimension有名称、详细描述和评分量表 - 描述要具体到裁判模型能按此标准执行
- “Relevance” 和 “Usefulness” 是两个独立维度,分开评分才能定位问题
组装完整的 Model Judge
(22:32)
var evaluators: Evaluators {
// 定量评估器
Evaluator { _, subject in /* tag count check */ }
Evaluator { _, subject in /* word count check */ }
Evaluator { _, subject in /* genre check */ }
// 定性评估器
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"]
}
)
)
}
关键点:
- 定量 Evaluator 和 ModelJudgeEvaluator 可以混用
ModelJudgePrompt提供三个关键上下文:instructions:告诉裁判模型它在评估什么 AppevaluationTarget:格式化被测输出供裁判查看reference:提供期望输出作为参考
reference是可选的,有期望标签时裁判可以对比,没有时也能独立评分
核心启发
1. 给现有 AI 功能加质量护栏
做什么:选一个已经在用 Foundation Models 的功能,写 20-30 个代表性样本的 Evaluation。
为什么值得做:大多数 AI 功能上线后没有持续的质量监控,开发者只能靠用户反馈发现问题。Evaluation 让你能在 CI 中捕获回归。
怎么开始:从最简单的定量指标入手——输出长度、格式校验、关键词包含。跑通后再加 Model Judge。
2. 用 Evaluation Report 做 Prompt 迭代
做什么:把”调 Prompt”的工作流改成”改 Prompt -> 跑 Evaluation -> 看报告”的闭环。
为什么值得做:Session 里演示了 hill-climbing 的威力。一个 @Guide(.count(3...8)) 的改动,从 50% 通过率提升到 100%。报告中的 rationale 直接告诉你下一步改哪里。
怎么开始:在 Xcode 中跑 @Test(.evaluates),打开 Report Navigator 查看 Evaluations 标签页,逐行分析 failing 样本的 rationale。
3. 用 SampleGenerator 构建回归测试集
做什么:用手写种子样本加 SampleGenerator 自动生成上千个测试用例。
为什么值得做:AI 模型版本升级、Prompt 微调、系统更新都可能改变输出行为。一个足够大的回归测试集能在发版前发现问题。
怎么开始:写 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 中设定合理的 #expect 阈值(如 >= 0.8),把测试 Target 加入 CI 构建流程。
关联 Session
- Improve your prompts by hill climbing with Evaluations — 深入讲解如何用 Evaluations 框架迭代优化 Prompt,与 Session 298 的 hill-climbing 方法论直接衔接
- Create robust evaluations for agentic apps — 扩展 Evaluations 框架在 Agentic 应用中的高级用法,包括 SampleGenerator 的深入使用
- Bring your model to Apple silicon — 了解 Foundation Models 框架的基础能力,Evaluations 框架的评估对象
- Explore machine learning with MLX — MLX Swift 的本地 AI 模型部署,可与 Evaluations 配合做模型性能评估
- Swift Testing — Evaluations 框架深度集成 Swift Testing,了解测试宏和 trait 的完整用法
评论
GitHub Issues · utterances