构建 RAG 系统的第一步,就是把散落在各处的文档(PDF、Word、网页、纯文本等)读取出来,并切分成大小合适的文本片段(chunk),以便后续向量化和检索。这一环节看似基础,却直接影响最终回答的准确性和完整性——块太大可能引入噪音,太小又可能丢失上下文。
1. 文档加载:统一入口,适配多种格式
实际业务中的文档格式五花八门,逐个编写解析逻辑效率很低。社区已有成熟工具(如 LangChain 的 Document Loader 系列)提供了统一接口,能覆盖绝大多数常见格式。
常用加载器示例:
- PDF:
PyPDFLoader按页读取,适合多数文字型 PDF;扫描版需结合 OCR(如UnstructuredPDFLoader)。 - Word:
Docx2txtLoader直接提取.docx纯文本。 - 网页:
WebBaseLoader抓取网页内容并清洗标签。 - 纯文本/Markdown:
TextLoader直接读取。
下面是一个多格式加载的实用片段:
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
)
def load_documents(file_paths):
docs = []
for path in file_paths:
ext = path.suffix.lower()
if ext == '.pdf':
loader = PyPDFLoader(str(path))
elif ext == '.docx':
loader = Docx2txtLoader(str(path))
else:
loader = TextLoader(str(path), encoding='utf-8')
docs.extend(loader.load())
return docs
加载后,每个文档对象包含 page_content(文本内容)和 metadata(如来源文件名、页码),保证后续可溯源。
2. 分块策略:平衡上下文与精准度
文档加载后往往是长文本,需要切分成适合嵌入模型处理的片段。分块的核心矛盾是:块太小,可能截断完整段落,导致检索到的片段缺少必要上下文;块太大,可能包含无关内容,降低检索匹配精度,且容易超出模型输入长度限制。
常用分块方法:
- 固定大小切分:指定块长度和重叠量,按字符数机械分割。
- 按语义切分:基于分隔符(如
\n\n、句号)优先在自然边界处断开。 - 递归切分:先用块级分隔符尝试,不满足时逐级降级到更小的分隔符,兼顾语义和大小一致性。
LangChain 的 RecursiveCharacterTextSplitter 是最便捷的选择,它按照优先级列表 ["\n\n", "\n", "。", ".", " ", ""] 递归切分,尽量保持段落或句子完整。
参数设定建议(实用经验值):
chunk_size:通常设为 500~1000 个 token(约 300~700 汉字或英文词)。知识粒度越细、回答越需精确引用的场景宜偏小。chunk_overlap:设为chunk_size的 10%~20%(如 100 字符),避免关键信息恰好被截断在边界处。- 如需保留长上下文(如技术规范说明),可适当提高
chunk_size并降低重叠。
代码示例:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 块大小(字符数)
chunk_overlap=50, # 重叠量
separators=["\n\n", "\n", "。", ".", "?", "!", " ", ""],
)
chunks = text_splitter.split_documents(documents)
3. 处理特殊文档的注意事项
- 表格、代码块:固定大小切分容易破坏表格结构或代码完整性。可先用定制解析器提取为结构化文本(如 Markdown 表格),再决定是否独立分块。
- 标题关联:如果文档有清晰的标题层级,可在分块时将标题注入每个块的元数据或正文前缀(如“## 标题”),增强检索时的上下文关联。
- 去噪:加载阶段应剥离页眉/页脚、水印、多余空白行等,避免污染向量相似度。
4. 验证分块效果
分块完成后,建议检查少量样本:
- 随机抽取几个块,阅读是否语义完整。
- 确认没有“半个句子”或明显的上下截断。
- 检查重叠部分是否合理衔接。
一个实用的质量检查函数:
def inspect_chunks(chunks, n=5):
for i, chunk in enumerate(chunks[:n]):
print(f"--- Chunk {i} (length: {len(chunk.page_content)}) ---")
print(chunk.page_content[:200])
print(f"Source: {chunk.metadata.get('source')}")
5. 下一步衔接
分块后的文档块将进入嵌入与向量数据库环节。合理的分块不仅决定了检索的命中质量,也直接影响后续生成的上下文丰富度与精确度。在实际项目中,chunk_size 和 chunk_overlap 往往需要根据业务场景进行少量反复调整,才能找到最佳平衡点。