为语义代码搜索构建 RAG 流水线
摘要
JetBrains 开发者分享了构建生产级语义代码搜索(Air Context)RAG 流水线的实战笔记,涵盖解析、分块和向量化,这是该系列文章的第一部分,旨在为 LLM 编程智能体提供来自大型代码库的精确、可引用的证据。
<p><a href="https://lobste.rs/s/ok0m3n/building_rag_pipeline_for_semantic_code">评论</a></p>
查看缓存全文
缓存时间: 2026/10/05 05:08
# 构建面向语义代码搜索的 RAG 流水线:开发者日记与实地笔记 — JetBrains 博客
来源:https://blog.jetbrains.com/ai/2026/09/building-a-rag-pipeline-for-semantic-code-search-a-developer-diary-and-field-notes/
AI 标志(https://blog.jetbrains.com/ai/)为众多 JetBrains 产品提供 AI 驱动的功能,让您的工具如虎添翼
Agentic AI(https://blog.jetbrains.com/ai/category/agentic-ai/)AI(https://blog.jetbrains.com/ai/category/ai/)
## 构建面向语义代码搜索的 RAG 流水线:开发者日记与实地笔记
Adam Malek、Ashot Kazaryan
**第一部分:解析、分块与向量化**
不久前,我们着手打造我们能构建的最好的语义代码搜索平台:一条 RAG 流水线,它能为 LLM 智能体提供来自真实仓库的精确、可引用的证据,而不是指望 grep 随机蹦出来的结果。最终的解决方案是 Air Context(https://www.jetbrains.com/context/)。我们让它跑通了、上线了,过程中也留下了一身伤疤。在这个系列中,我们会分享那些希望第一天就有人告诉我们的经验。
> ---
编程智能体无疑是本十年软件开发领域最大的技术飞跃。智能体与前沿模型在看似无解的代码复杂度面前,正在证明它们的产出能力,生产出看起来相当可靠的代码。
然而,随着越来越多的开发流程由智能体驱动,智能体的效率和产出代码的质量变得越来越重要。问题不在于智能体能否完成任务——只要有足够的时间和 token 资源,它总会完成——而在于它需要多少时间、多少精力、多少引导才能产出可投产的结果。对于大规模代码库尤其如此:智能体会花费大量时间搜索与它正在开发的功能相关的代码片段,并把它们拉进上下文。
## 为什么语义搜索很重要
在寻找正确代码片段时,智能体通常会诉诸关键词搜索和 grep 等传统代码搜索工具。但这些工具的局限在于,它们要求智能体预先知道要搜索的确切文本。例如,一个寻找会话令牌刷新位置的智能体,不能指望代码里恰好包含“refresh”这个词。要对抽象领域进行推理,智能体需要按语义搜索代码的能力,也就是语义搜索。这时检索增强生成(RAG)就登场了。如果我们能以一种能捕捉源代码语义的方式为其建立索引,然后允许智能体用自由文本按需检索相关片段,我们就创造了一个能发挥智能体长处的接口。
## 从原型到生产
在 Agentic 时代,许多伟大的想法一样,一个原生的原型实现极其简单,而一个经过充分评估的生产级方案则绝非如此。在这系列博客文章中,我们想分享构建一个有效的 RAG 系统所涉及的内容,以及我们在打造自己的 Air Context 过程中走过的弯路。我们会逐一讲解每个阶段,从预处理到存储再到智能体集成,提供一些技术背景与建议。
本系列的第一部分将介绍流水线的初始阶段:解析与分块——把原始源文件划分为作用域恰当的单元;以及向量化——将这些单元转化为支持语义搜索的表示形式。
## 解析与分块的精妙之处
解析与分块是优秀 RAG 方案中至关重要的预处理步骤,却常常被忽视。要让 LLM 对源代码进行嵌入或其他形式的索引,我们必须先把它喂入原始代码行。这听起来可能微不足道,对于小型演示项目或许确实如此。然而生产级系统包含数千个文件,每个文件又可能有数百乃至数千行代码。更麻烦的是,智能体反而加剧了问题——它们是高产的写手,进一步膨胀了代码库。每个文件可能包含大量的类、字段和方法,彼此之间的关联程度各不相同。
### 找到合适的分块大小
即使能把这些巨大的代码文件完整地塞进嵌入模型,这一昂贵的操作最终也是得不偿失的。由于整个文件被嵌入为一个单元,搜索返回的将是整个文件。这与智能体代码探索和导航的目标背道而驰,因为这些场景关注的主要是找到某个特定的函数、符号或代码片段。
反过来,如果我们走向另一个极端,把每一行代码都单独嵌入,我们又会面临另一类问题。这些单独的行脱离上下文后可能毫无语义价值。一个泛泛的函数名或注释不值得嵌入,只会产生错误的检索结果。从某种意义上说,我们会见木不见林,智能体则会被大量、往往无关紧要的微观结果所淹没。
因此,必须找到合适的方法来对代码进行分块或分组,使分组作用域恰当。每个分组应包含足够的必要上下文,并代表某种共同的语义含义。
### 为什么固定大小的分块不够用
分块是这样一个技术的通用名称:将要喂给智能体的内容切分为一组组块。一种朴素的分块方法就是简单地把大文件按固定行数切分。但如果采用这种方法,我们会发现生成的分组在语义上是错误的。互不相关的代码会被归到一起,例如一条 import 语句和某段函数体,从而在检索时造成错误。
为了解决这个问题,我们可以利用每个源文件都有相当明确定义的结构这一事实。以 Java 为例——import 通常位于文件顶部,其后是类定义,类头之前可能带有文档注释。类中包含字段和方法,字段和方法又可能有各自的文档注释。了解定义类结构的惯例和规则,使我们能够进行更智能的分块,实现上下文信息的恰当平衡。
### 解析与结构感知分块
在过去 26 年中,我们 JetBrains 团队开发了一批足够智能的解析器,能够适应各种语言的怪癖、不规范之处、惯例和细微差别。这些解析器与其他工具一起,构成了我们内部的 JetBrains Code Engine 平台,Air Context 就是在这个平台上开发的。在撰写本文时,Air Context 支持对九种主要语言进行解析和结构感知分块:Kotlin、Java、Python、JavaScript、TypeScript、C#、PHP、Go 和 Rust。对于所有其他语言,我们的实现简单地回退到朴素的按行切分,以确保任何语言或文档都能被索引和搜索。
解析器让我们可以把源文件拆分为一串语法节点,每个节点都携带有关其身份的信息——注释、空白、修饰符列表等等。分块算法消费这一节点流,并应用逻辑来决定某个给定块的作用域。算法根据节点的类型、大小及其后代节点作出判断。如果一个节点超过大小阈值但没有子节点,它会回退到更原始的切分策略。
某些特定语言的结构即使超过首选大小也会被保留为单一切片。诸如文档、注解、可见性修饰符和关键字之类的前缀会与声明保持在一起;后缀(通常是收尾语法)则与其所关闭的构造保持关联。此外还有一些针对特定语言的清理工作,例如移除像 `@NotNull` 或 `@Override` 这类常见且无语义价值的 Java 注解。
该算法与 Zhang 等人于 2025 年发表的 cAST(https://arxiv.org/abs/2506.15655)有若干相似之处。我们的实现与 cAST 都会保留能放得下的最大语法单元,只对过大的单元进行细分,并将相邻的较小单元归并,以避免产生通常不具备语义价值的碎片块。最大的区别在于,我们在实现中编码了更多语言语义——把 Python 装饰器与定义保持在一起、把 KDoc 与 Kotlin 声明放在一起,等等。
分组之后会进行块的规范化,包括:
- 删除首尾空白
- 删除空行
- 移除公共缩进,同时保留相对缩进
规范化之后,块会连同元数据(包含一个相对路径,它会与规范化后的块内容一同嵌入)一起传递到下一步——嵌入。
## 评估分块质量
很难给出一个确切的答案,说明嵌入模型的输入应该是什么样子。分块大小很重要,但如前所述,越大并不总是越好。此外,与代码一同嵌入的某些元数据可能有用,而另一些则可能引入噪声,最终降低搜索质量。
我们选择采用 LLM-as-a-judge(大模型评审)策略,作为评估的一部分来检查分块。评审模型根据一个块和源文件,判断该边界划分是否合理。它会寻找意外的产物,例如孤立的文档、悬空的收尾语法,或被从有意义的构造中切开的代码片段。此外,源代码处理流程的任何改动也都要经过完整的端到端检索评估。我们会在本系列的下一部分回到这个评估流水线。
## 向量化
完成了源代码的预处理后,我们终于得到了大小可能刚刚好、分组也正确的文本块,可以用于语义检索。接下来的任务是通过一个称为向量化的过程,把这些片段转换为一种可以支持语义搜索的表示。
通过向量化,嵌入模型读取一段文本并输出一个定长的数字列表(向量),也就是一个几千维空间中的一个点。重要的是,模型经过训练,使含义相近的文本在空间中彼此靠近。传统搜索可能错过了这种联系,但在这里,一个刷新缓冲写入操作的函数和一个排空待处理队列的函数,尽管不共享任何关键词,却可能最终彼此靠近。于是,向量之间的距离就成了相关性的度量。查询被转换为同一空间中的一个位置,检索结果就是离它最近的那些。
### 以字节为单位优化:存储层面的取舍
任何对大型代码库进行向量化的尝试,都必须同时考虑成本与性能。单次嵌入很便宜,但一个大型仓库会产生数百万个块,进而产生数百万个向量,这些向量必须被存储、驻留在内存中,并与每个传入的查询进行比较。一个几千维、采用 32 位浮点数的向量约有 16 千字节,所以几百万个块在任何管理开销之前就构成了几十 GB 的索引。在这样的规模下,每个向量的字节分配就成了成本瓶颈,首要问题也迅速从"我们能做到多准确?"变成了"每个字节能换来什么?"换句话说,我们需要在尽可能保留搜索质量的同时,找到降低成本的方法。
降低向量成本有两种方式。第一种是保留更少的维度。现代嵌入模型经过训练,使得向量的前缀切片可以独立工作。这种降维损失会同时施加在多个嵌套的前缀长度上,把最粗粒度的结构推向最前面的维度。这意味着你可以把向量截短并重新归一化,它仍然能正常检索。另一种方式是保留所有维度,但通过牺牲精度来降低每个维度的成本,从而为每个向量节省字节数。
这两个选项彼此独立,可以组合使用,这意味着任何存储预算都可以通过维度数量与数值精度的不同组合来满足。真正的问题在于:在相同字节数下,哪种组合的检索效果最好。这种取舍远不是对等的。假设预算为每向量 512 字节。你可以用它来保存 128 个维度并保持完整的 32 位精度,或者保留全部 4,096 个维度、每个维度只占 1 个字节。两种方案都恰好符合预算,但在测试中你会发现,第二种方案的检索效果要好得多。
### 为什么维度比精度更重要
要理解原因,可以想象每个维度是模型学会对文本提出的一个小问题:这是关于错误处理的吗?它涉及网络吗?这是测试代码吗?还有几千个类似的、尚未命名的主题和问题。(真实的维度比这更模糊,但这是一个有用的抽象。)
单独一个答案本身意义不大。当我们判断两个块相似时,依据的是它们对这些问题中的许多给出相同的答案。因此,我们应该通过问题的覆盖程度,而不是答案的精确度来评估向量。
保留全部 4,096 个维度、每个维度 1 个字节,能为每个问题保留一个大致的"是/否"答案。截断到 128 个维度,只保留了 3% 的问题的非常精确的答案,其余全部丢弃,而幸存维度上再多的精度也无法恢复被丢弃维度所承载的信息。从某种意义上说,一份填满勾选标记的长问卷胜过一份精确到小数点后六位的短问卷。维度是你要保留的东西;精度是你可以承受损失的东西,而且以后更容易补偿。
于是我们选择保留所有维度,并把精度缩减推到极限,将向量压缩到每个维度 1 个比特,这比同向量的 32 位浮点表示小 32 倍。量化过程出乎意料地简单:大于等于零的分量变为 1,所有负分量变为 0,而数值大小则被丢弃。
改变表示方式也会改变度量方式。余弦相似度需要我们刚丢弃的数值大小,因此二进制向量改用汉明距离进行比较,即两个比特模式不一致的位置数量。例如,比较 10110100 和 10010110。它们在两个位置上不同,所以距离为 2。在全长情况下,计算依然同样简单。一个 4,096 位的向量被存储为 64 个 64 位的字,比较两个这样的向量意味着对每对字执行异或操作——异或结果中为 1 的位置表示两个向量不一致——然后统计 1 的个数。CPU 对每个字只需一条指令即可完成,因此一次完整比较的成本大约在一百条指令量级,而原始浮点数上的余弦相似度需要数千次乘法运算。
值得注意的是,度量方式从来不是单独做出的决定。我们选择 1 位精度是为了节省存储空间,而一旦每个分量都是符号位,汉明距离就成为唯一说得通的比较方式。选择精度就决定了度量方式。
二进制量化相比未量化的向量仍然会损失几个百分点的召回率。考虑到结果将被一个推理型智能体消费,我们接受了这一代价。为智能体提供服务的代码搜索,对"正确的邻域"的需求远远超过对完美排序的 top 10 的需求。当智能体询问
相似文章
构建金融文档高精度语义证据/RAG系统——寻求反馈
本文概述了一个用于金融文档的高精度语义证据和RAG系统的多门架构,强调了可追溯性、对账和混合检索,并征求对其设计的反馈。
@DanKornas:你的RAG流水线不需要重复检索同一份证据。LeanRAG是一个开源的RAG框架,它利用……
LeanRAG是一个开源的RAG框架,它利用知识图谱、语义聚合和分层检索来减少检索流水线中的冗余,并提供带有简洁证据路径的接地答案。
@LearnWithBrij:别再像2022年那样构建RAG了。分块→嵌入→检索→生成 这条流水线能用……直到你尝试上线……
一个帖子解释了构建生产级RAG超越简单分块-嵌入-检索-生成所需的四个关键层次:智能查询路由、高级索引、多类型检索和持续评估。
@TeachTheMachine:决定你的RAG是否有效的7种分块策略
来自MachineLearningMastery的一篇教育性文章,涵盖了RAG流水线的七种分块策略,从固定大小令牌分块到高级语义和层次化方法,并提供了每种策略的适用场景指导。
@tom_doerr: 构建RAG系统的分步指南笔记本 https://github.com/langchain-ai/rag-from-scratch…
一个从头构建RAG系统的分步笔记本系列,涵盖索引、检索和生成,并附有视频播放列表。