跳到主要内容

RAG 优化

学习目标

完成本节后,你将能够:

  • 识别 RAG 系统里最常见的优化点
  • 理解 chunk、top-k、rerank、prompt 对结果的影响
  • 学会做一个简单的上下文拼装策略
  • 建立“先找瓶颈,再调参数”的优化思路

一、优化前先定位问题出在哪一段

1.1 一个 RAG 系统通常有四段

可以粗略拆成:

  1. 文档处理
  2. 检索召回
  3. 上下文拼装
  4. 生成回答

如果回答效果差,你首先要问:

  • 是没找到对的资料?
  • 还是找到了但没塞进去?
  • 还是塞进去了但模型没用好?

1.2 不同问题,对应不同优化方向

现象常见问题点
明明有答案却没检索到切块 / embedding / 检索策略
检索到了但答案还是偏prompt / context packing / 模型总结
回答很慢很贵top-k 过大 / 上下文太长 / 重排过多

二、从文档处理开始优化

2.1 Chunk 大小不是越大越好

chunk 太大:

  • 召回不精准
  • 上下文占用大

chunk 太小:

  • 信息容易被切碎
  • 证据不完整

所以常见优化不是“越大越保险”,而是找平衡。

2.2 保留结构信息经常很重要

很多文档的价值不只在句子本身,还在:

  • 标题
  • 段落层级
  • 表格归属
  • 页面位置

如果清洗时把这些结构全抹掉,后面检索质量常常会变差。


三、召回阶段最常调的几个杠杆

3.1 top_k:不是越多越好

很多人一开始觉得:

多拿一些资料,总不会错吧?

其实不一定。
top_k 太大时,可能会把无关内容也带进来,反而干扰模型。

3.2 Rerank:先广撒网,再精筛

当粗召回里混进了很多边缘内容时,rerank 很有帮助。
它不是单纯“多做一步”,而是在提高上下文质量密度。


四、上下文拼装比很多人想的更重要

4.1 模型不是“看到资料就一定会用”

即使召回到了正确内容,也可能出现:

  • 关键证据埋在中间
  • 多个 chunk 顺序混乱
  • 信息重复太多

所以“把哪些块按什么顺序塞进去”本身就是优化点。

4.2 一个可运行的上下文打包示例

chunks = [
{"score": 0.95, "text": "退款政策:购买后 7 天内且学习进度低于 20% 可退款。"},
{"score": 0.80, "text": "证书说明:完成所有项目并通过测试后可获得证书。"},
{"score": 0.76, "text": "学习顺序:建议先学 Python,再学机器学习。"},
{"score": 0.72, "text": "补充条款:退款申请需提交订单信息。"}
]

def pack_context(chunks, max_chars=60):
packed = []
total = 0
for item in sorted(chunks, key=lambda x: x["score"], reverse=True):
text = item["text"]
if total + len(text) > max_chars:
continue
packed.append(text)
total += len(text)
return packed

selected = pack_context(chunks, max_chars=60)
print("最终塞进上下文的 chunk:")
for c in selected:
print("-", c)

这就是最简单的“上下文预算管理”。


五、生成阶段怎么优化?

5.1 Prompt 要明确告诉模型“怎么用资料”

很多时候不是资料没找到,而是模型没有被明确要求:

  • 只能依据给定资料回答
  • 证据不足时要承认不知道
  • 要引用来源

一个常见的提示思路是:

“请仅根据以下资料回答;如果资料不足,请明确说资料不足。”

5.2 引用来源能显著提升可控性

让答案带上来源,通常有几个好处:

  • 用户更信任
  • 方便人工核查
  • 便于调试哪段资料生效了

六、一个简单的优化实验思路

6.1 不要一口气改五个参数

建议按这种顺序:

  1. 固定评估集
  2. 先设一个 baseline
  3. 一次只改一个变量

例如:

  • 先只改 chunk size
  • 再只改 top-k
  • 再只加 rerank

6.2 一个小型配置对比脚本

configs = [
{"chunk_size": 200, "top_k": 3},
{"chunk_size": 400, "top_k": 3},
{"chunk_size": 200, "top_k": 5}
]

fake_scores = {
(200, 3): 0.78,
(400, 3): 0.71,
(200, 5): 0.74
}

for cfg in configs:
key = (cfg["chunk_size"], cfg["top_k"])
print(cfg, "-> 评估得分", fake_scores[key])

虽然这是玩具数据,但它表达了一个很重要的工程习惯:
优化要靠对比实验,不靠感觉。


七、RAG 优化经常会遇到的权衡

7.1 质量 vs 成本

  • 更大的 top-k:可能更全,但更贵
  • 更强的 reranker:可能更准,但更慢

7.2 召回率 vs 精准率

  • 召回过少:可能漏答案
  • 召回过多:可能引入噪声

7.3 实时性 vs 稳定性

  • 实时查询新资料更灵活
  • 预处理得更充分通常更稳

没有万能最优解,只有场景最优解。


八、初学者常见误区

8.1 一上来就换更大的模型

很多 RAG 问题其实不是模型太弱,而是检索链路没调好。

8.2 只看单次 Demo,不做稳定评估

一次答对不代表系统稳定。

8.3 把 top-k 一路调大

更多上下文并不总是更好,尤其当上下文里混了太多无关块时。


小结

这一节最重要的认识是:

RAG 优化不是只改一个参数,而是在“召回质量、上下文质量、生成约束、成本速度”之间找平衡。

真正有效的优化,通常从定位瓶颈开始,而不是盲目堆更多组件。


练习

  1. 修改 pack_context() 里的 max_chars,观察被选中的 chunk 会如何变化。
  2. 自己构造一组不同的 chunk_size / top_k 配置,练习做小型对比实验。
  3. 想一想:如果系统总是“检索到了正确资料,但回答还是偏”,下一步你最该优化哪里?