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