作者 8 个月的实验:只用文件+搜索框做 RAG,效果经常不输完整的向量检索 pipeline,质疑 RAG 流程中哪些环节是真正必要的。
假设检索必须不依赖 embeddings、不依赖向量数据库、不依赖任何非手工录入的索引。没有余弦相似度,没有分块策略,没有重排器。只有文件和搜索框,其他什么都没有。大约八个月来,这就是我为一个语言模型提供所需上下文的全部配置,而最令我惊讶的是,它败给所跳过的那套 machinery 的次数少得可怜。
我想把这作为一个思想实验而非推荐来讨论,因为真正的问题不是"grep 对你够不够用",而是:一个检索系统中,哪些部件在规模上才起支撑作用,哪些只是在我不具备的规模下才起支撑作用。当我把自己的设置剥离到只剩本质,问自己到底是什么在实际干活,答案几乎全部来自 RAG 教程里根本没出现的那部分。
检索增强生成通常被画成一条 pipeline:把文档 embed 成向量、存储、embed 查询、找最近邻、把排名靠前的结果塞进 prompt、生成。在模型读到一个字之前,已经有六个活动部件。
把它剥离到只剩那句承重的话,只有一个分句存留下来。A RAG 把你已经拥有的相关文本放进 prompt,这样模型就用你自己的世界来回答,而不是用它在所有人平均数据上的那套东西。检索是整个想法的核心。Embeddings 是做检索的一种方式——那种方式在机器必须在没人从头读过的语料库上猜测"相关"是什么意思时才会发挥价值。
最后这个条件正是我悄悄不满足的那一条。我的语料库是一个日期标注的纯文本文件夹,里面的每一行我都自己读过、写过的、反复看过。几千行,不是几百万份文档。Embeddings 漂亮解决的那个检索问题,在我这里基本不存在。
说实话,就是这样。我的笔记是 markdown,每个月一份只追加的文件,每一行都标注日期,用我预期日后会去搜索的词语写成。当我坐下来用 Claude 或在 Cursor 里处理一个问题时,我不会召唤一个 pipeline。我在这个文件夹里搜索与当前问题相关的五到二十行,读一遍,把经过那轮阅读后还存活的部分粘贴到 prompt 里。
搜索是一条命令,这是整个系统里最不花哨的部分:
rg -i --sort path 'outbox|retry|idempoten' notes/
这就是整个检索层。"索引"不是数据结构,是一种习惯:我写每一行的时候,仿佛一年后会有一个陌生人带着和我一模一样的问题来 grep 它,所以把名词放在前面,而不是感觉放在前面。模型从不接触我的笔记。是我在接触。模型看到的永远只是我选择搬运过去的那几行。
没有任何 embed 步骤,因为根本不存在一个我需要机器来猜测哪些行相关的时刻。我自己就知道。亲自做检索者的代价是几秒钟的阅读;我用那几秒钟买来的,是让我在模型看到上下文之前先看到进入 context window 的究竟是什么。
这种感觉像是异端的原因在于,大多数关于 RAG 的写作是为团队和产品构建的,很容易假设它的形态会直接下放到一张办公桌前。不会的。几乎每一条使向量 pipeline 变得合理的轴线,在语料库变成一个人的笔记时都转向了反面。
顺着右边那一列读下来,向量数据库不是被偷懒省略了。它被要求在与一个小到可以装进一个脑袋里的语料库和一个免费的查询规划器(我)竞争时证明自己的价值。在那一列上它基本做不到。
三个属性在起作用,没有一个是检索算法。
第一个是规模,或者说规模的缺失。对几千行做字面搜索,返回时间短到我把手从键盘上挪开就够了。没有召回天花板需要去工程化绕过,因为我可以把每条命中都扫一眼。Embeddings 大放异彩的地方恰恰在于扫完所有命中已不可能,而那条边界远在个人笔记的规模之上。
第二个是我就是查询规划器,而且对于我自己的材料来说,我是一个非常优秀的查询规划器。向量搜索必须推断"outbox"和"存放未发邮件的那个东西"是同一个概念。我不需要推断;我记得两个都写过,搜索的时候用哪个都行。RAG 里那笔昂贵的智能花在了重建意图上,而对于我自己的笔记来说,那份意图从未离开过这个房间。
第三个是信任,这是我一开始低估了的那一条。因为我在检索结果进入 prompt 之前先读了它们,所以我也在审查它们。我会捕捉到那条过时的笔记、那条看似当前但其实是猜测的文字、那个我不应该作为事实交出去的数字。静默注入前八个 chunk 的 pipeline 会剥夺我那唯一一道审核步骤,而正是那道步骤防止了模型在我已知已错的东西上信心满满地继续构建。我在之前的一篇文章里论证过,上下文的塑造才是产品,不是 prompt;手动做检索是我保持对那个产品所有权的的方式,而不是把它委托给一个相似度分数。
如果我在讲有效的部分就停住,就是说谎省略了真正的问题。这个笨版本有一个真实的漏洞,而那恰恰是 embeddings 被发明出来填补的那个洞。
字面搜索只能找到我记得存在的笔记。如果我忘了自己写过某行,或者用了我今天不会想到去搜索的词语来写它,grep 救不了我,因为它匹配字符串,而我输入的是错误的字符串。语义搜索不在乎我记得什么。当我搜索"the outbox keeps double-sending"的时候,它会把我六个月前写的那条关于 retry storms 的笔记捞上来,尽管两者没有一个共同的词。对于"遗忘材料的召回"这个场景,这是那套 machinery 真正的论据,而且随着文件夹的增长,这个论据每个月都在变强。
只要第二个人需要查询我的笔记,这套方案也就失效了,因为现在那个免费的专业查询规划器——我——不在循环里了,而所有那些我从未需要写下来的意图最终还是要由机器来重建。而且它会随着纯规模增长而缓慢失效:当我扫完所有命中的时间不再"瞬间"的那一刻,就是我悄悄跨过了教程们正确的那片领域的时刻。
这些都还没有严重咬到我。但我能看到边缘,而一个只美化自己设置的思维实验就只是一个广告。
这是就算明天加上向量索引我也会保留的部分,因为它在每种检索方法的上游。一条笔记可被发现的程度,取决于它的词语与未来查询会用的词语之间匹配得有多好。Embeddings 放宽了那个约束;它们没有移除它。一条写成"felt off, fixed it"的笔记,用任何方法——语义或字面——都几乎不可发现,因为它没有编码任何后续问题会抓住的手柄。
所以这种自律和让笔记对未来的读者可读的自律是同一套:我把一行写出来的时候,让它自己的主题就在里面。我之前写过关于让笔记可以被未来的读者和未来的模型都能解析的文章,而个人 RAG 原来就是戴着时尚缩写的同一套习惯。检索方法在下游。写作才是索引。
不,而这就是整张表的用意所在。规模翻转了答案。超过你能扫完所有命中的规模,或者超过除了作者之外的其他人开始查询语料库的那一刻,向量 pipeline 就从开销变成了唯一能工作的东西。论点很窄:对于一个人的几千条笔记,检索不是瓶颈,先为它付费是在解决错误的问题。
是的,一点没错,而更差的召回率是唯一值得花钱去解决的弱点。Pipeline 提供的其他一切,我要么不需要,要么宁可自己用眼去做。所以合理的混合方案很明显:把字面搜索作为主路径,把语义搜索仅作为"我知道我写过这方面的内容但找不到"这种召回失败场景的备选。
从来没有。它只读我粘贴过去的那几行。把语料库挡在 context window 外面是特性,不是限制;正是这个小的、手工挑选的集合让答案保持 grounded,而不是漂移到最大的那个相邻 chunk 恰好说的话里。
在一个思想实验结尾诚实的做法是去检验那个我始终半信半疑的部分,所以这是我一直在回避的那个。我要加一个语义备选,但只用于召回未命中场景:字面搜索留在正门,embedding 索引只在 grep 什么也没返回而我仍然相信存在一条笔记的时候才被查询。我会把它跑一个月,对同一个文件夹记两个数——备选触发的频率,以及它捞到真实内容的频率——因为一个永远得不到命中的备选只是我得独自维护的基础设施。
我猜它触发得很罕见,而在它触发的那些天里是值得的。但我之前也判断错了一个系统里哪一半在承担重量,所以与其继续叙述,不如测量。
真正我想回答的问题是:当模型因为一条我忘了自己写过的笔记而答对了最后一道题,那是怎么找到的——你是搜索找到的,还是检索在你没问的情况下把它浮上来的?我在收集那些检索而非模型悄悄干了活的案例,因为这些案例才是决定那套 machinery 对一个人来说值不值的东西。
Simple Memo 是我自己做的 iOS 应用;它唯一的工作就是把脑子里的一行字在蒸发之前弄成纯文本。我大约每周在这里写一篇关于围绕那一个习惯生长的 workflow 的文章,而那些笔记中经过 curation 的 markdown 侧就是慢的那一半所在的地方。