用 pi 写代码的人,大概都经历过这个瞬间:你让 agent 搜一下 TODO,结果它一口气把 30 多行命中的"文件路径 + 内容"摊成一整页塞进上下文。你以为它在"找问题",其实它得先读完这堆垃圾,才能看见真正想找的那个文件。
问题出在 pi 内置的 grep 上。
它其实就是 rg --json 的一层薄封装。我翻了它的实现,调用参数是这么拼的:["--json","--line-number","--color=never","--hidden"]。配合两个硬上限:DEFAULT_LIMIT = 100(最多 100 条命中)、DEFAULT_MAX_BYTES = 50 * 1024(输出截断在 50KB),每行再截到 500 字符。也就是说,默认 --hidden 全开、一次最多 100 条、丢给你 50KB。高频词一搜,上下文瞬间被灌满。更糟的是结果不加排序,系统里你最关心的那个文件被排在后面,先被读进去的反而是噪音。
Codex 那边很早就用调优过的 rg 解决了;pi 这边,我测过的几个方案里,最干净的是 @ff-labs/pi-fff。
它的思路不是再包一层 rg,而是把 FFF(Fast Fuzzy Finder)这个 Rust 原生库通过 N-API 直接编进 pi,不派生子进程——文件在后台预先建索引,搜索几乎是即时返回。核心改动就三件:
- frecency 排序置顶——你常打开、最近访问的文件排在前面;
- git 感知加权——被修改/暂存/未跟踪的文件在结果里权重更高;
- grep 结果支持光标分页——不再是"一把全丢",而是按页读。
我搭了个小型项目测了测:src/app.ts、utils.ts、README.md,外加塞了 30 行待修 TODO 的 noise.ts,总共命中 33 处。
- 内置 grep 等效于 rg "TODO",直接把 33 行一次性全吐出来;
- ffgrep 第一页只给 20 条,全是 noise.ts 的前 20 条,末尾挂一个 [Continue with cursor="fff_c1"];带上 cursor="fff_c1" 再请求,才拿到剩余 13 条,而 app.ts、utils.ts 的内容被排在后面——第一屏一点都不乱。
fffind 同理,fffind pattern:"app" 就能模糊命中 src/app.ts 和 README.md,frecency 把最近改过的文件往前放,连 glob 表达式都不用写。
关键是,它不需要你装 rg/fd 二进制(依赖是 @ff-labs/fff-node / @ff-labs/fff-bun 原生绑定),一条命令安装:
装完重启,ffgrep、fffind、fff-multi-grep 就绪,默认跑在 tools-and-ui 模式(额外工具集 + FFF 驱动的 @ 文件补全)。想直接顶替内置 grep,切到覆盖模式即可:PI_FFF_MODE=override,或 --fff-mode override 启动。
感受最直观的是首屏噪声:默认 ffgrep 限 20、内置限 100,首屏直接少 80%。配合光标,你变成"精确定位后再读",而不是"先塞 50KB 进去,再让模型自己翻"。/fff-health 可以看索引与访问频率状态。
要不要装,其实取决于你的使用习惯。pi 默认只开 read/bash/edit/write 四个工具,grep/find 反正是要手动开的——既然都要开,不如顺手把这个也开了。它的周下载量 6.8k、月下载 33.9k(版本 0.10.5),在搜索类插件里算很成熟;相比之下 pi-lean-grep 周下载只有 4。
老实说,它也不是没有代价:首次建索引会扫一遍仓库(甚至扫 $HOME),大仓得等一会儿;覆盖模式下的工具名和内置不完全一致,切换后要 /reload。但对你这种动不动就搜 TODO/FIXME、仓库又大的场景,换来的是上下文干净、定位精准,这笔账是划算的。