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-smallBAAI/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,切换时记得清空向量库重建索引。

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


参考资料

相关推荐
ZhengEnCi1 小时前
O04-马斯克起诉OpenAI世纪诉讼全解析 📜
人工智能
夜雨深秋来1 小时前
AI技术落地典型问题分析
人工智能
RxGc1 小时前
# Agent Skills评测:让AI编程工具拥有Google级工程成熟度
人工智能·github
卷积殉铁子1 小时前
OpenClaw不装了,GPT-6硬刚:谁能拿下未来5年AI红利?
人工智能·aigc·openai
前端不太难1 小时前
AISystem:鸿蒙游戏中的 AI 行为驱动
人工智能·游戏·harmonyos
Codebee1 小时前
Harness Engineering:AICode 的灵魂
前端·人工智能·前端框架
不加辣椒1 小时前
第 1 章 大语言模型的“阿喀琉斯之踵”
人工智能
上海锝秉工控1 小时前
超声波循测仪:工程检测领域的“智慧之眼”
人工智能
数据智能老司机1 小时前
人人都能学会的提示词工程——人人都能学会的提示词工程
llm