WWDC Quick Look 💓 By SwiftGGTeam
Meet the Evaluations framework

Meet the Evaluations framework

观看原视频

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,即被测代码的输出,返回 passingfailing
  • rationale 参数很重要,它在报告中显示失败原因

用 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 宏配合 .evaluates trait 将评估注册到 Swift Testing
  • EvaluationContext.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:告诉裁判模型它在评估什么 App
    • evaluationTarget:格式化被测输出供裁判查看
    • 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

评论

GitHub Issues · utterances