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 功能的优化从”凭感觉炼丹”变成了可量化的科学实验。
详细内容
评估框架基础组件
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)
}
}
}
关键点:
dataset是Loader类型,可以是数组、JSON 文件或其他数据源subject方法是异步的,支持调用 API 或本地模型evaluators可以包含多个评估器,每个评估器返回不同的指标aggregateMetrics中可以对指标进行分组统计
评分维度的精确设计
评分维度(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 裁判对齐评估
为了验证模型裁判是否与人类专家一致,需要建立一个专门的评估。这个评估的数据集是之前评估运行的输出,包含模型生成的标签和专家的手动评分。
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方法不需要重新生成标签,直接返回已生成的结果- 使用相同的
relevance和usefulness维度 - 评估目标是模型裁判的评分与专家评分的一致性
Cohen’s kappa 自定义聚合
在聚合阶段,计算 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 返回值
裁判校准测试
使用 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 - 测试失败意味着裁判需要进一步校准
裁判提示词的迭代改进
校准裁判的过程也是爬山法:建立实验组与对照组,每次修改一个变量。
对照组使用原始提示词,实验组使用更详细的提示词:
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: ", ") ?? ""]
}
)
)
}
}
关键点:
- 提供更多上下文,告诉裁判它的角色和任务
- 明确列出”好标签”和”坏标签”的特征
- 保持评估目标和参考信息格式一致
- 在测试套件中同时运行对照组和实验组
少样本示例校准
当描述性提示词仍然不够时,可以提供少量工作示例让模型学习评分模式:
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: ", ") ?? ""]
}
)
)
}
}
关键点:
- 示例要覆盖不同情况(完美匹配、明显失败、边界情况)
- 示例数量要少,避免模型过拟合
- 每个示例要包含输入、输出和专家评分
- 示例格式要统一,方便模型理解模式
工具调用与对比评估
裁判校准完成后,可以用它来评估业务功能的改进,比如添加工具调用:
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要清晰说明工具的用途和输入来源Arguments和Output使用@Generable和@Guide帮助模型理解call方法是异步的,可以执行任意逻辑
修改业务服务以支持工具:
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
}
}
创建带工具的评估,与原始评估对比:
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 相同
}
在测试套件中对比两个评估:
@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
- 298. Evaluations overview — Evaluations 框架入门,建立评估管道
- 299. Create robust evaluations — 创建健壮的评估,覆盖更多用例
- 241. Foundation Models — Apple 基础模型能力与选择
- 324. Core AI — Core AI 框架与模型集成
- 242. Agentic apps — 智能体应用的工具调用设计
评论
GitHub Issues · utterances