用 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_searchAgent 的对话记忆语义搜索"上次那个报错怎么解决的?"
mdfindMac 上所有文档关键词匹配"找一下关于丝绸之路的 PDF"

两者互补,不冲突。一个是私人日记,一个是公共图书馆。

方案:mdfind + pymupdf

mdfind

mdfind(Metadata Find)是 Spotlight 的命令行接口。Spotlight 在后台持续为磁盘上的文件建立全文索引——PDF 内的文字、Word 文档的正文、Excel 的单元格内容,无一遗漏。mdfind 让你从终端直接查询这个索引。

1
2
3
4
5
6
7
8
# 全盘搜索包含"机器学习"的文件
mdfind "机器学习"

# 限定目录
mdfind -onlyin ~/Desktop "机器学习"

# 按文件类型 + 内容搜索
mdfind 'kMDItemTextContent == "*机器学习*"cd && kMDItemContentType == "com.adobe.pdf"'

pymupdf 读取 PDF 页面

找到文件后,还需要读取具体内容。pymupdf 是一个 Python 库,能提取 PDF 指定页面的文字:

1
uv run read_pdf.py "/path/to/file.pdf" --pages 10-15

封装为 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 默认索引内置硬盘上的所有文件。检查索引状态:

1
2
3
4
5
# 查看某个卷的索引状态
mdutil -s /

# 查看某个文件的全部元数据
mdls /path/to/file.pdf

注意事项:

  • 外接硬盘和网络盘可能未被索引,需手动开启
  • 扫描版 PDF(纯图片)无法提取文字,需先 OCR
  • .gitignore 或隐藏目录中的文件仍会被 Spotlight 索引

常用文件类型标识

mdfind 使用 UTI(Uniform Type Identifier)过滤文件类型:

类型UTI
PDFcom.adobe.pdf
DOCXorg.openxmlformats.wordprocessingml.document
XLSXorg.openxmlformats.spreadsheetml.sheet
PPTXorg.openxmlformats.presentationml.presentation
TXTpublic.plain-text
Markdownnet.daringfireball.markdown

pymupdf 的依赖管理

使用 uv run 执行脚本时,pymupdf 会在首次运行时自动安装(约 22MB),无需手动 pip install。脚本头部声明了内联依赖:

1
2
3
4
# /// script
# requires-python = ">=3.10"
# dependencies = ["pymupdf>=1.25.0"]
# ///

索引瘦身:排除开发工件

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 文件是更精确的方式:

1
touch /path/to/node_modules/.metadata_never_index

自动化排除脚本

手动维护排除列表不现实——每次 npm installgit clone 都会产生新目录。更好的做法是写一个脚本,按模式自动扫描并排除:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/zsh
# spotlight-cleanup.sh

# 按名称匹配的目录模式
PATTERNS=(
  "node_modules" ".git" "__pycache__" ".tox"
  ".venv" "venv" ".mypy_cache" ".pytest_cache"
  ".next" ".nuxt" ".svelte-kit" ".turbo"
)

# 固定排除的顶层目录
FIXED_DIRS=(
  "$HOME/.npm" "$HOME/.bun" "$HOME/.nvm"
  "$HOME/.pnpm-store" "$HOME/Library/pnpm"
  "$HOME/.cache" "$HOME/Library/Caches"
  "$HOME/.vscode" "$HOME/.trae" "$HOME/.cargo"
)

count=0

for d in "${FIXED_DIRS[@]}"; do
  if [[ -d "$d" && ! -f "$d/.metadata_never_index" ]]; then
    touch "$d/.metadata_never_index"
    ((count++))
  fi
done

for pat in "${PATTERNS[@]}"; do
  find "$HOME" -maxdepth 6 -name "$pat" -type d -prune 2>/dev/null \
    | while read d; do
        if [[ ! -f "$d/.metadata_never_index" ]]; then
          touch "$d/.metadata_never_index"
          ((count++))
        fi
      done
done

echo "[spotlight-cleanup] $(date '+%Y-%m-%d %H:%M') — 新排除 $count 个目录"

配置定时执行

launchd 每天自动运行一次,从此不再操心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- ~/Library/LaunchAgents/com.user.spotlight-cleanup.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.user.spotlight-cleanup</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/zsh</string>
    <string>/path/to/spotlight-cleanup.sh</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key><integer>12</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>
1
launchctl load ~/Library/LaunchAgents/com.user.spotlight-cleanup.plist

实测效果

在一台日常开发的 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 查询返回的都是真正有价值的文档,不再被开发工件淹没。

如果希望立即清除已有的无用索引条目,可以强制重建:

1
sudo mdutil -E /

不执行也无妨——.metadata_never_index 生效后,旧条目会随时间自动过期。

跨平台替代方案

Spotlight 是 macOS 独有的。其他平台也有全文索引搜索方案:

Windows

工具说明
Windows Search系统内置,支持文档内容索引,但搜索体验一般
Everything极快的文件名搜索,附带命令行工具 es.exe,但只搜文件名不搜内容
DocFetcher开源的全文索引搜索工具(Java),支持 PDF/DOCX/XLSX 内容检索

Linux

工具说明
Recoll全文索引搜索引擎,功能最接近 Spotlight,支持 200+ 种文件格式,命令行工具 recollq
plocatelocate 的现代版,极快的文件名搜索,但不搜内容
ripgrep(rg实时内容搜索,速度极快,但不建索引,每次搜索都要遍历文件

跨平台通用方案

  • Recoll:Linux/macOS/Windows 均可运行,提供 recollq 命令行接口,是最接近"跨平台 mdfind"的选择
  • ripgrep + fdrg 搜内容、fd 搜文件名,两者组合覆盖大部分场景,但没有预建索引,大量文件时较慢
  • DocFetcher:基于 Java 的桌面全文搜索,跨平台,适合不想折腾命令行的用户

将上述方案中的搜索命令替换 search.py 中的 mdfind 调用,即可让 Agent Skill 在非 macOS 平台上工作。核心逻辑——搜索、定位、读取——是通用的。

结语

操作系统是沉默的档案员。从你按下开机键那一刻起,它就在为硬盘上的每一份文档编纂索引,从未间断。AI Agent 拥有理解语义的能力,却看不见近在咫尺的文档。缺的不是智能,是一个调用接口。

mdfind 补上了这个缺口。搜索交给操作系统,阅读交给 pymupdf,理解交给 Agent。各司其职,各安其位。而那些从未被需要的 node_modules,终于可以从索引中安静地退场。