Cursor,这个最近宣布达到3亿美元年经常性收入(ARR)的热门AI IDE,使用Merkle树来快速索引代码。本文将详细介绍其具体实现方法。
在深入了解Cursor的实现之前,让我们先理解什么是Merkle树。
Merkle树简单解释
Merkle树是一种树状结构,其中每个"叶子"节点都标记有数据块的加密哈希值,而每个非叶子节点都标记有其子节点标签的加密哈希值。这创建了一个分层结构,通过比较哈希值可以高效地检测任何级别的变化。
可以把它们想象成数据的指纹系统:
- 每个数据块(如文件)都有自己独特的指纹(哈希值)
- 成对的指纹被组合并给予新的指纹
- 这个过程一直持续到只有一个主指纹(根哈希值)
根哈希值总结了各个数据块中包含的所有数据,作为整个数据集的加密承诺。这种方法的美妙之处在于,如果任何单个数据块发生变化,它将改变其上方的所有指纹,最终改变根哈希值。
Cursor如何使用Merkle树进行代码库索引
Cursor使用Merkle树作为其代码库索引功能的核心组件。根据Cursor创始人的帖子和安全文档,其工作原理如下:

第1步:代码分块和处理
Cursor首先在本地对您的代码库文件进行分块,在进行任何处理之前将代码分割成语义上有意义的片段。
第2步:Merkle树构建和同步
启用代码库索引时,Cursor扫描编辑器中打开的文件夹,并计算所有有效文件哈希值的Merkle树。然后将此Merkle树与Cursor的服务器同步,如Cursor的安全文档中所述。
第3步:Embedding生成
将代码块发送到Cursor服务器后,使用OpenAI的embedding API或自定义embedding模型创建embeddings。这些向量表示捕获了代码块的语义含义。
第4步:存储和索引
Embeddings连同元数据(如起始/结束行号和文件路径)一起存储在远程向量数据库(Turbopuffer)中。为了在保护隐私的同时仍能进行基于路径的过滤,Cursor为每个向量存储混淆的相对文件路径。重要的是,根据Cursor创始人的说法,"您的代码都不会存储在我们的数据库中。它在请求的生命周期后就消失了。"
第5步:使用Merkle树进行定期更新
每10分钟,Cursor检查哈希值不匹配,使用Merkle树识别哪些文件发生了变化。只需要上传更改的文件,显著减少带宽使用,如Cursor的安全文档中所解释的。这就是Merkle树结构提供最大价值的地方——实现高效的增量更新。
代码分块策略
代码库索引的有效性很大程度上取决于代码如何分块。虽然我之前的解释没有详细介绍分块方法,但这篇关于构建类似Cursor的代码库功能的博客文章提供了一些见解:
虽然简单的方法按字符、单词或行分割代码,但它们经常错过语义边界——导致embedding质量下降。
您可以基于固定token数量分割代码,但这可能会中途切断函数或类等代码块。
更有效的方法是使用理解代码结构的智能分割器,例如使用高级分隔符(如类和函数定义)在适当的语义边界进行分割的递归文本分割器。
更优雅的解决方案是基于其抽象语法树(AST)结构分割代码。通过深度优先遍历AST,将代码分割成适合token限制的子树。为了避免创建太多小块,只要兄弟节点保持在token限制内,就将它们合并为更大的块。可以使用tree-sitter等工具进行AST解析,支持广泛的编程语言。
代码的检索增强生成(RAG)
Cursor的代码库索引本质上是一个专为代码定制的RAG系统。当您使用@Codebase或⌘ Enter查询代码库时,Cursor从向量数据库中检索相关的代码块,为大语言模型(LLM)调用提供上下文。这种方法允许AI"理解"您的整个代码库,而无需将所有内容都放入底层语言模型的上下文窗口中。
为什么Cursor使用Merkle树
这些细节中的许多都与安全相关,因此可以在Cursor的安全文档中找到。
1. 高效的增量更新
通过使用Merkle树,Cursor可以快速识别自上次同步以来确切发生了变化的文件。它不需要重新上传整个代码库,只需要上传已修改的特定文件。这对于大型代码库很重要,因为重新索引所有内容在带宽和处理时间方面成本太高。
2. 数据完整性验证
Merkle树结构允许Cursor高效地验证正在索引的文件与服务器上存储的文件是否匹配。分层哈希结构使得在传输过程中容易检测任何不一致或损坏的数据。
3. 优化缓存
Cursor将embeddings存储在按块哈希值索引的缓存中,确保第二次索引同一代码库时速度快得多。这对于多个开发人员可能使用同一代码库的团队来说非常有用。
4. 保护隐私的索引
为了保护文件路径中的敏感信息,Cursor通过按'/'和'.'字符分割路径并使用存储在客户端的密钥加密每个段来实现路径混淆。虽然这仍然会泄露一些关于目录层次结构的信息,但它隐藏了大部分敏感细节。
5. Git历史集成
在Git存储库中启用代码库索引时,Cursor还索引Git历史。它存储commit SHA、父信息和混淆的文件名。为了让同一Git存储库和同一团队的用户能够共享数据结构,用于混淆文件名的密钥是从最近commit内容的哈希值派生的。
Embedding模型和考虑因素
embedding模型的选择显著影响代码搜索和理解的质量。虽然一些系统使用开源模型如all-MiniLM-L6-v2,但Cursor可能使用OpenAI的embedding模型或专为代码调优的自定义embedding模型。对于专门的代码embeddings,Microsoft的unixcoder-base或Voyage AI的voyage-code-2等模型在代码特定的语义理解方面表现良好。
embedding挑战变得更加复杂,因为embedding模型有token限制。例如,OpenAI的text-embedding-3-small模型的token限制为8192。有效的分块有助于保持在token限制内,同时保留语义含义。
握手过程
Cursor的Merkle树实现的一个关键方面是同步期间发生的握手过程。来自Cursor应用程序的日志显示,在初始化代码库索引时,Cursor创建一个"merkle client"并与服务器执行"启动握手"。这个握手涉及将本地计算的Merkle树的根哈希值发送到服务器,如GitHub上的Issue #2209和Issue #981所示。
握手过程允许服务器确定代码库的哪些部分需要同步。基于握手日志,我们可以看到Cursor计算代码库的初始哈希值并将其发送到服务器进行验证,如GitHub上的Issue #2209中所记录的。
技术实现挑战
虽然Merkle树方法提供了许多优势,但它也不是没有实现挑战。Cursor的索引功能经常遇到重负载,导致许多请求失败。这可能导致文件需要多次上传才能完全索引。用户可能会注意到对'repo42.cursor.sh'的网络流量比预期的要高,这是由于这些重试机制造成的,如Cursor的安全文档中所提到的。
另一个挑战涉及embedding安全性。学术研究表明,在某些情况下逆向embeddings是可能的。虽然当前的攻击通常依赖于访问embedding模型并处理短字符串,但存在潜在风险,即获得Cursor向量数据库访问权限的攻击者可能从存储的embeddings中提取有关索引代码库的信息。