第121题:Chunk 按函数切会丢失跨过程流;按文件切会噪声过大,你如何建模层级?

1. 核心回答
我会把 Retrieval Granularity 和 Generation Granularity 分开设计。
核心原则是:
小粒度 Chunk 负责精准召回,结构关系负责跨过程扩展,较大的上下文单元负责最终推理。
具体可以建成:
Repository
└── Module / Package
└── File
└── Class / Struct
└── Function / Method
└── Statement / AST Subtree
同时建立横向结构边:
Call Graph
Data Flow
Import
Inheritance
Reference / Definition
因此最终检索流程是:
先召回 Function / Symbol
↓
沿 Call Graph / DFG 等结构关系扩展
↓
必要时提升到 Parent File / Class
↓
在固定 Token Budget 内重排
↓
组装最终 Context
这样能够同时控制两个问题:
- Function Chunk 太小导致跨过程证据丢失;
- File Chunk 太大导致无关代码挤占上下文。
2. 为什么固定 Token Chunk 不适合代码
普通文本 RAG 经常使用:
每 512 Token 切一块
但代码具有明确语法边界。
例如:
def check_permission(...):
...
如果正好从中间切开:
Chunk A:
函数签名 + 前半部分
Chunk B:
后半部分
就可能丢失:
- 函数签名;
- 参数含义;
- Return;
- Control Flow;
- 调用关系。
安全代码分析中问题更明显。
例如一条完整漏洞链是:
HTTP Input
↓
Controller
↓
Service
↓
DAO
↓
SQL Execution
固定 Token Chunk 很容易把这条链拆成多个互不关联的文本块。
所以代码切分首先应该尊重:
Symbol / AST Boundary
。
3. 为什么只按 Function 切也不够
函数是一个很好的基本 Retrieval Unit。
但很多漏洞具有跨过程性质。
例如:
def handler(request):
name = request.args["name"]
query_user(name)
另一个文件:
def query_user(name):
sql = "SELECT * FROM users WHERE name='" + name + "'"
execute(sql)
如果只看:
query_user()
可以看到:
SQL Sink
但可能不知道:
name
是否真的由攻击者控制。
如果只看:
handler()
又看不到最终危险 Sink。
完整证据是:
Source
request.args["name"]
↓ interprocedural propagation
query_user(name)
↓
Sink
execute(sql)
所以 Function Chunk 适合作为召回入口,但跨过程安全分析还需要结构扩展。
4. 为什么整个 File 作为 Chunk 又太大
假设一个文件有:
2000 LOC
30 个函数
10 个类
大量常量和 Helper
当前 Query 只关心:
parse_request()
把整个文件放入 Context 会产生:
Relevant Information≪Total Context Relevant\ Information \ll Total\ Context Relevant Information≪Total Context
结果可能是:
- Embedding 被大量无关代码稀释;
- Retriever 排名下降;
- LLM 注意力被无关代码占用;
- Token 成本提高;
- 真正漏洞证据被挤出 Context Window。
因此文件适合作为:
Parent Context
通常不适合作为默认最小检索单元。
5. 我会设计三层核心结构
5.1 Leaf Layer:Symbol Chunk
基本 Chunk 以代码符号为单位:
Function
Method
Constructor
Class
Struct
Interface
Macro
Global Definition
每一个 Chunk 保存:
chunk_id
repository_id
file_id
symbol_id
symbol_type
qualified_name
signature
start_line
end_line
language
content
例如:
symbol:
UserDAO.query_user
file:
dao/user.py
type:
function
Leaf Chunk 主要用于:
BM25 / Dense Retrieval
。
5.2 Structural Layer:Relationship Graph
每个 Symbol 之间建立结构关系。
例如:
CALLS
CALLED_BY
IMPORTS
DEFINES
REFERENCES
INHERITS
OVERRIDES
DATA_FLOWS_TO
形成:
G=(V,E) G=(V,E) G=(V,E)
其中:
V=Code Symbols V=\text{Code Symbols} V=Code Symbols
E=Structural Relations E=\text{Structural Relations} E=Structural Relations
例如:
handler()
│ CALLS
▼
validate()
│ CALLS
▼
query_user()
│ DATA_FLOW
▼
execute()
这一层负责恢复函数 Chunk 切分以后丢失的跨过程关系。
5.3 Parent Layer:File / Class / Module Context
再建立 Parent Relationship:
Function
↓
Class
↓
File
↓
Module
↓
Repository
例如:
chunk_id = func_123
parent_file = file_17
parent_class = UserDAO
module = database
当一个 Leaf Chunk 缺乏局部定义时,可以向父级扩展。
例如函数中出现:
self.config.db_url
可以进一步补入:
Class Fields
Relevant Imports
Config Definition
而不需要把整个仓库全部加入 Context。
6. 一个完整的层级索引可以这样画
Repository
│
├── File A
│ ├── Function A1
│ ├── Function A2
│ └── Function A3
│
├── File B
│ ├── Function B1
│ └── Function B2
│
└── File C
└── Function C1
Structural Edges:
A1 ──CALLS──────► B2
B2 ──CALLS──────► C1
A2 ──DATAFLOW───► B1
B1 ──INHERITS───► C1
于是系统同时拥有:
Tree Hierarchy
+
Graph Relations
。
Tree 表达代码组织关系。
Graph 表达程序语义关系。
7. 查询时第一步仍然召回小块
假设 Query 是:
这段代码是否存在命令注入?
首先使用:
BM25
Dense Retrieval
Hybrid Retrieval
找到 Top-K Leaf Chunk:
func_A
func_B
func_C
...
这个阶段使用小 Chunk 的好处是匹配更加精准。
例如:
subprocess.Popen
os.system
exec
shell=True
这类安全相关 symbol 不会被整个大型文件中的无关内容严重稀释。
8. 第二步再做结构扩展
假设首先命中:
def run_command(cmd):
os.system(cmd)
只看它无法确定:
cmd 是否攻击者可控
此时根据:
CALLED_BY
向上扩展:
run_command()
↑
process_request()
↑
HTTP Handler
如果存在 Data Flow Edge,则优先沿:
Source
→
Propagation
→
Sink
扩展。
最终 Context 可能从:
1 个函数
扩展成:
Source Function
+
Intermediate Function
+
Sink Function
这比直接放整个文件更加聚焦。
9. 对漏洞检测,我会优先做 Evidence-oriented Expansion
普通代码补全关注:
什么代码有助于生成下一段?
漏洞检测更关心:
什么代码能够证明或反驳漏洞成立?
因此扩展优先级可以设计成:
Data-flow neighbor
>
Direct caller/callee
>
Definition/reference
>
Class context
>
File context
>
Generic semantic neighbor
例如 Injection 类型漏洞优先寻找:
Source
Sanitizer
Sink
Propagator
生命周期问题优先寻找:
Allocation
Ownership
Release
Alias
Use
权限问题优先寻找:
Sensitive Operation
Authorization Check
Caller Identity
所以扩展策略应该与当前安全假设相关。
10. 检索分数可以同时考虑语义、结构和成本
例如对候选 Chunk ccc 定义:
$$
Score©
\alpha S_{dense}©
+
\beta S_{lexical}©
+
\gamma S_{graph}©
+
\delta S_{evidence}©
\lambda Cost©
$$
其中:
- SdenseS_{dense}Sdense:语义相关性;
- SlexicalS_{lexical}Slexical:BM25/identifier 精确匹配;
- SgraphS_{graph}Sgraph:与命中节点的图距离;
- SevidenceS_{evidence}Sevidence:是否补全当前漏洞证据;
- CostCostCost:加入该 Chunk 消耗的 Token。
例如:
Caller 距离 = 1
可以获得较高 Graph Score。
一个完全无关但语义相似的函数即使 Dense Score 较高,也可能因为结构距离远而降低最终优先级。
11. 最终 Context Assembly 需要受 Token Budget 约束
假设模型允许使用:
B=16K B=16K B=16K
Token。
不能无限沿 Call Graph 扩展。
可以定义:
Query / Target Code:
4K
Direct Evidence:
6K
Structural Expansion:
4K
Metadata / Prompt:
2K
具体比例应通过验证集决定。
选择过程可以理解为:
max∑iUtility(ci) \max \sum_i Utility(c_i) maxi∑Utility(ci)
满足:
∑iTokens(ci)≤B \sum_i Tokens(c_i)\le B i∑Tokens(ci)≤B
即在有限上下文中选择证据价值最高的 Chunk。
12. 小块负责 Retrieval,大块负责 Comprehension
这一点是整个设计的核心。
我会显式区分两个 Context。
12.1 Retrieval Context
用于算 Embedding / BM25。
尽量紧凑:
Function Signature
Docstring
Function Body
Key Metadata
目的:
精准找到候选
。
12.2 Comprehension Context
候选被检索以后扩展:
Function
+
Caller
+
Callee
+
Relevant Definition
+
Data-flow Neighbor
+
Necessary Parent Context
目的:
让模型完成推理
。
因此同一个代码节点可以具有:
Compact Retrieval Representation
和:
Expanded Generation Representation
。
13. Parent File 不应该默认全部展开
如果命中:
func_A
不应立刻:
include entire file
更合理的是逐级提升。
例如:
Level 0:
func_A
Level 1:
func_A + imports + signature dependencies
Level 2:
func_A + relevant class members
Level 3:
func_A + relevant file symbols
Level 4:
whole file
只有当前证据不足且 Token Budget 允许时继续扩展。
这样可以避免:
Parent Expansion
→
重新退化成 File Chunk
。
14. 超长函数怎么办
Function Chunk 本身也可能超过预算。
例如:
一个函数 3000 行
此时进一步按照语法结构拆。
例如:
Function
├── Initialization
├── If Branch
├── Loop
├── Try/Catch
└── Return
可以基于:
AST Subtree
Statement Block
Basic Block
建立子 Chunk。
但每个子 Chunk 必须继续保存:
parent_function_id
function_signature
local_scope
start/end position
这样召回子 Chunk 后能够重新恢复其父函数上下文。
15. AST / Symbol 边界为什么比纯 Token 边界更合理
代码解析工具可以得到:
function_definition
class_definition
call_expression
if_statement
assignment
等结构节点。
所以可以将:
Function
Method
Class
作为稳定的语义边界。
如果节点超过 Token Budget,再递归进入:
child AST node
。
形成:
File
↓
Function
↓
Statement Block
↓
Expression
的多粒度结构。
这比:
每 512 Token 截断
更容易保持程序结构。
16. 跨过程 Source-Sink 怎么组装
假设已经识别:
Source = f1
Sink = f4
并存在:
f1
→
f2
→
f3
→
f4
我不会机械地加入:
四个函数的所有代码
。
可以优先抽取:
f1:
Source 附近语句
f2:
参数接收 + 关键传播
f3:
Sanitizer / Transformation
f4:
Sink 附近语句
然后保留调用关系:
f1 → f2 → f3 → f4
最终 Context 更接近:
Evidence Path
而不是若干彼此独立的文件片段。
17. 一个安全场景例子
假设仓库中存在:
# controller.py
def handle(req):
username = req.args["name"]
service.find_user(username)
# service.py
def find_user(name):
dao.query(name)
# dao.py
def query(name):
sql = "select * from users where name='" + name + "'"
db.execute(sql)
Function Retrieval 首先可能命中:
dao.query()
因为它与 SQL Injection 最相关。
随后 Structural Expansion 查询:
caller(query)
得到:
service.find_user()
继续得到:
controller.handle()
最终恢复:
request.args
↓
find_user
↓
query
↓
db.execute
完整 Source-Sink Evidence 才进入模型。
18. 怎样验证这种层级方案确实有效
不能只展示 Architecture。
至少比较:
A. Fixed Token Chunk
B. Function Chunk
C. File Chunk
D. Function + Parent Expansion
E. Function + Call Graph Expansion
F. Function + Data-flow / Structural Expansion
G. Hierarchical Full Method
所有实验固定:
- Dataset;
- Split;
- Retriever;
- Generator;
- Token Budget;
- Evaluation Metric。
这样才能隔离 Chunking / Expansion 本身的贡献。
19. 检索层应该测什么
首先测普通 Retrieval 指标:
Recall@K Recall@K Recall@K
MRR MRR MRR
NDCG@K NDCG@K NDCG@K
但安全任务还应该测:
19.1 Evidence Recall
Ground Truth 需要的证据节点有多少被召回。
例如:
$$
EvidenceRecall
\frac{
|\text{Retrieved Evidence}\cap\text{Required Evidence}|
}{
|\text{Required Evidence}|
}
$$
19.2 Path Coverage
对于 Source-Sink 类型漏洞:
Source
→
Intermediate
→
Sink
是否完整覆盖。
可以定义:
$$
PathCoverage
\frac{
\text{被 Context 覆盖的关键 Path Node}
}{
\text{Ground Truth Path Node}
}
$$
这样能够直接判断:
函数级 Chunk 是否真的因为缺少跨过程证据而失败。
20. 端到端还必须继续测
最终还要比较:
- Vulnerability Recall;
- Precision;
- F1;
- Localization Accuracy;
- Evidence Correctness;
- False Positive;
- Context Tokens;
- Retrieval Latency;
- Generation Latency;
- Cost。
例如可能出现:
Hierarchical Retrieval
Recall@20 ↑
但:
End-to-End F1 ≈
这说明 Retriever 已经找到证据,Generator 没有有效利用。
此时继续扩大 Chunk 已经没有意义。
21. Oracle Context 可以定位瓶颈
再加入:
Oracle Context
人工给模型完整 Ground Truth Evidence。
得到:
Moracle M_{oracle} Moracle
与真实 Retrieval:
Mretrieval M_{retrieval} Mretrieval
比较。
如果:
Moracle≫Mretrieval M_{oracle}\gg M_{retrieval} Moracle≫Mretrieval
说明主要瓶颈位于:
Retrieval / Context Assembly
。
如果:
Moracle≈Mretrieval M_{oracle}\approx M_{retrieval} Moracle≈Mretrieval
但整体效果仍低,瓶颈更可能在:
Generator / Reasoning
。
这样能够防止把所有问题都归因于 Chunk Size。
22. Chunk 大小应该通过消融决定
这里没有一个跨任务固定的最佳值。
Chunk 太小:
语义不完整
跨过程关系丢失
调用背景不足
Chunk 太大:
噪声增加
Embedding 被稀释
Token Cost 增加
有用证据被挤压
所以应该画:
Performance=f(ChunkSize) Performance=f(ChunkSize) Performance=f(ChunkSize)
以及:
Cost=g(ChunkSize) Cost=g(ChunkSize) Cost=g(ChunkSize)
最后在:
Retrieval Quality
End-to-End Quality
Latency
Token Cost
之间选择 Pareto 合理点。
23. 结构本身也存在失败边界
层级检索并不能保证完整恢复程序语义。
例如:
- Dynamic Dispatch;
- Reflection;
- Function Pointer;
- Runtime Dependency Injection;
- Native Call;
- Missing Library;
- Incomplete Call Graph;
- Incomplete DFG。
都可能让结构边缺失或包含假边。
因此 Graph Edge 应保存:
edge_type
analysis_method
confidence
并允许:
Lexical / Dense Retrieval
作为补充通道。
不能把静态 Call Graph 或 DFG 直接当作完整 Ground Truth。
24. 面试时可以压缩成下面这段
我会把检索粒度和生成粒度分开。
底层先按照 Function、Method、Class 等 Symbol/AST Boundary 建小 Chunk,因为小 Chunk 更适合 BM25 和 Dense Retrieval;每个 Chunk 同时保留 parent file、class、module,以及 caller/callee、import、inheritance、data-flow 等结构边。
Query 到来后,先召回 Function-level Leaf Chunk,然后根据任务进行结构扩展。对于漏洞检测,我会优先沿 source-sink、caller/callee 和 definition/reference 扩展,只把与当前安全证据相关的邻居加入 Context;如果仍缺少定义,再逐级向 Class 或 File Parent 提升。这样小 Chunk 负责精准匹配,扩展后的 Context 负责跨过程推理。
整个扩展过程受固定 Token Budget 控制。超长函数则继续按 AST Subtree 或 Statement Block 切分,但保留 parent function 信息。
实验上我会比较 fixed-token、function、file、function+parent、function+graph 和完整 hierarchical retrieval,在相同 Generator 和 Token Budget 下同时报告 Recall@K、source-sink path coverage、最终漏洞检测 F1、Context Token 和 Latency。
如果 Oracle Context 明显优于真实检索,说明问题主要在 Retrieval/Context Assembly;如果 Oracle 也没有提高,则需要检查 Generator。这样能够真正判断层级 Chunking 是否解决了跨过程证据缺失,而不只是增加了更多上下文。
25. 当前资料能够支持到什么程度
源文件明确支持以下设计:
- 固定 Token Chunk 可能切断函数、调用链和 Source-Sink 证据;
- 代码优先按照 Symbol / AST Boundary 建立小块;
- 保留 Parent File、Call Graph、Data Flow 和 Version Metadata;
- 小块用于匹配;
- 大块或邻接扩展用于最终生成上下文;
- Chunk 过小会缺语义;
- Chunk 过大会增加噪声和成本;
- 必须同时通过 Retrieval Coverage 和 End-to-End Result 做消融。
当前材料没有提供这位候选人真实系统中的:
具体 Chunk Token 数
AST Parser
Graph Construction Algorithm
Expansion Hop
Token Budget
Recall@K
Path Coverage
最终 F1
因此正式面试中不能声称某个具体层级设计已经经过实验验证。
如果实验尚未完成,应该把:
fixed-token
function
file
hierarchical expansion
oracle context
列为待补消融。
26. 来源
- 源文件第121题:Symbol/AST 小块、Parent File、Call Graph、Data Flow、邻接扩展以及 Chunk Size 消融。
- Tree-sitter Documentation:可以将源代码解析成带层级、节点类型和源码位置的语法树,为 Symbol/AST Boundary 切分提供基础。
- Zhang et al., RepoCoder: Repository-Level Code Completion Through Iterative Retrieval and Generation, EMNLP 2023:说明 repository-level 有用上下文会分散在不同文件中,并采用跨仓库片段检索增强代码模型。
- Cheng et al., Dataflow-Guided Retrieval Augmentation for Repository-Level Code Completion (DraCo), ACL 2024:将代码解析成实体,并通过扩展数据流关系构造 repository-specific context graph,再据此检索相关跨文件上下文。
- Oh & Lee, Late Code Chunking, ACL 2026:区分面向检索的 Code Retrieval Context 与检索后扩展的 Code Comprehension Context,用于降低 Chunking 导致的结构/行为语义损失。
- Shi et al., AIRCoder, ACL 2026:将 textual similarity、dependency existence 和 structural hierarchy 作为互补检索维度,并使用 structure-preserving chunking。
- GitHub CodeQL Documentation, Exploring Data Flow with Path Queries:Path Query 将漏洞数据流表示为 Source 到 Sink 的一系列传播步骤,可作为安全场景中结构化 Evidence Path 的参考。

418

被折叠的 条评论
为什么被折叠?



