首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >代码 RAG 的切片之痛:从 token 切片到 Wiki 化预处理

代码 RAG 的切片之痛:从 token 切片到 Wiki 化预处理

作者头像
陆业聪
发布2026-07-13 17:51:31
发布2026-07-13 17:51:31
190
举报

📰 科技要闻

• AI 工作流 100% Vibe Coding 完成 Game Jam 游戏开发:少数派作者实战分享,用纯 AI Agent 工作流完成独立游戏全流程——从设计到编码到调试,"Agent 和人一样离不开闭环"。

• Sean Lynch 论 MCP:「也许 MCP 的理想形态就是一个 Auth 网关」——Simon Willison 转载的这个观点在 HN 引发热议,MCP 协议正从"工具调用标准"往更基础的架构定位演化。

• ZuzuZoos 完成数千万元 Pre-A 轮融资:锦秋领投,主打 AI 陪伴机器人+AI 潮玩,moody 前高管搭档大疆骨干入局,又一个"AI + 硬件"赛道新玩家。

🔪 代码 RAG 的切片之痛:从 token 切片到 Wiki 化预处理

最近在折腾内部的代码补全系统时,我被一个问题反复折磨:明明向量检索能从文档里召回相当精准的片段,为什么一切换到代码仓库,召回质量就断崖式下跌?

不是 embedding 模型不行,也不是检索策略有问题。根因很朴素——代码被切烂了

打个比方:想象你把一本推理小说按固定页数撕开寄快递,每一摞纸恰好从一句话中间断开——收件人拼起来发现凶手的名字前半截在第三包、后半截在第四包。代码按 token 切片,就是这个效果。

一段 300 行的 Android Activity,按 512 token 一刀切下去,lifecycle callback 被拦腰截断,`onCreate` 的前半段和 `onResume` 的后半段缝在同一个 chunk 里,向量模型拿着这坨"语义碎片"去做 embedding,能算出什么有意义的语义?答案是——垃圾进,垃圾出。

这篇文章就来把这个痛点完整拆开,分析三条不同的预处理路线:AST 切片、LSP-based 切片、和 Qoder repoWiki 的"Wiki 化"方案。最后给出我自己在 Android 大型工程上跑下来的选型建议。

💀 一、按 token 切片有多惨?实际案例说话

先看一个真实场景。假设我们有一个 `PaymentManager.kt`,400 行,包含 3 个核心方法:

代码语言:javascript
复制
class PaymentManager(
private val api: PaymentApi,
private val db: OrderDatabase,
private val analytics: AnalyticsTracker
) {
// 方法 A: 120 行, 处理支付流程
suspend fun processPayment(...) { ... }// 方法 B: 80 行, 处理退款
suspend fun handleRefund(...) { ... }// 方法 C: 150 行, 对账逻辑
suspend fun reconcile(...) { ... }
}

按 512 token 切片(大约 200 行代码),第一个 chunk 会包含:类声明 + `processPayment` 全部 + `handleRefund` 的前 80 行。第二个 chunk 从 `handleRefund` 的中间某行开始,一直到 `reconcile` 的一半。

问题来了:

语义割裂:`handleRefund` 被切成两半,前半段和 `processPayment` 混在一起,后半段和 `reconcile` 混在一起。检索"退款逻辑"时,两个 chunk 都只能算半对。

上下文丢失:第二个 chunk 里没有类名、没有 import、没有依赖声明。模型连这段代码属于哪个类都不知道。

语法破坏:切片位置可能恰好在一个 `when` 表达式中间,chunk 甚至不是合法代码。

我在内部做过一个对比实验:同一个 60 万行的 Android 工程,用 token 切片 vs AST 切片,同样的 embedding 模型(text-embedding-3-large),检索 Top-5 的 MRR(Mean Reciprocal Rank)差了 0.31——token 切片 0.42,AST 切片 0.73。这不是微调能弥补的差距。

🌲 二、路线 A:AST 切片——基于抽象语法树的结构化切分

核心思路

用 Tree-sitter 或者语言原生 parser 把代码解析成 AST,然后按语法节点的自然边界切分——函数是一个 chunk,类是一个 chunk,如果一个函数太长就按内部 block 递归拆。

这是 Cursor 当前采用的主要方案。根据社区逆向分析(参见 Towards Data Science 的 "How Cursor Actually Indexes Your Codebase"),Cursor 的 codebase indexing pipeline 大致是:

1. 文件变更检测(Merkle Tree diff)

2. Tree-sitter 解析 → 提取函数/类/模块级节点

3. 每个节点加上文件路径 + 所属类名作为 metadata prefix

4. Embedding → 存入本地向量索引

5. 查询时 hybrid search(向量 + 关键词 BM25)

实际代码长什么样

用 Tree-sitter 的 Python binding 做一个最小可用的 AST chunker:

代码语言:javascript
复制
import tree_sitter_kotlin as tskotlin
from tree_sitter import Language, Parserdef chunk_kotlin_file(source: str, max_tokens: int = 512):
parser = Parser(Language(tskotlin.language()))
tree = parser.parse(source.encode())
chunks = []def visit(node, context_prefix=""):
# 函数/类级别节点作为切片单元
if node.type in (
"function_declaration",
"class_declaration",
"object_declaration",
):
text = source[node.start_byte:node.end_byte]
if token_count(text) <= max_tokens:
chunks.append(context_prefix + text)
else:
# 超长:递归拆分子节点
for child in node.children:
visit(child, context_prefix +
extract_signature(node) + "\n")
else:
for child in node.children:
visit(child, context_prefix)# 文件级 context: package + imports
file_prefix = extract_file_header(source)
for child in tree.root_node.children:
visit(child, file_prefix)return chunks

优点与局限

优点:

• 语义完整性好——每个 chunk 至少是一个完整的语法单元

• 实现成熟——Tree-sitter 支持 200+ 语言,社区活跃

• 速度快——增量解析能力强,适合 IDE 实时索引场景

局限:

看不见跨文件关系:一个 chunk 里只有当前函数的代码,不知道它调了谁、被谁调。检索"支付流程"时可能只召回 `processPayment`,漏掉关键的 `OrderValidator` 和 `PaymentGateway`。

大函数仍然是灾难:Android 老项目里动辄 500 行的 Activity 方法,递归拆分后子 chunk 语义很碎。

不理解业务语义:AST 只知道"这是一个函数",不知道"这是支付核心链路的第三步"。

🔗 三、路线 B:LSP-based 切片——基于 Language Server 的语义边界

核心思路

既然 AST 只能看到"语法结构"看不见"语义关系",那我们能不能用更高级的语言理解工具?答案是 Language Server Protocol (LSP)——它能提供类型推导、引用查找、符号跳转等能力。

具体做法:

1. 用 LSP 启动完整项目编译(Kotlin: kotlin-language-server / IntelliJ backend)

2. 对每个函数节点,查询其「出站引用」(calls / references)

3. 将引用目标的签名(不是全部代码)附加到 chunk 的 metadata 中

4. 构建调用图(call graph)辅助检索时的扩展召回

这种方式的本质是:每个 chunk 不再是孤立的代码片段,而是带有"关系网络"的语义单元。检索"退款流程"时,不只召回 `handleRefund`,还能通过 call graph 拉出它依赖的 `RefundValidator`、`TransactionRepository` 等。

实操代价

听起来很美,但这条路的工程成本极高

编译依赖:LSP 要正常工作,项目必须能成功编译。Android 大型工程编译一次 3-5 分钟是常态,索引更新延迟不可接受。

多语言适配:一个 Android 项目可能同时有 Kotlin、Java、C++(JNI)、XML(布局)。每种语言需要不同的 Language Server,整合成本高。

增量更新难:改一个文件可能影响数十个引用,call graph 需要大面积重建。

资源消耗:IntelliJ 的 Kotlin language server 跑一个大工程就要 4-8GB 内存。CI 环境上跑索引,成本不低。

据我了解,JetBrains 的 AI Assistant 走的就是类似路线——它有天然优势,因为 IntelliJ 本身就是最强的 Kotlin/Java LSP。但这个优势几乎不可能被第三方工具复现。

📚 四、路线 C:Qoder repoWiki——先把代码仓库"翻译"成文档

一个反直觉的思路

前两条路都是在"切得更好"上做文章——怎么让每个 chunk 的语义更完整。但 Qoder 的 repoWiki 提出了一个完全不同的思路:

与其费劲把代码切好,不如先把代码"翻译"成文档,再用成熟的文档 RAG 流程处理。

类比一下:你去一个巨大的博物馆找一幅画。AST 切片相当于把每个展厅的每幅画都编了号让你逐个搜索;而 repoWiki 的做法是——先给博物馆写一份导游手册,你查手册定位到“印象派展厅”,再走进去找具体那幅画。效率完全不是一个量级。

这就是 repoWiki 的核心理念。

具体实现流程

Step 1 — 结构提取:解析代码仓库的目录结构、模块划分、依赖关系

Step 2 — 语义摘要生成:对每个模块/类/关键函数,用 LLM 生成自然语言描述

Step 3 — Wiki 文档组装:按照"项目概览→模块说明→类职责→函数功能→调用关系"的层次结构,组装成 Wiki 文档

Step 4 — 标准 RAG 索引:对 Wiki 文档做常规的语义切片 + 向量索引

Step 5 — 检索增强:召回 Wiki 片段后,通过 Wiki→Source 的映射表定位原始代码

用伪代码表示核心流程:

代码语言:javascript
复制
class RepoWikiBuilder:
def build(self, repo_path: str) -> WikiIndex:
# 1. 解析项目结构
structure = self.parse_structure(repo_path)
# 2. 对每个模块生成描述
wiki_pages = []
for module in structure.modules:
page = self.llm.generate(
prompt=WIKI_PROMPT,
context={
"module_name": module.name,
"files": module.file_summaries,
"dependencies": module.deps,
"public_api": module.public_signatures,
}
)
wiki_pages.append(WikiPage(
content=page,
source_refs=module.file_paths
))
# 3. 标准文档 RAG 索引
index = VectorIndex()
for page in wiki_pages:
chunks = semantic_chunker(page.content)
for chunk in chunks:
index.add(
text=chunk,
metadata={"source_files": page.source_refs}
)
return index

为什么这个方案在大型工程上有独特优势?

1. 天然解决跨文件关系问题。

Wiki 文档在生成时就已经把模块间的依赖关系、调用链路写清楚了。不需要检索时再去做 call graph 扩展。

2. 检索粒度和代码粒度解耦。

检索"退款流程"时,Wiki 里可能有一段完整描述:"退款流程由 PaymentManager.handleRefund 发起,经过 RefundValidator 校验后,通过 TransactionRepo 写库,最后由 NotificationService 推送结果"。这一段 Wiki 比任何代码 chunk 都更适合被向量检索召回。

3. 适合 Android 这种大型单体工程。

一个 60 万行的 Android 项目,直接做 AST 切片会产生几十万个 chunk,向量索引的召回精度随规模急剧下降。但翻译成 Wiki 后,可能只有几百页文档,检索效率和精度都大幅提升。

4. 对非代码文件也能覆盖。

Android 项目里有大量 XML 布局文件、Gradle 配置、ProGuard 规则。这些东西用 AST 切片不好处理,但 Wiki 化方案天然能把它们翻译成"layout_main.xml 定义了首页的 ConstraintLayout 布局,包含顶部 Toolbar 和 RecyclerView..."这样的自然语言描述。

代价与取舍

当然,这条路也不是免费的:

LLM 调用成本:生成 Wiki 需要对每个模块调用 LLM。60 万行的项目,可能需要数百次 LLM 调用,首次索引成本不低。

信息压缩损失:Wiki 是"翻译"后的摘要,必然丢失了具体实现细节。检索"这个 for 循环为什么用 downTo 而不是 until"这种细粒度问题时,Wiki 方案不如 AST 切片直接。

一致性维护:代码改了,Wiki 要跟着更新。增量更新策略是必须解决的工程问题(git diff → 识别受影响模块 → 重新生成对应 Wiki 页面)。

幻觉风险:LLM 生成的 Wiki 可能描述了代码实际不存在的行为。需要有验证机制。

⚖️ 五、三条路线的横向对比

跑了几个月不同方案后,我做了一个对比总结。三条路各有脾气,拉个表格让它们当面 PK:

维度

AST 切片

LSP-based

repoWiki

语义完整性

⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐⭐⭐

跨文件关系

✅✅

细粒度检索

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐

实现复杂度

极高

中高

增量更新成本

多语言支持

200+ (Tree-sitter)

逐语言适配

语言无关

首次索引耗时

秒级

分钟级

十分钟级

运行时开销

高(LSP常驻)

低(离线生成)

适合场景

IDE 实时补全

JetBrains 内部

离线问答/Code Review

🎯 六、选型建议:什么时候选什么

基于实际落地经验,我的建议是分层组合,而不是非此即彼

🏗️ 推荐架构:两层检索

第一层(粗召回):repoWiki

用户 query → Wiki 向量检索 → 召回相关模块/文件范围 → 缩小搜索空间

第二层(精排序):AST 切片

在第一层缩小的文件范围内 → AST chunk 精确检索 → 返回具体代码段

这种两层架构的好处是:

Wiki 层负责"理解用户想要什么"——召回正确的模块范围

AST 层负责"定位具体代码"——在正确范围内找到精确的代码段

• 避免了在几十万个 AST chunk 里大海捞针

落到具体的工程选型上:

如果你是做 IDE 插件(类 Cursor/Continue):

• 主力用 AST 切片(响应速度要求高)

• 可选加一层 repoWiki 做 "project context" 辅助理解大方向

如果你是做离线 Code Review / 代码问答:

• 主力用 repoWiki(回答质量重要,延迟容忍度高)

• AST 切片做精排补充

如果你是做代码搜索(类 Sourcegraph):

• BM25 关键词搜索 + AST 切片向量搜索混合

• repoWiki 可以作为"语义搜索"的补充入口

🔧 七、一些实战中踩过的坑

最后分享几个我在 Android 大型工程上做代码 RAG 时踩过的具体坑,希望能帮你少走弯路:

坑 1:Kotlin DSL 代码的 AST 切片效果极差

Gradle Kotlin DSL、Compose 的声明式 UI 代码,结构是嵌套 lambda 而不是传统的函数/类。Tree-sitter 能解析但切出来的 chunk 语义很碎。我的解法是:对这类文件直接按"文件粒度"做 chunk(一般一个 Composable 文件不会超过 300 行)。

坑 2:代码注释是宝藏,别扔掉

很多人做 AST 切片时会"清洗"掉注释。大错特错!注释里的自然语言描述恰恰是 embedding 模型最容易理解的部分。保留注释,甚至可以把注释单独提取出来做一个并行索引。

坑 3:import 声明要回填到每个 chunk

Cursor 的做法是把文件路径 + 关键 import 作为每个 chunk 的 prefix。这个看起来简单的操作,对召回精度的提升有 10-15% 的 MRR 改善。原因是 import 里包含了关键的包名信息(如 `com.example.payment.refund`),这对向量模型理解 chunk 语义帮助很大。

坑 4:Android 资源文件需要特殊处理

`strings.xml`、`dimens.xml`、`colors.xml` 这些资源文件不该被 AST 切片。它们更像配置文件,适合整文件作为一个 chunk,或者 key-value 级别做 chunk(每个 `` 是一个 chunk)。

坑 5:不要对测试代码和生产代码做同等权重的索引

测试代码的 chunk 经常会"污染"检索结果。我的做法是把测试代码的向量分到单独的 collection,检索时根据 query 意图动态选择是否包含测试代码。

🤔 八、一个更大的问题:代码 RAG 的终局是什么?

写到最后,我想聊一个更大的问题。

内网有篇文章标题很有意思:《Anthropic、Cursor、Aider、Cline 用 2 年半收敛到了同一个答案:用文件,不用向量库》。它指出一个趋势——最强的 AI 编程工具正在放弃向量检索,转向"直接读文件 + 靠大模型的长上下文能力"。

Cline 团队在 HackerNews 上公开说过"Why Cline doesn't index your codebase"——核心论点是:与其花大量工程精力做 chunking + embedding + retrieval 这套复杂管线,不如直接把相关文件全文塞进 200K context window,让模型自己理解。

Claude Code 也是这个路子:它没有向量索引,靠的是 `grep` + `find` + `read_file` 组合出来的"暴力检索"。

这意味着什么?

短期:对于 context window 还受限的场景(IDE 补全要求低延迟,小模型推理成本高),AST 切片 + 向量检索仍然是刚需。

中期:repoWiki 这类"中间表示"的价值会增大——不是为了给向量检索服务,而是为了给大模型一个高效的"项目地图",让它知道往哪个方向找文件。

长期:当模型 context window 足够大且成本足够低时,代码 RAG 可能真的退化成"文件系统索引 + grep"。到那时候,Qoder repoWiki 的价值反而最独特——它不是检索工具,而是理解工具,帮人(而不是帮模型)快速理解一个陌生代码库。

✍️ 总结

代码 RAG 的切片问题,本质上是"代码的语义密度和结构复杂度远超自然语言"这个事实的工程映射。三条路线各有取舍:

AST 切片:工程成熟,适合实时场景,但看不见跨文件关系

LSP-based:理论最优,但工程代价太高,几乎只有 JetBrains 能玩

repoWiki:思路最新颖,适合离线深度理解,但有 LLM 成本和一致性问题

我的实战结论是:两层检索(Wiki 粗召回 + AST 精排序)是当前落地效果最好的组合,特别是对于 Android 这种 60 万行级别的大型工程。

但也要看到趋势:随着 context window 扩大和推理成本下降,代码 RAG 的形态会持续演化。今天花大力气做的 chunking 优化,可能三年后变得不再关键。但"理解代码仓库"这个需求永远存在,只是实现方式会变。

如果你在做代码 RAG 相关的工作,欢迎留言交流你的切片方案——尤其是 Android/iOS 这种大型客户端工程,踩坑经验互相分享最有价值。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档