WWDC Quick Look 💓 By SwiftGGTeam
Improve your prompts by hill-climbing with Evaluations

Improve your prompts by hill-climbing with Evaluations

观看原视频

Highlight

优化 AI 功能需要先用 Cohen’s kappa 系数校准模型裁判与人类专家的一致性,确保评估尺子准确后再迭代改进业务提示词。

核心内容

在 App 中集成 AI 功能时,一个常见难题是:模型生成的结果看起来还行,但总觉得哪里不对劲,又说不出具体问题。

Apple 的 Book Tracker 应用就面临这个问题。这个应用允许读者记录书评,自动为书籍生成标签,方便后续检索和浏览。但生成的标签经常出现两类问题:要么把读者的个人感受(如”poignant”)当成了书的标签,要么直接从书评中提取短语(如”quiet-steadiness”)作为标签,这些标签对检索毫无帮助。

开发者可能第一反应是直接修改生成标签的提示词。这种做法存在两个问题:首先,修改提示词后如何客观判断效果是否变好?其次,即使改进了生成标签的提示词,评估这些标签的”模型裁判”本身可能就是一把歪尺子——它的评分标准和人类专家不一致。

Apple 提出的解决方案是爬山法(Hill-climbing):建立评估管道,每次只改一个变量,通过评估结果指导下一步改进。但有效爬山的前提是评估工具本身必须可靠。

这就引入了”裁判对齐”问题。当数据集规模较小时,人类专家和模型裁判的评分偏差可能不明显。但随着数据集增长,这种漂移(Drift)会越来越大,最终评估结果失去参考价值。

更棘手的是,传统使用的准确率(Accuracy)指标在这里会失效。如果数据集中 80% 的样本都是高质量输出,人类专家和模型裁判都倾向于打高分,哪怕模型在瞎猜,准确率看起来也会很高。这掩盖了两者评分标准不一致的事实。

Apple 引入了 Cohen’s kappa 系数解决这个问题。这个统计指标从准确率中减去了”随机碰巧一致”的概率,能更真实地反映两个评分者的一致性。只有当 kappa 值达到 0.6 以上,才能认为模型裁判与人类专家有意义的对齐。

校准裁判的过程本身也是爬山法:修改裁判的提示词、加入评分维度的详细描述、提供少样本示例,每次改动一个变量,通过 kappa 值判断效果。一旦裁判校准完成,就可以用它来评估业务功能的改进,比如尝试添加工具调用让模型获取更多上下文信息。

这套方法论让 AI 功能的优化从”凭感觉炼丹”变成了可量化的科学实验。

详细内容

评估框架基础组件

03:54

Evaluations 框架的核心是 Evaluation 协议,一个完整的评估由四个组件构成:

数据集(Dataset):提供测试样本,每个样本包含输入和期望输出。

主题(Subject):通过 subject(from:) 方法调用待评估的功能,获取模型输出。

评估器(Evaluators):定义如何评判输出质量,包括启发式评估器和模型裁判评估器。

聚合(Aggregation):通过 aggregateMetrics 方法统计各项指标的均值、标准差等。

struct BookTaggingEvaluation: Evaluation {
    func subject(from sample: ModelSample<BookTags>) async throws -> ModelSubject<BookTags> {
        let result = try await BookTaggingService.generateTags(for: sample.promptDescription)
        return ModelSubject(value: result)
    }

    var dataset = ArrayLoader(samples:
        Book.sampleBooks.map { book in
            ModelSample(prompt: book.review, expected: BookTags(tags: book.tags))
        }
    )

    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")
        }
        // ... 更多评估器
    }

    func aggregateMetrics(using aggregator: inout MetricsAggregator) {
        aggregator.group("Heuristics") { group in
            group.computeMean(of: tagCount)
        }
    }
}

关键点

  • datasetLoader 类型,可以是数组、JSON 文件或其他数据源
  • subject 方法是异步的,支持调用 API 或本地模型
  • evaluators 可以包含多个评估器,每个评估器返回不同的指标
  • aggregateMetrics 中可以对指标进行分组统计

评分维度的精确设计

04:05

评分维度(ScoreDimension)定义了定性评估的标准。设计时要避免模糊描述,给出每个分数等级的具体含义。

let relevance = ScoreDimension(
    "Relevance",
    description: """
        每个标签是否描述了书本本身——类型、主题、基调或背景设定,
        而不是读者的反应、对书评的元评论,或关于作者的事实。
        书可以是"suspenseful"(文本属性),
        读者是"exhausted"(读者反应)。
        错误标记类型是严重失败。
        """,
    scale: .numeric([
        4: "每个标签都描述了书本本身",
        3: "大多数标签描述了书本,有一个 picked up 了读者反应或次要细节",
        2: "大多数标签是表面细节或个人反应,不是书本描述符",
        1: "标签没有有意义地描述书本"
    ])
)

let usefulness = ScoreDimension(
    "Usefulness",
    description: """
        标签能否作为书架上的分类标签——足够宽泛让多本书共享,
        又足够具体来缩小搜索范围。
        标准的类型和主题标签有效;编造的短语、角色名、
        超具体描述符和"interesting"这样的泛词不行。
        """,
    scale: .numeric([
        4: "每个标签都能让多本书分组,同时仍能缩小搜索",
        3: "大多数标签在正确水平,有一个太宽泛或太狭窄",
        2: "大多数标签太宽泛无法过滤或太狭窄无法分组",
        1: "标签对浏览没有帮助"
    ])
)

关键点

  • description 要明确什么是好的、什么是坏的
  • scale 是从 1 到 4 的数值刻度,每个分数对应具体描述
  • 维度名称使用英文,但描述可以是中文
  • 避免使用”有趣”、“强大”这类主观词汇

Cohen’s kappa 裁判对齐评估

12:31

为了验证模型裁判是否与人类专家一致,需要建立一个专门的评估。这个评估的数据集是之前评估运行的输出,包含模型生成的标签和专家的手动评分。

struct BookTagJudgmentCalibration: Evaluation {
    static let samples: [ModelSample<BookTagJudgmentValue>] = {
        guard let url = Bundle(for: BundleToken.self).url(
                forResource: "BookTaggingEvaluation-extracted", withExtension: "json"),
              let data = try? Data(contentsOf: url) else { return [] }
        // 从 JSON 构建 ModelSample 数组,包含专家评分
        // ...
    }()

    var dataset: some Loader { ArrayLoader(samples: Self.samples) }

    func subject(from sample: ModelSample<BookTagJudgmentValue>) async throws -> ModelSubject<BookTagJudgmentValue> {
        // 标签已生成,直接返回
        ModelSubject(value: sample.expected ?? BookTagJudgmentValue(
            tags: [], expertRelevanceScore: 0, expertUsefulnessScore: 0))
    }

    var evaluators: Evaluators {
        ModelJudgeEvaluator(
            judge: .default,
            dimensions: [relevance, usefulness],
            prompt: ModelJudgePrompt(
                instructions: "你正在评估为 Book Tracker 自动生成的标签...",
                evaluationTarget: { output in output.tags.joined(separator: ", ") },
                reference: { input, _ in
                    ["Expected Tags": input.expected?.tags.joined(separator: ", ") ?? ""]
                }
            )
        )
    }
}

关键点

  • 数据集来自之前评估的 JSON 附件,包含专家评分
  • subject 方法不需要重新生成标签,直接返回已生成的结果
  • 使用相同的 relevanceusefulness 维度
  • 评估目标是模型裁判的评分与专家评分的一致性

Cohen’s kappa 自定义聚合

13:00

在聚合阶段,计算 Cohen’s kappa 系数来量化对齐程度:

func aggregateMetrics(using aggregator: inout MetricsAggregator) {
    let expertRelevance = Self.samples.map { Double($0.expected?.expertRelevanceScore ?? 0) }
    let expertUsefulness = Self.samples.map { Double($0.expected?.expertUsefulnessScore ?? 0) }

    aggregator.group("Relevance") { group in
        group.computeMean(of: relevance.metric)
        group.computeStandardDeviation(of: relevance.metric)
        group.custom(of: relevance.metric, label: "Relevance Alignment Score") { judge in
            cohensKappa(ratings1: expertRelevance, ratings2: judge) ?? 0
        }
    }
    aggregator.group("Usefulness") { group in
        group.computeMean(of: usefulness.metric)
        group.computeStandardDeviation(of: usefulness.metric)
        group.custom(of: usefulness.metric, label: "Usefulness Alignment Score") { judge in
            cohensKappa(ratings1: expertUsefulness, ratings2: judge) ?? 0
        }
    }
}

关键点

  • 从样本中提取专家评分作为基准
  • 使用 custom 方法添加自定义聚合指标
  • Cohen’s kappa 返回值范围是 -1 到 1,0.6 以上代表有意义的一致性
  • 使用 ?? 0 处理可能的 nil 返回值

裁判校准测试

13:24

使用 Swift Testing 编写测试,验证对齐分数是否达标:

@Suite("Book Tag Judge Calibration")
struct BookTagJudgmentCalibrationTests {
    static let evaluation = BookTagJudgmentCalibration()

    @Test("Judge Calibration", .evaluates(evaluation))
    func evaluateJudgeCalibration() async throws {
        let result = EvaluationContext.current.result

        let usefulnessMetric = BookTagJudgmentCalibrationTests.evaluation.usefulness.metric
        let relevanceMetric = BookTagJudgmentCalibrationTests.evaluation.relevance.metric

        #expect(result.aggregateValue(.custom(label: "Relevance: Judge vs Expert")) > 0.6)
        #expect(result.aggregateValue(.custom(label: "Usefulness: Judge vs Expert")) > 0.6)
    }
}

关键点

  • 使用 .evaluates(evaluation) 注解放置评估配置
  • 通过 EvaluationContext.current.result 获取评估结果
  • 使用 #expect 断言对齐分数超过 0.6
  • 测试失败意味着裁判需要进一步校准

裁判提示词的迭代改进

16:33

校准裁判的过程也是爬山法:建立实验组与对照组,每次修改一个变量。

对照组使用原始提示词,实验组使用更详细的提示词:

struct BookTagJudgmentCalibrationExperimental: Evaluation {
    var evaluators: Evaluators {
        ModelJudgeEvaluator(
            judge: .default,
            dimensions: [relevance, usefulness],
            prompt: ModelJudgePrompt(
                instructions: """
                    你是一位经验丰富的读者和图书管理员,
                    正在评估为 Book Tracker 自动生成的标签...
                    从两个独立维度对标签集打分:相关性和有用性。

                    ## 好标签的样子
                    - 类型/形式、主题/题材、基调/氛围、背景/时代

                    ## 常见失败模式
                    - 读者反应、元评论、作者事实、类型矛盾
                    """,
                evaluationTarget: { output in output.tags.joined(separator: ", ") },
                reference: { input, _ in
                    ["Book Review": input.promptDescription,
                     "Tags Generated for the Review": input.expected?.tags.joined(separator: ", ") ?? ""]
                }
            )
        )
    }
}

关键点

  • 提供更多上下文,告诉裁判它的角色和任务
  • 明确列出”好标签”和”坏标签”的特征
  • 保持评估目标和参考信息格式一致
  • 在测试套件中同时运行对照组和实验组

少样本示例校准

20:12

当描述性提示词仍然不够时,可以提供少量工作示例让模型学习评分模式:

struct ExperimentalBookTagJudgmentCalibration: Evaluation {
    var evaluators: Evaluators {
        ModelJudgeEvaluator(
            judge: SystemLanguageModel(),
            dimensions: [relevance, usefulness],
            prompt: ModelJudgePrompt(
                instructions: """
                    你正在与一位专家图书管理员校准,该管理员评分
                    为 Book Tracker 自动生成的标签...
                    你的目标是匹配管理员的打分方式。使用工作示例进行校准。

                    ## 工作示例
                    ### 示例 A — 完美匹配(傲慢与偏见)
                    标签: romance, historical-fiction, love, redemption, passion
                    管理员评分: 相关性 4, 有用性 4

                    ### 示例 E — 明显类型矛盾(科学怪人)
                    标签: horror, science-fiction, ... self-help, self-improvement
                    管理员评分: 相关性 2, 有用性 3
                    """,
                evaluationTarget: { output in output.tags.joined(separator: ", ") },
                reference: { input, _ in
                    ["Book Review": input.promptDescription,
                     "Tags Generated for the Review": input.expected?.tags.joined(separator: ", ") ?? ""]
                }
            )
        )
    }
}

关键点

  • 示例要覆盖不同情况(完美匹配、明显失败、边界情况)
  • 示例数量要少,避免模型过拟合
  • 每个示例要包含输入、输出和专家评分
  • 示例格式要统一,方便模型理解模式

工具调用与对比评估

22:03

裁判校准完成后,可以用它来评估业务功能的改进,比如添加工具调用:

struct BookLookupTool: Tool {
    let name = "lookupBook"
    let description = "根据从读者书评中提取的区分细节——如角色名、背景设定、引用语句或关键情节点——查找书籍的标题和作者。"

    @Generable
    struct Arguments {
        @Guide(description: "从书评中提取的区分细节,用于识别书籍,如角色名、背景设定、引用语句或关键情节点。")
        var details: String
    }

    @Generable
    struct Output {
        @Guide(description: "识别出的书籍标题,如果没有匹配则返回空字符串。")
        var title: String

        @Guide(description: "识别出的书籍作者,如果没有匹配则返回空字符串。")
        var author: String
    }

    func call(arguments: Arguments) async throws -> Output {
        let needles = arguments.details
            .lowercased()
            .split(whereSeparator: { !$0.isLetter && !$0.isNumber })
            .map(String.init)
            .filter { $0.count >= 4 }

        let best = Book.sampleBooks
            .map { book -> (book: Book, score: Int) in
                let review = book.review.lowercased()
                let score = needles.reduce(0) { partial, needle in
                    partial + (review.contains(needle) ? 1 : 0)
                }
                return (book, score)
            }
            .max(by: { $0.score < $1.score })

        guard let match = best, match.score > 0 else {
            return Output(title: "", author: "")
        }
        return Output(title: match.book.title, author: match.book.author)
    }
}

关键点

  • 工具名称要描述性,让模型知道何时调用
  • description 要清晰说明工具的用途和输入来源
  • ArgumentsOutput 使用 @Generable@Guide 帮助模型理解
  • call 方法是异步的,可以执行任意逻辑

22:36

修改业务服务以支持工具:

struct BookTaggingService {
    static func generateTags(for review: String, tools: [any Tool] = []) async throws -> BookTags {
        let prompt = tagsPrompt(review: review)
        let session = LanguageModelSession(
            model: SystemLanguageModel(guardrails: .permissiveContentTransformations),
            tools: tools,
            instructions: instructions
        )
        let response = try await session.respond(to: prompt, generating: BookTags.self)
        return response.content
    }
}

22:57

创建带工具的评估,与原始评估对比:

struct BookTaggingWithLookupEvaluation: Evaluation {
    func subject(from sample: ModelSample<BookTags>) async throws -> ModelSubject<BookTags> {
        let result = try await BookTaggingService.generateTags(
            for: sample.promptDescription,
            tools: [BookLookupTool()]
        )
        return ModelSubject(value: result)
    }
    // ... 数据集、评估器、聚合与 BookTaggingEvaluation 相同
}

23:09

在测试套件中对比两个评估:

@Suite("Book Tag Evaluations")
struct BookTagEvaluationTests {
    static let evaluation = BookTaggingEvaluation()
    static let lookupEvaluation = BookTaggingWithLookupEvaluation()

    @Test("Book Tag Evaluations", .evaluates(evaluation, info: evaluationInfo))
    func evaluateBookTagging() async throws {
        let result = EvaluationContext.current.result
        let rangeMetric = BookTagEvaluationTests.evaluation.tagCount
        let dupeMetric = BookTagEvaluationTests.evaluation.noDuplicates
        #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)
        #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1)
    }

    @Test("Book Tag Evaluations (with BookLookupTool)", .evaluates(lookupEvaluation, info: lookupEvaluationInfo))
    func evaluateBookTaggingWithLookup() async throws {
        let result = EvaluationContext.current.result
        let rangeMetric = BookTagEvaluationTests.lookupEvaluation.tagCount
        let dupeMetric = BookTagEvaluationTests.lookupEvaluation.noDuplicates
        #expect(result.aggregateValue(.mean(of: rangeMetric)) >= 0.8)
        #expect(result.aggregateValue(.mean(of: dupeMetric)) == 1)
    }
}

关键点

  • 两个评估共享相同的数据集和评估器
  • 只有 subject 方法不同,一个不传工具,一个传工具
  • 可以在 Xcode 的评估报告中并排查看两个评估的结果
  • 差异分数显示了工具调用带来的改进

核心启发

1. 为生成式 AI 功能添加评估管道

做什么:任何使用 LLM 生成内容的功能都应该建立评估管道。先定义评估指标(如标签数量、相关性、有用性),收集 50-100 个真实样本,手动标注期望输出。

为什么值得做:没有评估管道,每次改提示词或换模型都只能凭感觉判断好坏。评估让迭代有量化依据,能精确定位是”输出格式不对”还是”内容质量下降”。

怎么开始:定义 Evaluation 协议实现,用 ModelSample 包装样本数据,写 Evaluator 检查关键指标,用 @Test(.evaluates) 注册到 Swift Testing。入口:Evaluation 协议 + ModelSample + Evaluator

2. 校准模型裁判后再优化业务提示词

做什么:在修改业务提示词之前,先验证模型裁判的可靠性。抽取部分样本让人类专家评分,计算 Cohen’s kappa 系数。

为什么值得做:如果裁判本身是一把”歪尺子”,它的评分无法指导改进。准确率指标会掩盖评分标准不一致的问题,Cohen’s kappa 从准确率中减去”随机碰巧一致”的概率,更真实反映对齐程度。

怎么开始:从已有评估运行结果中提取样本和专家评分,建立 BookTagJudgmentCalibration 评估,在聚合阶段用 custom 方法计算 kappa,断言分数超过 0.6。入口:custom 聚合 + cohensKappa(ratings1:expert, ratings2:judge)

3. 用控制变量法迭代改进

做什么:每次只改一个变量(提示词、评分维度、工具调用等),通过评估对比判断效果。

为什么值得做:同时改多个变量,无法知道哪个改动带来了效果变化。控制变量法让因果关系清晰,Xcode 的对比视图可以并排查看对照组和实验组的分数差异。

怎么开始:建立实验组和对照组两个 Evaluation 结构体,共享数据集和评估器,只有 subject 方法不同。在测试套件中同时运行,对比聚合指标。入口:对照组/实验组双 Evaluation + Xcode 对比视图

4. 监控评估漂移

做什么:随着数据集增长,定期重新计算 Cohen’s kappa 系数。将裁判校准测试加入 CI/CD。

为什么值得做:数据集扩大后,模型裁判和人类专家的评分偏差可能越来越明显。自动化监控能在漂移发生时及时发现,避免基于失效评估做错误决策。

怎么开始:在 Swift Testing 中用 #expect 断言 kappa 分数超过阈值,把测试加入 CI 构建流程。每次数据集更新后自动跑校准测试。入口:#expect(result.aggregateValue(.custom(label: "Relevance: Judge vs Expert")) > 0.6)

5. 用少样本示例提升裁判对齐度

做什么:当描述性提示词仍然不够时,给裁判模型提供少量工作示例,让它学习专家的评分模式。

为什么值得做:有些评分标准很难用文字精确描述,但几个具体例子就能让模型快速理解。示例覆盖完美匹配、明显失败、边界情况等不同场景,比长篇描述更有效。

怎么开始:在 ModelJudgePrompt 的 instructions 中加入”工作示例”章节,每个示例包含输入、输出和专家评分。保持示例数量精简(3-5 个),避免过拟合。入口:ModelJudgePrompt(instructions: "## 工作示例\n...")

关联 Session

评论

GitHub Issues · utterances