登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

RAG 怎样为不同文档类型设置分块策略

来源:17golang原创

时间:2026-10-19 22:21:08 443浏览 收藏

我在排查 RAG “明明把文档放进去了,却总是答非所问”时,最先怀疑的往往不是 embedding 模型,而是分块边界。普通说明文、代码和表格共用一套按字符截断的规则,常见结果是标题留在上一块、正文落到下一块,代码被拆成半个函数,表头又和数据行分离。分块策略的核心不是追求一个固定 chunk_size,而是让每个块在自己的文档类型里保留足够完整的语义。

要点速览
  • 长段落优先按标题、段落和句子递归切分,必要时再用 token 上限兜底。
  • 代码块要以函数、类或完整示例为边界,并把文件、符号和语言写入元数据。
  • 表格不能只按换行切开,应保留表头、单位和一组相关行;查询复盘要按文档类型拆开看。

先从“召回不对”反推是哪种边界坏了

一个很典型的症状是:问概念时能召回,问“某个字段为什么这样填”却召回不到;或者代码解释看起来相关,却缺少函数签名和返回值。此时我会先把问题分成三类:块太大导致多个主题混在一起,块太小导致上下文断裂,或者解析阶段已经把标题、代码、表格的结构抹平。

这一步很重要,因为“把 chunk_size 调大”只对第三类问题之外的部分情况有效。Hugging Face 的 RAG 示例也把目标表述为语义相关的片段:太小不足以支撑答案,太大又会稀释单个观点。先确认症状属于哪一类,再决定是否调整大小。

RAG 中标题段落代码表格与统一元数据的静态关系说明图
图1:结构说明图,查看不同文档元素如何保留自身边界,并通过统一元数据进入 embedding 与检索层;这不是截图或运行证据。

普通长文:先保住标题,再递归处理段落

普通技术文档适合从大到小处理:先按章节标题分组,再按段落切分,最后才在单个段落过长时按句子或 token 上限拆分。每个块最好带上父级标题,而不是只保存一段孤立正文。这样用户问“连接池为什么要设置上限”时,召回内容仍能带着所属章节的语义。

重叠窗口可以作为补救,但不要把它当作结构解析的替代品。我的起点通常是让相邻块保留少量句子或约一成的 token 重叠,然后用真实问题检查:答案需要跨段落时是否仍能同时召回,单段落查询是否会被相邻主题干扰。

def split_long_paragraph(text, limit=420, overlap=40):
    # 先按句号切分,避免在中文观点中间硬切。
    sentences = [s.strip() for s in text.replace("!", "。\n").replace("?", "。\n").split("。") if s.strip()]
    chunks, current = [], ""
    for sentence in sentences:
        candidate = (current + "。" + sentence).strip("。")
        if current and len(candidate) > limit:
            chunks.append(current + "。")
            # 只保留尾部短上下文,避免相邻主题大量重复。
            current = current[-overlap:] + sentence
        else:
            current = candidate
    if current:
        chunks.append(current + "。")
    return chunks

这段代码只是说明“先按句子、再按上限”的最小策略;生产解析器还应把父标题、页码、段落序号和文档标识写入 metadata。不要把这个字符数直接当成所有模型的 token 限制。

代码文档:函数和类比固定长度更值得优先保留

代码的自然单位不是连续字符,而是文件、模块、类、函数和代码块。最容易复现的故障是把函数签名切到上一块,把函数体切到下一块;用户检索函数名时,向量可能只能看到一个无上下文的调用片段。更稳妥的做法是先按 fenced code block 或语法节点切分,再对超长函数按逻辑子块处理,并把语言、文件路径、符号名写入元数据。

def code_chunk(code, language, file_name, symbol):
    # 一个函数或类先作为完整单元,避免符号和实现分离。
    return {
        "text": code,
        "metadata": {
            "doc_type": "code",
            "language": language,
            "file": file_name,
            "symbol": symbol,
        },
    }

def should_split_code(chunk, max_chars=1800):
    # 超长代码才进入二次切分,普通函数不使用重叠字符硬切。
    return len(chunk["text"]) > max_chars

代码块的检索内容还可以补一行简短的结构前缀,例如“文件:router.py;符号:match_route;语言:Python”。前缀不是为了伪造摘要,而是让 embedding 同时看到符号身份和实现内容。

表格文档:表头、单位和相关行必须绑定

表格的召回问题经常被误判为“向量模型不懂表格”。实际上,单纯按换行拆分会让“延迟(毫秒)”留在第一块,而具体数值被分散到后面;模型得到数字,却不知道列含义。表格应先作为结构元素保存,再根据行数或 token 上限切成若干行组,每个行组重复携带表头、单位和表格标题。

如果表格存在合并单元格或多级表头,解析阶段应先把列路径展开成稳定字段,例如“华东 / 读取 / P95(毫秒)”。这比在向量库查询时临时猜列名可靠得多。对跨页表格,还要把页码和表格编号写进 metadata,方便回答时回到原文位置。

RAG 普通长文代码块和表格分别设置分块边界的静态对照图
图2:结构对照图,普通长文保留标题层级,代码保留符号边界,表格绑定表头与行组;这是原创说明图,不是软件界面截图。

用统一元数据和小样本查询防止策略漂移

分块完成后,我会要求每个 chunk 至少有 doc_type、document_id、section 和 source_locator 四类字段;代码再加 symbol,表格再加 table_id 与列信息。这样当召回偏移时,可以先按 doc_type 分组,看问题只发生在表格还是所有文档都发生。

验证不要只拿一条“标准问题”。为每类文档准备几条真实查询:长文问一个观点,代码问一个符号和参数,表格问一个字段与数值关系。记录“命中块是否包含完整支撑信息”,而不是只看相似度排序。若代码问题命中调用点却没有定义,就回到代码边界;若表格只命中数值没有表头,就回到行组封装。

文档类型优先边界建议元数据常见故障
普通长文标题 → 段落 → 句子章节、段落、页码观点被相邻主题稀释
代码文件 → 类/函数 → 逻辑子块语言、文件、符号签名与实现分离
表格标题/表头 → 相关行组表格编号、列路径、单位数值脱离列含义

常见问题

所有文档都用相同的 chunk_size 可以吗?

可以作为初始基线,但不应作为最终策略。只要文档结构差异明显,就应先按结构元素切分,再让长度上限处理极端长块。

chunk_overlap 越大越好吗?

不是。重叠只能补充相邻上下文,过大会增加重复向量和噪声。先保证标题、代码符号、表头这些关键边界没有被拆开。

为什么表格要重复表头?

因为数据行脱离列名和单位后,数值本身缺少可检索语义。每个行组携带必要表头,才能让召回片段独立解释数据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>