RAG系统 进阶 RAG 文本分块 Chunking 文档处理

文本分块策略:RAG系统的切割艺术

AIEng Hub
阅读约 25 分钟

引言

文本分块(Chunking)是RAG系统中被低估却至关重要的环节。Embedding模型再好、Reranker再精确,如果分块策略不当,检索质量也会大打折扣。

分块的目标看似简单——将长文本切成适合检索和模型处理的片段——但其中的权衡远比想象中复杂:

  • 块太小:语义被切断,检索到的块缺少完整上下文
  • 块太大:噪声过多,超出LLM的上下文窗口,检索精度下降
  • 切错位置:在关键语义处截断,导致检索结果无法理解
┌────────────────────────────────────────────────────────────────────┐
│                    分块策略对RAG质量的影响                          │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│        文本内容: "RAG系统由检索器、生成器和知识库三部分组成。"         │
│                                                                     │
│   ❌ 分块不佳:                                                     │
│    → Chunk 1: "RAG系统由检索器、生成器"                              │
│    → Chunk 2: "和知识库三部分组成。"                                 │
│      (检索"生成器"得到Chunk 1,但完整的语义被切断了)                  │
│                                                                     │
│   ✅ 分块合理:                                                     │
│    → Chunk: "RAG系统由检索器、生成器和知识库三部分组成。"              │
│      (语义完整,检索即可获得完整信息)                                │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘

本文将深入剖析6种主流分块策略的实现原理、代码示例和选型指南,帮助你在不同场景下找到最优的分块方案。

1. 固定大小分块(Fixed-Size Chunking)

最简单的分块方式:按固定的字符或Token数量切割文本。

实现原理

固定大小分块 + 重叠机制示意

原始文本: "A B C D E F G H I J K L M N O P"

chunk_size = 6, chunk_overlap = 2

Chunk 1: A B C D E F
                | 重叠
Chunk 2:      E F G H I J
                      | 重叠
Chunk 3:          I J K L M N
                            | 重叠
Chunk 4:              M N O P

代码实现

from langchain.text_splitter import TokenTextSplitter

# 按Token数分块(推荐,比字符数更准确)
token_splitter = TokenTextSplitter(
    chunk_size=512,          # 每块512个token
    chunk_overlap=128,       # 128个token的重叠
    encoding_name="cl100k_base",  # OpenAI的编码器
)

text = "..." * 1000
chunks = token_splitter.split_text(text)
print(f"分块数: {len(chunks)}")
print(f"第一块长度: {len(chunks[0])} tokens")
from llama_index.core.node_parser import SimpleNodeParser

# LlamaIndex的固定大小分块
parser = SimpleNodeParser.from_defaults(
    chunk_size=1024,
    chunk_overlap=200,
)

from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("./data").load_data()
nodes = parser.get_nodes_from_documents(documents)
print(f"生成节点数: {len(nodes)}")

优缺点

方面说明
✅ 优点实现简单、计算开销小、块大小均匀
❌ 缺点无视语义边界,可能在句子中间截断、丢失文档结构信息

适用场景

  • 快速原型验证
  • 文本结构简单且统一(如日志文件、固定格式报告)
  • 对检索精度要求不高的场景

2. 递归字符分块(RecursiveCharacterTextSplitter)

LangChain最推荐的默认分块策略。它优先尝试按段落(双换行)分割,如果块太大则逐级回退到更细的分隔符。

实现原理

RecursiveCharacterTextSplitter 分割流程

输入文本


尝试用 ["\n\n"] 分割

    ├── 所有子块 < chunk_size?──► 完成

    ▼ 否
尝试用 ["\n"] 分割过大的块

    ├── 所有子块 < chunk_size?──► 完成

    ▼ 否
尝试用 ["。"] 分割(中文句号)

    ├── 所有子块 < chunk_size?──► 完成

    ▼ 否
尝试用 [". "] 分割(英文句点+空格)

    ├── 所有子块 < chunk_size?──► 完成

    ▼ 否
尝试用 [" "] 分割(空格,最后手段)


按字符分割(保证每个块不超过 chunk_size)

代码实现

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import PyPDFLoader

# 加载文档
loader = PyPDFLoader("report.pdf")
docs = loader.load()

# 配置递归分割器——中文优化版
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=[
        "\n\n",       # 段落分隔(最高优先级)
        "\n",         # 行分隔
        "",         # 中文句号
        "",         # 中文感叹号
        "",         # 中文问号
        ".\n",        # 英文句号+换行
        ". ",         # 英文句号+空格
        " ",          # 空格
        "",           # 字符级分割(最低优先级)
    ],
    length_function=len,
    keep_separator=True,  # 保留分隔符在块尾
)

chunks = text_splitter.split_documents(docs)
print(f"文档数: {len(docs)}, 分块数: {len(chunks)}")
from llama_index.core.node_parser import SentenceSplitter

# LlamaIndex的SentenceSplitter(类似递归分割)
parser = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=64,
    paragraph_separator="\n\n",
    secondary_chunking_regex="[^,.;。!?]+[,.;。!?]?",
)

nodes = parser.get_nodes_from_documents(documents)
for i, node in enumerate(nodes[:3]):
    print(f"Node {i}: {len(node.text)} chars")
    print(f"  来源: {node.metadata.get('file_name')}")

选用建议

# 根据文档类型调整 separators

# Markdown文档:保留标题层级
markdown_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1024,
    chunk_overlap=128,
    separators=[
        "\n## ",     # H2标题
        "\n### ",    # H3标题
        "\n#### ",   # H4标题
        "\n\n",      # 段落
        "\n",        # 行
        "",        # 句号
        " ",         # 空格
        "",
    ],
)

# 代码文档:保留代码块结构
code_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1024,
    chunk_overlap=64,
    separators=[
        "\ndef ",     # 函数定义
        "\nclass ",   # 类定义
        "\n\n",       # 空行
        "\n",         # 行
        " ",          # 空格
        "",
    ],
    is_separator_regex=True,
)

3. 语义分块(Semantic Chunking)

利用Embedding模型计算句子间的语义相似度,在语义边界处切割。这是目前最先进的分块策略,适合对检索质量要求极高的场景。

实现原理

语义分块流程

原始文本 → 句子级分割 → 计算相邻句子相似度 → 


                          ┌─────────────────────┐
                          │  相似度变化分析       │
                          │                      │
                          │  得分 │              │
                          │   1.0├─┬─┬──        │
                          │   0.8│ │ │ ┌─       │
                          │   0.6│ │ │ │ ┌───   │
                          │   0.4│ │ │ │ │ ┌──  │
                          │      └─┬─┬─┬─┬─┬────  │
                          │        │ │ │ │ │      │
                          │        句子序列        │
                          └────────┬────────────┘

                 相似度骤降点 ←─────┘


                 在语义边界处切分


                语义完整的文本块

代码实现

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# OpenAI方式
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

semantic_splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",  # 或 "standard_deviation" / "interquartile"
    breakpoint_threshold_amount=80,          # 百分位数阈值,越高分块越少
)

text = open("long_article.txt").read()
chunks = semantic_splitter.split_text(text)
print(f"语义分块数: {len(chunks)}")
# 手动实现语义分块(更精细控制)
from sentence_transformers import SentenceTransformer
import numpy as np

class SemanticChunkerManual:
    def __init__(
        self,
        model_name: str = "BAAI/bge-small-zh-v1.5",
        threshold: float = 0.75,
        min_chunk_size: int = 100,
        max_chunk_size: int = 1000,
    ):
        self.model = SentenceTransformer(model_name)
        self.threshold = threshold
        self.min_chunk_size = min_chunk_size
        self.max_chunk_size = max_chunk_size
    
    def split_text(self, text: str) -> list[str]:
        # 1. 先按句子分割
        sentences = self._split_sentences(text)
        if len(sentences) <= 1:
            return [text]
        
        # 2. 计算句子Embedding
        embeddings = self.model.encode(sentences)
        
        # 3. 计算相邻句子余弦相似度
        similarities = []
        for i in range(len(embeddings) - 1):
            sim = np.dot(embeddings[i], embeddings[i+1])
            sim /= (np.linalg.norm(embeddings[i]) * 
                    np.linalg.norm(embeddings[i+1]))
            similarities.append(sim)
        
        # 4. 在相似度骤降点处切割
        chunks = []
        current_chunk = []
        current_size = 0
        
        for i, sent in enumerate(sentences):
            current_chunk.append(sent)
            current_size += len(sent)
            
            is_last = (i == len(sentences) - 1)
            is_too_big = current_size >= self.max_chunk_size
            is_semantic_break = (
                i < len(similarities) and 
                similarities[i] < self.threshold and
                current_size >= self.min_chunk_size
            )
            
            if is_last or is_too_big or is_semantic_break:
                chunks.append("".join(current_chunk))
                current_chunk = []
                current_size = 0
        
        return chunks
    
    def _split_sentences(self, text: str) -> list[str]:
        """中文句子分割"""
        import re
        # 按中文句号/问号/感叹号/换行分割
        sentences = re.split(r'(?<=[。!?\n])', text)
        return [s.strip() for s in sentences if s.strip()]

优缺点

方面说明
✅ 优点语义完整度高、检索精度最优、自适应文本结构
❌ 缺点需要额外Embedding计算、速度较慢(约2-5x)、依赖模型质量

适用场景

  • 长文档(书籍、研究报告、法律文书)
  • 对检索质量要求极高的生产系统
  • 文本结构复杂、语义跨度大的文档

4. 基于文档结构的分块

利用文档本身的层级结构(标题、章节、列表等)进行分块,保持原文的逻辑框架。

实现原理

文档结构分块示例

# 用户手册
## 第一章:安装指南
### 1.1 系统要求
内容...
### 1.2 安装步骤
内容...

## 第二章:使用说明
### 2.1 基本操作
内容...
### 2.2 高级功能
内容...

         ↓ 基于标题层级分块

Chunk 1: "第一章:安装指南 > 1.1 系统要求\n内容..."
Chunk 2: "第一章:安装指南 > 1.2 安装步骤\n内容..."
Chunk 3: "第二章:使用说明 > 2.1 基本操作\n内容..."
Chunk 4: "第二章:使用说明 > 2.2 高级功能\n内容..."

代码实现

from langchain.text_splitter import MarkdownHeaderTextSplitter

# Markdown文档按标题分块
markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ("#", "h1"),
        ("##", "h2"),
        ("###", "h3"),
    ]
)

md_text = """
# 产品文档

## 安装指南
### 系统要求
需要Python 3.9以上版本。
### 配置说明
修改config.yaml中的参数。

## API参考
### 认证
使用API Key进行身份验证。
"""

sections = markdown_splitter.split_text(md_text)
for section in sections:
    print(f"\n--- 章节: {section.metadata} ---")
    print(section.page_content[:80])
# HTML文档按标签分块
from langchain.text_splitter import HTMLHeaderTextSplitter

html_splitter = HTMLHeaderTextSplitter(
    headers_to_split_on=[
        ("h1", "h1"),
        ("h2", "h2"),
    ]
)

html_text = """
<html>
<body>
<h1>产品文档</h1>
<h2>安装指南</h2>
<p>安装步骤说明...</p>
<h2>API参考</h2>
<p>API文档内容...</p>
</body>
</html>
"""

html_docs = html_splitter.split_text(html_text)
# LlamaIndex的MarkdownNodeParser
from llama_index.core.node_parser import MarkdownNodeParser

parser = MarkdownNodeParser()
nodes = parser.get_nodes_from_documents(documents)

for node in nodes:
    print(f"Node: {node.text[:50]}...")
    print(f"Metadata: {node.metadata}")

优缺点

方面说明
✅ 优点保留文档逻辑结构、元数据丰富、便于溯源
❌ 缺点对非结构化文档无效、标题缺失时效果差、块大小不均匀

适用场景

  • Markdown/HTML文档(技术文档、Wiki、博客)
  • 学术论文(有明确章节)
  • 法律合同和规范文档

5. 基于递归合并的分块(Recursive Chunk Merging)

在递归分割的基础上,将过小的相邻块合并,达到最小块大小要求。

实现原理

递归合并流程

输入文本


递归分割(按照 RecursiveCharacterTextSplitter 逻辑)


┌────────────────────────────────────────────┐
│  Chunk 1 (300 chars)  ← 太小                  │
│  Chunk 2 (250 chars)  ← 太小                  │
│  Chunk 3 (800 chars)  ← 合适                  │
│  Chunk 4 (200 chars)  ← 太小                  │
│  Chunk 5 (900 chars)  ← 合适                  │
└────────────────────────────────────────────┘


┌────────────────────────────────────────────┐
│  Chunk 1+2 (550 chars)  ← 合并后合适         │
│  Chunk 3 (800 chars)    ← 保持               │
│  Chunk 4+5 (1100 chars) ← 合并后合适         │
└────────────────────────────────────────────┘

代码实现

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.text_splitter import MarkdownHeaderTextSplitter
from langchain.schema import Document
from typing import List

class MergingChunkSplitter:
    """递归合并分块器"""
    
    def __init__(
        self,
        min_chunk_size: int = 300,
        max_chunk_size: int = 1500,
        separators: list = None,
    ):
        self.min_chunk_size = min_chunk_size
        self.max_chunk_size = max_chunk_size
        self.separators = separators or ["\n\n", "\n", "", ". ", " ", ""]
    
    def split_documents(self, documents: List[Document]) -> List[Document]:
        chunks = []
        for doc in documents:
            # 先递归分割
            splitter = RecursiveCharacterTextSplitter(
                chunk_size=self.max_chunk_size,
                chunk_overlap=0,
                separators=self.separators,
            )
            raw_chunks = splitter.split_text(doc.text)
            
            # 合并过小的块
            merged = self._merge_chunks(raw_chunks)
            
            for chunk_text in merged:
                chunks.append(Document(
                    page_content=chunk_text,
                    metadata={**doc.metadata, "merged": len(raw_chunks) > len(merged)}
                ))
        
        return chunks
    
    def _merge_chunks(self, chunks: List[str]) -> List[str]:
        merged = []
        buffer = ""
        
        for chunk in chunks:
            if not buffer:
                buffer = chunk
            else:
                # 如果合并后不超过最大大小,则合并
                candidate = buffer + "\n" + chunk
                if len(candidate) <= self.max_chunk_size:
                    buffer = candidate
                else:
                    # 如果buffer太小,强制合并
                    if len(buffer) < self.min_chunk_size:
                        buffer = candidate
                    else:
                        merged.append(buffer)
                        buffer = chunk
        
        if buffer:
            merged.append(buffer)
        
        return merged


# 使用示例
splitter = MergingChunkSplitter(
    min_chunk_size=300,
    max_chunk_size=1200,
)
result = splitter.split_documents(docs)
print(f"分块前: 自动分割, 分块后: {len(result)}")

6. 基于Agent的智能分块

使用LLM智能识别文档中的语义边界。这是最灵活但也最昂贵的方式。

实现原理

from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import Document
from typing import List

class LLMBasedChunker:
    """基于LLM的智能分块"""
    
    def __init__(self, model: str = "gpt-4o-mini"):
        self.llm = ChatOpenAI(model=model, temperature=0)
        self.chunk_prompt = ChatPromptTemplate.from_messages([
            ("system", """你是一个文档分块专家。请分析以下文本,将其分割成语义完整的块。
每个块应包含一个独立且完整的主题。输出格式为JSON数组:
[
  "第一个块的内容...",
  "第二个块的内容..."
]

分块原则:
1. 每个块必须有独立的主题和完整语义
2. 不要切断段落中间
3. 尽量让每个块在200-800个汉字之间
4. 列表和代码块不要分开
5. 如果文档太短,返回单个块"""),
            ("human", "{text}"),
        ])
    
    def split(self, text: str) -> List[str]:
        if len(text) < 1000:
            return [text]  # 短文档不分块
        
        try:
            response = self.llm.invoke(
                self.chunk_prompt.format(text=text[:8000])
            )
            import json
            chunks = json.loads(response.content)
            return chunks if isinstance(chunks, list) else [text]
        except Exception as e:
            print(f"LLM分块失败: {e}, 回退到递归分割")
            # 回退方案
            from langchain.text_splitter import RecursiveCharacterTextSplitter
            splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
            return splitter.split_text(text)

# 使用示例
chunker = LLMBasedChunker()
text = open("complex_document.txt").read()
chunks = chunker.split(text)
print(f"LLM智能分块数: {len(chunks)}")

应用场景

  • 高度复杂的文档结构
  • 需要理解和保留领域知识的分块
  • 对分块质量要求极高,且预算充足的场景

7. 分块策略对比总览

策略语义完整性实现复杂度计算开销执行速度适用文档类型
固定大小★★★☆☆★☆☆☆☆★☆☆☆☆极快任意(但效果差)
递归字符★★★★☆★★☆☆☆★★☆☆☆通用(推荐默认)
语义分块★★★★★★★★★★★★★★★长文档、复杂文档
文档结构★★★★☆★★★☆☆★★☆☆☆Markdown/HTML
递归合并★★★★☆★★★☆☆★★☆☆☆通用(块大小优化)
LLM智能★★★★★★★★★★★★★★★★极慢复杂文档、预算充足

8. 块大小优化:关键权衡

块大小与检索质量的关系

检索质量 vs 块大小(典型曲线)

检索质量

1.0 │          ╱╲
    │        ╱    ╲
0.8 │      ╱        ╲
    │    ╱            ╲
0.6 │  ╱                ╲
    │╱                    ╲
0.4 │                        ╲
    │                          ╲
0.2 │                            ╲
    │                              ╲
0.0 └──────────────────────────────────→ 块大小 (tokens)
    0    256   512   768  1024  1536  2048
         ↑     ↑          ↑
         |     |          |
      代码片段 通用最佳   长文档
      问答     配置       摘要

按文档类型推荐块大小

文档类型推荐块大小推荐重叠说明
代码片段/问答128-25616-32每个块聚焦单个概念
新闻文章/博客256-51232-64段落级粒度
技术文档/教程512-102464-128章节级粒度
学术论文512-102464-128按小节分割
法律合同/政策1024-2048128-256需要较长上下文
书籍/手册1024-2048128-256按章节分割
对话/聊天记录256-51232-64按对话轮次分割

块大小优化的实验方法

import numpy as np
from langchain.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter

def evaluate_chunk_sizes(
    documents: list,
    test_queries: list,
    chunk_sizes: list = [256, 512, 768, 1024, 1536],
    chunk_overlap_ratio: float = 0.2,
):
    """评估不同块大小的检索质量"""
    
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    results = {}
    
    for chunk_size in chunk_sizes:
        overlap = int(chunk_size * chunk_overlap_ratio)
        
        # 分块
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=overlap,
        )
        chunks = splitter.split_documents(documents)
        
        # 建索引
        vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=embeddings,
        )
        retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
        
        # 评估
        relevance_scores = []
        for query in test_queries:
            retrieved = retriever.get_relevant_documents(query)
            # 简单的评估:计算检索到的块与查询的语义相似度
            query_emb = embeddings.embed_query(query)
            chunk_emb = embeddings.embed_documents([d.page_content for d in retrieved])
            scores = [
                np.dot(query_emb, ce) / (np.linalg.norm(query_emb) * np.linalg.norm(ce))
                for ce in chunk_emb
            ]
            relevance_scores.append(np.mean(scores))
        
        results[chunk_size] = {
            "mean_relevance": np.mean(relevance_scores),
            "num_chunks": len(chunks),
            "avg_chunk_len": np.mean([len(c.page_content) for c in chunks]),
        }
        
        print(f"chunk_size={chunk_size}: "
              f"相关性={np.mean(relevance_scores):.4f}, "
              f"分块数={len(chunks)}")
    
    return results

# 运行评估
results = evaluate_chunk_sizes(
    documents=docs,
    test_queries=[
        "RAG系统的核心组件有哪些?",
        "如何优化检索精度?",
        "文本分块的最佳实践",
    ],
)

9. 最佳实践与常见陷阱

最佳实践

  1. 从递归字符分割器开始:大多数场景下,RecursiveCharacterTextSplitter 是平衡质量和复杂度的最佳选择。

  2. 根据文档类型定制 separators:中文文档必须包含中文标点(。!?)作为分隔符。

  3. 重叠很重要:建议重叠比例在10%-25%之间,避免关键信息落在块边界。

  4. 块大小不超过 Embedding 模型最大输入长度:如 text-embedding-3-small 最大 8191 tokens,bge-small-zh 最大 512 tokens。

  5. 保留元数据:将源文件名、页码、章节标题等作为每个块的元数据。

# 最佳实践示例
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document

def chunk_with_metadata(
    text: str,
    metadata: dict,
    chunk_size: int = 512,
    chunk_overlap: int = 64,
) -> list[Document]:
    """带元数据的智能分块"""
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "", "", "", ". ", " ", ""],
    )
    
    chunks = splitter.split_text(text)
    documents = []
    
    for i, chunk_text in enumerate(chunks):
        doc = Document(
            page_content=chunk_text,
            metadata={
                **metadata,
                "chunk_index": i,
                "total_chunks": len(chunks),
                "chunk_size": len(chunk_text),
            }
        )
        documents.append(doc)
    
    return documents

常见陷阱

陷阱问题解决方案
❌ 忽略重叠关键信息被切断,搜索不到设置10%-25%的重叠率
❌ 块太大超过模型上下文窗口控制在LLM最大输入长度的30%以内
❌ 中文用英文分隔符中文句号未被识别必须包含。!?等中文分隔符
❌ 所有文档用同一策略不同类型文档结构差异大按文档类型定制策略
❌ 忽略Embedding限制块超过模型最大输入确认模型的max_seq_length
❌ 不保留元数据无法追溯来源记录源文件、页码、标题等

10. 总结

文本分块是RAG系统中技术含量高、影响深远的环节。没有”放之四海而皆准”的最佳策略,关键在于理解不同策略的适用场景和权衡:

  • 快速原型 → 固定大小分块
  • 通用生产系统 → 递归字符分块(RecursiveCharacterTextSplitter
  • 长文档/高精度需求 → 语义分块
  • 结构化文档 → 基于文档结构的分块
  • 块大小不均匀问题 → 递归合并分块
  • 复杂文档/预算充足 → LLM智能分块

选择分块策略后,务必通过实验验证效果:构建评估集、测试不同块大小、监控检索质量指标。只有经过严格实验验证的分块策略,才能支撑起一个高质量的RAG系统。

# 最终推荐:生产级分块配置
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

# 1. 分块配置(中文优化)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", "", "", "", ". ", " ", ""],
    keep_separator=True,
)

# 2. 加载和分块
documents = text_splitter.split_documents(raw_docs)

# 3. 向量化(确保模型支持您的chunk_size)
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5",  # 最大512 tokens
    encode_kwargs={"normalize_embeddings": True},
)

# 4. 构建索引
vectorstore = Chroma.from_documents(
    documents=documents,
    embedding=embeddings,
    collection_metadata={"hnsw:space": "cosine"},
)

记住:分块策略的选择永远服务于检索质量。多实验、多评估,找到最适合您数据的分块方案。