你好,我是江小湖。在 LLM 记不住、会编造、不懂你的业务?RAG 一次解决这三个问题 中,你理解了 RAG 的核心思想。这篇文章深入 RAG 的基础——文档切分与向量化,解决三个核心问题:怎么切文档、用什么模型向量化、存到哪里。
在切分之前,需要将各种格式的文档转为纯文本:
| 格式 | 解析工具 | 注意事项 |
|---|---|---|
| PyMuPDF、pdfplumber、Unstructured | 扫描版 PDF 需要 OCR | |
| Word | python-docx、mammoth | 保留标题层级 |
| HTML | BeautifulSoup、Trafilatura | 去除导航、广告等噪音 |
| Markdown | 直接读取 | 保留标题结构 |
| 代码 | 直接读取 | 保留函数/类边界 |
关键原则:解析时保留文档结构(标题、段落、列表),后续切分可以利用这些结构。
# PDF 解析示例 import fitz # PyMuPDF defparse_pdf(file_path: str) -> list[dict]: """解析 PDF,返回页面列表""" doc = fitz.open(file_path) pages = [] for page_num in range(len(doc)): page = doc[page_num] text = page.get_text() pages.append({ "page": page_num + 1, "content": text, "metadata": {"source": file_path, "page": page_num + 1} }) return pages
切分策略决定文档被分割成多大的片段,直接影响检索效果:

| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度 | 实现简单,可预测 | 可能切断语义 | 快速原型 |
| 语义边界 | 保持语义完整 | 实现复杂 | 生产系统 |
| 递归切分 | 平衡粒度和完整性 | 需要调参 | 通用场景 |
按字符数或 Token 数切分,简单但可能切断句子:
# 固定长度切分 deffixed_length_split(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]: """按固定长度切分,带重叠""" chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append(chunk) start = end - overlap # 重叠部分 return chunks
重叠(Overlap)的作用:让相邻片段有重叠,避免切断上下文。通常设置为 chunk_size 的 10%-20%。
按自然段落、标题、代码块等语义边界切分:
# 语义边界切分 defsemantic_split(text: str, max_chunk_size: int = 1000) -> list[str]: """按语义边界切分(段落、标题)""" import re # 先按标题切分 sections = re.split(r'\n(?=#{1,3} )', text) chunks = [] for section in sections: section = section.strip() if not section: continue # 如果 section 太大,继续按段落切分 if len(section) > max_chunk_size: paragraphs = section.split('\n\n') for para in paragraphs: if len(para) > max_chunk_size: # 仍然太大,用固定长度切分 chunks.extend(fixed_length_split(para, max_chunk_size)) else: chunks.append(para) else: chunks.append(section) return chunks
LangChain 推荐的策略——先按大边界切分,如果仍然太大则递归切分:
# 递归切分(LangChain 风格) from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", "。", "!", "?", ",", " "] ) chunks = splitter.split_text(document)
切分策略选择建议:
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 快速原型 | 固定长度 | 实现简单,效果够用 |
| 文档问答 | 语义边界 | 保持段落完整性 |
| 代码检索 | 语义边界 | 按函数/类切分 |
| 长文档 | 递归切分 | 平衡粒度和完整性 |
Embedding 模型将文本转为向量,是 RAG 检索质量的关键:
| 模型 | 维度 | 特点 | 适用场景 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 性价比高,效果好 | 通用场景 |
| OpenAI text-embedding-3-large | 3072 | 效果最好,价格贵 | 高精度需求 |
| BGE-large-zh | 1024 | 中文优化,开源 | 中文场景 |
| Jina Embeddings v3 | 1024 | 多语言,支持长文本 | 多语言场景 |
| Cohere Embed v3 | 1024 | 多语言,支持检索优化 | 检索场景 |
选型原则:
1. 语言匹配:中文场景优先选 BGE 或 Jina
2. 成本控制:OpenAI 模型按 Token 计费,开源模型免费
3. 效果优先:如果预算充足,用 text-embedding-3-large
# OpenAI Embedding 示例 from openai import OpenAI client = OpenAI() defget_embedding(text: str) -> list[float]: """获取文本的 Embedding 向量""" response = client.embeddings.create( model="text-embedding-3-small", input=text ) return response.data[0].embedding
向量数据库存储 Embedding 向量,支持高效的相似度检索:
| 数据库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Chroma | 嵌入式 | 轻量,易上手 | 原型开发 |
| Qdrant | 独立服务 | 高性能,功能丰富 | 生产系统 |
| Milvus | 分布式 | 高可用,可扩展 | 大规模场景 |
| pgvector | PostgreSQL 扩展 | 与现有数据库集成 | 已有 PG 的团队 |
| Pinecone | 托管服务 | 免运维,开箱即用 | 不想自己运维 |
选型建议:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 本地开发 | Chroma | 轻量,无需额外服务 |
| 小型生产 | Qdrant | 性能好,易部署 |
| 大型生产 | Milvus | 高可用,可扩展 |
| 已有 PostgreSQL | pgvector | 无需引入新组件 |
# Chroma 示例 import chromadb # 创建客户端 client = chromadb.Client() # 创建集合 collection = client.create_collection("documents") # 添加文档 collection.add( documents=["文档1内容", "文档2内容"], ids=["doc1", "doc2"], metadatas=[{"source": "file1.pdf"}, {"source": "file2.pdf"}] ) # 检索 results = collection.query( query_texts=["查询问题"], n_results=3 )

完整的索引流程:
# RAG 索引流程 from openai import OpenAI import chromadb defbuild_rag_index(documents: list[dict], collection_name: str = "documents"): """构建 RAG 索引""" # 初始化 openai_client = OpenAI() chroma_client = chromadb.Client() collection = chroma_client.create_collection(collection_name) # 处理每个文档 for doc in documents: # 1. 解析文档(假设已经是纯文本) text = doc["content"] metadata = doc.get("metadata", {}) # 2. 切分 chunks = semantic_split(text, max_chunk_size=1000) # 3. 向量化并存储 for i, chunk in enumerate(chunks): # 获取 Embedding response = openai_client.embeddings.create( model="text-embedding-3-small", input=chunk ) embedding = response.data[0].embedding # 存入 Chroma collection.add( documents=[chunk], embeddings=[embedding], ids=[f"{metadata.get('source', 'unknown')}_{i}"], metadatas=[{**metadata, "chunk_index": i}] ) return collection
• 文档解析:将 PDF/Word/HTML 转为纯文本,保留结构信息
• 切分策略:固定长度简单,语义边界保持完整,递归切分平衡两者
• Embedding 模型:中文优先 BGE/Jina,通用用 OpenAI,高精度用 text-embedding-3-large
• 向量数据库:本地用 Chroma,生产用 Qdrant/Milvus,已有 PG 用 pgvector
下一篇,我们将深入检索与重排序——如何从向量数据库中找到最相关的文档,以及如何用 Reranker 进一步提升检索质量。
• LangChain — Text Splitters
• OpenAI — Embeddings Guide
• Chroma — Getting Started
• Qdrant — Quick Start
以上就是本文的全部内容。如果觉得有收获,欢迎点赞、在看、转发,这是对 江小湖 最大的鼓励。
如有疑问或建议,欢迎在评论区留言交流。
📖 近期文章
• LLM 记不住、会编造、不懂你的业务?RAG 一次解决这三个问题
• 上下文工程的坑都在哪?四种失效模式 + 七个反模式避坑指南
• 压缩还不够?上下文卸载与隔离:Agent 长任务保持清醒的两个高阶策略
夜雨聆风