深度实验对比三种代码分块策略,发现直观的固定20行分块在向量检索上反而超过复杂的AST语义分块。揭示RAG系统中的实践陷阱。
你需要把代码切分成多个 chunk,交给向量模型处理。最“专业”的切分方式是什么?
大多数工程师都会脱口而出:按照 AST(抽象语法树)切分,每个函数一个 chunk。这个推理无懈可击——函数是最自然的语义单元,AST 能精准对齐语义边界,而且绝不会把一个函数从中间切断。相比之下,按照固定行数切分(比如每 20 行一段)显得既粗糙又愚笨:它会把函数硬生生切成碎片。
听起来无可辩驳。但实验数据狠狠打了这种直觉的脸。
在同一份 266 行的 Python 代码上,我测试了三种 chunking 策略。结果是:AST 函数级切分得分最低,而那个“粗糙又愚笨”的固定行切分,得到了完美的 1.000。
这并不是随机噪声。背后有一套值得深挖的机制。
数据集:266 行 Python 代码,覆盖 5 个模块——auth、database、cache、payment、notification——共 27 个函数。全程使用的 Embedding 模型是 bge-large-zh-v1.5;评估使用 12 条自然语言查询。
忽略代码结构,每 20 行切一段;相邻 chunk 重叠 3 行,以避免丢失边界信息。
Chunk 1: lines 1-20
Chunk 2: lines 18-37 ← overlaps 3 lines with the previous chunk
Chunk 3: lines 35-54
...
暴力解决。整个文件就是一个 chunk。
Chunk 1: lines 1-266 ← everything crammed in
使用 Python 的 ast 模块解析语法树,精确地按照函数定义进行切分。
Chunk 1: def validate_jwt_token(...) lines 9-18
Chunk 2: def hash_password(...) lines 20-28
Chunk 3: def create_payment_intent(...) ...
...
先来看看固定行数切分究竟有多“粗糙”。以 validate_jwt_token 为例(第 9~18 行,共 10 行):
Function 'validate_jwt_token' (lines 9-18, 10 lines)
Splits across 2 chunks:
Chunk lines 1-20: contains lines 9-18 (10/10 lines of function)
Chunk lines 18-37: contains lines 18-18 (1/10 lines of function)
Problem: Neither chunk contains the complete function.
A query about 'validate_jwt_token' will retrieve a partial implementation.
看到了吗?这个函数的最后一行(第 18 行)被切进了下一个 chunk。这正是固定行数切分饱受诟病的地方——它不知道函数边界在哪里,想在哪儿切就在哪儿切。
按理说,这种碎片化一定会伤害检索效果。AST 能保证每个函数完整无缺,应该毫无悬念地胜出。
Strategy Chunks AvgLen R@3 R@5 vs AST R@5
──────────────────────────── ─────── ─────── ─────── ─────── ──────────
1_fixed_lines_20 16 755 0.917 1.000 +0.042
2_file_level 1 10270 1.000 1.000 +0.042
3_ast_function 27 356 0.889 0.958 base
结果却反转了:
固定行数切分:Recall@5 = 1.000(满分)
文件级切分:Recall@5 = 1.000(满分)
AST 函数级切分:Recall@5 = 0.958(最低)
把函数切碎的“粗糙方法”赢了。精准对齐语义边界的“专业方法”输了。
在 12 条查询中,有 11 条在三种策略下都得到了完美的 1.000。唯一出现差异的是第 8 条查询:
Query Fixed File AST
────────────────────────────────────────────────── ────── ────── ──────
process payment and create Stripe charge 1.00 1.00 0.50 ←
(the other 11 queries all score 1.00 across strategies)
一条查询决定了胜负。在这条查询上,AST 得分为 0.50,而固定行数和文件级切分都得到了 1.00。
要理解这个反转,就必须弄清楚这唯一一条查询究竟发生了什么。
查询内容是 process payment and create Stripe charge。
它的 ground truth 包含两个相关函数:create_payment_intent 和 calculate_order_total。
create_payment_intent 包含 stripe.PaymentIntent、create() 等 token,与“Stripe charge”高度匹配。三种策略都能轻松命中它。
问题出在 calculate_order_total。它的职责是“汇总商品价格、应用折扣、计算税费”——函数签名和函数体里充满了 sum、discount、tax,却完全没有“payment”或“Stripe”的踪迹。在向量空间中,它距离查询“create Stripe charge”非常遥远。
关键区别就在这里:
AST chunking:calculate_order_total 被单独切成一个孤立的 chunk,独自在向量空间中存在。因为它距离查询太远,所以无法被检索到——AST 得分为 0.50(两个相关函数只命中一个)。
固定行数切分/文件级切分:calculate_order_total 和 create_payment_intent 恰好落在同一个 chunk 中(它们在源码中彼此相邻)。当查询命中 create_payment_intent 时,整个 chunk 都会被检索出来,calculate_order_total 也跟着被带了出来——这就是搭便车效应。
[图片:向量空间示意图。左侧是 AST chunking:calculate_order_total 是一个远离查询的孤立点,因此未被检索。右侧是文件级/固定行数切分:calculate_order_total 和 create_payment_intent 被框在一起;当查询命中这个方框时,两个函数都会被取出。]
AST chunking 就像一家超市,每件商品都有自己的货架和条形码。搜索“啤酒”,你只能找到啤酒。大 chunk 则像是把啤酒和尿布捆成一个促销组合包。搜索“啤酒”,命中这个组合包,尿布也会进入你的购物车——尽管你从没搜索过尿布。
在这个只有 266 行代码的小型数据集上,这些“组合包”碰巧正确地把语义上相邻的函数配到了一起,于是原本无法检索到的 calculate_order_total 得以搭便车。AST 切得太干净,反而失去了这次“意外获胜”的机会。
这就是结果反转的全部真相:并不是 AST chunking 更差,而是大 chunk 借助搭便车效应,在一条查询上走了运。
你很容易得出这样的结论:“以后直接用固定行数/文件级切分不就完了。”先等等——这个结论大错特错。真正的原因藏在表格刻意省略的一列中。
文件级切分的 Recall@5 是完美的,但它的 precision 为零。
想想文件级切分的工作方式:整个代码库只有 1 个 chunk。任何查询都会检索到同一个 chunk——全部 266 行代码。Recall@5 衡量的是“正确答案是否出现在前 5 个结果中”。既然只有 1 个包含所有函数的 chunk,它当然每次都能命中。
但这有什么用?你问“JWT 验证在哪里”,它却把整个文件交给你。你还是得自己在 266 行代码里寻找。检索系统的意义在于定位,而文件级切分什么也定位不了。它的满分是在作弊。
固定行数切分也没那么美好。这个数据集里的函数短小而紧凑,平均长度较小。一个 20 行的 chunk 加上 3 行重叠,碰巧能把很多函数完整地容纳在同一个 chunk 中,甚至还能让相邻函数搭便车。这是数据集特征与 chunk 大小刚好匹配的巧合,并不是固定行数切分本身的优点。
放到真正的大型项目中试试:函数动辄超过一百行,20 行的 chunk 连半个函数都装不下。开头演示中 validate_jwt_token 的碎片化将成为常态——你检索到的永远只是函数片段:函数签名在一个 chunk,核心逻辑在另一个 chunk,异常处理又在第三个 chunk。到了这个时候,固定行数切分的 Recall 会彻底崩塌。
这个实验的规模(266 行)实在太小——它恰好掩盖了 precision 上的差异,让两个“作弊者”看起来像赢家。
把规模因素考虑进去,情况就清晰了:
小型代码库(< 1000 行):文件级切分尚可接受——反正把所有代码都塞进 LLM 的上下文窗口,成本也很低。但要清楚,它的 precision 为零;本质上是在“把检索问题甩给 LLM”。适用于“代码规模小到可以整份交给模型”的场景。
中型代码库(1K~10K 行):固定行数切分加上合理的 overlap 通常已经足够,而且实现起来最简单(不需要解析 AST)。关键在于,让 chunk 大小与函数的平均长度相匹配——最好让大多数函数都能完整地落入一个 chunk,再用 overlap 作为边界安全网。
大型代码库(> 10K 行):到了这个规模,AST 函数级切分在 precision 上的优势才真正显现。代码太大,无法全部放进上下文时,你必须精确定位“就是这个函数”。而每个 AST chunk 都是一个完整、独立、可以直接跳转到的语义单元。这个实验的数据集太小,完全掩盖了 AST 的核心优势。
一句话总结:Recall 看起来很亮眼,但在生产环境中,真正决定体验的是 precision——而 precision 恰恰是这个小型实验无法衡量的指标。
那么,对于大型项目,正确答案是什么?不是从这三种策略中挑一个,而是使用 AST chunking 保证 precision,再通过 metadata 和 call graph 主动恢复搭便车效应。
回想一下 AST 输在哪里:它把 calculate_order_total 切成了一座孤岛,丢掉了“它与 create_payment_intent 同属于一个 payment 流程”这层上下文。大 chunk 依靠物理位置上的相邻,碰巧保留了这层上下文。我们可以显式地把它补回来:
{
"content": func_body, # complete function body from AST, fed to embedding
"metadata": {
"name": "calculate_order_total",
"module": "payment", # module name: enables per-module filtering
"file": "services/payment.py",
"line_start": 42,
"line_end": 55,
"called_by": ["create_payment_intent"], # call graph: who calls me
"calls": ["get_tax_rate", "apply_discount"],
}
}
有了 called_by 这条边,检索流程就可以这样恢复:
查询 process payment and create Stripe charge;向量搜索命中 create_payment_intent。
沿着 call graph 继续查找,发现 create_payment_intent 调用了 calculate_order_total。
将 calculate_order_total 一并召回。
这种方式利用 call graph 中确定性的结构关系,主动复现大 chunk 只能靠运气获得的搭便车效应,同时又不牺牲 AST 的 precision。那些语义相关、但在向量空间中相距遥远的函数,可以通过调用关系重新被拉回来。
这正是向量检索的能力边界:calculate_order_total 与“Stripe charge”之间的语义鸿沟,无法通过任何 Embedding 策略弥合(文章 03 已经验证了这一结论)。要跨越这道鸿沟,你需要利用代码自身的结构信息——call graph、imports 和模块归属关系。
而这正是本系列下一篇文章的主题:向量检索与知识图谱。当语义相似度失效时,代码的结构关系就是救命稻草。
反直觉的结果是:AST 函数级切分的 Recall@5 最低(0.958);固定行数和文件级切分都得到了完美的 1.000。但这场“失败”只是假象。
决定结果的是搭便车效应。在唯一出现差异的查询(Q8)中,calculate_order_total 在语义上距离查询很远;AST 把它切成一座孤岛,因此没能命中,而大 chunk 把它与 create_payment_intent 捆在一起,让它搭便车回到了检索结果中。
文件级切分的满分是在作弊——它的 precision 为零。由于只有 1 个 chunk,它无法精确定位某个具体函数;本质上是在把检索问题甩给 LLM。
固定行数切分的满分则是巧合。数据集中的函数短小紧凑,碰巧与 chunk 大小匹配。放到函数动辄上百行的大型项目中,固定行数切分会把函数撕成碎片。
规模决定策略选择:小型项目使用文件级切分,中型项目使用固定行数切分加 overlap,大型项目使用 AST。这个 266 行代码的实验规模太小,掩盖了真正的 precision 差异。
最佳实践:AST 函数级切分 + metadata + call graph。利用 call graph 中确定性的结构关系,主动恢复大 chunk 只能靠运气获得的搭便车效应,同时保留 AST 的 precision。
完整演示代码:codebase-kb-04-chunking
欢迎了解 PrimeSkills——一个经过精心筛选的 AI Agent 和 Skill 市场,其中的产品都已在真实的企业级工作流中得到验证。没有华而不实的噱头,只有真正有效的工具。
你可以在我的 Homepage 上发现更多实用知识和有趣产品。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。