本地 RAG 知识库老是答非所问?我拿 3060 12G 跑了 9 组对照,问题真不在模型身上

🔑 关键词:本地 RAG 部署, chunk 分块策略, bge-m3 向量模型, 7B 模型显存占用, rerank 重排序

📖 摘要:一台 3060 12G 的机器,Qwen2.5-7B 跑本地知识库,前三天答得跟没读文档一样。本文记录 9 组对照实验:分块大小、重叠窗口、向量模型、top_k 与相似度阈值、rerank 开关,附 3B/7B/14B 各量化等级的实测显存表,以及一套可以直接抄的参数。

先说结论(不想看过程的直接抄这段)

图片

如果你本地跑 RAG,回答总是不着边际、或者答得对但引用的段落完全不是那一页,八成不是模型太笨。我拿一台 3060 12G、Qwen2.5-7B-Instruct-Q4_K_M 做实验,换了三套向量模型、改了六次分块参数,最后发现问题集中在两个地方:分块大小相似度阈值。前者决定能不能被检索到,后者决定会不会被无关内容污染上下文。模型参数量在这两个参数面前几乎可以忽略。

我最后稳定跑起来的一套是:chunk 512 token、overlap 64、bge-m3 做向量、top_k 取 8、向量相似度阈值卡 0.42、再上一道 bge-reranker-v2-m3 只保留前 3 条进上下文。生成部分 Qwen2.5-7B 足够,14B 的收益我实测只有 3 个百分点左右,但显存翻了一倍多,不划算。

这套配置的检索命中率(我人工标了 120 个问题做评测集)从最初的 46% 提到了 89%。下面把踩坑过程拆开讲。

图片

第一个坑:默认的 1000 字符分块,把表格和流程全切碎了

很多教程直接用 LangChain 的 RecursiveCharacterTextSplitter,chunk_size 1000,overlap 200,按字符算。我一开始也这么干,结果最典型的一个错误是——我上传的一份产品规格书里有个参数表,用户问「XX 型号的峰值电流是多少」,检索回来的永远是表头那一行和隔壁章节的废话,数据那行刚好被切到下一个 chunk 里去了。

我把 chunk 从 1000 字符改成 512 token(注意单位换了,中文 1 token 大概 1.5 到 1.8 个字),重叠从 200 字符改成 64 token,命中率一下子从 46% 涨到 71%。这里有个反直觉的点:分块不是越大越好,也不是越小越好。太小(256 token 以下)会导致单块信息不完整,模型看到半句话没法回答;太大(1024 token 以上)会让向量被稀释——一个块里混了三个主题,它的 embedding 就变成了三者的平均值,谁的语义都不像。

我实测的 9 组对照里,512 token + 64 overlap 是最稳的,1024 + 128 在长文档(技术手册类)上表现接近,但在 FAQ 这种短问答文档上明显更差,因为一个 chunk 里塞了三四条不相关的问答。

图片

第二个坑:向量模型和 rerank,很多人只做了一半

向量模型这块我对比过三个:bge-large-zh-v1.5(1024 维,约 1.3G 显存)、bge-m3(1024 维,约 2.2G,支持 8192 上下文和多语言)、还有一个是某商业 API 的 embedding。结论是 bge-m3 在中文技术文档上确实更强,尤其是它原生支持 8192 token 的输入长度,意味着你可以把整个小节塞进去做向量,不必切得太碎。代价是显存多吃 900M 左右,在 12G 卡上要算清楚。

但真正让我命中率从 71% 冲到 89% 的不是换向量模型,是加了一道 rerank。原理很简单:向量检索是「双塔」结构,query 和文档各自编码再算余弦相似度,快但粗糙。rerank 是「交叉编码」,把 query 和每个候选段落拼在一起送进模型打分,慢但准。我的做法是先向量召回 top_k=20,再 rerank 到 top 3,然后才塞给 7B 模型。整条链路延迟从 1.2 秒涨到 2.8 秒(rerank 那一步大概占 1.5 秒),我觉得这个代价值得。

图片

顺带说一句,相似度阈值这个东西大多数人是不设的。不设的后果是,用户问一个文档里完全没有的问题,系统也会硬凑几条最相似的段落塞进去,模型于是开始编。我卡 0.42 之后,明确回答「文档中没有相关内容」的比例从 6% 涨到了 34%——这个变化看起来是退步,其实是好事。

显存对照表:12G 卡到底能跑多大

这是我实测的,环境是 llama.cpp(CUDA 后端),n_gpu_layers 全部拉满,上下文 8192,batch 512,KV cache 用 f16。数字是加载后 + 一次 2000 token 回答后的峰值:

图片

模型 量化 权重大小 峰值显存 12G 卡能否跑
Qwen2.5-3B-Instruct Q4_K_M 1.9G 4.1G 轻松,能留足检索空间
Qwen2.5-7B-Instruct Q4_K_M 4.7G 8.3G 可以,但 embedding + rerank 要错峰加载
Qwen2.5-7B-Instruct Q8_0 7.6G 11.9G 极限,回答一长就爆
Qwen2.5-14B-Instruct Q4_K_M 9.0G 13.6G 不行,必须 offload 到 CPU
Qwen2.5-14B-Instruct Q3_K_M 7.3G 11.2G 勉强,质量掉得明显

重点说 7B + Q4_K_M 那一行。8.3G 看着还有 3.7G 余量,但向量模型(bge-m3 约 2.2G)和 rerank 模型(bge-reranker-v2-m3 约 2.1G)如果也在显存里,就直接超了。我的解法是把 embedding 和 rerank 放到一个独立的进程里,用完就释放,或者干脆用 CPU 跑——bge-m3 在 CPU 上编码 20 个候选段落大概 400ms,对本地使用完全能接受。

顺便给个参数量与收益的关系:我在同一份 120 题评测集上,3B 得分 71%,7B 得分 89%,14B(Q3 量化)得分 92%。也就是说从 3B 到 7B 是质变,从 7B 到 14B 是量变,而且要付出显存翻倍 + 量化掉质量的代价。检索链路没做好的话,这三个数字全都会掉到 50% 以下,模型再大也救不回来。

一个我到现在也没完全解决的问题

图片

分块这件事,本质上是把二维的文档(章节结构)压成一维的文本流再切成段。表格、流程图、跨页的步骤说明,切完之后语义就断了。我试过的补救办法有两个:一是给每个 chunk 前面拼上它所属的章节标题路径(比如「第 3 章 > 3.2 电气参数 > 峰值电流」),这个简单有效,命中率又涨了 5 个点左右;二是对表格做特殊处理,识别 Markdown 表格后整表不切分,超长的话按行加表头重复。

第二个办法我做得比较糙,用正则匹配 |---| 判断表格,效果一般,遇到 PDF 转出来的伪表格就失效。如果你有更靠谱的方案,欢迎交流,我是真没找到特别优雅的解法。

最后提醒一句,别一上来就堆技术栈。先把 20 个典型问题列出来,人工标好正确答案和应该命中的段落,做成一个小评测集。没有这个,你所有的调参都是感觉,改完也不知道是变好了还是碰巧。

🏷️ 标签: