RAG 配置怎么调最好?6GB 显存上的 4 组对比实验

副标题: 分块多大最优?top-k 取多少?Python 和 C++ 差多少?数据说话


一、引子:系统搭好了,参数怎么设?

上一篇我们搭了一个完整的本地 RAG 系统------200 行 Python,从文本分块到 Chroma 存储再到检索推理,一条龙跑通。

但"能跑"和"跑得好"之间还有一堆问题:

  • 分块切多大? 128 字符会不会太碎?1024 字符会不会太粗?
  • 检索几条最合适? 条数越多越准,但 prompt 越长、KV Cache 越大------6GB 显存撑不撑得住?
  • Python 版 Chroma 够快吗? 如果知识库膨胀到上万条,要不要换 C++?

这篇文章用 4 组实验回答这些问题。所有数据都在同一台 GTX 1660 Ti(6GB)上实测,沿用上一篇搭建的 RAG 系统。

贯穿全文的一条线索:RAG 的参数不是孤立的------分块大小影响检索精度,top-k 影响 KV Cache 大小,最终落地到同一个问题:6GB 显存怎么分配最划算?


二、实验①:有 RAG vs 无 RAG------定量看差距

上一篇已经定性展示了 RAG 的效果,这一节上定量数据。

测试问题: "什么是 Flash Attention?"

Token 用量

维度 无 RAG +RAG(chunk=256, top-3)
Prompt tokens 11 207
Completion tokens 1,093 257
回答长度 ~600+ 字(详细但泛化) 148 字(精简但紧扣博客)

Prompt 从 11 tok 涨到 207 tok------涨了约 19 倍。但 207 tok 的成本是多少?

KV Cache 代价

用上一篇的公式算一下这笔账:

复制代码
KV Cache = 2 × layers × kv_heads × d_head × ctx × dtype_size
DeepSeek-7B: 2 × 28 × 4 × 128 × 2 = 56 KiB/token
模型 无 RAG (11 tok) +RAG (207 tok) 增量
Qwen3-8B 1.5 MiB 29.1 MiB +27.6 MiB
DeepSeek-7B 0.6 MiB 11.3 MiB +10.7 MiB

11 MiB(DeepSeek)的 KV Cache 增量,换来了一次从泛化回答到博客级精确回答的跃升。

如果对比"把整篇博客(~10K 字)塞进上下文"的方案(KV Cache 约 576 MiB),RAG 的性价比极其突出------只需要 1/50 的 KV Cache,就能得到同样精确的信息。


三、实验②:分块大小------128/256/512/1024 逐个测

文本分块的 chunk_size 是 RAG 最基础的参数。太大太小都有问题。

实验设计

用 RecursiveCharacterTextSplitter,分别以 chunk_size=128/256/512/1024 重建知识库,对同一个问题("什么是 KV Cache?")做检索 + 推理。

数据

chunk_size 片段数 检索内容总字符 质量
128 765 111 chars ❌ 信息碎片化
256 355 496 chars ✅ 均衡
512 176 1,449 chars ✅ 信息完整
1024 79 2,412 chars ⚠️ 含无关上下文

具体看 chunk=128 时的检索结果:

复制代码
[1] KV Cache = 2 × 36 × 8 × 128 × ctx × 2
[2] 每个值只用 1 byte
[3] 6GB 显存 = 6,144 MiB

------每段只有几十个字符,信息被切得支离破碎。模型拿到的是一堆"碎片"而非完整段落。

Prompt Token 与实际推理

chunk_size Prompt Tokens Completion Tokens 回答质量
128 100 291 简略:只说它是"缓存中间计算结果的技术"
256 306 439 详细:含计算公式和 Qwen3 具体数值
512 821 342 意外简略:仅提到"存储中间状态的机制"
1024 1,318 481 最完整:涵盖定义、线性增长、量化、Flash Attention

分析

这里有个反直觉的现象:chunk=512 的回答反而比 chunk=256 还简略。

原因大概率是:512 字符的 block 中包含了更多"无关上下文",而 RecursiveCharacterTextSplitter 的天然切点有时会把关键定义和解释性文字分到不同 block。检索到的 top-3 中有 2 块是上下文背景、只有 1 块包含关键信息------模型被带偏了。

结论:chunk=256 是最优值。 128 碎片化,512/1024 噪声增多,256 在信息密度和上下文完整性上取得了最佳平衡。


四、实验③:top-k 数量 vs KV Cache 开销

这个实验直接接上上一篇 KV Cache 的数据。

实验设计

固定 chunk=256,测试 top-k = 1/3/5/10 四种配置。记录每次的 prompt token 数和 KV Cache 占用。

数据

top-k Prompt Tokens Completion Tokens DeepSeek KV Cache 回答质量
1 72 283 3.9 MiB 基本定义
3 306 510 16.7 MiB 定义 + 每 token 144KB 具体数据
5 547 460 29.9 MiB 同上 + 线性增长
10 1,061 891 58.0 MiB 含公式和量化细节(但太多冗余)

可视化

复制代码
KV Cache 占用(DeepSeek-7B, ctx=4096
默认 chunk=256):

无 RAG(纯推理)       3.9 MiB  ━━
RAG top-1             3.9 MiB  ━━            (回答泛化)
RAG top-3            16.7 MiB  ━━━━━━━━━    (回答含具体数据) ✅
RAG top-5            29.9 MiB  ━━━━━━━━━━━━━━━━
RAG top-10           58.0 MiB  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━

关键拐点:top-3 到 top-5。 回答质量从 3 到 5 几乎不再提升,但 KV Cache 却从 16.7 MiB 涨到 29.9 MiB(+79%)。top-10 更是涨到 58.0 MiB。

结论

top-3 是性价比拐点。 对于 6GB 显存,每个 MiB 都珍贵------用 16.7 MiB 得到满意的回答质量,而不是用 58.0 MiB 换来几乎同样的结果。


五、实验④:Python(Chroma) vs C++(FAISS) 检索性能

这是个"未雨绸缪"的实验。目前知识库只有 355 条向量,Chroma 完全够用。但如果知识库膨胀到十万级呢?什么时候该换 C++?

基准测试

1000 次检索,每次找出 top-3。公平起见,两边都用随机生成的查询向量(排除 embedding 时间的影响)。

复制代码
测试环境:
  向量库:355 条 × 512 维(bge-small-zh)
  硬件:CPU(RAG 系统的 embedding 和检索都在 CPU 上跑,GPU 留给推理)
方案 1000 次总耗时 单次平均 QPS
Python (Chroma 纯检索) 1,649 ms 1.65 ms 606
C++ (手动向量搜索) 286 ms 0.29 ms 3,500

差距约 6 倍。 但更重要的是看清楚这 6 倍差在哪里

延迟分解

在一次完整的 RAG 检索中,时间都花在哪:

复制代码
一次性 RAG 检索延迟分解(Python + Chroma):
┌──────────────────────────────────────────────┐
│  Embedding (bge-small encode)  12.73 ms 88.5%│
├──────────────────────────────────────────────┤
│  Chroma 向量搜索                 1.65 ms 11.5%│
├──────────────────────────────────────────────┤
│  总计                          14.38 ms 100% │
└──────────────────────────────────────────────┘

检索瓶颈在 embedding,不在向量搜索! embedding 占了 88.5% 的时间,而 Chroma 本身的检索只占 11.5%。即使把检索端换成 C++/FAISS(快 6 倍),整体延迟也只会从 14.38ms 降到约 12.88ms------只快 10%。

什么时候该换 C++?

这张表告诉你什么时候需要考虑:

知识库规模 Chroma 够用? 换 C++ 的理由
< 1 万条 ✅ 完全够用 几乎没有
1 万 - 10 万条 ⚠️ 可能够用 如果每秒查询量 > 100,考虑换
> 10 万条 ❌ 需要优化 换 FAISS + 可能需要 GPU 索引

对于我们的场景(几百条博客文章),Chroma 绰绰有余。 这篇实验更多是为"什么时候该升级"提供一个决策参考。

C++ 实战(约 60 行代码)

为了方便你亲自上手体验,这里给出 C++ 版的简化实现。它不依赖任何第三方库,用余弦相似度暴力搜索 top-3:

cpp 复制代码
/**
 * 简易向量检索基准(C++ 版)
 * 编译: g++ -std=c++17 -O2 -o bench faiss_bench.cpp -lm
 * 运行: ./bench vectors.bin 355 512
 */
#include <cstdio>
#include <cmath>
#include <chrono>
#include <vector>
#include <random>

int main(int argc, char* argv[]) {
    const char* path = argv[1];
    size_t N = atoi(argv[2]);  // 向量数
    int D = atoi(argv[3]);     // 维度

    // 读取二进制向量文件
    std::vector<float> data(N * D);
    FILE* f = fopen(path, "rb");
    fread(data.data(), sizeof(float), N * D, f);
    fclose(f);

    // 随机查询 + 预热
    std::mt19937 rng(42);
    std::uniform_real_distribution<float> dist(-1, 1);
    std::vector<float> query(D);

    const int ITERS = 1000;

    auto start = std::chrono::steady_clock::now();
    for (int iter = 0; iter < ITERS; iter++) {
        // 生成随机查询向量
        for (int d = 0; d < D; d++) query[d] = dist(rng);

        // 暴力搜索 top-3
        float best_dot[3] = {-999, -999, -999};
        int best_idx[3] = {-1, -1, -1};

        for (size_t i = 0; i < N; i++) {
            float dot = 0;
            for (int d = 0; d < D; d++)
                dot += data[i * D + d] * query[d];

            for (int k = 0; k < 3; k++) {
                if (dot > best_dot[k]) {
                    for (int j = 2; j > k; j--)
                        best_dot[j] = best_dot[j-1],
                        best_idx[j] = best_idx[j-1];
                    best_dot[k] = dot;
                    best_idx[k] = i;
                    break;
                }
            }
        }
    }
    auto end = std::chrono::steady_clock::now();
    double ms = std::chrono::duration<double, std::milli>(end-start).count();

    printf("向量数: %zu, 维度: %d\n", N, D);
    printf("检索 %d 次, 总耗时: %.2f ms\n", ITERS, ms);
    printf("单次平均: %.4f ms, QPS: %.0f\n",
           ms / ITERS, ITERS / (ms / 1000));
    return 0;
}

这个版本的思路很直接:向量点积 → 排序取 top-3 → 计时。如果你安装了 FAISS,可以替换为 IndexFlatIP,代码量差不多,但在更大数据集上(10K+)性能提升显著。这也是从 Python 原型到 C++ 生产的自然演进路径。


六、实战配置对照表

把 4 组实验的结论合在一起:

场景 chunk_size top-k 检索方案 预期 KV Cache(DeepSeek)
简单问答(日常聊天) 256 1 Chroma ~4 MiB
技术问题 RAG(推荐) 256 3 Chroma ~17 MiB
长文档分析 512 5 Chroma ~30 MiB
百万级知识库(生产) 256 3 FAISS C++ ~17 MiB

标粗行是通用推荐配置------覆盖大多数场景,性价比最高。


七、总结

跑完 4 组实验,我最大的感受是:RAG 的配置不是"越极端越好",而是"找到拐点"。

复制代码
chunk_size 的拐点在 256------再大噪声增加,再小信息碎片化
top-k 的拐点在 3------再多一条,KV Cache 涨 79% 但质量不提升
检索方案的拐点在 1 万条------以下 Chroma 够用,以上考虑 FAISS/ C++

RAG 省 token → 省 KV Cache → 省显存,这条链路在实验③中被量化验证。从纯模型到 RAG top-3,你多花了 10-15 MiB 的 KV Cache,但换来的是:

  • 模型不再凭空编造答案(实验②测试②③)
  • 可以回答知识截止期之后的问题(--cache-type-v 参数)
  • 定量问题从"10-20GB"的幻觉变成"448 MiB"的精确数字

这一点点显存开销,是目前为止我们做过的性价比最高的优化


系列全篇(CSDN):

  1. 从零到一:用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战 --- 部署全流程
  2. 只有 B 级能力的大模型,怎么干出 A 级的活? --- 任务拆解方法论
  3. Agent 不是更聪明的模型,而是长了手脚的模型 --- Agent 能力框架
  4. 从 Ollama 到 llama.cpp:一次"降一层"的本地推理探索 --- 推理引擎对比
  5. KV Cache 优化实战:6GB 显存上的每一 MB 都算数 --- 上下文优化
  6. 从零搭建本地 RAG 系统:200 行 Python 让你的模型"带着资料回答问题" --- RAG 搭建
  7. RAG 配置怎么调最好?6GB 显存上的 4 组对比实验 --- 本文