首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LangChainGo 文档切分器:如何为 RAG 灌入高质量的文本块

LangChainGo 文档切分器:如何为 RAG 灌入高质量的文本块

作者头像
技术圈
发布2026-07-15 13:57:01
发布2026-07-15 13:57:01
660
举报

做过 RAG 的开发者大概都踩过一个坑:明明向量库里灌了大量文档,检索出来的片段却驴唇不对马嘴。排查下来,往往不是 Embedding 模型不行,而是文档切分出了问题——关键信息被一刀切断,或者一个文本块塞了太多无关内容,导致向量化后语义被稀释。LangChainGo 的 textsplitter 包提供了一套递归切分方案,核心思路是在保持语义完整的前提下控制块大小。这篇文章拆解它的 RecursiveCharacter 切分原理,并给出 Markdown 文档的实战切分示例。

为什么不能简单粗暴地定长截断

最直觉的切分方式就是按字符数截断:每 500 字切一刀。问题在于,切分位置是随机的,一段完整的代码块、一个关键的结论段落,都可能被从中间劈开。对于 RAG 场景,这种"断章取义"的文本块检索出来后,交给模型生成的答案自然也不靠谱。

RecursiveCharacter 换了一个思路。它维护一个分隔符优先级列表,默认是 ["\n\n", "\n", " ", ""]——分别对应段落分界、换行、空格和逐字符拆分。拿到一段长文本后,先尝试用双换行(段落边界)拆开。如果拆出来的某一段仍然太长,就退而用单换行继续拆;再不够,按空格拆;最后实在不行,才逐字符截断。

换句话说,段落能完整保留就绝不拆段落,只有段落本身超限了才往下降级。这样切出来的文本块,语义完整性比定长截断好得多。

核心结构与默认行为

textsplitter 包的主力类型是 RecursiveCharacter,字段不多但每个都有明确用途:

代码语言:javascript
复制
   type RecursiveCharacter struct {
    Separators    []string
    ChunkSize     int
    ChunkOverlap  int
    LenFunc       func(string) int
    KeepSeparator bool
}

通过 NewRecursiveCharacter 创建实例,支持函数选项:

代码语言:javascript
复制
   splitter := textsplitter.NewRecursiveCharacter()

几个默认行为值得留意。Separators 前面已经提过,四级分隔符从粗到细。LenFunc 默认用的是 utf8.RuneCountInString,也就是按 Unicode 字符数计算长度,一个汉字算 1 而不是按 UTF-8 编码算 3 个字节。实际项目里不建议依赖默认的 ChunkSizeChunkOverlap,后面会演示如何显式指定。

ChunkOverlap:边界信息不丢失的关键

ChunkOverlap 这个参数容易被忽略,但对检索质量影响很大。

假设文档被切成 A、B 两个块,Overlap 设为 50,那 A 块末尾的 50 个字符会和 B 块开头的 50 个字符重复。这看起来浪费了存储和向量化的开销,但好处是:如果某条关键信息刚好跨越了 A 和 B 的切分边界,重叠区域能保证两个块都包含这段内容。检索的时候,至少有一个块能命中。

Overlap 设多大合适?一般建议是 ChunkSize 的 10%~20%。设太大,冗余严重、向量库膨胀;设太小或干脆不设,边界信息就可能"两头都查不到"。

实战:切分 Markdown 文档

下面演示一个常见场景——读取本地的 Markdown 文件,切分成适合灌入向量库的文本块。

先创建切分器,显式指定块大小和重叠量:

代码语言:javascript
复制
   splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(500),
    textsplitter.WithChunkOverlap(50),
)

500 个字符的块大小适合中等篇幅的技术文档,50 个字符的重叠刚好是 10%。

然后读取文件并切分:

代码语言:javascript
复制
   content, err := os.ReadFile("docs/architecture.md")
// 错误处理省略...
chunks, err := splitter.SplitText(string(content))

SplitText 返回 []string,每个元素就是一个文本块。如果需要给每个块打上来源标签(比如文件名),方便后续检索时过滤,可以用 CreateDocuments

代码语言:javascript
复制
   docs, err := textsplitter.CreateDocuments(
    splitter,
    []string{string(content)},
    []map[string]any{{"source": "architecture.md"}},
)

返回的 []schema.Document 里,每个 Document 同时携带文本内容(PageContent)和元数据(Metadata)。元数据存入向量库后,检索时就能按来源文件做筛选。

针对 Markdown 的专用切分器

RecursiveCharacter 是通用方案,如果文档本身就是 Markdown 格式,还有更"懂"结构的选择——MarkdownTextSplitter

代码语言:javascript
复制
   mdSplitter := textsplitter.NewMarkdownTextSplitter(
    textsplitter.WithChunkSize(500),
    textsplitter.WithHeadingHierarchy(true),
)

这里重点说 WithHeadingHierarchy(true) 这个选项。开启后,切分出来的每个文本块会自动在头部补上它所属的标题层级。比如一段正文在 ## 架构设计 > ### 数据库选型 下面,切分后这个块的开头就会带上对应的标题路径。

这对 RAG 的检索质量帮助很大。举个例子:用户问"数据库选型的依据是什么",但正文里可能从头到尾都没出现"数据库选型"这几个字(正文直接在讨论 MySQL 和 PostgreSQL 的对比),如果文本块里没有标题上下文,向量相似度可能不够高,导致这个块排不到前面。带上标题层级后,这个问题就缓解了。

自定义分隔符与 Token 级切分

默认的四级分隔符适合通用文本,但如果文档格式比较特殊,可以通过 WithSeparators 自定义。比如切分 Go 源码时,优先按函数边界拆分:

代码语言:javascript
复制
   splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithSeparators([]string{
        "\nfunc ", "\n\n", "\n", " ", "",
    }),
    textsplitter.WithChunkSize(800),
)

"\nfunc " 放在最前面,切分器会优先在函数声明处下刀,尽量保证一个完整的函数不会被拆散到两个块里。

另一个常见需求是按 Token 数控制块大小。字符数和 Token 数并不总是线性对应的(尤其是中英混合文本),如果想精确控制每个块的 Token 量,可以直接用 TokenSplitter

代码语言:javascript
复制
   tokenSplitter := textsplitter.NewTokenSplitter(
    textsplitter.WithChunkSize(256),
    textsplitter.WithModelName("gpt-4"),
)

它内部会用指定模型的 Tokenizer 来计算长度,比按字符数估算更准确。

怎么判断切分效果好不好

切分参数没有标准答案,但有几个实操经验可以参考。

最简单的办法是把前几个块打出来看看。如果频繁出现句子从中间断开、或者一个块里塞了好几个不相关的段落,说明 ChunkSize 不合适或者分隔符没贴合文档格式。

更靠谱的验证方式是用实际问题做端到端测试。准备一组典型的用户问题,跑一遍 RAG 流程,看检索出来的文本块是否包含正确的答案。如果某个问题持续检索不到对应的内容,回头检查原文的切分情况,通常能找到切分不合理的地方。

关于块大小,技术文档一般设在 300~800 字符之间效果不错。对话记录或者比较碎的短文本,可以缩小到 200 左右。

写在最后

langchaingo/textsplitter 包把文档切分这件事做了很好的抽象:RecursiveCharacter 处理通用文本,MarkdownTextSplitter 专门针对 Markdown 做了结构感知,TokenSplitter 则解决 Token 精确计量的问题。在搭建 RAG 管线时,与其花大量时间调优 Embedding 模型或 Prompt,不如先回头审视一下文档切分的质量——很多时候,检索效果差的根源就藏在这里。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 技术圈子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么不能简单粗暴地定长截断
  • 核心结构与默认行为
  • ChunkOverlap:边界信息不丢失的关键
  • 实战:切分 Markdown 文档
  • 针对 Markdown 的专用切分器
  • 自定义分隔符与 Token 级切分
  • 怎么判断切分效果好不好
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档