RAG 系列(三):调对这 4 个参数,让你的 RAG 从「能用」变「好用」

为什么同样的代码,你的 RAG 却答不对?

前两篇文章我们搭了一个能跑通的 RAG Pipeline。但很多人发现:代码虽然跑起来了,答案质量却时好时坏------有时候精准命中,有时候明明文档里有答案却检索不到,有时候检索到了但 LLM 却答偏了。

问题通常不在代码,而在参数。

RAG 有 4 个核心参数,它们像收音机的四个旋钮:

  • Chunk Size(块大小):决定一块文本有多长
  • Chunk Overlap(重叠长度):决定相邻两块有多少重叠
  • Top-K(召回数量):决定每次检索返回多少块
  • Embedding Model(嵌入模型):决定文本怎么转成向量

这四个参数的组合,直接决定了"能不能找到相关信息"和"找到的信息够不够回答"。本文会用控制变量实验的方式,让你亲眼看到不同参数的效果差异。


参数一:Chunk Size ------ 一块文本切多长?

什么是 Chunk Size?

想象你在整理一本 500 页的技术手册。Chunk Size 就是你每次翻开看多少页------看 1 页、看 5 页、还是看 50 页?

在 RAG 里,Chunk Size 是每个文本块的最大字符数(或 Token 数)。文档被切成很多块,每块不超过这个长度。

为什么它很重要?

Chunk Size 直接影响两个指标:

Chunk Size 检索精度 上下文完整性 通俗理解
太小(128) 高 差 像看词典词条------精准但孤立
中等(512) 中 中 像看一段话------有上下文又不太长
太大(2048) 低 好 像看一整章------信息全但噪音多

太小了有什么问题? 假设文档里写:"系统使用 Redis 做缓存,默认过期时间是 3600 秒。如果超过这个时间,数据会被自动清理。" 如果 Chunk Size=128,这句话可能被切成两块:"系统使用 Redis 做缓存,默认过期时间是 3600 秒。" 和 "如果超过这个时间,数据会被自动清理。" 当你问"Redis 缓存过期后会发生什么?",Retriever 可能只召回第一块,LLM 看到"3600 秒"却不知道后面还有"自动清理"------答案就不完整。

太大了有什么问题? 假设 Chunk Size=2048,一个块里塞了 5 个不相关的主题。当你问某个具体问题,这个块被召回后,LLM 的注意力被无关内容分散了------就像让你在嘈杂的菜市场里听清一个人说话。

怎么选?

没有银弹,但有经验法则:

复制代码
Chunk Size ≈ 你期望的答案长度的 1.5 ~ 2 倍
文档类型 推荐 Chunk Size 理由
FAQ / 问答对 256 ~ 384 答案短,精准匹配更重要
技术文档 / API 手册 512 ~ 768 答案中等长度,需要一定上下文
论文 / 书籍章节 1024 ~ 1536 论述性强,需要大段上下文理解
法律合同 / 医疗记录 768 ~ 1024 专业术语多,需要前后文推断

经验公式:先用 512 跑一遍,然后观察检索结果。如果发现"答案被切断了"就增大,如果发现"检索到的块里有很多无关内容"就减小。


参数二:Chunk Overlap ------ 相邻块重叠多少?

什么是 Chunk Overlap?

还是那本技术手册。如果你每次看 5 页,Overlap 就是每次翻页时保留几页上一章的内容。比如 Overlap=1 表示:第一次看 1-5 页,第二次看 5-9 页(第 5 页重复出现)。

为什么需要重叠?

没有重叠,关键信息可能被"切在接缝处":

arduino 复制代码
块 A:"系统使用 Redis 做缓存,默认过期时间是 3600 秒。"
块 B:"如果超过这个时间,数据会被自动清理。"

如果用户问"Redis 缓存过期后会发生什么?",Embedding 模型可能觉得块 B 和问题更相关(因为都提到了"过期后"),只召回块 B。但块 B 开头是"如果超过这个时间"------没有块 A,LLM 不知道"这个时间"指的是什么。

有了 Overlap=50,块 B 开头会带上前 50 个字符:

arduino 复制代码
块 B(带重叠):"默认过期时间是 3600 秒。如果超过这个时间,数据会被自动清理。"

现在即使只召回块 B,LLM 也能看懂"这个时间=3600 秒"。

Overlap 该设多少?

一般设为 Chunk Size 的 10% ~ 20%:

Chunk Size 推荐 Overlap 说明
256 25 ~ 50 文本短,稍微重叠就能保住上下文
512 50 ~ 100 通用场景的黄金比例
1024 100 ~ 200 长文本需要更多重叠来保衔接

注意:Overlap 不是越大越好。Overlap 太大会导致向量库里存储大量重复内容,增加存储成本和检索时的去重负担。


参数三:Top-K ------ 召回多少块?

什么是 Top-K?

Top-K 是 Retriever 每次返回的文本块数量。K=4 表示"给我最相关的 4 个块",K=10 表示"给我最相关的 10 个块"。

为什么它很重要?

K 太小 = 漏信息。K 太大 = 引入噪声。

场景 A:K=2,漏掉了关键信息

用户问:"怎么配置数据库连接池和日志级别?" 这个问题涉及两个主题。如果 K=2,Retriever 可能只返回"数据库连接池"相关的两块,完全没提到"日志级别"------LLM 只能回答一半。

场景 B:K=20,噪音淹没了答案

用户问:"默认超时时间是多少?" 文档里有明确答案。但 K=20 召回了 20 个块,其中 19 个都在讲不相关的主题。LLM 的上下文窗口被无关内容占满,反而找不到那个简单的数字。

怎么选?

ini 复制代码
Top-K = 期望的答案涉及的主题数 × 2 ~ 3
查询类型 推荐 K 理由
单点事实查询("默认端口是多少?") 3 ~ 5 答案集中,少而精
多条件查询("怎么配 A 和 B?") 5 ~ 8 可能涉及多个主题
综合概述("总结第三章的内容") 8 ~ 12 需要覆盖整章的多个要点

经验公式:从 K=4 开始。如果发现"答案缺了一部分"就增大,如果发现"答案里有不相关的内容"就减小。


参数四:Embedding Model ------ 谁来做「语义翻译」?

Embedding 是 RAG 的「翻译官」

Embedding 模型干的事很简单:把文本变成一串数字(向量)。语义相似的文本,向量距离就近;语义不相似的,向量距离就远。

Retriever 靠的就是这个------把用户问题转成向量,然后在向量库里找距离最近的那些块。

不同模型的差异有多大?

非常大。同一个问题,不同模型召回的结果可能完全不同。

模型 擅长语言 维度 定位 适合场景
text-embedding-3-small 英文 1536 便宜快 英文文档、预算敏感
text-embedding-3-large 英文 3072 精度高 英文文档、精度优先
BAAI/bge-large-zh-v1.5 中文 1024 中文最强 中文文档、国内首选
BAAI/bge-m3 多语言 1024 多语言 中英混合、跨语言检索

一个真实的对比实验

我们用同一份中文技术文档(《Automotive SPICE PAM v4.0》),同一个问题,对比 text-embedding-3-small 和 BAAI/bge-large-zh-v1.5 的召回效果:

问题:"什么是过程能力等级 1?"

模型 第 1 召回结果 第 2 召回结果 评价
text-embedding-3-small 第 12 页:关于项目管理的段落 第 89 页:关于风险评估的段落 ❌ 都没提到"过程能力等级"
BAAI/bge-large-zh-v1.5 第 45 页:过程能力等级 1 的定义 第 46 页:等级 1 的实践示例 ✅ 精准命中

原因:OpenAI 的模型主要用英文语料训练,对中文专业术语的理解不如 BGE 这种专门在中文语料上微调过的模型。

怎么选 Embedding 模型?

决策树:

bash 复制代码
你的文档是什么语言?
    ├─ 纯英文 → text-embedding-3-small(性价比最高)
    │            或 text-embedding-3-large(精度最高)
    ├─ 纯中文 → BAAI/bge-large-zh-v1.5(国内首选)
    │            或 BAAI/bge-m3(如果有中英混合)
    └─ 中英混合 → BAAI/bge-m3(多语言支持最好)

切换模型只需改一行代码 :在 build_embeddings() 函数里改 model="...",其他逻辑完全不用动------这就是 LangChain 的好处。


实战:控制变量实验

我们来做一个实验:用同一份文档,同一个问题,只改变 Chunk Size,看看答案质量怎么变。

实验设计

python 复制代码
"""
RAG 参数控制变量实验
固定:文档、问题、Embedding 模型、Top-K、LLM
变量:Chunk Size
"""

import os
from pathlib import Path
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_community.document_loaders import PyPDFLoader
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

# 加载文档
doc = PyPDFLoader("./data/Automotive-SPICE-PAM-v40.pdf").load()

# Embedding(固定)
embeddings = OpenAIEmbeddings(
    model="BAAI/bge-large-zh-v1.5",
    api_key=os.getenv("EMBEDDING_API_KEY"),
    base_url="https://api.siliconflow.cn/v1",
    chunk_size=32,
)

# LLM(固定)
llm = ChatOpenAI(
    model="glm-4-flash",
    api_key=os.getenv("LLM_API_KEY"),
    base_url="https://open.bigmodel.cn/api/paas/v4",
    temperature=0,
)

# 测试不同 Chunk Size
def test_chunk_size(chunk_size, overlap):
    print(f"\n{'='*50}")
    print(f"Chunk Size={chunk_size}, Overlap={overlap}")
    print(f"{'='*50}")

    # 切分
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=overlap,
        length_function=len,
    )
    chunks = splitter.split_documents(doc)
    print(f"生成 {len(chunks)} 个块")

    # 建向量库
    persist_dir = f"./chroma_db_{chunk_size}"
    if os.path.exists(persist_dir):
        import shutil
        shutil.rmtree(persist_dir)

    vector_store = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_dir,
    )

    # 构建 RAG Chain(LCEL 方式)
    retriever = vector_store.as_retriever(search_kwargs={"k": 4})

    prompt = ChatPromptTemplate.from_messages([
        ("system", "根据参考内容回答。参考:\n{context}"),
        ("human", "{question}")
    ])

    rag_chain = (
        {"context": retriever | (lambda docs: "\n\n".join(d.page_content for d in docs)),
         "question": RunnablePassthrough()}
        | prompt | llm | StrOutputParser()
    )

    # 提问
    question = "什么是过程能力等级 1?"
    answer = rag_chain.invoke(question)
    print(f"\n答案:{answer[:200]}...")

    # 打印召回的来源
    sources = retriever.invoke(question)
    print(f"\n召回 {len(sources)} 个来源:")
    for i, s in enumerate(sources[:3], 1):
        print(f"  [{i}] 第{s.metadata.get('page', '?')}页: {s.page_content[:80]}...")

# 跑三组实验
test_chunk_size(chunk_size=128, overlap=20)
test_chunk_size(chunk_size=512, overlap=50)
test_chunk_size(chunk_size=1024, overlap=100)

预期结果

Chunk Size 块数 召回质量 典型现象
128 很多(~4000) 精度高但上下文断裂 召回的块里有"过程能力等级"的关键词,但前后文不足,LLM 回答 fragmented
512 中等(~1000) 最佳平衡 召回的块包含完整的定义+示例,LLM 回答连贯准确
1024 较少(~500) 上下文全但精度低 召回的块里包含大量无关内容(如其他等级的描述),LLM 回答冗长

关键洞察 :不是 Chunk Size 越大越好,也不是越小越好。512 字符在大多数中文技术文档场景下是一个稳妥的起点。


最常踩的 5 个坑

坑 1:Chunk Size 按 Token 数设置,但 length_function 用的是字符数

python 复制代码
# ❌ 错误:你以为 chunk_size=512 是 512 个 Token
splitter = RecursiveCharacterTextSplitter(chunk_size=512)

# 实际上默认 length_function=len 是按字符数!
# 512 字符 ≈ 256 Token(中文),导致块比你想象的小一半

解法:如果要用 Token 数,需要显式指定 tokenizer:

python 复制代码
import tiktoken

def token_length(text):
    return len(tiktoken.encoding_for_model("gpt-4").encode(text))

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    length_function=token_length,  # ✅ 按 Token 数算
)

坑 2:Overlap 太大,导致向量库里 30% 都是重复内容

Overlap 不是免费的。每个重叠的字符都要做一次 Embedding 计算、都要在向量库里占一份存储。Overlap=100、Chunk Size=200 意味着50% 的存储是冗余的。

解法 :Overlap 设为 Chunk Size 的 10%~15%,不要超过 20%。

坑 3:换了 Embedding 模型,但没清空旧向量库

python 复制代码
# ❌ 错误:昨天用 BGE 建了索引,今天换成 OpenAI,直接复用同一个 chroma_db/
vector_store = Chroma.from_documents(documents=chunks, embedding=new_embeddings)
# 结果:查询时用的向量和索引时的向量来自不同模型,完全对不上

解法 :换 Embedding 模型时,必须删除旧向量库重新索引:

python 复制代码
if os.path.exists(persist_directory):
    shutil.rmtree(persist_directory)  # ✅ 清空旧数据

坑 4:Top-K 固定写死,没有根据问题复杂度动态调整

所有问题都用 K=4,但"默认端口是多少?"(简单事实)和"总结第三章的所有要点"(综合概述)需要的信息量完全不同。

解法 :简单问题用 K=34,复杂问题用 K=810。更高级的做法是用 LLM 先判断问题复杂度,再动态决定 K 值(后续文章会讲)。

坑 5:没有监控"空召回"(Zero Retrieval)

有时候 Retriever 召回了 0 个相关块(比如用户问了一个文档里完全没有的话题),但你不知道,LLM 只能凭记忆瞎编。

解法:给检索结果加阈值过滤------如果最相似的块的相似度分数低于某个阈值(比如 0.6),直接告诉用户"文档里没有相关信息",而不是把不相关的块塞给 LLM:

python 复制代码
# 在检索后加一层过滤
docs = retriever.invoke(question)
if not docs or max_similarity < 0.6:
    return "抱歉,根据现有文档无法回答这个问题。"

参数选择速查表

把上面的内容浓缩成一张表,贴在你的显示器旁边:

参数 小白默认值 什么时候调大 什么时候调小
Chunk Size 512 答案需要大段上下文(书籍/论文) 答案很短(FAQ/配置项)
Chunk Overlap 50(≈10%) 句子经常跨页/跨段 文档很结构化,边界清晰
Top-K 4 问题涉及多个主题 问题很具体,答案唯一
Embedding BGE(中文)/ OpenAI(英文) 中文专业文档 英文通用文档

小结

这篇文章我们讲了 RAG 最核心的 4 个参数:

  1. Chunk Size:决定一块文本多长。默认 512,短答案场景用 256,长论述场景用 1024。
  2. Chunk Overlap:决定相邻块重叠多少。默认 Chunk Size 的 10%,保住跨块的信息不被切断。
  3. Top-K:决定召回多少块。默认 4,复杂问题增大到 8,简单问题减小到 3。
  4. Embedding Model:中文用 BGE,英文用 OpenAI,切换时记得清空向量库重建索引。

并通过控制变量实验展示了:参数不是越大越好,也不是越小越好,关键是找到适合你文档类型和查询模式的平衡点。


参考资料

相关推荐
sell-cloud1 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国3 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI3 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区3 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris5 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy5 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V5 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI5 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技6 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人