用 Spotlight 让 AI Agent 搜索本地文档
被遗忘的索引
硬盘上堆着几百个 PDF 论文、Word 报告、Excel 数据表。AI Agent 对它们视而不见。
这不是能力问题,而是视野问题。Agent 的记忆系统——无论底层用 BM25、向量搜索还是 LLM 重排序——通常只索引 Markdown 文件。那些真正承载知识的二进制文档,静静地躺在文件系统里,像古籍善本锁在库房中,有目录卡片却无人翻阅。
然而操作系统一直在做这件事。macOS 的 Spotlight 从你开机那一刻起,就在后台为每一份文档建立全文索引。我们需要做的,只是把这个已经存在的能力暴露给 Agent。
Agent 的记忆盲区
以 OpenClaw 的 memory_search 为例。它底层使用 qmd 引擎,支持 BM25 + 向量语义搜索 + LLM 重排序,搜索质量很高。但 qmd 只能索引 Markdown 文件。
也就是说:
- Agent 能记住你和它聊过什么(对话日志是
.md) - Agent 不知道你硬盘上有哪些 PDF 论文、Word 文档、Excel 数据
这两个工具解决的是不同层面的问题:
| 工具 | 搜索对象 | 搜索方式 | 适用场景 |
|---|---|---|---|
| memory_search | Agent 的对话记忆 | 语义搜索 | "上次那个报错怎么解决的?" |
| mdfind | Mac 上所有文档 | 关键词匹配 | "找一下关于丝绸之路的 PDF" |
两者互补,不冲突。一个是私人日记,一个是公共图书馆。
方案:mdfind + pymupdf
mdfind
mdfind(Metadata Find)是 Spotlight 的命令行接口。Spotlight 在后台持续为磁盘上的文件建立全文索引——PDF 内的文字、Word 文档的正文、Excel 的单元格内容,无一遗漏。mdfind 让你从终端直接查询这个索引。
| |
pymupdf 读取 PDF 页面
找到文件后,还需要读取具体内容。pymupdf 是一个 Python 库,能提取 PDF 指定页面的文字:
| |
封装为 Agent Skill
将 mdfind 搜索和 pymupdf 阅读封装为两个脚本,注册为 Agent 的 Skill:
mdfind/
├── SKILL.md # Skill 定义(触发词、用法说明)
└── scripts/
├── search.py # 搜索:关键词 → 文件列表
└── read_pdf.py # 阅读:PDF 路径 + 页码 → 文字内容
search.py 封装了 mdfind 命令,支持按目录、文件类型、数量过滤,输出按修改时间倒序排列的文件列表。
read_pdf.py 使用 pymupdf 提取指定页面的纯文字,支持单页(--pages 5)、范围(--pages 3-8)和多段(--pages 1,5,10-12)。
实际效果
用户:搜一下本地有没有关于丝绸之路的 PDF
Agent:调用 search.py "丝绸之路" --type pdf
→ 找到 113 个 PDF,列出前 30 个
用户:读一下《丝绸之路:一部全新的世界史》的第 20 页
Agent:调用 read_pdf.py "/path/to/丝绸之路.pdf" --pages 20
→ 返回第 20 页全文
用户:总结一下这页的内容
Agent:(直接基于上文总结)
搜索、阅读、理解,三步完成。整个过程不需要用户手动打开文件。
关键细节
Spotlight 的索引范围
Spotlight 默认索引内置硬盘上的所有文件。检查索引状态:
| |
注意事项:
- 外接硬盘和网络盘可能未被索引,需手动开启
- 扫描版 PDF(纯图片)无法提取文字,需先 OCR
.gitignore或隐藏目录中的文件仍会被 Spotlight 索引
常用文件类型标识
mdfind 使用 UTI(Uniform Type Identifier)过滤文件类型:
| 类型 | UTI |
|---|---|
com.adobe.pdf | |
| DOCX | org.openxmlformats.wordprocessingml.document |
| XLSX | org.openxmlformats.spreadsheetml.sheet |
| PPTX | org.openxmlformats.presentationml.presentation |
| TXT | public.plain-text |
| Markdown | net.daringfireball.markdown |
pymupdf 的依赖管理
使用 uv run 执行脚本时,pymupdf 会在首次运行时自动安装(约 22MB),无需手动 pip install。脚本头部声明了内联依赖:
| |
索引瘦身:排除开发工件
Spotlight 索引一切。这个特性在普通用户那里是美德,在开发者的机器上却成了负担。
node_modules、.git、__pycache__、.venv、包管理器缓存——这些目录里藏着数百万个文件,Spotlight 照单全收。后果是:
- 索引膨胀:无用文件占据索引空间,拖慢后台进程
- 搜索污染:搜一个关键词,结果被
node_modules里的第三方源码淹没 - 资源浪费:CPU 和磁盘 I/O 花在索引永远不会被搜索的文件上
排除机制
一个自然的想法是:能不能只指定需要索引的目录?遗憾的是,Spotlight 不支持白名单模式,只提供黑名单排除。
排除目录有两种方法:
| 方法 | 适用范围 | 需要 sudo |
|---|---|---|
mdutil -i off | 整个卷(volume) | 是 |
.metadata_never_index 文件 | 任意目录 | 否 |
mdutil 是卷级别的开关,粒度太粗。在目标目录下创建空的 .metadata_never_index 文件是更精确的方式:
| |
自动化排除脚本
手动维护排除列表不现实——每次 npm install、git clone 都会产生新目录。更好的做法是写一个脚本,按模式自动扫描并排除:
| |
配置定时执行
用 launchd 每天自动运行一次,从此不再操心:
| |
| |
实测效果
在一台日常开发的 MacBook 上首次运行,排除了 357 个目录:
| 类别 | 示例 | 规模 |
|---|---|---|
| 系统缓存 | ~/Library/Caches、~/.cache | ~23 GB |
| 包管理器 | ~/.npm、~/.bun、~/.pnpm-store | ~7 GB |
| 编辑器扩展 | ~/.vscode、~/.trae | ~1 GB |
| 项目依赖 | 各项目的 node_modules | ~5 GB |
| 版本控制 | 各仓库的 .git | ~70 个 |
| Python 缓存 | __pycache__、.venv | ~200 个 |
排除后,mdfind 查询返回的都是真正有价值的文档,不再被开发工件淹没。
如果希望立即清除已有的无用索引条目,可以强制重建:
| |
不执行也无妨——.metadata_never_index 生效后,旧条目会随时间自动过期。
跨平台替代方案
Spotlight 是 macOS 独有的。其他平台也有全文索引搜索方案:
Windows
| 工具 | 说明 |
|---|---|
| Windows Search | 系统内置,支持文档内容索引,但搜索体验一般 |
| Everything | 极快的文件名搜索,附带命令行工具 es.exe,但只搜文件名不搜内容 |
| DocFetcher | 开源的全文索引搜索工具(Java),支持 PDF/DOCX/XLSX 内容检索 |
Linux
| 工具 | 说明 |
|---|---|
| Recoll | 全文索引搜索引擎,功能最接近 Spotlight,支持 200+ 种文件格式,命令行工具 recollq |
| plocate | locate 的现代版,极快的文件名搜索,但不搜内容 |
ripgrep(rg) | 实时内容搜索,速度极快,但不建索引,每次搜索都要遍历文件 |
跨平台通用方案
- Recoll:Linux/macOS/Windows 均可运行,提供
recollq命令行接口,是最接近"跨平台 mdfind"的选择 - ripgrep + fd:
rg搜内容、fd搜文件名,两者组合覆盖大部分场景,但没有预建索引,大量文件时较慢 - DocFetcher:基于 Java 的桌面全文搜索,跨平台,适合不想折腾命令行的用户
将上述方案中的搜索命令替换 search.py 中的 mdfind 调用,即可让 Agent Skill 在非 macOS 平台上工作。核心逻辑——搜索、定位、读取——是通用的。
结语
操作系统是沉默的档案员。从你按下开机键那一刻起,它就在为硬盘上的每一份文档编纂索引,从未间断。AI Agent 拥有理解语义的能力,却看不见近在咫尺的文档。缺的不是智能,是一个调用接口。
mdfind 补上了这个缺口。搜索交给操作系统,阅读交给 pymupdf,理解交给 Agent。各司其职,各安其位。而那些从未被需要的 node_modules,终于可以从索引中安静地退场。