RAG 深度:Chunking 策略、向量数据库和优化

发布: (2026年3月9日 GMT+8 07:11)
10 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的具体文本内容,我将为您将其译成简体中文。

几个月前,一位客户要求我构建一个针对 40,000 份法律文档——合同、保密协议、服务条款——的搜索系统。

我交付的第一版简直是一场灾难。用户提出的都是完全合理的问题,但系统返回的却是与他们需求毫不相关的文本片段。一位律师甚至直言:

“这比 Ctrl+F 还糟糕。”

他说得对。

问题不在模型本身,而在模型之前的所有环节:如何划分文档、使用哪种向量数据库以及如何检索信息。接下来的两周,我拆解了整个流水线并有条理地重新构建。这里分享的内容就是该过程的产物——包括我犯下的错误,以及你可以避免的坑。

为什么 512 token 的 chunking 是个陷阱

当我开始使用 RAG 时,我做了大家都会做的事:把文档划分为固定大小的 chunks,通常是 512 token,并有 50‑100 token 的 overlap。这是几乎所有教程的默认设置。对于简单的情况,它还能……勉强工作。

问题出现在文档具有 真实结构 时。法律合同有条款,而一个条款可能跨越三段。使用固定大小的 chunking,可能会把一个条款截成两半,这样得到的两个片段单独来看就不完整。当系统检索到其中一个片段时,模型只能基于不完整的信息进行工作。

我花了一段时间才明白,chunking 并不是软件工程的问题——而是一个 语义 问题。正确的问题不是“多少 token?”而是“哪种信息单元可以作为独立答案有意义?”

对于法律文档,答案是:完整的条款
对于源代码,答案是 函数
对于技术文章,答案是 段落小节

Source:

我实际尝试的三种 chunking 策略

1. 递归 Chunking

这是我从固定大小跳到的第一步,立刻带来了改进。LangChain 提供了 RecursiveCharacterTextSplitter,它会先尝试按段落拆分,然后按句子,最后按单词。对散文类文本效果很好,但仍然对语义视而不见。

2. 语义 Chunking

这里的情况变得有趣起来。思路是:使用 embeddings 来衡量相邻句子之间的相似度,当相似度低于阈值时进行切分。这样可以自然地捕捉主题的变化。

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# LangChain 0.3 的 SemanticChunker — 需要 langchain-experimental
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

chunker = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",   # 或 "standard_deviation"
    breakpoint_threshold_amount=85,           # 当不相似度超过第 85 百分位时切分
)

chunks = chunker.create_documents([texto_del_contrato])
# 在我的测试中:chunks 更大但在语义上更连贯
# 缺点:速度慢 — 在切分前需要处理所有 embeddings

生成的 chunks 更大(平均 300‑800 token, 对比固定的 512),但能够捕获完整的思想。检索效果明显提升——虽然不是立竿见影的戏剧性变化,但在多次测试中表现出一致的改进。

3. 父文档检索

这是让我最惊讶的技术。思路是 为搜索索引小块 chunk(获得更好的语义精度),但 在匹配时检索更大的“父” chunk。相当于同时拥有两种粒度。

我使用 LangChain 的 ParentDocumentRetriever 实现,并把完整文档存入 InMemoryStore(生产环境可换成 Redis)。结果是:检索精准且不失上下文。这是我在处理结构化层次长文档时的首选配置。

我犯的错误: 忘记了用于索引的小块必须足够小(50‑100 token),才能让 embedding 捕获到具体的概念。如果块太大,就会失去精度优势。

向量数据库:在试用五种后我的真实评价

数据库优点缺点推荐使用场景
Chroma非常适合原型开发;快速配置;与 LangChain 集成良好。对混合搜索(向量 + 关键词)和基于元数据的复杂过滤支持有限。原型和概念验证。
Pinecone托管服务;易于扩展。迁移到 v3 API 时很混乱;文档更新滞后;价格快速上升;存在真实的供应商锁定风险。预算灵活且需要完整托管的项目。
Weaviate内置 BM25;支持每个对象的多向量。相较于多数场景,配置过于复杂。需要高级混合搜索且有时间进行配置时。
pgvector不受欢迎的观点: 对于多数情况已经足够且运维更简单。如果已经在使用 PostgreSQL,添加 pgvector 非常容易。搜索速度比专用大规模解决方案慢,但在中等规模数据下表现良好。当你已有 PostgreSQL 数据库并希望使用轻量级方案时的理想选择。

使用 cross‑encoder 进行重新排序的示例(使用 pgvector)

# Paso 1: recuperar más candidatos de los que necesitamos
candidatos = retriever.get_relevant_documents(query, k=20)

# Paso 2: crear pares (query, documento) para el cross‑encoder
pares = [(query, doc.page_content) for doc in candidatos]

# Paso 3: reranking — devuelve scores, no índices
scores = reranker.predict(pares)

# Paso 4: ordenar por score y retornar top_k
candidatos_con_score = sorted(
    zip(candidatos, scores),
    key=lambda x: x[1],
    reverse=True,
)

return [doc for doc, _ in candidatos_con_score[:top_k]]

当我向法律客户展示结果时,差异显而易见:我们从“模糊相关的答案”转变为“引用准确条款的答案”。本地 cross‑encoder 的延迟开销约为 200‑400 毫秒,对于法律搜索应用来说完全可接受。

混合检索(向量 + BM25)

对于非常特定的法律术语——条款名称、条目、交叉引用——单纯的向量检索会失效,因为嵌入倾向于捕获一般意义。BM25 在稀有术语的精确匹配方面表现更好。将两者结合并使用可调权重,使我们的评估集精度从 70 % 提升到 88 %。在 Qdrant 中,这通过 sparse vectors 实现。

我今天会使用的方案

领域推荐
ChunkingParent‑document retrieval 使用小块(≈100 tokens)进行索引并检索父块(300‑500 tokens)作为上下文。如果文档结构清晰(章节、条款、函数),请显式提取,而不是盲目切分。
向量数据库Qdrant 如果你需要控制、先进功能且不想为托管服务付费。pgvector 如果你已经有 Postgres 且数据量可控。除非客户需要完全托管的云解决方案,否则我会避免使用 Pinecone
检索始终进行 reranking。相较于质量提升,计算成本微乎其微。如果预算允许,使用 Cohere Rerank;否则本地模型 ms-marco-MiniLM 也表现良好。对于专业术语密集的领域(法律、医学、源代码),加入混合 BM25。
评估如果没有指标,你根本不知道你的流水线是否提升。RAGAS 是一个合理的库,可在无需人工标注的情况下评估 RAG,使用 LLM 本身来判断相关性和 faithfulness。它并不完美——在非常特定的领域其指标并不总是能很好地扩展——但总比仅凭直觉要好。

三周前给我说我的系统不如 Ctrl + F 的律师后来给我写信:“现在我在 10 秒内找到以前要花一个小时的内容。”
那并不是魔法——而是 正确的 chunkingQdrant,以及在普通 EC2 实例上运行的 22 MB cross‑encoder

0 浏览
Back to Blog

相关文章

阅读更多 »