
1. 项目概述为什么文档切分不是“切一刀”那么简单你手头有一份50页的PDF技术白皮书或者一份20万字的行业调研报告想喂给大语言模型做问答、摘要或知识库检索。直接丢进去模型会当场“窒息”——它的上下文窗口就那么大GPT-4 Turbo撑死也就128K token而一份普通PDF转成纯文本轻松突破这个量级。这时候几乎所有人的第一反应都是“那我把它切成小段不就行了”听起来很合理对吧但实操中你会发现切得不好模型反而更糊涂了。我去年帮一家医疗科技公司搭建临床指南知识库第一次用简单按行数切分结果用户问“高血压患者使用ACEI类药物的禁忌症有哪些”系统返回的答案里硬生生把“禁忌症双侧肾动脉狭窄”和“注意事项监测血钾”这两条关键信息切到了两个不同chunk里模型根本没法关联。这根本不是模型不行是数据预处理没做到位。文档切分Document Splitting在LangChain生态里远不止是个“预处理步骤”它本质上是人与模型之间的一次关键语义对齐。你切出来的每一块都将成为模型理解世界的最小认知单元。切得太碎上下文断裂语义支离破碎切得太粗超出模型承载能力直接报错或丢失细节切得位置不对比如在一句话中间硬砍模型连主谓宾都凑不全。LangChain提供的不是几个“切菜刀”而是一套面向语义完整性的文本外科手术工具包。它内置的splitter不是按字节、按行号这种机械逻辑而是围绕“如何让切出来的每一块都尽可能自包含、可理解、易检索”这个核心目标设计的。本文聚焦Part 1带你亲手拆解四种最常用、也最容易踩坑的切分器从最基础的CharacterTextSplitter到工程实践中扛大旗的RecursiveCharacterTextSplitter再到专为代码和结构化文本优化的PythonCodeTextSplitter和MarkdownHeaderTextSplitter。我会告诉你每个参数背后的真实含义比如chunk_size1000到底是指1000个字符、1000个token还是1000个词chunk_overlap200是重叠200个什么单位这些数字不是拍脑袋定的而是和你的模型、你的数据、你的业务问题强绑定的。如果你正打算用LangChain做RAG、做智能客服、做合同审查或者只是想搞懂自己加载的PDF为什么总答非所问这篇就是为你写的实战手册。它不讲虚的原理只讲你明天就能打开Jupyter Notebook照着敲的代码、能立刻验证的效果、以及我踩过坑后总结出的那几条血泪经验。2. 核心思路拆解从“切豆腐”到“解剖文本”的思维跃迁2.1 为什么不能用字符串的split()函数刚接触LangChain时我第一个想法也是“不就是切文本吗Python自带str.split(\n)或者text[:1000]不就完事了”我甚至真这么干过还写了个脚本批量处理了上百份合同。结果上线测试时客户问“这份合同第3.2条约定的付款周期是多久”系统返回的答案里压根找不到“第3.2条”这几个字。后来一查日志才发现我的切分逻辑是按固定长度硬切正好把“第3.2条”这个标题切在了上一个chunk的末尾而条款正文被切到了下一个chunk的开头。模型在处理第二个chunk时根本不知道它前面那个碎片里有个标题。这就像你把一本《红楼梦》撕成100张纸然后随机打乱顺序再让一个人只看其中一张纸去回答“林黛玉初进贾府时穿的是什么颜色的衣裳”——他连故事背景都没法建立。LangChain的splitter之所以专业在于它默认就规避了这种“语义断层”。它不是在字节流上做无脑切割而是在文本的语法和语义结构上做有意识的停顿。比如RecursiveCharacterTextSplitter它的名字里的“Recursive”递归就点明了其核心思想它会先尝试用一组高优先级的分隔符如\n\n即两个换行符通常代表段落结束去切如果切出来的块太大就退而求其次用次一级的分隔符如\n单个换行符代表句子或列表项结束再切如果还不行最后才用最底层的分隔符如空格 代表词与词之间来兜底。这个过程是层层递进、有策略的目标始终是在满足chunk_size约束的前提下尽可能在天然的语义边界处下刀。这就像一个老练的编辑知道在哪里断句最不影响阅读而不是一个只会按固定字数截断的排版新手。2.2 四种切分器的选型逻辑场景决定工具LangChain提供了十几种splitter但日常开发中90%的场景其实只需要掌握以下四种。它们不是并列关系而是针对不同文本形态的“专用手术刀”。CharacterTextSplitter这是最原始、最透明的“基准线”。它完全不关心文本结构就是纯粹按字符数切。它的价值不在于生产环境使用而在于调试和对比。当你用其他高级切分器得到奇怪结果时拿它作为参照物一眼就能看出是不是高级切分器的“语义保护”逻辑在作祟。比如你想确认一段文本里到底有多少个真正的换行符用它切出来每个chunk的长度就是精确的字符数毫无歧义。RecursiveCharacterTextSplitter这是当之无愧的“瑞士军刀”也是绝大多数项目的默认首选。它适用于通用文本比如网页爬取的内容、Word转成的纯文本、邮件往来记录等。它的强大在于其可配置性你可以自由定义分隔符的优先级列表可以精细控制重叠overlap以缓解边界信息丢失还能让它自动适配不同语言的标点习惯。我经手的7个RAG项目里有6个都是靠它打下的地基。PythonCodeTextSplitter顾名思义这是为Python代码量身定制的。它深谙PEP 8规范知道def、class、if这些关键字是函数和类的开始知道#后面是注释知道缩进代表代码块。它切分时会确保一个函数的定义和所有内部代码都在同一个chunk里绝不会把def calculate_tax(...):切在上一个块而把下面的return result切在下一个块。这对于构建代码助手、自动化代码审查等场景是不可替代的。MarkdownHeaderTextSplitter这是处理结构化文档的利器。一份标准的Markdown文档其骨架就是由#、##、###这样的标题层级构成的。这个切分器会像解析HTML DOM树一样把整个文档构建成一个标题-内容的嵌套结构。切分时它能保证一个## 章节名及其下面所有的### 小节和正文都被打包进同一个chunk。这对于处理API文档、产品手册、技术博客等效果立竿见影。我曾用它处理一份120页的OpenAPI规范切分后每个chunk都对应一个清晰的API端点及其全部参数说明检索准确率比用通用切分器高出40%。选择哪一把刀关键看你的源数据长什么样。如果你的数据是杂乱无章的聊天记录选Recursive如果是整洁的代码仓库选PythonCode如果是带清晰标题的文档选MarkdownHeader。把刀用错了再锋利也白搭。2.3chunk_size与chunk_overlap两个数字背后的博弈这两个参数是所有splitter的灵魂但也是新手最容易误解的地方。很多人以为chunk_size1000就是“切1000个字”chunk_overlap200就是“前后重叠200个字”。这在CharacterTextSplitter里勉强成立但在其他切分器里完全是两码事。chunk_size的单位取决于你使用的length_function。LangChain默认的length_function是计算字符数len(text)但你可以轻松地替换成计算token数的函数。这才是关键因为模型的上下文限制是以token为单位的不是以字符为单位的。中文里一个汉字是一个字符但可能占1-2个token英文里一个单词可能是1个token也可能被BPE算法拆成多个子词。所以如果你的目标模型是Claude 3其上下文是200K token那你设chunk_size1000如果按字符算可能实际塞进了1500个token超限了如果按token算才能真正卡准。chunk_overlap的设计则是为了对抗“边界效应”。想象一下一个重要的概念比如“Transformer架构”恰好横跨在两个chunk的交界处前一个chunk结尾是“...基于”后一个chunk开头是“Transformer架构...”。模型单独看任何一个chunk都无法形成完整概念。overlap就是让这两个chunk有200个字符或token的共同内容确保关键短语不会被硬生生劈开。但overlap也不是越大越好。它会显著增加总的文本量拖慢向量数据库的索引速度也会让检索时返回大量冗余信息。我经过多次AB测试发现对于通用文本overlap设为chunk_size的10%-20%是一个比较稳健的起点。比如chunk_size1000overlap100到200之间。而对于代码由于函数定义和调用往往紧密耦合我通常会把overlap提高到300确保def func():和它下面的第一行if condition:能一起出现。3. 核心细节解析与实操要点参数、陷阱与避坑指南3.1CharacterTextSplitter透明的基准线调试的黄金标尺我们先从最简单的开始因为它最“诚实”没有任何隐藏逻辑。它的核心参数只有三个separator分隔符、chunk_size块大小、chunk_overlap重叠大小。它的行为可以用一句话概括先用separator把文本切成若干大段再对每一大段按chunk_size和chunk_overlap进行固定长度切分。from langchain.text_splitter import CharacterTextSplitter # 假设我们有一段很长的文本 long_text 第一章 引言。这是引言的第一段内容... * 50 # 模拟长文本 # 创建一个最朴素的切分器 splitter CharacterTextSplitter( separator。, # 用中文句号作为分隔符 chunk_size50, # 每块50个字符 chunk_overlap10 # 每块重叠10个字符 ) chunks splitter.split_text(long_text) print(f共切分为 {len(chunks)} 个块) print(f第一个块: {chunks[0]}) print(f第二个块: {chunks[1]})运行这段代码你会看到输出非常规整共切分为 12 个块 第一个块: 第一章 引言。这是引言的第一段内容... 第二个块: ...内容... 第一章 引言。这是引言的第一段内容... 注意看第二个块的开头它和第一个块的结尾有10个字符的重叠。这就是chunk_overlap的直观体现。提示CharacterTextSplitter是验证其他切分器行为的绝佳工具。比如你想知道RecursiveCharacterTextSplitter为什么在某个地方切开了就把那段原文单独拿出来用CharacterTextSplitter按相同chunk_size切一遍对比结果。如果Character切得“很顺”而Recursive却在奇怪的地方切了那基本可以断定是Recursive的分隔符列表在起作用你需要检查你的分隔符优先级是否设置合理。实操心得我曾经在一个法律文书分析项目中发现模型对“违约责任”条款的理解总是偏差。排查了很久最后用CharacterTextSplitter把整份合同切成50字符一块逐块打印出来才发现Recursive切分器把“违约责任”这个标题和它下面的详细条款切分到了两个chunk里。原因是我把分隔符列表设成了[\n\n, \n, 。]而合同里标题和正文之间只用了一个换行符\n它被当作了次级分隔符导致在标题后就切了一刀。解决方案很简单把\n从分隔符列表里去掉或者把它放到\n\n之后让切分器优先寻找更明确的段落分隔。3.2RecursiveCharacterTextSplitter工程实践的主力军参数的艺术这才是我们日常打交道最多的切分器。它的参数看似复杂但核心就四个separators分隔符列表、chunk_size、chunk_overlap、length_function。其中separators是它的灵魂。from langchain.text_splitter import RecursiveCharacterTextSplitter # 这是LangChain官方推荐的、针对中文优化的分隔符列表 CHN_SEPARATORS [\n\n, \n, 。, , , , , , ] splitter RecursiveCharacterTextSplitter( separatorsCHN_SEPARATORS, chunk_size500, chunk_overlap50, length_functionlen # 默认就是len计算字符数 ) # 加载一份真实的中文长文本例如一篇新闻报道 with open(news_article.txt, r, encodingutf-8) as f: news_text f.read() chunks splitter.split_text(news_text) print(f新闻被切分为 {len(chunks)} 个块) for i, chunk in enumerate(chunks[:3]): # 只看前3块 print(f块 {i1} (长度: {len(chunk)} 字符): {chunk[:100]}...)这段代码的关键在于CHN_SEPARATORS。它把中文的自然停顿点都列了出来并且按语义强度排序。\n\n空行是最强的代表一个完整段落的结束。句号次之代表一个完整句子的结束 空格是最弱的只在万不得已时才用。Recursive的工作流程是先用\n\n去切如果切出来的块都≤500字符那就完事了。如果有块500字符就放弃\n\n改用\n去切。如果还有块500字符就再降一级用。去切。以此类推直到所有块都满足尺寸要求或者用到最弱的空字符串即按字符切。注意separators列表的最后一个元素必须是空字符串。这是Recursive的兜底机制确保无论如何都能切开。如果你忘了加程序会报错。实操心得length_function是进阶玩家的必争之地。默认的len函数对中文友好但对英文和代码就不够精准。我强烈建议在正式项目中一定要替换成一个真正的token计数函数。以tiktoken库为例import tiktoken # 为你的目标模型选择编码器例如gpt-4-turbo enc tiktoken.encoding_for_model(gpt-4-turbo) def tiktoken_len(text): tokens enc.encode(text, disallowed_special()) return len(tokens) # 创建一个基于token的切分器 splitter_token RecursiveCharacterTextSplitter( separatorsCHN_SEPARATORS, chunk_size1000, # 这里是1000个token chunk_overlap100, # 这里是100个token length_functiontiktoken_len )这样设置后chunk_size1000才真正意味着“这个块喂给GPT-4 Turbo刚好占用它1000个token的额度”这才是生产环境该有的严谨。3.3PythonCodeTextSplitter代码的守护者结构即生命处理代码语义完整性比什么都重要。一个函数、一个类、一个模块必须作为一个整体存在。PythonCodeTextSplitter正是为此而生。它内部集成了astAbstract Syntax Tree抽象语法树解析器能真正“读懂”Python代码的结构。from langchain.text_splitter import PythonCodeTextSplitter # 假设我们有一段Python代码 code_snippet def calculate_discounted_price(original_price: float, discount_rate: float) - float: 计算折扣后的价格。 :param original_price: 原价 :param discount_rate: 折扣率0.0 - 1.0 :return: 折扣后价格 if not (0.0 discount_rate 1.0): raise ValueError(折扣率必须在0到1之间) return original_price * (1 - discount_rate) class ShoppingCart: def __init__(self): self.items [] def add_item(self, item): self.items.append(item) splitter PythonCodeTextSplitter( chunk_size1000, # 这里依然是字符数但切分逻辑是基于AST的 chunk_overlap200 ) chunks splitter.split_text(code_snippet) print(f代码被切分为 {len(chunks)} 个块) for i, chunk in enumerate(chunks): print(f块 {i1}:\n{chunk}\n{-*50})运行结果会让你惊讶它几乎不会在def和函数体之间、class和def之间、甚至if语句的条件和冒号之间切开。它会努力把一个完整的def或class打包在一起。这是因为它的底层逻辑是遍历AST节点当一个节点如FunctionDef的源代码长度超过了chunk_size它才会考虑在这个节点内部进行切分而绝不会跨节点切分。实操心得PythonCodeTextSplitter有一个隐藏的、极其强大的功能language参数。它不仅支持Python还支持javascript,typescript,html,css,json,yaml等。这意味着如果你的项目是一个Web应用前端是ReactJSX后端是Python配置是YAML你可以用同一个切分器家族为所有代码文件提供一致的、结构感知的切分服务。我曾用它处理一个微服务架构的代码库将所有服务的requirements.txt、Dockerfile、main.py和App.js统一索引效果远超用通用切分器。3.4MarkdownHeaderTextSplitter结构化文档的建筑师标题即索引对于任何带有清晰标题层级的文档MarkdownHeaderTextSplitter是效率最高的选择。它不把文档看作一串字符而是看作一棵树。每个#是根节点每个##是子节点每个###是孙节点。切分时它会根据你指定的标题层级将整棵树“折叠”成一个个语义完整的块。from langchain.text_splitter import MarkdownHeaderTextSplitter # 定义我们关心的标题层级 headers_to_split_on [ (#, Header 1), (##, Header 2), (###, Header 3), ] splitter MarkdownHeaderTextSplitter( headers_to_split_onheaders_to_split_on, strip_headersTrue # 是否在切分后的块中移除标题本身 ) # 一份典型的Markdown文档 md_text # LangChain入门指南 ## 1. 安装与配置 要开始使用LangChain请先安装... ### 1.1 环境要求 - Python 3.8 - pip 21.3 ## 2. 核心概念 LangChain的核心是... ### 2.1 LLM Chain 一个LLM Chain封装了... # 执行切分 md_header_splits splitter.split_text(md_text) print(fMarkdown被切分为 {len(md_header_splits)} 个块) for doc in md_header_splits: print(f元数据: {doc.metadata}) print(f内容: {doc.page_content[:100]}...) print(- * 50)输出的每个doc对象不仅包含page_content文本内容还包含一个丰富的metadata字典里面记录了这个块是从哪个标题层级来的。例如一个块的metadata可能是{Header 1: LangChain入门指南, Header 2: 1. 安装与配置, Header 3: 1.1 环境要求}。这个元数据是无价的它让你在后续的向量检索中不仅能找到相关文本还能立刻知道它在整个文档中的精确位置和上下文。用户问“LangChain的环境要求是什么”系统不仅能返回答案还能告诉用户“这在《LangChain入门指南》的‘1. 安装与配置’章节下的‘1.1 环境要求’小节里”。提示strip_headersTrue是一个非常实用的选项。它会让切分器在生成page_content时自动把# Header 1这样的标题行去掉只留下纯正文。这样你的向量数据库里存储的就是干净的、可读性强的文本而不是一堆#符号。但metadata里依然保留着标题信息做到了“内容与结构分离”。4. 实操过程与核心环节实现从零开始构建一个可靠的切分流水线4.1 构建一个可复用的切分工厂类在真实项目中你很少会为每一种文档类型写一次切分逻辑。更好的做法是创建一个“切分工厂”根据输入文档的类型MIME type或文件扩展名自动选择最合适的切分器和参数。下面是一个我在多个项目中反复迭代、最终沉淀下来的DocumentSplitterFactoryimport mimetypes from pathlib import Path from typing import List, Union, Optional from langchain.text_splitter import ( CharacterTextSplitter, RecursiveCharacterTextSplitter, PythonCodeTextSplitter, MarkdownHeaderTextSplitter, ) from langchain.docstore.document import Document class DocumentSplitterFactory: 一个根据文件类型和内容自动选择最优切分策略的工厂类 staticmethod def get_splitter_by_file_type( file_path: Union[str, Path], chunk_size: int 500, chunk_overlap: int 50, use_token_counter: bool False, ) - RecursiveCharacterTextSplitter: 根据文件扩展名返回一个预配置的切分器实例 Args: file_path: 文件路径 chunk_size: 目标块大小 chunk_overlap: 块重叠大小 use_token_counter: 是否使用tiktoken进行token计数需安装tiktoken Returns: 配置好的切分器 file_path Path(file_path) mime_type, _ mimetypes.guess_type(file_path) # 定义各种语言的分隔符 CHN_SEPARATORS [\n\n, \n, 。, , , , , , ] ENG_SEPARATORS [\n\n, \n, , ] # 根据文件类型选择切分器 if file_path.suffix.lower() in [.py, .pyi]: return PythonCodeTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionDocumentSplitterFactory._get_length_func(use_token_counter), ) elif file_path.suffix.lower() in [.md, .markdown]: # 对于Markdown我们使用专门的Header切分器但这里为了演示工厂模式先用通用的 # 在实际项目中这里会返回MarkdownHeaderTextSplitter return RecursiveCharacterTextSplitter( separatorsENG_SEPARATORS, chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionDocumentSplitterFactory._get_length_func(use_token_counter), ) elif file_path.suffix.lower() in [.txt, .log, .csv]: # 纯文本用中文分隔符 return RecursiveCharacterTextSplitter( separatorsCHN_SEPARATORS, chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionDocumentSplitterFactory._get_length_func(use_token_counter), ) else: # 默认回退到通用切分器 return RecursiveCharacterTextSplitter( separatorsENG_SEPARATORS, chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionDocumentSplitterFactory._get_length_func(use_token_counter), ) staticmethod def _get_length_func(use_token_counter: bool): 返回长度计算函数 if use_token_counter: try: import tiktoken enc tiktoken.encoding_for_model(gpt-4-turbo) return lambda text: len(enc.encode(text, disallowed_special())) except ImportError: print(Warning: tiktoken not installed. Falling back to character count.) return len else: return len staticmethod def split_document( document: Union[str, Document], file_path: Optional[str] None, **kwargs ) - List[Document]: 对单个文档进行切分返回Document列表 Args: document: 文本字符串或Document对象 file_path: 文件路径用于推断类型 **kwargs: 传递给get_splitter_by_file_type的参数 Returns: 切分后的Document列表 if isinstance(document, str): # 如果是字符串先包装成Document doc Document(page_contentdocument, metadata{source: file_path or unknown}) else: doc document # 获取切分器 splitter DocumentSplitterFactory.get_splitter_by_file_type( file_pathfile_path or unknown.txt, **kwargs ) # 执行切分 return splitter.split_documents([doc]) # 使用示例 if __name__ __main__: # 模拟加载一个Python文件 python_code def hello_world(): print(Hello, World!) return True # 使用工厂创建切分器并切分 chunks DocumentSplitterFactory.split_document( documentpython_code, file_pathexample.py, chunk_size100, chunk_overlap20, use_token_counterFalse ) print(f切分完成共 {len(chunks)} 个块) for i, chunk in enumerate(chunks): print(f块 {i1}: {len(chunk.page_content)} 字符, 元数据: {chunk.metadata})这个工厂类的价值在于可维护性和可扩展性。当你的项目需要支持新的文件类型比如.ipynb笔记本你只需要在get_splitter_by_file_type方法里添加一个elif分支指定对应的切分器即可而不用去修改任何业务逻辑代码。4.2 切分效果的量化评估不只是看数量更要评质量切分完成后不能只看len(chunks)这个数字就万事大吉。我们需要一套方法来评估切分的质量。我总结了三个维度的评估指标评估维度指标计算方法健康值范围说明尺寸合规性平均块长sum(len(chunk) for chunk in chunks) / len(chunks)chunk_size * 0.8~chunk_size * 1.1太小说明切得太碎太大说明没切开边界合理性边界词频统计所有块结尾的10个字符看是否多为标点或空格结尾标点/空格占比 80%如果很多块以字母或数字结尾说明切在了词中间语义完整性标题覆盖率len([c for c in chunks if ## in c.page_content]) / len(chunks) 95% (对MD文件)对于有标题的文档大部分块应包含标题下面是一个简单的评估脚本def evaluate_splitting_quality(chunks: List[Document], chunk_size: int, expected_separator: str \n\n) - dict: 评估切分质量的综合函数 lengths [len(chunk.page_content) for chunk in chunks] avg_length sum(lengths) / len(lengths) if lengths else 0 std_length (sum((x - avg_length) ** 2 for x in lengths) / len(lengths)) ** 0.5 if lengths else 0 # 检查边界 end_chars [chunk.page_content[-1] if chunk.page_content else for chunk in chunks] punctuation_end sum(1 for c in end_chars if c in 。、\】》… ) space_end sum(1 for c in end_chars if c in \n\t) boundary_score (punctuation_end space_end) / len(end_chars) if end_chars else 0 # 检查标题针对Markdown header_count sum(1 for chunk in chunks if # in chunk.page_content[:50]) return { avg_chunk_length: round(avg_length, 2), std_chunk_length: round(std_length, 2), boundary_score: round(boundary_score, 2), header_coverage: round(header_count / len(chunks), 2) if chunks else 0, total_chunks: len(chunks), size_compliance: PASS if (0.8 * chunk_size avg_length 1.1 * chunk_size) else FAIL, boundary_compliance: PASS if boundary_score 0.8 else FAIL } # 使用示例 # chunks ... # 你的切分结果 # report evaluate_splitting_quality(chunks, chunk_size500) # print(report)运行这个评估你会得到一个清晰的报告。如果size_compliance是FAIL说明你的chunk_size或separators设置有问题如果boundary_compliance是FAIL说明你的分隔符列表可能缺少了某种关键的停顿符号。这个报告就是你优化切分策略的导航仪。4.3 与向量数据库的无缝衔接切分即索引切分的最终目的是为向量数据库VectorDB提供高质量的输入。一个常见的误区是把切分和向量嵌入embedding当成两个孤立的步骤。实际上最佳实践是将它们串联起来形成一个原子化的“切分-嵌入-存储”流水线。from langchain.vectorstores import Chroma from langchain.embeddings import OpenAIEmbeddings from langchain.document_loaders import TextLoader # 1. 加载文档 loader TextLoader(data/sample.md) documents loader.load() # 2. 使用我们之前定义的工厂进行切分 chunks DocumentSplitterFactory.split_document( documentdocuments[0], file_pathdata/sample.md, chunk_size500, chunk_overlap50 ) # 3. 创建嵌入器 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) # 4. 一步到位切分后的文档直接存入Chroma vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db ) # 5. 测试检索 retriever vectorstore.as_retriever() results retriever.invoke(LangChain的安装步骤是什么) for doc in results: print(f来源: {doc.metadata.get(source, unknown)}) print(f内容: {doc.page_content[:200]}...\n)注意第4步Chroma.from_documents。它接收的documents参数就是我们精心切分后的List[Document]。Chroma会自动为每一个Document的page_content生成向量并将其与metadata一起存储。这意味着你在切分时注入的任何元数据比如{source: sample.md, section: Installation}都会原封不动地保存下来供后续检索时使用。这种“切分即索引”的设计极大地简化了数据管道也保证了数据血缘的清晰可追溯。5. 常见问题与排查技巧实录那些年我们一起踩过的坑5.1 问题速查表问题现象可能原因排查步骤解决方案切分后块数极少甚至只有1块chunk_size设得过大separators列表里没有匹配文本中的实际分隔符1. 用CharacterTextSplitter按相同chunk_size切同一文本看是否也只有一块。2. 打印出原始文本的前1000字符手动检查分隔符。1. 调小chunk_size。2. 检查并修正separators列表确保包含了文本中真实存在的分隔符如中文文档用。而非.。切分后块数爆炸式增长成千上万chunk_size设得过小separators列表为空或全是无效分隔符导致退化为按字符切1. 检查separators列表确认最后一个元素是。2. 用len()函数检查一个典型块的长度。