给 RAG 加上多轮记忆:从原始追问到 standalone question
RAG实践

给 RAG 加上多轮记忆:从原始追问到 standalone question

基础 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 的第一版目标是:

  1. 能创建和复用会话。
  2. 能读取最近几条消息。
  3. 能把追问改写成 standaloneQuestion。
  4. 能用 standaloneQuestion 做 HyDE 和检索。
  5. 能保存用户问题和助手回答。

暂时不急着做长期记忆、用户画像、复杂摘要。

因为多轮问答最先要解决的是“追问能不能被正确理解”,不是一开始就做很重的记忆系统。

7. 小结

多轮 RAG 的核心不是把所有历史都塞进检索,而是把职责拆开:

真实问题负责对话体验
standaloneQuestion 负责检索
HyDE 负责增强召回
history 负责帮助回答

这样做之后,系统会更容易观察,也更容易优化。

你可能也会喜欢...

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注