avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
  • 首页
主页 27 对话历史:让多轮 RAG 正确理解追问
文章

27 对话历史:让多轮 RAG 正确理解追问

发表于 最近 更新于 最近
作者 Administrator
44~57 分钟 阅读

对话历史:让多轮 RAG 正确理解追问

对话历史的核心不是把所有聊天记录不断塞给模型,而是保存完整会话、选择必要上下文,并把依赖上文的追问改写成可以独立检索的问题。

[TOC]

1. 本节目标

单轮 RAG 只能处理相对独立的问题:

上传文件最大是多少?  

真实用户会继续追问:

那超过以后返回什么错误码?  
这个限制管理员也一样吗?  
刚才提到的配置在哪里修改?  

这些问题包含:

那  
这个限制  
刚才提到的配置  

如果直接向量化,检索系统不知道具体指什么。

完成本节后,应能够:

设计conversation和message表  
区分原始问题和独立检索问题  
使用最近历史改写追问  
控制历史消息的token预算  
设计短期窗口和长期摘要  
保存回答来源和模型用量  
处理并发消息和消息顺序  
在权限变化后重新校验旧来源  
设计会话删除、保留和审计  

2. 多轮 RAG 流程

graph TD;  
 A["用户提交追问"] --> B["读取当前会话最近历史"];  
 B --> C{"问题是否依赖历史?"};  
 C -- "否" --> D["直接作为检索问题"];  
 C -- "是" --> E["改写为独立问题"];  
 D --> F["生成问题向量"];  
 E --> F; F --> G["按当前权限检索 chunks"]; G --> H["组装历史摘要和检索上下文"];  
 H --> I["大模型生成回答"];  
 I --> J["保存用户消息、回答和来源"];  

关键点:

用户展示的问题 = originalQuestion用于检索的问题 = standaloneQuestion```  
  
两者必须同时保留,方便调试问题改写是否正确。  
  
---  
  
## 3. 为什么不能直接塞全部历史  
  
无限追加消息会导致:  
  
```text  
输入token持续增长  
模型调用费用增加  
响应延迟增加  
旧话题干扰新问题  
错误回答在后续被反复引用  
历史中的旧权限内容继续进入Prompt  
达到模型上下文上限  

例如一段 100 轮对话中,当前问题只依赖最近两轮。

合理策略:

数据库保存完整历史  
模型只读取必要窗口  
长会话生成摘要  
检索前按需改写问题  
事实答案仍以最新检索资料为准  

4. 会话表设计

CREATE TABLE rag_conversation (  
 id uuid PRIMARY KEY, tenant_id varchar(64) NOT NULL, user_id varchar(64) NOT NULL, knowledge_base_id uuid NOT NULL, title varchar(255), summary text, summary_message_sequence bigint NOT NULL DEFAULT 0, status varchar(32) NOT NULL DEFAULT 'ACTIVE', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());  

字段说明:

字段 作用
tenant_id 租户隔离
user_id 会话所有者
knowledge_base_id 默认检索知识库
title 会话列表展示
summary 长会话压缩摘要
summary_message_sequence 摘要覆盖到哪条消息
status ACTIVE、ARCHIVED、DELETED

会话必须绑定租户和用户。

客户端知道 conversationId 不代表有权读取该会话。


5. 消息表设计

CREATE TABLE rag_message (  
 id uuid PRIMARY KEY, conversation_id uuid NOT NULL, sequence_no bigint NOT NULL, role varchar(32) NOT NULL, content text NOT NULL, standalone_question text, sources jsonb, model varchar(128), prompt_tokens integer, completion_tokens integer, trace_id varchar(128), created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT rag_message_conversation_fk FOREIGN KEY (conversation_id) REFERENCES rag_conversation(id) ON DELETE CASCADE, CONSTRAINT rag_message_sequence_uk UNIQUE (conversation_id, sequence_no), CONSTRAINT rag_message_role_ck CHECK (role IN ('USER', 'ASSISTANT', 'SYSTEM')));  

建议保存:

用户原始问题:content  
改写后的检索问题:standalone_question  
模型回答:ASSISTANT消息的content  
当时使用的sources快照  
模型名和token用量  
traceId  

5.1 为什么需要 sequence_no

仅按 created_at 排序可能遇到:

同一毫秒写入多条消息  
不同服务器时钟差异  
并发请求导致顺序不明确  

会话内递增的 sequence_no 更适合表达严格消息顺序。


6. 问题改写

6.1 示例

历史:

用户:上传文件最大是多少?  
助手:单个上传文件最大为20MB。  
用户:那超过以后返回什么错误码?  

改写:

上传文件超过20MB限制后返回什么错误码?  

改写后的问题用于:

Embedding  
向量检索  
关键词检索  
Rerank  

原始问题用于:

用户界面  
消息历史  
审计  
最终回答语境  

6.2 改写 Prompt

请根据最近对话,把用户最新问题改写成一个不依赖上下文、可以独立检索的问题。  
  
要求:  
1. 保留用户原意。  
2. 不要回答问题。  
3. 不要添加对话中不存在的事实。  
4. 不要改变知识库或权限范围。  
5. 如果问题已经独立,原样返回。  

输入:

最近对话:  
{recentMessages}  
  
最新问题:  
{question}  

输出最好使用结构化格式:

{  
 "requiresHistory": true, "standaloneQuestion": "上传文件超过20MB限制后返回什么错误码?"  
}  

7. 是否每轮都要改写

不一定。

独立问题:

什么是RAG?  
PostgreSQL如何创建HNSW索引?  

可以直接检索。

依赖问题:

那它有什么缺点?  
这个配置在哪里改?  
第二种方案呢?  

需要历史改写。

第一版也可以每轮都调用改写模型,逻辑简单但会增加一次模型调用。

后续可以使用规则快速判断:

是否出现指代词  
问题是否过短  
是否包含“刚才、上面、第二个、它、这个”等表达  

规则只能做优化,不能保证完全准确。


8. 历史窗口

第一版可以读取最近:

3到5轮问答  

一轮通常包括:

一条USER消息  
一条ASSISTANT消息  

不要简单按最近 10 条数据库记录截取,而应考虑:

消息角色是否完整  
是否包含失败回答  
是否包含系统事件  
总token是否超限  

窗口选择伪代码:

List<Message> selected = new ArrayList<>();  
int usedTokens = 0;  
  
for (Message message : messagesFromNewestToOldest) {  
 int tokens = estimateTokens(message.content()); if (usedTokens + tokens > historyTokenBudget) { break; } selected.add(message); usedTokens += tokens;}  
  
Collections.reverse(selected);  

9. Token 预算

一次多轮 RAG 调用包含:

System Prompt  
会话摘要  
最近消息  
检索上下文  
当前问题  
输出预留空间  

推荐按优先级分配:

系统规则:必须保留  
当前问题:必须保留  
检索证据:高优先级  
最近相关历史:中优先级  
长期摘要:按需  
旧消息:优先删除  

不要因为历史太长而截掉真正的检索证据。


10. 会话摘要

当历史超过阈值时,可以把较早消息压缩成摘要:

当前话题  
用户已经明确的约束  
已经确认的事实  
尚未解决的问题  

示例:

用户正在询问文件上传限制。知识库资料表明单个文件上限为20MB,  
超过限制会返回ERR_FILE_TOO_LARGE。用户尚未询问批量上传总容量。  

摘要注意事项:

摘要是模型生成内容,可能出错  
事实问题仍应重新检索知识库  
摘要不能绕过当前文档权限  
摘要要记录覆盖到哪个sequence_no  

摘要更新流程:

graph TD;  
 A["消息超过历史预算"] --> B["读取旧摘要"];  
 B --> C["选择待压缩的旧消息"];  
 C --> D["生成新摘要"];  
 D --> E["保存summary"];  
 E --> F["更新summary_message_sequence"];  
 F --> G["Prompt只使用摘要和最近窗口"];  

11. 事实来源优先级

多轮对话中可能同时存在:

知识库原文  
历史助手回答  
历史摘要  
用户陈述  

事实优先级应明确:

当前已授权知识库证据 > 用户明确提供的信息 > 历史摘要 > 旧助手回答  

旧助手回答不能自动成为新的可靠知识。

如果上一轮答错,把它原样加入下一轮 Prompt,错误会被放大。


12. 保存时机

推荐顺序:

1. 校验会话权限  
2. 保存USER消息  
3. 改写检索问题  
4. 执行RAG问答  
5. 保存ASSISTANT消息和sources  
6. 更新conversation.updated_at  

模型调用失败时:

USER消息可以保留  
可以增加FAILED状态或错误事件  
不要保存一条伪造的正常ASSISTANT回答  
允许用户重试  

也可以先生成成功后一次事务保存本轮问答,但用户消息在失败时会丢失。两种方案需要按产品体验选择。


13. 并发与重复提交

同一会话可能发生:

用户连续点击发送  
浏览器重试  
两个标签页同时提问  
网络超时后客户端重复提交  

建议请求携带:

clientMessageId  

建立唯一约束:

conversationId + clientMessageId  

处理重复请求时返回原结果或当前处理状态,避免重复调用模型。

同一会话是否允许并发生成也要明确:

简单方案:同一会话串行处理  
复杂方案:建立分支消息和父消息关系  

第一版建议串行,保证历史顺序清晰。


14. 权限和隔离

读取会话时必须满足:

conversation.tenant_id = 当前tenantId  
conversation.user_id = 当前userId  
conversation.status != DELETED  

会话中的来源在每次使用时重新鉴权。

不能因为:

用户以前看过该文档  
旧答案中包含该片段  
摘要中保存了该事实  

就绕过当前权限。

权限撤销后,问题改写也不应把旧无权内容扩写进新的检索问题。


15. 会话 API

可以设计:

POST   /conversations  
GET    /conversations  
GET    /conversations/{id}  
POST   /conversations/{id}/messages  
DELETE /conversations/{id}  
PATCH  /conversations/{id}/title  

发送消息请求:

{  
 "clientMessageId": "client_msg_001", "question": "那超过以后返回什么错误码?",  
 "stream": true}  

响应:

{  
 "conversationId": "conv_001", "userMessageId": "msg_010", "assistantMessageId": "msg_011", "originalQuestion": "那超过以后返回什么错误码?",  
 "standaloneQuestion": "上传文件超过20MB限制后返回什么错误码?",  
 "answer": "超过限制时返回 ERR_FILE_TOO_LARGE [C1]。",  
 "sources": [], "traceId": "trace_xxx"}  

16. Chat Memory 框架与业务历史

Spring AI、LangGraph 等框架都提供 Chat Memory 能力。

需要区分:

模型记忆:为了给本次模型调用提供上下文  
业务会话历史:为了展示、审计、删除、权限和统计  

框架内存不能自动替代业务消息表。

业务系统通常仍需要自己保存:

消息ID  
用户和租户  
来源  
token用量  
反馈  
删除状态  
traceId  

框架可以帮助选择和注入消息,但数据生命周期仍由业务系统负责。


17. 删除与保留

需要定义:

用户能否删除会话  
删除是软删除还是物理删除  
日志和审计保留多久  
来源快照是否一起删除  
敏感信息如何脱敏  
备份中的数据如何到期清理  

软删除示例:

UPDATE rag_conversation  
SET status = 'DELETED',  
 updated_at = now()WHERE id = :conversation_id  
 AND tenant_id = :tenant_id AND user_id = :user_id;  

软删除后普通查询必须排除 DELETED。


18. 可观测性

每轮建议记录:

conversationId  
userMessageId  
assistantMessageId  
originalQuestion摘要  
standaloneQuestion摘要  
使用的历史消息sequence范围  
使用的summary版本  
检索chunkId  
模型和token用量  
总耗时  

排查多轮问题时重点检查:

问题是否需要改写  
改写结果是否改变原意  
历史窗口是否缺少关键消息  
旧错误答案是否污染上下文  
权限变化是否及时生效  

19. 测试设计

19.1 独立问题

问题本身完整  
期望:standaloneQuestion与原问题一致  

19.2 指代追问

上一轮讨论上传限制  
追问“那错误码是什么”  
期望:改写包含上传限制语境  

19.3 话题切换

前面讨论上传  
新问题问PostgreSQL索引  
期望:不被旧话题污染  

19.4 长历史

消息超过预算  
期望:旧消息被摘要  
期望:当前问题和检索上下文不被截掉  

19.5 权限撤销

旧消息含某文档来源  
撤销权限后继续追问  
期望:旧来源不进入新Prompt  

19.6 重复提交

相同clientMessageId提交两次  
期望:只生成一次回答  

20. 常见问题

20.1 最近几轮取多少

第一版可以取 3 到 5 轮,再根据 token 和评测调整。

20.2 每轮都需要会话摘要吗

不需要。

只有历史超过预算或达到消息阈值时再更新摘要。

20.3 用户说的内容能当知识库事实吗

不能默认当作可信事实。

可以用于理解当前对话,但企业知识问答仍应以授权资料为依据。

20.4 对话标题怎么生成

第一版可以截取第一条问题。

后续可以异步生成短标题,不要阻塞第一次问答。

20.5 多轮对话一定要使用 Agent 吗

不需要。

固定的“历史读取、问题改写、检索、生成”链路就能实现可靠的多轮 RAG。


21. 练习清单

创建conversation和message表  
保存originalQuestion和standaloneQuestion  
实现最近3到5轮历史读取  
编写问题改写Prompt  
控制历史和检索上下文token预算  
实现长会话摘要  
保存sources、model、token和traceId  
使用clientMessageId防止重复提交  
完成会话租户和用户隔离  
测试指代追问、话题切换、长历史和权限撤销  

22. 小结

多轮 RAG 链路:

original question  
 -> recent history -> standalone question -> retrieval -> authorized context -> answer -> persisted messages```  
  
本节最重要的结论:  
  
> 数据库可以保存完整历史,但模型只应该看到与当前问题相关、在预算内并且仍然有权限使用的上下文。  
  
## 23. 参考资料  
  
- [Spring AI Chat Memory](https://docs.spring.io/spring-ai/reference/api/chat-memory.html)  
- [LangGraph Memory](https://docs.langchain.com/oss/python/langgraph/add-memory)  
- [Spring AI ChatClient](https://docs.spring.io/spring-ai/reference/api/chatclient.html)
AI自学路线
AI
许可协议: 
分享

相关文章

6月 16, 2026

27 对话历史:让多轮 RAG 正确理解追问

对话历史:让多轮 RAG 正确理解追问 对话历史的核心不是把所有聊天记录不断塞给模型,而是保存完&#

6月 16, 2026

26 权限隔离:让无权内容无法进入 RAG 上下文

权限隔离:让无权内容无法进入 RAG 上下文 RAG 权限隔离的底线不是“最终答案不展示秘密”,而

6月 16, 2026

25 引用来源:让 RAG 答案可以核对和追溯

引用来源:让 RAG 答案可以核对和追溯 引用来源的核心是让答案中的事实能够回到具体文档、页码&#

下一篇

上一篇

26 权限隔离:让无权内容无法进入 RAG 上下文

最近更新

  • 27 对话历史:让多轮 RAG 正确理解追问
  • 26 权限隔离:让无权内容无法进入 RAG 上下文
  • 25 引用来源:让 RAG 答案可以核对和追溯
  • 24 基础 RAG 问答
  • 23 向量检索

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy