RAG 的七个“默认参数陷阱“
我照着一篇教程把 RAG demo 跑通了:LangChain 一把梭,chunk_size、top_k 全用示例里的值,示例问答对答如流。然后我把自己的财报 PDF 灌进去,问“毛利率同比变了多少“,答案开始离谱——一会儿引用错年份,一会儿干脆编数。我把代码逐行过了一遍,没有 bug。坏的是那一排从教程里原样抄下来、一个都没动过的参数。
一、默认值是一组隐形假设
教程的默认值不是随便拍的,它是按教程的 demo 数据调出来的:短、干净、英文、单一文档。512、50、5 这一串数字背后,是一组关于“你的数据长什么样“的隐形假设。demo 数据遵守这些假设,所以能跑;我的财报 PDF 不遵守,所以翻车。
我自己整理全流程笔记时记过一句旁证:向量库的索引参数也一样,“默认参数往往是通用场景的折中”。折中的意思是——对谁都不算错,对谁都不算对。
所以下面七个陷阱,我按同一副骨架讲:这个默认值是哪来的、它在我的场景里为什么错、怎么定我自己的值。骨架只说这一次,后面直接上事故。
二、陷阱一:chunk_size = 512
事故先讲。我在库里灌了一份年报,问“营业利润是多少“,系统答得斩钉截铁,数字却是错的。查到最后不是模型的锅:一张利润表被 512 token 的切分拦腰截断,表头在 chunk A,数字在 chunk B,分属两个 chunk,检索回哪一半都拼不出完整的行列对应。
512 这个数从哪来?一大批 LangChain 风格教程的示例代码——固定分块,每 512 token 一刀。它不是哪个框架强制的官方默认,更不是业界标准,只是被抄得足够多。它假设的是“通用文本段落远小于 512“:博客、FAQ 这类数据,一段几十到一两百 token,怎么切都安全。财报表格不遵守这个假设,一张三行表头的完整利润表轻松超过 512。
这里还埋着一个更细的坑:很多人嘴里的“512 token“,在代码里其实不是 token。LangChain 的 CharacterTextSplitter 和 RecursiveCharacterTextSplitter 默认按字符数算 chunk_size,只有用 from_tiktoken_encoder() 创建的 splitter 才真的按 token 计。抄教程时以为是 512 token,实际切的是 512 个字符——中文一个字一个字符,两者差出快一倍,切出来的东西和你想的根本不是一回事。
我的改法是不再全局设 chunk_size,按语义边界切:段落归段落,表格归表格,章节归章节。切完是一个 200 到 1500 token 的不均匀分布——一张 900 token 的完整表格就让它独占一个 chunk。关键指标不是“平均大小“,是“语义完整“:每个 chunk 单独拿出来,人能看懂它在说什么。
反过来也得防一手。既然 512 会切烂,那往大了切总行吧?也不行。一个 1000 token 的 chunk 里如果只有一句话跟 query 相关,embedding 会被大量无关内容稀释,相似度反而掉下来。检索粒度和上下文粒度是两回事:检索要的是小且准,上下文要的是完整,这两个目标没法靠一个全局数字同时满足。
三、陷阱二:chunk_overlap = 50
和 512 同根的问题,所以这一节故意短。
固定重叠 50 token 的本意是好的:让跨边界的语义有个缓冲。问题在于 50 是按 token 数硬数出来的,重叠区的起止位置纯靠运气——可能从一句话的中间开始,到另一句话的中间结束。你以为重叠上了,其实重叠了个寂寞,两边都是残句。
改法:把上一个 chunk 的最后一个完整句子(如果做过结构化解析,就是最后一个 TextBlock)作为下一个 chunk 的开头。跨边界的语义承接用完整句子做,不用 token 数做。说完即走。
四、陷阱三:top_k = 5
先看一个我库里的真实现象。库里有同一家公司多个年份的财报,问“研发投入占比“,top-5 回来的 chunk 全是同一年的。不是那一年最相关,是这五个 chunk 彼此最相似——同一模板、同一段话术,只有年份数字不同。相似度排出来的 top-5,是“最像的一簇“,不是“信息最多元的五条“。
top_k = 5 出自各处 demo 里 similarity_search 的默认写法,它隐含假设“库里相关文档少而分散“。所以 top_k 不该是常数,应该是库规模的函数。我在自己库上的起步值:小于 1000 个 chunk 的小库,5 到 10;上万个 chunk 的大库,10 到 20。说清楚,这是我摸出来的起步值,不是标准,你的库分布不同就得重调。
对时间敏感的库还有第二层问题:光调大 k 没用,得加多样性约束。MMR(最大边际相关性)或者带 diversity 的重排,专门防 top-k 被同一簇相似内容挤满。
代价也得摆出来:k 太小会遗漏关键信息,k 太大引入噪声、浪费 token、白花钱。这个度只能在检索质量评估里找,拍脑袋定不下来。
五、陷阱四:embedding 直接用默认模型
反问一句:你的文档是中文财报,为什么用一个主要为英文语料调出来的模型?
很多教程的默认 embedding 是 OpenAI 的 ada-002——不是因为它差,而是教程写的时候它调起来最省事。放到今天还要补一刀:ada-002 已经是 OpenAI 官方标注的 legacy 模型,现行推荐是 text-embedding-3-small / 3-large,照着老教程抄配置前先意识到这一点。坑不在“差“,在“它假设了你的数据语言分布“。我的文档几乎全是中文,这个假设从一开始就不成立。我在自己的中文财报库上换用中文/多语言模型后,召回率提升了 5–15%【待核实:该数字出自旧稿,无测量口径;发布前若补不出“在什么库、什么评估方式下测的“,此处降级为“可感知的召回差距“】。
改法分两步。第一步,去 MTEB 的中文/多语言榜单上筛候选,比如 Qwen3-Embedding、multilingual-e5-large、bge-m3(名单有时效,和第 12 篇是同一份盘点,2026 年 5 月口径)。第二步,也是不能省的一步:在自己的库上跑一个小评估——50 个真实 query,人工标注哪些 chunk 该被召回,用召回率定案。榜单只负责给候选池,不负责给结论。
反向的迷信也得破:不是越新越大越好。通用语料上的 SOTA,在金融这种专业领域可能打不过一个领域微调过的小模型。候选模型的横向盘点是第 12 篇《Embedding 模型选型:从 ada-002 到 Qwen3-Embedding》的内容,那里展开过,本篇只留方法:先榜单筛,再自库测。
六、陷阱五:没有 metadata filter
这个严格说不算“默认值“,是“默认缺失“:向量检索开箱不带任何约束,只靠相似度排序。
后果很直白:问“2025 Q4 的毛利“,系统返回 2020 Q4 的数字,相似度分数还挺高——年份错配、口径混合,全从这个“默认缺失“里长出来。向量相似度看不见时间这类强约束,这是它的能力边界,不是 bug。
但这是七个陷阱里改动最小、收益最大的一个:花一天把你领域的强约束维度列出来——财报场景就是公司、年份、季度、报表类型——每个维度加一个 metadata filter。怎么列、怎么加、query 里没写年份怎么办,第 6 篇《当向量相似度把“2020 Q4“当成“2025 Q4“》完整展开过,这里不重复。
七、陷阱六:把检索到的 chunk 全塞给 LLM
top_k 调好了,下一步的默认动作是:把 top-k 全部拼进 prompt。我翻过自己的 top-5 成分——里面混着一个目录页、一个页脚。它们因为关键词命中,相似度分数很高;信息量却是零。白占 prompt 的 token 是小事,误导模型是大事:模型看到目录里出现“毛利率“三个字,会以为真找到了相关上下文。
改法有两层。第一层是发送前做一次信息价值过滤:小于 30 token 的短 chunk,可能只是标题或页码,降权或不发;纯数字和标点占比过高的,可能是噪声,降权。30 这个阈值是我在自己数据上试出来的起步值,启发式,不是定律,换库要重新校准。
第二层更根本:破除“上下文越多越好“。Lost in the Middle 是实测过的效应——LLM 对中间位置的内容注意力最弱,塞 20 条泛泛的上下文,通常不如给 5 条以内精准的。我在生成端的做法是宁可少给,不给杂的。
八、陷阱七:迷信 reranker 总能提升
最后一个陷阱,先把一个看似矛盾的说法摆上桌。我的全流程笔记里记着一条:“不加 reranker 的检索结果,通常有 30–50% 不相关”,所以 reranker 是 ROI 最高的组件——注意这是笔记里的经验估计,没有统一测量口径。而这一节我又要说“默认加 reranker“是陷阱。矛盾吗?不矛盾,分歧在前提。
reranker 的能力边界是:只能在候选集里重排,不能引入候选集外的正确答案。候选集好,它锦上添花;候选集烂,它无能为力。
所以开不开 reranker,取决于一个可验证的事实:你的向量检索 top-20 里,是否稳定包含正确答案。稳定包含——开,reranker 能把它提到 top-3,值得;top-20 里都不稳定——别急着开,先修上游:metadata filter、chunk 策略、query 改写,哪环烂修哪环。这时候加 reranker,是白付 200–500ms 的延迟(同样是我笔记里的经验数),换一个排得更精致的错误结果。
第 10 篇《Rerank 不是瓶颈,前面的脏数据才是》把“先上游后精排“这个顺序完整展开过。这里的结论带个边界:不是“reranker 没用“,是“先验证候选集,再决定开不开“。验证方法很朴素:抽一批 query,看 top-20 命中率,够了再谈精排。
九、收尾:默认值不是起点,是别人 demo 的终点
七个默认值,在各自的 demo 里都合理。坑不在值本身,在“默认“两个字——那是别人按别人的数据调出来的终点,不该是你的起点。
而且这里面有一顿免费午餐:
- chunk 按语义边界切;
- overlap 用完整句子承接;
- top_k 按库规模定,加多样性约束;
- embedding 在自库上小评估后定案;
- 每个强约束维度加 metadata filter。
这五件事,不改模型、不加 GPU、不引入新组件,纯参数和策略调整。先吃这些,再谈 reranker 和 Agent。
但参数全调完之后,还有一个问题没解决:凭什么说系统真的行了?“感觉变好了“不算数——今天换个值,明天换个模型,没有评估基准,你分不清自己在优化还是在掷骰子。调参解决的是“哪里烂”,评估回答的是“凭什么说行“。下一篇《RAG 评估体系:为什么 benchmark 85%,真实问题只有 62%》,就讲这套凭据怎么建——以及为什么连 RAGAS 分数高都不算数。