在这里插入图片描述

当你的RAG系统把整份代码文件生吞活剥,AI生成的代码就像拆盲盒——永远不知道下一句是宝藏还是灾难。代码不是散文,它有函数、有类、有模块的钢铁骨架。本文将带你穿透这些语法边界,手把手拆解如何按函数、类和模块对代码文件进行科学切块,让你的生成式AI真正“读懂”程序结构,从此告别文不对题、张冠李戴的AI生成代码,把RAG的召回精度拉满。

代码文件切块进阶策略

痛点:整文件喂给RAG的灾难

函数级:原子化切割与召回

类级:面向对象完整性保护

模块级:架构宏观视角保留

跨层级:关联图谱与链路检索

实战:Chunk参数与元数据设计

文字目录

  • 痛点:整文件喂给RAG的灾难
  • 函数级:原子化切割与召回
  • 类级:面向对象完整性保护
  • 模块级:架构宏观视角保留
  • 跨层级:关联图谱与链路检索
  • 实战:Chunk参数与元数据设计

嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《大模型RAG生成式AI开发实战》126.[第13章 文档切块进阶] 代码文件切块:按函数、类和模块分割

俗话说,“饭要一口一口吃,代码要一行一行敲”。但在RAG这桌大餐面前,很多新手却恨不得把整个代码仓库像倒饺子馅一样,哗啦一下全倒进向量数据库里。结果呢?AI检索出来的代码块,上半身是个函数定义,下半身是个类声明,中间还夹杂着两行毫不相干的import。你把这玩意儿喂给大模型,它生成的代码不翻车才怪!今天这堂课,咱们就把代码切块这件事掰开了、揉碎了讲清楚。别让错误的切块姿势,毁了你辛辛苦苦搭起来的RAG系统。

1. 别再整文件硬塞进RAG了——代码切块的必要性

兄弟们,咱们先回到最底层的问题:为什么代码文件不能像普通文本那样,直接按字符数一刀切?

普通的博客文章、产品说明书,确实可以按固定长度切。因为它们的语义是流式的,上一段和下一段虽然有关联,但即便在边界处断开,损失也不会致命。可代码不一样啊!代码是结构化的,它的语义单元是函数、是类、是语句块。你一刀砍在函数中间,就跟把一句话拦腰截断一样,前半句是“如果”,后半句没了,大模型看了直挠头。

更关键的是,代码里的信息密度极高。一行 import tensorflow as tf 可能就承载了整个环境依赖的上下文。你把无关的导入和一个核心业务函数塞进同一个chunk,向量在语义空间里就会被污染。这就好比把红烧肉和冰淇淋倒进同一个搅拌机,虽然都是食物,但混在一起谁都不想尝。检索的时候,相似度计算会把这种“噪音”也带进去,导致召回的精度断崖式下跌。

所以啊,代码切块的第一步,就是放弃“字符数优先”的思维,拥抱“语法边界优先”。

你是不是也经常这么干?拿到一个项目的源码,二话不说,把 .py 文件当成 .txt 往里怼,设置个 chunk_size=1000overlap=200,然后就觉得大功告成了?别笑,我当年也是这么干的。结果呢?用户问一句“怎么连接数据库”,RAG从向量库里捞出来一大段代码,里面既有 import json 这种无关紧要的导入,又有 def helper() 这种八竿子打不着的工具函数,中间还夹杂着半个 class DatabaseConfig——你说,大模型看了这玩意儿,它能不懵吗?

我见过最典型的新手配置,长这样:

# 错误的切块配置示例
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)

看起来没问题?问题大了去了。假设你有一个 utils.py,里面既有工具函数,又有配置类,还有全局常量。按1000字符一切,很可能出现下面这种惊悚场面:Chunk A里包含了 def format_date() 的后半段、class AppConfig 的前半段(刚好到 self.debug = True),以及两个无关函数的中间部分。这时候用户问:“如何开启调试模式?”向量检索把Chunk A捞了出来。大模型一看,上下文里既有日期格式化逻辑,又有半个类定义,它根本无法判断 self.debug 属于哪个类、怎么初始化。生成的代码大概率是凭空捏造一个 AppConfig 类,或者干脆告诉你“在 format_date 里设置 debug”。这不是AI笨,是你喂给它的食材,本身就是一盘大杂烩。

思维误区就在于:很多新手觉得“上下文越多越好”。在RAG里,上下文的质量远比数量重要。无关代码就是噪音,而噪音会淹没信号。

那正确的姿势是什么?在切块之前,先做一次“语法感知”的预分析。简单说,就是让AST(抽象语法树)当你的导游。对于Python,你可以用内置的 ast 模块;对于JavaScript,可以用 acorn;对于Java,可以用 JavaParser。这些工具能帮你精准定位每一个函数、每一个类、每一个模块的起止行号。

核心思路是:先按语法边界预切分,再决定入库粒度。哪怕你最终还是要控制chunk的token数,也应该保证切割点落在函数之间、类之间,而不是一句 if 语句的中间。

来看一个正确的预处理逻辑:

import ast

def split_by_syntax(source_code):
    tree = ast.parse(source_code)
    chunks = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
            start, end = node.lineno, node.end_lineno
            func_chunk = "\n".join(source_code.splitlines()[start-1:end])
            chunks.append({
                "content": func_chunk,
                "type": "function" if isinstance(node, ast.FunctionDef) else "class",
                "start_line": start,
                "end_line": end
            })
    return chunks

这样做的好处立竿见影。每个chunk的边界都是语法完整的单元,向量表征更纯净,检索时再也不会出现“半个函数”的尴尬局面。而且,你还能顺手把 typestart_line 这些元数据存下来,为后续的过滤和排序打下基础。

代码不是散文,不能拿文本切分的钝刀来砍。尊重语法边界,是代码切块的第一性原理。

2. 函数级切块——保留“最小可复用单元”的原子性

如果说代码是一座城市,那函数就是里面的独立公寓。每个函数都有自己的门牌号(函数名)、入住条件(参数)和内部装修(函数体)。在RAG检索里,用户问得最多的就是:“这个函数怎么用?”“有没有现成的工具函数可以做某某事?”所以,函数级切块,是我们整个代码切块体系的基石。

一个标准的函数chunk,应该像一颗完整的鸡蛋,蛋壳、蛋白、蛋黄都得在。这意味着什么?装饰器、async 关键字、类型注解、文档字符串(docstring)、函数签名、函数体、返回语句,一个都不能少。

我见过最离谱的切块,是把一个函数硬生生劈成两半。为啥?因为 chunk_size 设了300,而这个函数有350个token。切分器可不管你是不是函数,到300就下刀。

假设你有这样一个函数:

def calculate_discounted_price(original_price, discount_rate, member_level):
    """
    根据会员等级计算折扣价
    member_level: 1-普通, 2-银卡, 3-金卡
    """
    if original_price < 0:
        raise ValueError("原价不能为负数")
    
    base_discount = discount_rate * original_price
    
    if member_level == 3:
        extra = 0.05
    elif member_level == 2:
        extra = 0.02
    else:
        extra = 0
    
    final_price = base_discount * (1 - extra)
    return round(final_price, 2)

如果一刀切在 extra = 0.02 后面,下一个chunk开头就是 else: extra = 0。用户问:“金卡会员的额外折扣怎么算?”检索系统把第二个chunk返回给大模型。大模型一看,上下文里只有半个 if-else,根本看不到 member_level == 3 的分支,也看不到函数签名里的参数含义。它要么回答“没有金卡逻辑”,要么瞎编一个。

还有一种常见的错误,是把两个毫不相干的函数缝进一个chunk:

def connect_db(uri):
    return psycopg2.connect(uri)

def close_db(conn):
    conn.close()

这俩函数虽然主题相关,但语义上各自独立。如果用户只想了解“如何关闭连接”,另一个 connect_db 的存在就会干扰向量相似度的计算,导致检索时引入不必要的噪音。

新手往往把“代码文件”当成“代码故事”,觉得放在一起的函数就一定有强关联。其实不然,很多工具函数只是物理上邻近,逻辑上完全可以解耦。

函数级切块的金科玉律是:完整收录,独立成块。只要函数不是巨长无比(比如超过2000行的God Function),就应该把它完整地包裹进一个chunk。

函数级 Chunk 结构

装饰器
@require_auth

函数签名
def update_user...

Docstring
更新用户资料

参数与类型
user_id: int

函数体
逻辑实现

返回语句
return user

一个合格的函数chunk应该长这样:

{
    "content": "@require_auth\ndef update_user_profile(user_id: int, data: dict) -> User:\n    \"\"\"更新用户资料,返回更新后的用户对象\"\"\"\n    user = User.query.get(user_id)\n    if not user:\n        raise NotFoundError(f\"用户 {user_id} 不存在\")\n    \n    allowed_fields = ['nickname', 'avatar', 'bio']\n    for field in allowed_fields:\n        if field in data:\n            setattr(user, field, data[field])\n    \n    db.session.commit()\n    return user",
    "type": "function",
    "name": "update_user_profile",
    "decorators": ["require_auth"],
    "args": ["user_id", "data"],
    "returns": "User",
    "start_line": 45
}

看到没?装饰器 @require_auth 被保留了,类型注解 -> User 被保留了,docstring也在。大模型拿到这个chunk,立刻就能明白:调用这个函数需要认证,传入用户ID和数据字典,返回User对象,失败会抛NotFoundError。

那如果函数真的超长怎么办?比如一个500行的数据处理函数。这时候,不要暴力截断,而是考虑在函数内部按逻辑子块提取,或者做二级拆分。但切记,拆分点要落在子函数的调用之间,或者独立的逻辑段落之间,绝不能落在一个 for 循环的中间。

这样做的好处是什么?检索精度直接起飞。因为函数名本身往往就是最强关键词,update_user_profile 这个名称在向量空间里会和“更新用户资料”这种查询高度对齐。而大模型看到的又是完整的实现,生成代码时几乎不会出错。

函数是代码世界里的原子,别把它切成夸克。保持函数的完整性,就是保持RAG检索的最小可用单元。

3. 类级切块——别让OOP的封装优势在切块中瓦解

如果说函数是公寓,那类(Class)就是一个带物业管理的住宅小区。小区里的住户(方法)共享花园(类属性)、物业(self状态)和门禁(封装边界)。在面向对象的语言里,类是代码组织的中坚力量。你把类拆开,就相当于把小区里所有住户的房门砸掉,让大模型去猜“这个self.conn到底是哪家的”。

类级切块的核心挑战在于:类的方法之间往往存在强状态依赖。self.config__init__ 里初始化,在 connect 里使用,在 close 里释放。三者缺一不可。

新手最容易犯的错,就是为了控制chunk size,把类的方法一个一个拆出来,当成独立的函数chunk去入库。乍一看,每个方法都短小精悍,完美符合chunk size限制。但实际上,这是捡了芝麻丢了西瓜。

来看一个典型的翻车案例。假设你有一个数据库管理类:

class DatabaseManager:
    def __init__(self, connection_string):
        self.connection_string = connection_string
        self.conn = None
        self.cursor = None
        
    def connect(self):
        self.conn = psycopg2.connect(self.connection_string)
        self.cursor = self.conn.cursor()
        
    def query(self, sql, params=None):
        if not self.cursor:
            raise RuntimeError("未建立连接,请先调用 connect()")
        self.cursor.execute(sql, params or ())
        return self.cursor.fetchall()
        
    def close(self):
        if self.cursor:
            self.cursor.close()
        if self.conn:
            self.conn.close()

如果把 query 方法单独切成一个chunk,大模型看到的是:

def query(self, sql, params=None):
    if not self.cursor:
        raise RuntimeError("未建立连接,请先调用 connect()")
    self.cursor.execute(sql, params or ())
    return self.cursor.fetchall()

这时候用户问:“怎么执行SQL查询?”RAG把这个chunk丢给大模型。大模型一看,self.cursor从哪来的?怎么初始化?connect方法干了什么?它一概不知。生成的回答很可能是:“先创建一个DatabaseManager实例,然后直接调用query方法。”——完全漏掉了 connect 这一步!用户copy过去一跑,直接报错RuntimeError。

更隐蔽的坑在于继承关系。如果你的类继承自 BaseHandler,而 BaseHandler 里定义了 self.loggerself.config,你把子类的方法单独切块,大模型根本不知道 self.logger 已经存在了,可能会建议用户“在使用前记得初始化logger”,造成冗余代码。

把“控制token数”凌驾于“语义完整性”之上,是类级切块里的头号大忌。对于类来说,方法列表和属性定义就是它的上下文,少了任何一个,类的语义都不完整。

对于普通规模的类(比如20个方法以内,总长度在1500 tokens左右),我的建议是:整个类作为一个chunk。别心疼那点token,类内部的连贯性比这几十上百个token珍贵得多。

类级 Chunk

类注释与签名

\_\_init\_\_ 与属性

核心方法组

辅助方法组

self 状态流转

一个标准的类chunk应该包含完整的类定义、文档字符串、__init__ 方法、属性声明和核心方法。如果类真的很大,比如一个3000行的 UserService,里面既有CRUD方法,又有权限校验、缓存逻辑、事件发布,这时候怎么办?硬塞肯定不行,检索时相似度会被稀释。

这时候的策略是“主从切块”:

  1. 把类的定义、__init__、核心公共方法(如权限校验)作为“主chunk”。
  2. 把具体的业务方法(如 create_user, delete_user)按功能组拆成多个“子chunk”。
  3. 在子chunk的元数据里,通过 parent_class: "UserService" 建立回指关系。

这样,当用户问“用户创建逻辑”时,检索到的是专注的 create_user 子chunk,元数据告诉系统“这属于UserService”,如果需要更多上下文,可以再做二次检索把主chunk拉进来。这种“分层检索”策略,既保证了精度,又保留了类的上下文。

这样做的好处是什么?大模型终于能看懂“状态”是怎么流转的了。它知道 self.conn__init__ 里被设为None,在 connect 里被赋值,在 close 里被释放。生成的代码建议不再是空中楼阁。

类是一个自洽的小宇宙,切开了就只剩孤星。要么完整保留,要么主从分层,千万别把方法当孤儿扔出去。

4. 模块级切块——在宏观架构与微观细节之间找平衡

聊完了函数和类,咱们把镜头拉远一点,看看模块(Module/File)。如果说函数是公寓、类是小区,那模块就是一整条街道。街道上有路牌(模块级docstring)、有公交枢纽(全局导入)、有市政设施(全局变量、路由注册)。如果你只盯着每一栋楼(函数/类)看,很可能会迷路——因为你不知道公交车怎么坐,路牌指向哪里。

在很多框架里,模块级代码承担着“编排”和“声明”的职责。比如Python的 __all__、Flask的 @app.route、Django的 urlpatterns、React的组件导出。这些内容不属于任何一个函数或类,但它们对理解代码结构至关重要。

新手在切块时,常常完全忽略模块级内容。他们把文件里的函数和类切完后,发现还剩下一堆“零碎”:import语句、全局常量、注册代码、执行逻辑。于是顺手就把这些扔了,或者随便塞进某个chunk的边角料里。

这种操作会带来什么后果?咱们拿Flask举个栗子:

from flask import Flask, jsonify
from .handlers import UserHandler, OrderHandler

app = Flask(__name__)

@app.route('/api/users', methods=['GET'])
def get_users():
    return jsonify(UserHandler.list_all())

@app.route('/api/orders', methods=['POST'])
def create_order():
    return jsonify(OrderHandler.create())

如果你只按函数切块,get_userscreate_order 被单独提取。用户问:“这个项目提供了哪些API接口?”RAG检索到这两个函数chunk,但函数体里只有业务逻辑,看不到 @app.route 装饰器!大模型根本不知道这两个函数对应什么URL、支持什么HTTP方法。它只能瞎猜:“可能通过某种方式注册为接口。”这种回答对用户来说就是废纸。

再比如,很多Python模块开头有:

"""
data_utils.py
=============
本模块提供数据预处理、清洗和特征工程的工具函数。
所有函数默认使用 Pandas DataFrame 作为输入输出格式。
"""

DEFAULT_ENCODING = 'utf-8'
MAX_ROWS = 10000

这段模块docstring和全局常量,如果被丢弃,用户问“这个模块是干嘛的?”时,RAG只能返回零散的函数实现,大模型无法给出高层次的概述。还有 __all__ 变量,如果被忽略,当其他模块使用 from data_utils import * 时,哪些符号会被导出完全是个谜。

认为“只有函数和类才是干货,导入语句和装饰器都是边角料”?恰恰相反,在代码检索场景下,模块级的“声明性代码”往往比实现细节更有信息量。

模块级切块的目标是保留文件的“地图属性”。我建议采用“头部抽取”策略:

  1. 模块头部chunk:包含文件路径、模块docstring、所有import语句、全局变量、__all__ 声明、路由注册、装饰器声明等。
{
    "content": "\"\"\"auth.py - 用户认证模块\"\"\"\nfrom flask import Blueprint\nfrom .services import AuthService\n\nauth_bp = Blueprint('auth', __name__)\n__all__ = ['auth_bp', 'login_required']\n\n# 模块级路由注册在此...",
    "type": "module_header",
    "file_path": "src/routes/auth.py",
    "exports": ["auth_bp", "login_required"]
}
  1. 主体chunk:文件中的核心类和函数,按前面讲的规则切块入库。

  2. 尾部chunk(可选)if __name__ == '__main__': 这样的测试或启动代码,单独保留。

模块级切块策略

头部 Chunk
imports / docstring / 全局变量

主体 Chunk
类与函数

尾部 Chunk
\_\_main\_\_ / 测试代码

这样做的好处是,当用户问“这个项目怎么启动?”“有哪些API?”“依赖了哪些库?”时,模块级chunk能直接回答架构层面的问题;而具体的业务逻辑,则由函数和类chunk来承接。宏观和微观各司其职,RAG系统终于有了一个“从总览到细节”的层次结构。

模块是代码的地图,没有地图,函数只是散落的坐标。保留模块头部,就是保留代码的导航能力。

5. 跨层级关联——用知识图谱把散落的代码块串成网

到这一步,你已经有了函数chunk、类chunk、模块chunk。它们各自都很完整,但问题是——它们之间失联了。现实世界里的代码检索,往往不是问“某个函数怎么写”,而是问“这个功能怎么实现”“这个流程怎么走”。这涉及到跨函数、跨类、跨模块的调用链。如果chunk之间没有关联,RAG就退化成了一只“井底之蛙”,只能看到头顶那一片天。

跨层级关联,就是要在这些孤立的chunk之间架桥铺路,让检索能从A函数跳到B类,再跳到C模块,最终还原出完整的逻辑链路。

最痛的场景是什么?是用户提了一个“流程性”问题,而你的RAG只能返回一个“点”。

比如用户问:“在这个项目里,用户登录的完整流程是怎样的?”你的向量库里存了这些chunk:login 函数(在 auth.py)、verify_password 函数(在 utils.py)、User 类(在 models.py)、generate_token 函数(在 jwt_handler.py)。

如果没有关联信息,向量检索可能只召回 login 函数,因为query里“用户登录”和 login 的语义最接近。大模型拿到 login 的代码:

def login(username, password):
    user = User.query.filter_by(username=username).first()
    if not user or not verify_password(password, user.password_hash):
        return None
    return generate_token(user.id)

大模型看到 verify_passwordgenerate_token 的调用,但完全不知道这两个函数在哪、怎么实现、有什么副作用。它只能根据自己的预训练知识瞎编一个 verify_password 的实现,或者告诉你“请自行实现密码校验”。用户看了只想卸载IDE。

这就是典型的“单点检索”困境。向量相似度擅长找“最相关的那一个”,但代码的逻辑往往是网状的,不是点状的。

以为只要向量相似度够高,就能覆盖所有上下文需求?错!代码的上下文不仅包括“相似的语义”,还包括“调用的关系”、“继承的关系”、“导入的关系”。

我们需要在chunk的元数据里植入“关系钩子”,让RAG系统具备“导航”能力。

一个带关系的chunk元数据应该长这样:

{
    "content": "def login(username, password): ...",
    "type": "function",
    "name": "login",
    "module": "src.services.auth",
    "calls": ["verify_password", "generate_token", "User.query.filter_by"],
    "called_by": ["oauth_login", "admin_login"],
    "parent_class": null,
    "related_modules": ["src.models.user", "src.utils.crypto"]
}

看到没?calls 字段告诉系统:这个函数调了谁。called_by 告诉系统:谁调了它。related_modules 则把相关的模块文件也关联进来。

用户查询:登录流程

函数: login

函数: verify_password

函数: generate_token

类: User
models.py

模块: jwt_handler

模块: auth.py
头部

在检索阶段,你可以设计一个“多跳检索”策略:

  1. 第一轮向量检索,找到 login 函数chunk。
  2. 解析其元数据,发现它调用了 verify_passwordgenerate_token
  3. 第二轮检索(或图查询),把被调用的函数chunk和 User 类chunk一并拉回来。
  4. 把这些chunk按逻辑顺序拼接,喂给大模型。

这种“GraphRAG”或者“知识图谱增强RAG”的思路,在代码场景下简直是天作之合。因为你不需要去猜实体关系,AST已经帮你把调用关系、继承关系、导入关系算得清清楚楚。

如果暂时不想上图谱系统,至少也要在prompt里做“相关chunk推荐”。比如在检索到 login 后,根据元数据里的 calls 列表,主动把相关chunk加入上下文窗口。虽然粗暴一点,但效果远胜于单点检索。

这样做的好处是什么?大模型终于能“沿着调用栈看代码”了。它不再是一个只会背函数的复读机,而是一个能tracing的逻辑推理引擎。

好的代码切块不是切碎,而是切出一张关系网。让chunk之间能说话,RAG才能真正理解代码的流程。

6. 实战策略——Chunk Size、Overlap与元数据的最佳实践

理论讲得再多,落不了地就是白搭。这一节咱们来点实在的:chunk size到底设多少?overlap要不要加?元数据怎么设计才不鸡肋?这些问题没有标准答案,但我可以给你一套基于实战经验的“决策框架”,让你拿到新项目时,知道怎么下手,而不是拍脑袋乱设参数。

我见过太多“参数迷信”了。有人在项目A里设 chunk_size=512 效果还行,到了项目B也照搬512,结果全是半截类定义。还有人听说overlap能增加上下文连贯性,直接设个 overlap=200,导致两个无关的函数硬被缝合在一起。

更常见的是元数据设计过度。比如给每个chunk塞了十几二十个字段,什么圈复杂度、嵌套深度全往里怼。检索时根本用不上,白白浪费存储和计算。

错误案例:

# 拍脑袋的配置
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 不管代码类型一刀切
    chunk_overlap=100,   # 在函数内部硬重叠
    separators=["\n\n", "\n", " ", ""]  # 按空行切代码?灾难!
)

用这种配置切代码,一个80行的类会被切成3段,每段都带一点上一段的尾巴。检索时,第二段里既有上一个方法的尾部,又有下一个方法的头部,语义像一锅东北乱炖,啥都有,啥都不像。

把处理Markdown或普通文本的切分策略,原封不动套在代码上,是实战中最常见的翻车原因。代码的语义单元长度差异极大:一个getter可能只有3行,一个大型类可能有300行。一刀切是懒惰,更是灾难。

咱们分层来聊。

第一,Chunk Size的分层设计。

不要用一个size打天下。根据代码单元的类型,给它配不同的“房间大小”:

代码类型 建议Chunk Size 理由
函数级 300 - 800 tokens 大多数函数在这个范围内,保持原子性
类级 1000 - 2000 tokens 保留类内方法的上下文,允许稍大
模块头部 500 - 1500 tokens 包含imports和全局声明,通常不长
超大类/模块 按需拆分或摘要 超过3000 tokens必须做摘要或主从拆分

第二,Overlap的克制使用。

在代码场景下,overlap要非常克制。我的原则是:不要在函数或类的内部做overlap。两个chunk的交界,应该落在语法边界上。

推荐的overlap策略:

  • 函数/类之间:overlap = 0。各回各家,各找各妈。
  • 模块头部与第一个类/函数之间:可以保留1-2行overlap,让上下文平滑过渡。
  • 超大类的主从拆分之间:保留类的签名和 __init__ 作为公共头部。

第三,元数据的极简实用主义。

元数据不是越多越好,而是“检索时能用上”才好。我推荐的必备字段就这几个:

{
    "chunk_id": "uuid",
    "content": "...",
    "type": "function | class | module_header | module_tail",
    "name": "函数或类名",
    "module": "文件相对路径",
    "start_line": 42,
    "end_line": 88,
    "language": "python",
    "parent_class": "所属类名(函数才有)",
    "calls": ["被调用的函数/类名列表"],
    "imports": ["直接依赖的模块"]
}

这几个字段,足够支撑:按类型过滤(只查函数)、按文件过滤(只看某个模块)、关联检索(通过calls做图查询)、源码定位(start_line跳转到Git仓库)。

第四,混合检索(Hybrid Search)。

纯向量检索在代码场景下有个短板:对精确匹配不敏感。比如用户搜索 DatabaseManager.connect,向量检索可能会找到很多含 Databaseconnect 的chunk,但不一定最精确。

这时候要加上BM25或TF-IDF的关键词检索,对函数名、类名、模块路径做权重boost。向量负责“语义相关”,关键词负责“精确命中”,两者结合,召回率能再上一个台阶。

函数节点

类节点

模块头部

读取源码文件

AST语法解析

函数级 Chunk
size: 500-800
overlap: 0

类级 Chunk
size: 1000-2000
主从拆分

模块头部 Chunk
size: 500-1500

注入元数据
type / calls / module

向量化 + 关键词索引

混合检索召回

这样做的好处是什么?你的RAG系统终于从“玩具”变成了“生产力工具”。参数不再靠蒙,检索不再靠运气,大模型生成的每一行代码,都有据可查、有源可溯。

策略是死的,代码是活的。Chunk参数要跟着代码结构走,而不是让代码削足适履去适应你的参数。

写在最后

代码切块这件事,说小也小,不过是RAG流水线里的一个预处理环节;说大也大,它直接决定了你的大模型是“盲人摸象”还是“按图索骥”。很多新手在搭RAG系统时,把90%的精力花在调模型、调prompt上,却忽视了“数据怎么切”这个最底层的问题。结果呢?地基不稳,楼盖得越高,晃得越厉害。

今天咱们聊了整文件切块的灾难,聊了函数级切块怎么保持原子性,类级切块怎么保护OOP的完整性,模块级切块怎么保留架构地图,还聊了跨层级关联和实战参数设计。其实归根结底就一句话:尊重代码的语法结构,别让文本切分的钝刀,毁了代码的精密骨架

编程之路从来不容易,从写第一行“Hello World”到搭一个企业级RAG系统,中间隔着无数个踩坑的夜晚。但每一步成长都算数,每一次对细节的较真,都会在未来某个时刻给你回报。保持好奇,持续学习,别怕在AST和chunk size里钻牛角尖——你此刻啃下的硬骨头,终将成为你技术栈里最硬的底气。

去吧,把你的向量数据库重新整理一遍,让那些代码块各归其位。你会发现,AI突然变得“聪明”了许多。

关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:2026 年多模态大模型实战训练营》
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐