基础 RAG 只能回答单轮问题。
但真实用户经常会连续追问:
Redis 缓存是什么?
那它和缓存雪崩有什么区别?
怎么解决?
后两句如果单独拿去检索,语义是不完整的。
所以多轮 RAG 的关键,不是简单把历史聊天全部塞给向量库,而是先把当前追问改写成一个独立问题。
1. 原始问题和检索问题不是一回事
多轮对话里至少有两个问题:
| 名称 | 作用 |
|---|---|
| originalQuestion | 用户真实输入,负责对话体验 |
| standaloneQuestion | 根据历史改写后的独立问题,负责检索 |
比如用户问:
那它和雪崩有什么区别?
结合上一轮历史后,可以改写成:
Redis 缓存击穿和缓存雪崩有什么区别?
这个改写后的问题更适合向量检索。
2. 推荐的多轮 RAG 流程
我现在更推荐这样的链路:
originalQuestion + 最近几条 history
-> Query Rewrite
-> standaloneQuestion
-> HyDE(standaloneQuestion)
-> standaloneQuestion 检索
-> hypotheticalAnswer 检索
-> RRF 融合
-> 大模型基于 history + originalQuestion + context 回答
-> 保存 user / assistant 消息
这里每一步的职责要分清楚:
| 阶段 | 职责 |
|---|---|
| originalQuestion | 保留用户真实表达 |
| history | 帮助理解上下文 |
| standaloneQuestion | 生成适合检索的完整问题 |
| HyDE | 生成更像知识库正文的假设答案 |
| fusedDocs | 最终给模型回答的知识上下文 |
职责分清楚之后,debug 会容易很多。
3. 检索时要不要带上用户原始追问
我一开始也考虑过三路检索:
originalQuestion 检索
standaloneQuestion 检索
hypotheticalAnswer 检索
但第一版我更倾向于两路:
standaloneQuestion 检索
hypotheticalAnswer 检索
原因是多轮追问经常很短:
那它呢?
有什么区别?
怎么解决?
这种问题如果直接参与向量检索,可能会引入噪声。
当然这不是绝对的。后续可以通过评测判断原始追问是否有帮助,但第一版先保持链路干净。
4. 回答阶段仍然要保留真实问题
虽然检索用 standaloneQuestion,但回答时不能只看 standaloneQuestion。
因为用户看到的是自己的原始问题。
回答 Prompt 可以包含:
最近对话历史
用户当前原始问题
系统改写后的检索问题
检索到的知识库上下文
这样模型既知道用户真实表达,又知道系统检索时理解成了什么。
如果改写有偏差,模型也有机会从原始问题里纠正。
5. 会话需要和用户绑定
多轮记忆不是一个全局数组。
它应该和用户、会话、问答范围绑定:
userId
conversationId
scope: global / post
postId: 单篇问答时存在
messages: user / assistant
全库问答和单篇文章问答也要区分。
否则用户在一篇文章里问的上下文,可能影响全库问答,这是不合理的。
6. 第一版不用做得太复杂
我对多轮 RAG 的第一版目标是:
- 能创建和复用会话。
- 能读取最近几条消息。
- 能把追问改写成 standaloneQuestion。
- 能用 standaloneQuestion 做 HyDE 和检索。
- 能保存用户问题和助手回答。
暂时不急着做长期记忆、用户画像、复杂摘要。
因为多轮问答最先要解决的是“追问能不能被正确理解”,不是一开始就做很重的记忆系统。
7. 小结
多轮 RAG 的核心不是把所有历史都塞进检索,而是把职责拆开:
真实问题负责对话体验
standaloneQuestion 负责检索
HyDE 负责增强召回
history 负责帮助回答
这样做之后,系统会更容易观察,也更容易优化。
