为什么代码不能只按固定 token 切块?

第78题:为什么代码不能只按固定 token 切块?

在这里插入图片描述

1. 核心回答

代码 RAG 不能只依赖固定 token 数量切块,核心原因是:token 长度只描述文本长度,不描述代码的语法边界和程序依赖关系。

例如每 512 token 强制切一次,切割位置可能正好落在:

  • 一个函数内部;
  • 函数签名与函数体之间;
  • if/elsetry/catch 等控制结构内部;
  • 类定义与成员方法之间;
  • 调用方和被调用函数之间;
  • source 和 sink 之间。

这样得到的 chunk 在文本长度上满足要求,但可能缺少完成代码理解所需的上下文。

因此,更合理的方案是:

使用 AST / symbol boundary 构造语义相对完整的基础 chunk,再通过 call graph、data flow、父级作用域和跨文件依赖补充相关上下文,同时保留 token 上限控制。


2. 固定 token 切块为什么会破坏语法完整性

假设代码如下:

def verify_user(user):
    if user.is_admin:
        return check_permission(user)
    return False

如果固定 token 边界恰好把代码切成:

Chunk A:

def verify_user(user):
    if user.is_admin:

Chunk B:

return check_permission(user)
return False

两个 chunk 单独看都缺少完整语义。

Chunk A 不知道最终执行了什么操作。

Chunk B 缺少:

  • 函数名称;
  • 参数;
  • 条件判断;
  • 所属作用域。

Embedding 模型得到的表示也会受到影响,因为输入本身已经失去完整的程序结构。

Tree-sitter 等解析器可以把源代码解析成语法树,并识别:

  • function;
  • method;
  • class;
  • statement;
  • expression;
  • call expression;

等结构。

因此可以优先在这些自然语法边界上切分。


3. 为什么只按函数切块也不够

进一步使用:

一个函数 = 一个 chunk

能够解决一部分语法完整性问题,但仍然存在跨函数依赖。

例如:

def handle_request(req):
    data = req.get("cmd")
    execute(data)

另一个文件中:

def execute(cmd):
    os.system(cmd)

如果只检索 handle_request(),模型看到:

execute(data)

但不知道 execute() 最终调用了:

os.system(cmd)

如果只检索 execute(),模型又不知道参数 cmd 来自外部请求。

安全分析真正关心的可能是:

HTTP Request
    ↓
req.get("cmd")
    ↓
execute(data)
    ↓
os.system(cmd)

也就是:

Source→Propagation→Sink Source \rightarrow Propagation \rightarrow Sink SourcePropagationSink

这条数据流跨越了多个函数。

因此函数边界可以作为基础切块单位,但还需要保存函数之间的关系。


4. 漏洞检测为什么特别依赖跨块关系

很多漏洞需要联合多个代码位置才能判断。

例如命令注入、SQL 注入、路径遍历等问题通常需要同时判断:

  1. 数据从哪里进入;
  2. 中间经过哪些函数;
  3. 是否经过校验或 Sanitizer;
  4. 最终进入什么危险操作。

可以抽象成:

Source→Transform→Sanitizer?→Sink Source \rightarrow Transform \rightarrow Sanitizer? \rightarrow Sink SourceTransformSanitizer?Sink

如果:

Source

和:

Sink

位于不同函数甚至不同文件,固定 token chunk 很容易把证据分散到不同检索单元。

CodeQL 的安全分析也明确区分:

  • Local Data Flow;
  • Global Data Flow;
  • Taint Tracking;
  • Inter-procedural Data Flow。

局部数据流主要考虑单个函数内部。

全局数据流需要继续跨函数、调用和对象属性追踪值的传播。

这说明代码安全分析中的很多语义天然具有跨 chunk 特征。


5. 代码还有哪些跨文件依赖

除了数据流,还需要考虑:

5.1 调用关系

例如:

controller.py
    ↓
service.py
    ↓
repository.py

当前代码的真实语义可能依赖调用链上的其他函数。

5.2 类型定义

例如代码中出现:

user.role

理解 role 的含义可能需要找到:

class User

的定义。

5.3 Import 和依赖

例如:

from auth import verify

真正的行为由 auth.verify() 决定。

5.4 配置和常量

例如:

if mode == ADMIN_MODE:

需要找到:

ADMIN_MODE = ...

才能确定条件含义。

5.5 接口实现

例如:

storage.save(data)

实际调用对象可能由运行时类型决定。

因此代码检索需要同时保存:

  • symbol definition;
  • reference;
  • caller-callee;
  • import;
  • inheritance;
  • type relation;
  • data flow;

等关系。


6. 更合理的代码切块方法

我会采用一种结构感知的层级切块策略

首先解析代码:

Source Code
    ↓
Parser / Tree-sitter
    ↓
AST / Symbol Table

然后识别:

  • class;
  • function;
  • method;
  • declaration;
  • module;
  • import。

基础 chunk 可以优先按照完整 symbol 构造。

例如:

Chunk 1 = verify_user()
Chunk 2 = execute()
Chunk 3 = User class

同时为每个 chunk 保存 metadata:

repository
file_path
language
class_name
function_name
start_line
end_line
parent_symbol
imports
callers
callees
version

这样检索到一个函数之后,可以继续沿结构关系扩展上下文。


7. 函数超过模型长度怎么办

结构边界也不能完全取代 token 限制。

如果一个函数有 5000 token,而检索系统的目标 chunk 大小只有 512 或 1024 token,就需要继续拆分。

可以采用:

AST Boundary+Token Budget AST\ Boundary + Token\ Budget AST Boundary+Token Budget

的组合策略。

例如首先获得:

Function
    ├── initialization
    ├── validation
    ├── processing loop
    └── return

然后在 AST statement/block 边界上继续递归拆分。

最终满足:

∣chunki∣≤Lmax⁡ |chunk_i|\leq L_{\max} chunkiLmax

同时尽量保持完整的语法结构。

因此实际目标是:

在 token 长度约束下最大程度保持代码语义完整。


8. 为什么 chunk 也不能无限大

把整个文件甚至整个仓库放入一个 chunk 同样会产生问题。

Chunk 很大时通常会增加:

  • Embedding 中无关信息;
  • 检索噪声;
  • 上下文 token 成本;
  • 推理延迟;
  • reranking 成本。

例如一个 3000 行文件中只有一个函数与查询相关。

如果整个文件作为一个 retrieval unit:

relevant code = 50 lines
irrelevant code = 2950 lines

检索虽然命中了正确文件,但生成模型收到大量无关内容。

因此存在一个权衡:

Chunk Too Small⇒Context Incomplete Chunk\ Too\ Small \Rightarrow Context\ Incomplete Chunk Too SmallContext Incomplete

Chunk Too Large⇒Noise+Cost Chunk\ Too\ Large \Rightarrow Noise + Cost Chunk Too LargeNoise+Cost

合理 chunk 应位于两者之间。


9. 一个实用的层级检索方案

我会设计两级甚至三级结构。

9.1 第一级:小粒度检索

索引:

  • function;
  • method;
  • class;
  • declaration。

使用 BM25 或 Embedding 找到候选 symbol。

例如:

Query
    ↓
Top-K functions

这一层重点提高检索精度。

9.2 第二级:结构扩展

找到目标函数后,根据代码关系扩展:

Current Function
    ├── Caller
    ├── Callee
    ├── Definition
    ├── Type
    └── Data-flow Neighbor

例如检索到了:

execute(cmd)

系统继续加入:

handle_request()
sanitize()
os.system()

从而恢复完整调用和数据流上下文。

9.3 第三级:上下文装配

最终根据 token budget 选择最有价值的证据:

C={c1,c2,…,ck} C= \{c_1,c_2,\ldots,c_k\} C={c1,c2,,ck}

满足:

∑i=1k∣ci∣≤ContextBudget \sum_{i=1}^{k}|c_i| \leq ContextBudget i=1kciContextBudget

优先保留:

  • 当前 symbol;
  • 直接 caller/callee;
  • source-sink 路径;
  • 关键定义;
  • 必要配置。

这样检索和生成使用的上下文可以采用不同粒度。


10. AST、Call Graph 和 Data Flow 分别解决什么问题

三者解决的问题不同。

结构主要解决的问题
AST当前代码的语法结构
Symbol Table定义与引用
Call Graph函数之间谁调用谁
CFG一个函数内部控制如何流转
DFG / Data Flow数据从哪里来、流向哪里
Taint Flow不可信数据是否流到危险位置

例如固定 token chunk 只能看到:

A B C D E

AST 能告诉系统:

Function
    ├── Condition
    └── Call

Call Graph 能告诉系统:

Function A
    ↓
Function B

Data Flow 能告诉系统:

user_input
    ↓
cmd
    ↓
system()

所以对于代码理解和漏洞检测,结构信息具有直接价值。


11. 结构化切块一定比固定 token 切块好吗

这个结论仍然需要实验验证。

近期针对 Retrieval-Augmented Code Completion 的受控实验比较了:

  • Function chunking;
  • Declaration chunking;
  • Sliding Window;
  • cAST。

实验表明 chunking strategy 会显著影响最终结果。

同时有一个很重要的发现:

Function chunking 在该实验的 RepoEval 设置中并没有取得最优结果。

Sliding Window 和基于 AST 的 cAST 在成本—质量权衡上表现更好。

因此工程上不应该直接规定:

function chunking = 最优方案

更合理的方法是建立候选策略:

Fixed Token / Sliding Window
Function
Declaration
AST-aware
Hierarchical

然后在自己的任务数据上比较。


12. 怎么通过实验选择 chunk 策略

我会固定:

  • Retriever;
  • Embedding Model;
  • Generator;
  • Top-K;
  • Context Budget;
  • 数据集;
  • Query。

只改变 chunking strategy。

然后首先看检索指标:

12.1 Recall@K

$$
Recall@K

\frac{
\text{Top-K 中找到的相关证据}
}{
\text{全部相关证据}
}
$$

12.2 MRR

如果最关键证据越早出现,MRR 越高。

12.3 Context Precision

检查放入上下文中的 chunk 有多少真正相关。

随后还要看端到端指标,例如:

  • 漏洞检测 Recall / Precision / F1;
  • 代码生成 Pass@1;
  • Patch Success Rate;
  • 解释正确率。

同时报告系统成本:

  • 平均 chunk 数;
  • 索引大小;
  • Embedding 成本;
  • Retrieval latency;
  • Prompt tokens;
  • End-to-end latency。

最终依据:

$$
Quality

Cost
$$

的权衡选择方案。


13. 一个针对漏洞检测的具体例子

假设存在:

request.py

def handle(req):
    cmd = req["cmd"]
    run(cmd)

以及:

shell.py

def run(x):
    os.system(x)

固定 token 切块可能形成:

Chunk A:
cmd = req["cmd"]

Chunk B:
run(cmd)

Chunk C:
def run(x):

Chunk D:
os.system(x)

单独检索任何一个 chunk,都可能不足以证明命令注入。

结构化系统可以先识别:

req["cmd"] req["cmd"] req["cmd"]

是 Source。

再根据:

handle()→run() handle() \rightarrow run() handle()run()

的调用关系连接两个函数。

继续根据 Data Flow 建立:

req["cmd"]→cmd→x→os.system(x) req["cmd"] \rightarrow cmd \rightarrow x \rightarrow os.system(x) req["cmd"]cmdxos.system(x)

最终得到完整证据链。

这种情况下,chunk 的作用是提供可检索的局部代码单元;程序结构负责重新连接这些局部单元。


14. 面试时可以压缩成下面这段

代码不能只按固定 token 数切块,因为 token 边界和程序语义边界没有必然关系。固定长度切分可能把一个函数、控制结构甚至 source-sink 数据流直接截断,导致每个 chunk 都缺少判断所需的上下文。

我的做法会先用 Tree-sitter 之类的 Parser 获得 AST 和 symbol boundary,以函数、类、方法、声明等结构作为基础检索单元。对于过大的 symbol,再在 AST statement 或 block 边界内按照 token budget 递归切分。同时给 chunk 保存 caller、callee、definition、import、data-flow 和文件版本等 metadata。

检索阶段先用小粒度 chunk 做匹配,再根据 call graph、data flow 和父级作用域扩展上下文。这样可以兼顾检索精度和代码语义完整性。

同时我不会预设 AST 或函数切块一定最好。我会固定 Retriever、Embedding、Generator 和 Context Budget,对比 Sliding Window、Function、Declaration 和 AST-aware chunking,使用 Recall@K、MRR、端到端任务指标、延迟和 token 成本共同选择最终策略。


15. 来源

  1. Tree-sitter Documentation — Tree-sitter 可以将源码解析为 concrete syntax tree,并通过 syntax node 表示函数、表达式等程序结构。
  2. Guo et al., GraphCodeBERT: Pre-training Code Representations with Data Flow, ICLR 2021 — 指出单纯把代码视为 token 序列会忽略代码固有结构,并通过 data flow 建模变量之间的语义关系。
  3. Zhang et al., RepoCoder: Repository-Level Code Completion Through Iterative Retrieval and Generation, 2023 — 说明 repository-level 任务需要利用分散在不同文件中的相关上下文。
  4. Cheng et al., Dataflow-Guided Retrieval Augmentation for Repository-Level Code Completion (DraCo), 2024 — 使用代码实体和扩展数据流建立 repository-specific context graph,用于跨文件上下文检索。
  5. Zhang et al., cAST: Enhancing Code Retrieval-Augmented Generation with Structural Chunking via Abstract Syntax Tree, 2025 — 研究 AST-aware structural chunking,并报告其对代码检索和生成的提升。
  6. Wu et al., How Does Chunking Affect Retrieval-Augmented Code Completion? A Controlled Empirical Study, 2026 — 系统比较 Function、Declaration、Sliding Window 和 cAST,表明 chunking 策略显著影响代码 RAG,且不存在“函数切块天然最优”的普遍结论。
  7. CodeQL Documentation — Data Flow Analysis / Path Queries — 说明 Local Data Flow、Global Data Flow、Taint Tracking 以及 source-to-sink 路径分析在安全分析中的作用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白羊丨

开始面试题与解析

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值