Highlight
Apple 把 Core Spotlight 索引直接接入 Foundation Models 的 Tool-calling 机制,开发者只需捐赠
CSSearchableItem并挂载SpotlightSearchTool,大模型即可自动检索本地数据并生成回答,无需自建向量数据库或手写 RAG 流程。
核心内容
从手写搜索查询到让模型自己找答案
以前要在 App 里做基于本地数据的 AI 问答,开发者得自己搭建向量数据库、写 Embedding、拼 Prompt,把检索结果塞进模型上下文。这套流程工程量大,端侧跑起来更是吃力。
现在 Apple 提供了一条更短的路径。SpotlightSearchTool 遵循 Foundation Models 的 Tool 协议,让大模型可以直接对 App 的 Core Spotlight 索引发起搜索。模型自己决定什么时候查、查什么,然后把搜索结果作为上下文生成回答。
(00:07)Session 用一个徒步 App 举例。用户已经完成了一些徒步路线,每完成一条就在 App 里写下笔记。用户问”我去过哪些徒步路线?“,模型需要搜索完成日期、地点等属性才能组织答案。
(00:54)没有 SpotlightSearchTool 时,开发者只能让模型基于世界知识瞎猜。有了它,模型会调用 Tool 生成搜索查询,Spotlight 执行查询并返回结果描述,模型再基于这些真实数据生成回答。
端侧模型的 Token 限制怎么办
端侧模型的上下文窗口比云端小得多。如果把 Spotlight 索引里的所有元数据全塞给模型,轻则截断,重则推理速度断崖下跌。
Apple 的解法是 GuidanceProfile。它让开发者精确控制哪些搜索能力暴露给模型、哪些属性可以参与推理。
(08:42)比如徒步 App 没有捐赠人物关系,那就可以关闭 people 搜索,只保留 title、altitude、completionDate 等核心属性。对于上下文更受限的端侧模型,还可以直接用 .focused(.items) 级别,只返回最精简的数据。
这是一种明确的取舍:牺牲模型的”全局视野”,换取端侧推理的可用性。这也意味着,前期往 Spotlight 塞数据时,元数据必须设计得干净且结构化。
索引里存的是摘要,模型需要完整数据
Spotlight 索引为了节省空间,文本内容和 HTML 以紧凑格式存储,可以搜索但无法直接读取。模型通过 Tool 查到条目 ID 后,需要开发者提供完整数据。
(06:24)CSSearchableIndexDelegate 新增了 searchableItems(forIdentifiers:) 方法。模型查到 ID 后调用这个方法,开发者从自己的数据库(SwiftData、Core Data 等)拉取完整实体,组装成 CSSearchableItem 返回。这样模型既能高效管理百万级结果集,又能在需要时看到完整元数据。
复杂查询用 Pipeline 拆解
有些问题没法用一次搜索解决。比如”今年我每个月平均徒步多少英里?“,模型需要搜索已完成路线、按月份统计、再算平均值。
(09:46)SpotlightSearchTool 支持 Pipeline 搜索。模型把复杂查询拆成多个阶段:先搜索、再计数建表、最后求平均。每个阶段在 Spotlight 端执行,避免把大量数据拉回模型上下文。
(11:34)开发者还可以注册自定义 Pipeline 阶段。比如给徒步笔记打”开心分数”,模型在 Pipeline 中调用这个阶段,只基于高分结果生成回答。自定义阶段用 @Generable 标记,模型根据用户 Prompt 动态生成调用参数。
用 Evaluations 框架验证回答质量
AI 回答的质量不能靠肉眼逐条检查。Apple 提供了 Evaluations 框架,可以自动化测试模型的 Tool-calling 轨迹和结果覆盖率。
(13:47)开发者定义测试数据集,包含用户问题、期望的搜索结果 ID、以及期望的 Tool 调用轨迹。测试运行时,先把数据捐赠给 Spotlight,然后让模型回答问题,最后检查返回结果是否覆盖了期望的条目。
详细内容
基础接入:一行代码让 LLM 搜索你的 App 数据
(04:20)SpotlightSearchTool 的默认配置就是搜索 App 的 Core Spotlight 索引:
import CoreSpotlight
import FoundationModels
// 默认配置:搜索 App 的 Core Spotlight 索引
let tool = SpotlightSearchTool()
// 或者只搜索 App 沙盒中的文件路径
let fileTool = SpotlightSearchTool(
configuration: .init(
sources: [.files]
)
)
关键点:
SpotlightSearchTool()无需参数即可工作,前提是 App 已经捐赠了CSSearchableItemsources: [.files]限制搜索范围到文件路径,适合文档类 App
(04:50)把 Tool 加入 LanguageModelSession:
let tool = SpotlightSearchTool()
let session = LanguageModelSession(
model: model,
tools: [tool],
instructions: instructions
)
let response = try await session.respond(to: "What hikes have I gone on?")
关键点:
tools数组可以包含多个 Tool,模型自行决定调用哪个- 模型可能在一次回答中多次调用
SpotlightSearchTool,通过queryToken区分不同调用
按需加载完整数据:实现 Index Delegate
(06:24)当模型需要完整元数据时,通过 Delegate 向 App 索取:
import CoreSpotlight
class IndexDelegate: NSObject, CSSearchableIndexDelegate {
// 模型查到 ID 后,调用此方法获取完整 CSSearchableItem
func searchableItems(forIdentifiers identifiers: [String]) async -> [CSSearchableItem] {
let entries = await mystore.fetchEntries(ids: identifiers)
return entries.map { makeSearchableItem(from: $0) }
}
}
关键点:
- 这个方法在后台线程被调用,如果数据库查询耗时过长会导致模型推理超时
- 返回的
CSSearchableItem可以包含索引中没有的额外属性,专门供模型推理使用 - 适合把不适合搜索但适合模型理解的元数据(如长文本笔记)放在这里
用 queryToken 管理搜索结果流
(07:37)模型可能多次调用 Tool,每次调用产生一批结果。用 queryToken 区分:
let tool = SpotlightSearchTool()
for await reply in tool.searchResults {
if reply.queryToken != currentToken {
// 新的搜索调用,重置 UI 列表
currentToken = reply.queryToken
}
switch reply.content {
case .items(let searchItems):
// 追加到当前列表
displayItems.append(contentsOf: searchItems)
}
}
关键点:
searchResults是AsyncSequence,结果分批到达- 每次模型发起新的 Tool 调用,
queryToken会变,UI 需要据此判断是追加还是重置 - 忽略
queryToken会导致不同搜索批次的数据混在一起
精确控制模型可见的搜索能力
(08:42)GuidanceProfile 决定模型能使用哪些搜索功能:
let profile = SpotlightSearchTool.GuidanceProfile(
textMatch: true,
dates: true,
people: false,
attributes: [.title, .altitude, .completionDate]
)
let tool = SpotlightSearchTool(
configuration: .init(
guide: .init(level: .dynamic(profile))
)
)
// 端侧模型上下文更小,用 focused 级别
let focusedTool = SpotlightSearchTool(
configuration: .init(
guide: .init(level: .focused(.items))
)
)
关键点:
textMatch控制是否允许文本匹配搜索dates、people控制是否允许按日期、人物过滤attributes精确列出模型可以看到的元数据属性,未列出的属性不会进入模型上下文.focused(.items)是最精简模式,只返回条目本身,不返回额外元数据
自定义 Pipeline 阶段:让 Spotlight 帮你做计算
(11:34)注册自定义阶段,让 Spotlight 在搜索管道中执行 App 特定的计算:
import CoreSpotlight
import FoundationModels
@Generable
struct HappinessStage: CustomStage {
static var name = "happiness"
static var description = "Scores hike by how happy the author was"
static var inputTypes: [SearchPipelineDataType] = [.items]
static var outputTypes: [SearchPipelineDataType] = [.scoredItems]
@Guide(description: "Minimum happiness score (0.0-1.0) to include in results")
var threshold: Double?
func execute(on input: SearchPipelineData) async throws -> SearchPipelineData {
// 从 items 中提取笔记,跑情感分析模型
let scored = input.items.map { item in
let score = sentimentScore(for: item.attributeSet.contentDescription)
return ScoredItem(item: item, score: score)
}.sorted { $0.score > $1.score }
return SearchPipelineData(payload: .scoredItems(scored))
}
}
// 注册到 Tool 配置
let tool = SpotlightSearchTool(configuration: .init(
customStages: [.happinessBoost(threshold: 0.5)]
))
关键点:
@Generable标记让模型可以根据用户 Prompt 动态生成阶段参数@Guide给属性加描述,帮助模型理解何时使用该阶段inputTypes和outputTypes声明阶段的数据流类型,模型据此编排 Pipeline- 计算在 Spotlight 端执行,不占用模型上下文空间
Pipeline 返回的数据类型
(12:10)Pipeline 执行后,结果以多种数据类型返回:
for await reply in tool.searchResults {
let label = reply.label // LLM 生成的内容描述标签
switch reply.content {
case .items(let searchItems):
// 普通搜索结果
case .scoredItems(let scored):
// 带分数的排序结果
case .groupedItems(let groups):
// 分组结果
case .count(let count):
// 计数结果
case .table(let table):
// 表格数据
case .statistic(let statistic):
// 统计值
case .text(let text):
// 自由文本
continue
}
}
关键点:
reply.label是模型自动生成的内容描述,可以直接用作 UI 标题.table和.statistic适合回答”统计类”问题,比如”每个月平均多少英里”- 根据返回的数据类型选择不同的 UI 渲染方式
用 Evaluations 框架做自动化测试
(13:47)定义测试数据集:
import Evaluations
struct TrailRequest: ModelSampleProtocol {
typealias ExpectedValue = String
typealias Expectation = TrajectoryExpectation
var input: ModelSampleInput
var output: ModelSampleOutput<String, TrajectoryExpectation>
var expectedIdentifiers: [String] // 期望搜索返回的条目 ID
}
关键点:
ModelSampleProtocol定义测试样本的输入、期望输出和评估指标expectedIdentifiers用于验证模型是否找到了正确的数据条目
(15:06)定义期望的 Tool 调用轨迹:
TrajectoryExpectation(
unordered: [
ToolExpectation("searchSpotlight", arguments: [.keyOnly(argumentName: "query")])
]
)
关键点:
TrajectoryExpectation验证模型是否调用了预期的 Toolunordered表示不严格要求调用顺序arguments: [.keyOnly(argumentName: "query")]验证调用时传入了query参数
(15:17)运行测试并检查结果覆盖率:
@Test("Trail search evaluation meets quality thresholds")
func trailSearchEval() async throws {
let items = try Self.loadItems()
let samples = try Self.loadSamples()
// 把测试数据捐赠给 Spotlight
try await Self.indexDelegate.indexSearchableItems(items)
let tool = Self.makeSearchTool()
let evaluation = TrailSearchEvaluation(
tool: tool,
dataset: ArrayLoader(samples: samples)
)
let result = try await evaluation.run()
// 检查结果覆盖率是否达到 50%
let coverageMean = result.aggregateValue(.mean(of: Metric("ResultCoverage")))
#expect(coverageMean >= 0.5, "Result coverage should be at least 50% across queries")
}
关键点:
- 测试前先捐赠数据到 Spotlight,确保搜索环境干净
ArrayLoader加载测试样本集Metric("ResultCoverage")计算期望条目在结果中的覆盖比例- 可以用
#expect断言覆盖率阈值
核心启发
1. 给已有 App 加一个对话式搜索助手
如果你的 App 已经捐赠了 CSSearchableItem,只需引入 FoundationModels 框架,加三行代码挂载 SpotlightSearchTool,就能让大模型检索你的本地数据。用户可以用自然语言提问,模型自动决定搜索策略。
实现思路:创建 LanguageModelSession,把 SpotlightSearchTool 加入 tools 数组,监听 searchResults 流更新 UI。
2. 用自定义 Pipeline 阶段做 App 专属推理
不要什么都让主 LLM 做。比如笔记 App 可以注册一个 SentimentStage,在 Spotlight 端给笔记打情感分数。用户问”我最近心情好的笔记有哪些?“,Pipeline 先过滤出高分笔记,再交给模型生成回答。
实现思路:实现 CustomStage 协议,用 @Generable 和 @Guide 标记,注册到 SpotlightSearchTool 的 customStages 配置中。
3. 用 GuidanceProfile 适配端侧模型
端侧模型上下文有限,默认配置可能索要过多数据。根据你的数据特点裁剪 GuidanceProfile,关闭不需要的搜索能力,精确列出模型可见的属性。元数据质量直接决定 AI 回答的上限。
实现思路:分析你的 CSSearchableItemAttributeSet 中哪些属性对回答有帮助,在 GuidanceProfile 中显式声明,未声明的属性不会进入模型上下文。
4. 用 Evaluations 框架建立回归测试
AI 回答的质量会随模型版本、数据变化而波动。用 ModelSampleProtocol 定义测试集,覆盖常见用户问题,自动化验证 Tool-calling 轨迹和结果覆盖率。
实现思路:准备一批测试问题和期望答案,用 TrajectoryExpectation 验证模型是否调用了 SpotlightSearchTool,用 ResultCoverage 指标验证返回的条目是否完整。
5. 用 ContactResolver 解决”我”是谁的问题
当用户问”我和谁去的?“,模型需要知道”我”指谁。实现 ContactResolver 返回当前用户的身份信息,帮助 Spotlight 过滤出正确的结果。
实现思路:实现 ContactResolver 协议,从 App 的账户系统或 Contacts 框架获取用户信息,设置到 tool.contactResolver。
关联 Session
- Deep dive into the Foundation Models framework — Tool-calling 机制、Guided Generation 和 Generable 类型的详细讲解
- Build agentic apps with Foundation Models — 构建 Agent 应用的完整实践,包含更复杂的 Tool 编排
- Create robust evaluations for an agentic app — Evaluations 框架的深入用法,包括大规模数据集生成和自定义指标
- Swift Testing — 编写和运行自动化测试的基础
评论
GitHub Issues · utterances