为智能体 App 打造稳健的评估
Create robust evaluations for agentic apps
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:Apple 终于把大模型评估从“只看最终结果”升级到了“审查中间路径”,TrajectoryExpectation (轨迹期望) 让智能体 (Agentic) 应用那些暗箱操作的工具调用变得完全可测试、可量化。
这场 Session 讲了什么
做 AI 功能最怕什么?手写 13 个测试用例全过,一上真实用户数据直接翻车。尤其是现在流行做 Agent,模型在后台调了四五个工具,最后拼凑出一个看似正确的答案,其实中间逻辑全错。这次 Session 就是来解决“测试覆盖率”和“黑盒过程”这两个痛点。
Apple 在 Xcode 27 的 Evaluations 框架里加了两把斧头。第一把是 SampleGenerator (样本生成器),让你用代码和少量种子数据批量“伪造”几百条合成数据,并且自带 validator (验证器) 拦截垃圾数据。第二把是 TrajectoryExpectation,它不只看模型最后输出了什么,而是去查会话记录 (Session transcript) 里的“监控录像”,核实模型是不是按正确的顺序、传了正确的参数调用了正确的工具。这对所有在 iOS 端做复杂 AI 工作流的开发者来说,是直接从“盲人摸象”进化到了“全链路追踪”。
值得深挖的点
合成数据的“既要又要”与 SampleGenerator 的妥协
手写评估数据太慢,让大模型直接生成又容易产出垃圾,Apple 的解法是把生成和验证拆开。SampleGenerator 的设计很有意思,它没有搞一个巨大的黑盒,而是暴露了 sessionProvider (会话提供器) 和 validator (验证器) 两个闭包。
这里有个巨大的坑:Apple 明确提到,生成过程中如果上下文窗口 (Context window) 耗尽,框架会重新调用 sessionProvider 拿一个新会话。这意味着你的系统指令 (Instructions) 必须是“自包含”的,绝对不能依赖前几次生成的上下文。很多开发者习惯在 prompt 里写“参考前面的风格”,这在批量生成时会直接导致后半段数据画风突变。
validator 的设计非常务实。Apple 没有用“让另一个大模型来当裁判”这种昂贵且不稳定的方案,而是让你写硬编码规则(比如字符数大于 100、标签数量在 3-8 个之间)。这种“大模型发散生成 + 传统代码严格收敛”的组合,是目前工程化落地最稳的做法。
从“黑盒评判”到“白盒抓包”:TrajectoryExpectation 的降维打击
评估 Agent 最痛苦的不是结果错,而是“结果对,但路径错”。比如用户搜书,模型没调搜索工具,而是靠幻觉编了一本存在的书,最后碰巧蒙对了。TrajectoryExpectation 就是为了解决这个问题,它直接去扒 LanguageModelSession 的底层记录。
最惊艳的设计是参数匹配器里的 .naturalLanguage (自然语言匹配)。当你评估一个带有 mood (情绪) 参数的工具调用时,你很难用正则去穷举“cheerful”、“happy”、“uplifting”。Apple 允许你传入一段自然语言描述(如“应该与积极、充满希望的情感相关”),让评估框架在后台用小模型去判断参数意图是否匹配。这相当于用魔法打败魔法,既保留了代码测试的严谨结构,又兼容了大模型输入的模糊性。
代码片段
1. 用 SampleGenerator 批量造数据并过滤垃圾
场景:用 13 个种子书评生成 100 条新数据,并拦截掉太短或标签格式不对的废品。
// 定义生成器和验证规则
let generator = SampleGenerator<ModelSample<BookTags>>(
prompt,
samples: seedDataset,
targetCount: 100,
sessionProvider: {
// 使用 Private Cloud Compute 模型获取更大上下文
LanguageModelSession(model: PrivateCloudComputeLanguageModel())
},
validator: { sample in
guard let book = sample.expected else { return false }
// 拦截短评:书评必须大于 100 个字符
guard sample.promptDescription.count >= 100 else { return false }
// 拦截乱打标签:数量必须在 3 到 8 个之间
guard (3...8).contains(book.tags.count) else { return false }
// 拦截大小写混乱:标签必须全小写
guard book.tags.allSatisfy({ $0 == $0.lowercased() }) else { return false }
return true
}
)
// 运行并收集有效数据,垃圾数据会自动进入 invalidSamples
for try await sample in generator.run() {
expandedDataset.append(sample)
}
坑:sessionProvider 可能会在运行中途被多次调用(上下文爆满时),里面的 instructions 千万别写“保持和之前一致”这种依赖上下文的话。
2. 用 TrajectoryExpectation 审查 Agent 的工具调用路径
场景:验证模型在搜索“哥特式小说”并查看详情时,是否按顺序调用了正确的工具,且参数意图正确。
// 定义轨迹期望:必须是有序的两次工具调用
let expectation = TrajectoryExpectation(
ordered: [
// 第一步:必须调用 searchBooks,且 tag 参数精确匹配 "gothic"
ToolExpectation(
"searchBooks",
arguments: [.exact(argumentName: "tag", value: .string("gothic"))]
),
// 第二步:必须调用 getBookDetails,且传入了 bookId 参数
ToolExpectation(
"getBookDetails",
arguments: [.keyOnly(argumentName: "bookId")]
)
]
)
// 如果是模糊意图,比如搜索“让人开心的书”,可以用自然语言匹配
let moodExpectation = ToolExpectation(
"searchBooks",
arguments: [
.naturalLanguage(
argumentName: "mood",
criteria: "应该与令人愉悦、积极向上或充满希望的情感相关"
)
]
)
坑:别滥用 .naturalLanguage 匹配,它会在后台触发额外的模型推理,拖慢整个评估套件的运行速度,能用 .exact 或 .pattern 的地方优先用硬规则。
最佳实践
- 老项目迁移:把你现有的
XCTest里手写的几个 AI 测试用例抽离出来,作为SampleGenerator的samples种子。先跑一次生成,人工 review 一下invalidSamples,调整validator规则,把数据集扩充到 100-200 条再跑正式评估。 - 新项目架构:把“结果评估”和“轨迹评估”拆成两个独立的 Test Suite。结果评估跑得快,可以在每次改 Prompt 时跑;轨迹评估涉及真实工具调用和多次推理,耗时较长,建议放在 Nightly Build 或合入主干前的 CI 里跑。
- 避坑指南:在写工具的
@Generable参数时,尽量把参数设为 Optional (可选)。如果你强制要求所有参数必填,模型在遇到模糊指令时可能会为了凑参数而产生幻觉,导致TrajectoryExpectation验证失败。
还有什么值得关注
- Xcode 27 Evaluations Report 支持 Diff 对比:可以直接在 Xcode 里对比 13 条数据和 100 条数据跑出来的质量分数,分数下降会直观提醒你 Prompt 或功能存在隐患。
- Private Cloud Compute 模型开放给生成器:在
sessionProvider中可以指定使用 PCC 模型,利用其更大的上下文窗口来处理复杂的合成数据生成任务。 - 合成数据也能生成工具期望:因为
TrajectoryExpectation也遵循Generable协议,你甚至可以让大模型帮你自动生成测试 Agent 工具调用的测试用例。