主题
RAG 解决的是「用模糊的自然语言在代码库里找代码」——比如「重试逻辑在哪实现」「上下文压缩怎么做」这种关键词不明确的问题。它分两段:离线把代码库建索引(分块 + 向量化 + 关系图谱存进 SQLite),在线用混合检索(语义 + 关键词 + 图谱)返回最相关的代码块。对应的是 search_code 工具,入口是 CodeIndex(建索引)和 CodeRetriever(检索)。
RAG 代码检索流程
分「离线索引」和「在线检索」两大段。
阶段一:离线索引(/index 触发,CodeIndex)。
- 收集文件:遍历项目文件树,排除
node_modules/target/.git/.idea等非代码目录,只索引.java/.py/.js/.go等代码文件。 - 分块:这是关键——Java 文件用 JavaParser 做 AST 解析,按「类级 + 方法级」分块,每个方法是一个独立的 chunk;非 Java 文件才按 2000 字符兜底分段。
- 向量化:每个 chunk 调
EmbeddingClient.embed生成向量,支持 Ollama 本地模型和 OpenAI 兼容的远程 API。 - 关系分析:Java 文件额外用 JavaParser 提取代码关系——
extends/implements/imports/calls/contains。 - 持久化:全部写进 SQLite 两张表
code_chunks(内容 + 向量)和code_relations(关系图谱),批量插入用事务保护。
阶段二:在线检索(search_code 触发,CodeRetriever.hybridSearch)。 这是混合检索,不是纯向量:
- 语义检索:query 向量化 → 对库内所有 chunk 算余弦相似度 → 取 topK;
- 关键词检索:用
RagQueryTokenizer(jieba 中文分词 + ASCII token 正则)抽出类名/方法名等关键词,走 SQLLIKE精确匹配; - 合并加权:两类结果去重合并,语义和关键词双重命中的 +0.1 奖励,代码类型也加分(
method+0.15、class+0.10); - 限流:同一文件最多保留 2 个结果,避免一个文件霸占所有 topK。
【主动抛深度点 1:代码感知的分块】 一般 RAG 分块就是固定字符数硬切,但代码不是散文——把方法拦腰切断,embedding 语义就碎了。所以我对 Java 文件用 AST 按「类 + 方法」边界分块,每个 chunk 是一个语义完整的代码单元(一个方法、一个类的声明头),这样「重试逻辑」这个查询能精准命中包含 retry 的方法块,而不是一个被切烂的片段。解析失败才回退到按字符分段。
【主动抛深度点 2:为什么是混合检索,不是纯向量】 纯向量检索有个短板:精确符号匹配不行。你搜「RagQueryTokenizer 这个类」,向量相似度可能把它排到一堆「分词相关」的模糊结果后面;但关键词检索用 SQL LIKE 能一击即中。反过来,你问「上下文超了怎么办」,关键词检索(没有明确关键词)又会漏。所以我把两种检索结果合并加权——语义管「意思相近」,关键词管「名字精确命中」,这是代码检索里语义和精确的互补。
为什么用混合检索而不是纯向量检索?
因为代码检索有「精确符号」和「模糊语义」两种需求,单一方式覆盖不全。搜类名/方法名时关键词 LIKE 精确命中最可靠;问「怎么做重试」时语义向量最有用。混合检索把两者合并,还加了双重命中奖励和代码类型加分,兼顾两种场景。
分块为什么用 AST 而不是固定字符数?
固定字符数会切断代码的语义单元——一个方法被拦腰切两半,embedding 就失真了。AST 按「类 / 方法」边界切,保证每个 chunk 是一个完整的方法或类声明,语义完整。代价是引入 JavaParser 依赖、解析失败要回退;但对 Java 项目这个代价值得。
向量怎么存、怎么检索?为什么不用 FAISS/pgvector?
向量序列化成 JSON 数组存在 SQLite 里,检索时在内存里遍历所有 chunk 算余弦相似度。这是刻意的分层取舍:代码库规模通常几百到几千个 chunk,内存暴力算余弦完全够快、且零额外依赖;真到了几十万量级,再把存储层换成 FAISS / pgvector,接口不用改。这体现的是「先简单够用、留好升级口」的工程判断。
和 grep_code 有什么区别?什么时候用哪个?
grep_code 是精确代码定位(底层用 ripgrep 找符号/字符串),是首选;search_code 是语义辅助,适合模糊自然语言、关键词不明确、或常规搜索无果的场景。一句话:知道找什么用 grep_code,只知道大概想找什么用 search_code。两者互补,不是替代关系。
关系图谱有什么用?为什么要单独存一张表?
关系图谱记录类/方法的 extends / implements / imports / calls / contains,作用是支持图谱检索——比如查「谁调用了这个方法」,能顺着 calls 边找到调用者。语义和关键词检索都只能找「内容相似」的块,图谱检索能回答「结构关系」类问题,这是纯向量做不到的。
八股延伸|RAG 的具体流程
RAG 的具体流程
“四步。第一步分块。代码分块器对 Java 文件做 AST 解析,用 JavaParser 解析出所有的类声明和方法声明,类级别生成一个 chunk,每个方法单独生成一个 chunk。非 Java 文件按字符数分段,每段不超过 2000 字符。”
“第二步 Embedding。每个 chunk 的文本送给 Embedding 模型生成向量。支持 Ollama 本地模型和 OpenAI 兼容的远程 API。向量和 chunk 元信息一起存入 SQLite 数据库。”
“第三步建索引。SQLite 里有两张核心表:code_chunks 存代码块和向量,code_relations 存代码间的依赖关系(谁调用了谁、谁继承了谁)。索引按项目路径隔离。”
“第四步检索。用户输入一个自然语言查询,检索器同时走两条路:语义检索和关键词检索,结果合并去重后返回 TopK。”
八股延伸|具体是怎么做混合检索的?
具体是怎么做混合检索的?
“语义检索就是标准的向量相似度搜索。”
把用户查询文本送给 Embedding 模型得到查询向量,然后和数据库里所有 chunk 的向量逐一计算余弦相似度,按相似度降序排列取 TopK。
余弦相似度的计算是在内存里做的,因为代码库的 chunk 量级通常是几百到几千,暴力遍历完全撑得住。
“关键词检索不经过 Embedding,直接在 SQLite 里做 LIKE 查询,匹配 chunk 的名称和内容。”
关键词检索命中的结果,基础分设为 0.3,然后根据命中位置叠加额外分数:类名或方法名命中加 0.3,文件路径命中加 0.1,内容命中加 0.1。
“合并的逻辑是:以 filePath#name 为唯一键去重,如果同一个 chunk 在两路检索中都出现了(双重命中),额外加 0.1 的奖励分。”
“然后还有一个代码类型加分:method 类型加 0.15,class 类型加 0.10,因为方法和类比整个文件更直接回答‘怎么实现’的问题。最终按总分降序排列,同一个文件最多保留 2 个结果,防止某个大文件霸占所有位置。”
质量守护相关(引用)
- golden set:
CodeSearchGoldenSetTest用 golden-set.json 断言命中行号与 suggested_reads,给确定性搜索链路设质量下限(见 RAG代码检索)。
源码锚点(读码定位用,面试不主动讲)
CodeIndex.java:129CodeChunker.java:80EmbeddingClient.java:46CodeAnalyzer.java:38VectorStore.java:102CodeRetriever.java:133VectorStore.java:303