最近在cursor玩deepseek-r1,感觉cursor价格有点小贵,明明deepseek api价格那么良心了都。对标cursor的void开源且免费但是功能相比于cursor差距实在太大,尤其是codebase indexing功能我看void目前还没实现,composer功能void和cursor差距也很大就是个半成品。但我目前迫切需要codebase indexing功能来看开源项目源码,cursor充值了但是想看看有没有便宜的替代方案,或者有大佬分析一下原理,我自己捣鼓一个能用的demo出来?
最近宣布达到 3 亿美元年度经常性收入,已经成为vibe coding标配。核心能力来自于Codebase。根据官方说法,codebase实现是依赖使用 Merkle 树来快速索引代码。但是没有竞品模仿这种方法。
Merkle是如何工作的那?
Merkle 树是一种树状结构,其中每个“叶子”节点都标有数据块的加密哈希值,每个非叶子节点都标有其子节点标签的加密哈希值。这创建了一个层次结构,通过比较哈希值可以高效地检测任何层级的更改。
可以将其视为数据的指纹系统:
每条数据(如文件)都有自己的唯一指纹(哈希值);成对的指纹被组合并赋予新的指纹这个过程持续进行,直到只剩下一个主指纹(根哈希值)
根哈希值总结了各个部分包含的所有数据,作为整个数据集的加密承诺。这种方法的优点是,如果任何一条数据发生变化,都会改变其上方的所有指纹,最终改变根哈希值。
Cursor 将 Merkle 树作为其代码库索引功能的核心组件。根据 Cursor 创始人的帖子和安全文档,其工作原理如下:
第 1 步:代码分块和处理
Cursor 首先在本地对代码库文件进行分块,在进行任何处理之前将代码分割成语义上有意义的片段。
第 2 步:Merkle 树构建和同步
启用代码库索引时,Cursor 扫描在编辑器中打开的文件夹,并计算所有有效文件的哈希值的 Merkle 树。然后将此 Merkle 树与 Cursor 的服务器同步。
第 3 步:生成嵌入向量
将块发送到 Cursor 服务器后,使用 OpenAI 的嵌入 API 或自定义嵌入模型创建嵌入向量。这些向量表示捕获了代码块的语义含义。
第 4 步:存储和索引
嵌入向量与元数据(如起始/结束行号和文件路径)一起存储在远程向量数据库(Turbopuffer)中。为了在保持隐私的同时仍能启用基于路径的过滤,Cursor 为每个向量存储一个模糊化的相对文件路径。重要的是,根据 Cursor 创始人的说法,“您的代码不会存储在我们的数据库中。请求结束后就会被删除。”
第5 步:使用 Merkle树进行定期更新
每 10 分钟,Cursor 检查哈希值不匹配的情况,使用 Merkle 树识别哪些文件发生了变化。只需要上传已更改的文件,这大大减少了带宽使用。这正是 Merkle 树结构提供的最大价值——实现高效的增量更新。
代码库索引的效果在很大程度上取决于代码如何分块。虽然简单的方法按字符、单词或行分割代码,但它们往往会错过语义边界,导致嵌入质量下降。
可以基于固定的标记计数分割代码,但这可能会在中途切断函数或类等代码块。
更有效的方法是使用理解代码结构的智能分割器,例如使用高级分隔符(如类和函数定义)在适当的语义边界处分割的递归文本分割器。更优雅的解决方案是基于代码的抽象语法树(AST)结构进行分割。通过深度优先遍历 AST,它将代码分割成适合标记限制的子树。为了避免创建太多小块,只要保持在标记限制之内,就将兄弟节点合并成更大的块。可以使用 tree-sitter 等工具进行 AST 解析,支持多种编程语言。
在介绍完 Cursor 如何创建和存储代码嵌入后,一个自然的问题出现了:这些嵌入在生成后实际是如何使用的?本节将解释这些嵌入在正常使用过程中的实际应用。
语义搜索和上下文检索
当用户使用 Cursor 的 AI 功能(如使用 @Codebase 或 ⌘ Enter 提问)时,会发生以下过程:
查询嵌入:Cursor 为您的问题或正在处理的代码上下文计算嵌入向量。
向量相似度搜索:将查询嵌入发送到 Turbopuffer(Cursor 的向量数据库),后者执行最近邻搜索以找到与查询在语义上相似的代码块。
本地文件访问:Cursor 客户端接收结果,其中包括最相关代码块的模糊化文件路径和行范围。重要的是,实际的代码内容保留在您的机器上并在本地检索。
上下文组装:客户端从本地文件中读取这些相关代码块,并将它们作为上下文与您的问题一起发送到服务器供 LLM 处理。
知情响应:LLM 现在拥有来自您代码库的必要上下文,可以为您的问题提供更明智和相关的响应,或生成适当的代码补全。
这种基于嵌入的检索允许:
上下文代码生成:在编写新代码时,Cursor 可以参考现有代码库中的类似实现,保持一致的模式和风格。
代码库问答:您可以询问有关代码库的问题,并获得基于实际代码而不是泛泛而谈的答案。
智能代码补全:代码补全可以增强对项目特定约定和模式的感知。
智能重构:在重构代码时,系统可以识别代码库中所有可能需要类似更改的相关部分。
1. 高效的增量更新
通过使用 Merkle 树,Cursor 可以快速识别自上次同步以来哪些文件发生了变化。无需重新上传整个代码库,只需上传已修改的特定文件。这对于大型代码库来说很重要,因为重新索引所有内容在带宽和处理时间方面都太昂贵。
2. 数据完整性验证
Merkle 树结构允许 Cursor 高效地验证正在索引的文件是否与服务器上存储的内容匹配。分层哈希结构使得在传输过程中很容易检测到任何不一致或损坏的数据。
3. 优化缓存
Cursor 将嵌入存储在以块的哈希值为索引的缓存中,确保第二次索引相同的代码库要快得多。这对于多个开发人员可能在使用相同代码库的团队来说很好。
4. 保护隐私的索引
为了保护文件路径中的敏感信息,Cursor 通过按 ‘/’ 和 ‘.’ 字符分割路径并使用存储在客户端的密钥加密每个段来实现路径模糊化。虽然这仍然会显示一些有关目录层次结构的信息,但隐藏了大多数敏感细节。
5. Git历史集成
在 Git 仓库中启用代码库索引时,Cursor 还会索引 Git 历史。它存储提交 SHA、父信息和模糊化的文件名。为了使同一 Git 仓库和同一团队中的用户能够共享数据结构,用于模糊化文件名的密钥是从最近提交内容的哈希值派生的。
嵌入模型的选择显著影响代码搜索和理解的质量。虽然一些系统使用开源模型如 all-MiniLM-L6-v2,但 Cursor 可能使用 OpenAI 的嵌入模型或专门针对代码调优的自定义嵌入模型。对于专门的代码嵌入,Microsoft 的 unixcoder-base 或 Voyage AI 的 voyage-code-2 等模型适合代码特定的语义理解。
嵌入挑战变得更加复杂,因为嵌入模型有标记限制。例如,OpenAI 的 text-embedding-3-small 模型的标记限制为 8192。有效的分块有助于在保持语义含义的同时保持在标记限制内。
Cursor 的 Merkle 树实现的一个关键方面是同步期间发生的握手过程。Cursor 应用程序的日志显示,在初始化代码库索引时,Cursor 创建一个“merkle 客户端”并与服务器执行“启动握手”。这个握手涉及将本地计算的 Merkle 树的根哈希值发送到服务器。
握手过程允许服务器确定需要同步代码库的哪些部分。根据握手日志,我们可以看到 Cursor 计算代码库的初始哈希值并将其发送到服务器进行验证。
虽然 Merkle 树方法提供了许多优势,但实现起来并非没有挑战。Cursor 的索引功能经常遇到重负载,导致许多请求失败。这可能导致文件需要多次上传才能完全索引。用户可能会注意到到 ‘repo42.cursor.sh’ 的网络流量比预期高,这是由于这些重试机制造成的。
另一个挑战与嵌入安全性有关。学术研究表明,在某些情况下可以反转嵌入。虽然当前的攻击通常依赖于访问嵌入模型并处理短字符串,但存在一个潜在风险,即获得 Cursor 向量数据库访问权限的攻击者可能从存储的嵌入中提取有关索引代码库的信息。
强烈推荐continue这个开源的插件,支持vscode和JetBrains。
我来基于codebase indexing的原理,简单介绍一下为什么选择这个插件。
codebase indexing,顾名思义就是对你的vscode打开的代码库,建立索引(indexing)。
这个和RAG的技术原理是完全一致的,indexing是一个向量数据库,就是将你的代码进行分块后,然后embedding到高维向量空间中,所形成的由所有代码切块对应的向量数据库。
构建向量数据库的目的有两个:
- 加快检索速度:基于向量做检索,就是求向量的余弦相似度,计算效率很高;
- 实现语义检索:通过将代码块embedding到高维语义空间,实现语义匹配;也就是说两段代码/文本不用完全一致,只要他们语义一致,就能够被认为相似且匹配成功;
语义检索过程涉及两个模型:一个是embedding模型,将代码段转换为高维向量,即indexing;另一个是reranker模型,是在检索的时候用,具体就是通过余弦相似度会检索出k个相似的indexing,然后会再对这k个indexing对应的原始文本再重排序(reranker),这时候是直接用原始文本用一个模型直接去判断相似性,从而提高最终的匹配效果。embedding模型和reranker模型会直接影响到检索的效果。
以上就是codebase indexing的基本原理。有了一个codebase,那么后续大模型的回答就能够基于codebase上下文去回答,自然编码的效果和连贯性就会更好。
为什么推荐continue?
- continue这个插件具备完整的codebase功能;
- continue具备高度的定制化能力。它里面涉及到的构建codebase过程中所需要用到的embedding模型,reranker模型,都支持用户自定义;进一步的,如果你想修改代码的分块/chunking策略,还可以通过更底层的代码去修改,你甚至还可以选择更适合你场景的向量数据库。详情可以查看:Custom code RAG | Continue ;你可以通过去尝试不同的模型,不同的策略进一步去优化codebase的检索效果。这妥妥就是一个支持高度定制化的RAG系统;
- 当然,chat模型/自动代码补全模型,continue也支持个性化选择和接入。
cursor作为商业软件,以上提到的那些设置,肯定是已经帮你调整到了最优,这里面都需要成本;而天底下没有免费的午餐。continue是一个完整开源的智能体框架,给予了你非常灵活的选择和掌控感。它灵活到甚至可以当作一个RAG系统来使用。如果你愿意折腾,愿意了解和学习底层的原理,我相信它能够带来意向不到的性价比。
codebase在cursor、trae等ai ide工具的应用都挺多的,我们就参考了一些方案实现了一个codebase,具体内容可以看下面文章
为什么要做这个开源项目?
这两年AI发展非常快,有很多的新概念出现比如 embedding 、RAG、function call等,但这些技术有什么作用,在一个实际的项目中能带来什么样的价值,如何在项目中实践这些技术,这些都是我们很好奇的点,也就萌生了一个想法,构建一个项目,在项目中实践这些技术,在干中学是学习效率最高的,不然对这些知识只是浮于表层,于是我们就构建了一个 VoidMuse 项目,实现一个ai ide coding插件,把我们所想要学习的知识点都实践上
而为什么不直接在 cline 这种开源插件上进行扩展开发,而是从零实现呢, cline 的代码太复杂了,想要从中实践成本很高,直接从零开始做一个简单的项目来实现满足目标就足够了
而这一篇文章是介绍 codebase 的真正实现,我们完整实现了在 chat 页面中调用 codebase 进行代码检索然后加入到 llm 上下文中,这里面也包含了很多优化细节,比如embedding模型的选择坑点,还有单纯向量检索是有局限性的,我们利用了混合检索来提高检索的准确性,这些都是在实践中发现的点,看完本篇文章你会codebase实际的实现有一个更深入的了解
实现codebase功能的关键是有两部分的
- 建立codebase索引:对全量代码文件进行向量化,这里涉及到代码分块、向量化、文件选择策略等
- 检索codebase索引:根据查询进行向量化检索+文本检索,这里采用了混合检索来提高准确率,并且通过提示词加权来提高检索词的权重
以下详细介绍这两部分实现细节
一个代码仓库中,有众多文件,包含系统本身的隐藏文件、依赖库(比如node_modules),还有一些协议生成的大文件,比如java项目中使用pb协议生成的java文件非常庞大,这些文件对代码检索没有帮助,所以需要提前去除掉这些文件
筛选策略如下
.gitignore: 代码仓库中通常会有一个.gitignore文件,里面列出了需要忽略的文件和目录,我们根据这个文件来移除掉忽略的文件,一般大部分依赖文件都会在这个阶段就被排除掉了- 额外排除“pb 自动生成的文件”: 在java项目中使用pb协议生成的java文件非常庞大,这些文件对代码检索没有帮助,我们的项目中用到挺多pb协议所以专门做了一个移除处理
- 空文件和大文件排除掉:我们同时对超过
1MB的文件进行排除
具体代码可参考 : CheckAutoIndexingTask.startCheckAll 方法
一个文件通常包含多个函数,并且文件行数都是不确定的,有可能一个代码文件几千行代码都是有可能的,而embedding 模型是有上下文长度限制的,所以要对代码文件进行分块,每一个分块单独进行向量化
代码文件分块有两种策略(我们参考了 continue 插件的方案)
- 根据代码的结构(如函数、类等)将文件内容分割成多个块生成抽象语法树(AST):就是把代码转成一种更加结构化的数据,能提供更多文本特征,并且是完整的函数表示
- 使用字符数或者行数进行分块:
continue插件的兜底方法
在VoidMuse中我们只实现了最简单的按行数进行分块,对文件按随机行数分块(35-65行),当然这种缺点很明显,有可能一个完整函数会被切割成几块
备注:embedding 模型的踩坑点,一开始我们挑选embedding模型时希望能支持中文,那时选择了开源的 bge-large-zh-v1.5 模型,但这个模型的上下文长度限制是512个token,代码文件分块后挺容易就超过这个限制,就会导致向量化的结果不准确,后续我们是换成了 Qwen3-Embedding-0.6B 模型, 本地就能运行起来了,这个模型的上下文长度限制是32,000个token,一般也满足要求了,在一些做知识库的场景也能满足了,并且向量维度是 1536 ,这就表示一个信息能用到更多维度来表示,向量化更加准确
每一个代码块单独调用embedding接口向量化,然后把向量化结果保存到 lucene 索引中,lucene 索引中包含了代码块的文本内容、向量化结果、文件路径、行号等信息
而这里我们使用 lucene 来存储索引,lucene 是一个基于 java 实现的全文检索引擎,支持中文检索,并且性能非常好,我们在项目中使用 lucene 来存储索引,检索时直接调用 lucene 的 api 即可, 并且我们也想利用到 lucene 提供的文本检索 + 向量检索的功能,来提高检索的准确性
具体代码可参考:LuceneVectorStore.java
我们在测试中发现,直接对查询词进行向量化检索,效果不是特别好,所以我们在查询词向量化前,先对查询词进行了一些优化,转化成更符合进行向量检索的查询词,包括对一些关键词语进行重复3词来提升权重(这一点在平时写长prompt时,想要提升某一段规则的重要性,也可以把这段规则重复几次之后,权重也就提高了)
比如用户查询词是 找出关于订单号 OrderID 生成相关的信息,我们会把查询词优化成 OrderID OrderID OrderID 订单号生成 订单号生成规则 订单号生成逻辑 订单号生成文档,重复3次,来提升订单号这个关键词的权重
具体优化提示词是在: codebaseOptimizePrompt.txt 优化流程是在 GUI 中: IDEService.ts 的 buildWithCodebaseContext
这部分就是对查询内容进行向量化,这里就不再对向量化部分重复介绍
一开始我们采用的方案是向量检索,但这种方案是存在局限性的
1.Embedding的语义理解局限性
- 语义泛化:Embedding模型(如BERT、GPT等)通常通过捕捉文本的语义信息来进行检索。然而,代码的语义与自然语言不同,代码的精确性要求更高。Embedding模型可能会将类名、方法名等符号的语义泛化,导致检索时无法精确匹配。
- 符号信息丢失:Embedding模型在处理代码时,可能会忽略类名、方法名等符号的精确信息,尤其是在符号本身具有特定含义或命名规则时。这会导致即使提问中指定了类名,模型也无法准确捕捉到这些符号信息。
2.Embedding的上下文依赖
- 上下文理解偏差:Embedding模型在处理代码时,通常依赖于上下文信息来生成向量表示。如果提问中包含了过多的细节(如类名、方法名等),模型可能会过度依赖这些细节的上下文,而忽略了代码的整体结构或功能,导致检索结果偏离预期。
- 过拟合问题:当提问过于详细时,Embedding模型可能会“过拟合”到提问中的某些细节,而忽略了代码库中更广泛的相关性。这会导致检索结果过于狭窄,甚至无法找到相关代码片段。
3.代码的结构化特性
- 符号检索的重要性:代码库中的类名、方法名等符号具有特定的结构和命名规则,这些符号在代码检索中扮演着重要角色。单纯依赖Embedding的向量检索无法充分利用这些符号的结构化信息,导致检索效果不佳。
- 混合检索的必要性:为了提高代码检索的准确性,通常需要结合符号检索和Embedding的向量检索。符号检索可以精确匹配类名、方法名等符号,而Embedding检索可以捕捉代码的语义信息。两者结合可以更好地平衡检索的精确性和语义理解。
总结:代码检索要使用混合检索技术: 符号检索 + 向量检索
而刚好 lucene 提供了文本检索 + 向量检索的功能,我们是直接利用这个来进行实现
具体代码:LuceneVectorStore.hybridSearch 这里主要特点是
- 检索文本(根据语义相似度进行搜索 ) + 检索向量,比如输入一个
twoStageHybridSearch的具体实现是什么的,会去检索twoStageHybridSearch这个文字的相关数据块,以及整个句子向量化后的数据块,对这两部分数据进行一个混合分数计算,提高整体搜索效果 - 检索时都要扩大候选集:取多一点数据出来,能提升召回,比如预期是获取10条数据,那么扩大候选集获取30条数据,再进行一个排序
- 排序处理:检索出来的数据进行混合分数计算,获取top k的数据
cline插件介绍:cline也是一款开源的插件,但跟cursor有一点挺大的不同,就是cline是不会进行codebase检索的,也就是不会利用向量进行检索
cline是利用了另一种方案: 通过理解代码中的依赖项,比如import这种来不断地读取相关的文件,类似人在理解代码时,是会不断自己跳转到依赖的代码文件进行阅读理解的,这种缺点就是依赖多的话就得不断地进行阅读,上下文会被占挺多的,并且如何判断停止加载新文件了,一个大型仓库里面一直读取依赖项是有可能非常多的(利用的是AST 解析方案,我们之前在做autocompelete时,其实也用到了这种策略,因为要结合一些依赖作为上下文才能更好地预测下一句内容)
那么这点就挺有意思的,cursor是对整个项目进行向量检索,是从技术角度去提取相关性信息,而cline则是从代码的依赖项角度去提取相关性信息,更像是一个人阅读代码的步骤
codebas的实现简单说就是rag,对内容进行向量化,使用时使用向量检索,但要做得好的话就要对各种细节进行优化,比如代码分块的策略、搜索词的加权、文本检索的加入等,通过这一次实践,已经能基本了解到 cursor 这类型的ai coding工具的codebase具体实现原理,虽然他们的工程化肯定是更加复杂的,但大概的流程思路是有认识了
后续希望在记忆能力上也能进行一些实践,现在大模型的上下文是有限的,并且新开一次对话,之前累计对话的一些信息要点都丢失了,而记忆能力就能实现对之前对话的一些信息进行保留,并且在后续的对话中能够利用这些信息进行更准确的回答
- cline不检索代码块库的原因
- 官方介绍不用codebase index的原因
- 这里有讨论cursor的codebase和cline检索代码机制的优缺点
- 混合检索
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/215452.html