P1 AIP
会话记忆:让 AI 记得你刚问过什么
以前追问必须把条件说全,"上海的客户金额多少"说成"他们的金额呢"就会掉到兜底。V5 的会话记忆把每轮对话落库(anl_conversation_memory),查询时组装"最近 6 轮原文 + 更早轮次 LLM 摘要"注入 Text2SQL 上下文;查询成功才回写记忆,失败轮次不写,每天凌晨 4 点清理 30 天前的旧记忆。看完这 3 个故事,你就知道追问为什么会"记得住",以及怎么查看、清空、清理记忆。
业务分析师
数据工程师
管理员
会话记忆
多轮对话
摘要
共 3 个故事
能 / 不能速览
✅ 这个主题能做
- 每轮对话落库持久化(anl_conversation_memory,复合索引 session_id + created_at),服务重启不丢
- Append 时正则抽取实体提及:snake_case 表名/列名(sales_data、sales_amount)+ CamelCase 指标名(TotalRevenue、GMV)
- Context 组装:最近 6 轮保留原文 + 更早轮次按 summary;无摘要的旧轮滚动 LLM 批量压缩一次并回写,失败降级截断
- 查询成功才回写记忆(用户问题 + 助手回答摘要),失败轮次不写,避免污染上下文
- 记忆上下文按 token 预算截断(默认 1200),优先保留最近原文
- API:GET|DELETE /chat/sessions/:sid/memory、GET /chat/sessions;清理 job aip:memory-cleanup 每日 4 点删 30 天前
⛔ 这个主题做不了
- 会话键是 user_id(当前 NLQ 无显式会话参数,每个用户一条记忆流),不支持细分会话
- 实体提及抽取是正则启发式,只认 snake_case 表/列与 CamelCase 指标名,不含业务语义
- 旧轮压缩需要 LLM;无 LLM 时降级截断(跳过摘要段),不影响最近 6 轮原文
- 记忆不是对话历史库,最近 6 轮原文外更早的内容只剩摘要,原文被压缩丢弃
- 保留期 30 天是常量,清理 job 每天执行一次;助手回答记忆文本最多 2000 字符
适用角色
本主题面向三个角色:
- 业务分析师:最大受益者,连续追问时条件可以"省着说",对话更自然。
- 数据工程师:可查看记忆条目,确认系统实际"听懂"了哪些表/指标(entities 列)。
- 管理员:关注清理 job(aip:memory-cleanup 0 4 * * *)与敏感会话的删除。
记忆随登录用户自动生效(以 user_id 为会话键,匿名回退 default),管理页面 /memory(MemoryManagePage.vue)。
能力速览(能做什么)
记忆持久化
anl_conversation_memory 表:id/user_id/session_id/role/content/summary/entities/created_at,复合索引 (session_id, created_at)。
实体提及抽取
Append 时正则抽取:snake_case 表名/列名(sales_data、order_items)+ CamelCase 指标名(TotalRevenue、AverageOrderValue),存 entities JSON。
上下文组装(Context)
最近 6 轮原文(user+assistant 成对)+ 更早轮 summary;无摘要旧轮滚动 LLM 批量压缩一次并回写,失败降级截断。
查询成功回写
NLQ 查询成功才 Append(用户问题 + 助手回答 SQL/解释/结果概要,截断 2000 防膨胀);失败轮次不写。
记忆管理 API
GET /chat/sessions 会话概览;GET /chat/sessions/:sid/memory?limit= 条目列表;DELETE /chat/sessions/:sid/memory 清空(归属过滤)。
清理 job
aip:memory-cleanup 调度表达式 0 4 * * *,每日凌晨 4 点删除 created_at 早于 30 天前的记忆,返回删除条数。
调整指南(怎么调整)
- 改记忆强度:记忆注入发生在 Text2SQL 前的 rag_context("对话记忆"段);记忆服务未注入时行为与旧版本完全一致,可在接线处关掉。
- 改压缩质量:旧轮摘要由 LLM 批量压缩(一次最多 8 轮,输出形如"1. 摘要"每行一条,保留表名/指标名/关键结论 20~60 字);无 LLM 时降级截断跳过摘要段。
- 改上下文预算:默认 token 预算 1200(CJK 1 字 1 token,其余 4 字符 1 token),超预算优先保最近原文、从最旧摘要开始丢弃。
- 改会话隔离:会话键是 user_id,一个用户一条记忆流;换话题或多人共用账号时用 DELETE 清空,或换账号。
- 改保留期:30 天是常量(MemoryRetentionDays),要调需改代码;清理 job 注册在 scheduler,spec 为 0 4 * * *。
做得好的场景
会话记忆在"连续追问、口径逐步收窄"的会话里收益最明显:
- 追问不"失忆":先查"上海的客户",再问"他们的金额呢",省略句也能接上上一轮的筛选条件,不用把条件说全。
- 失败不污染:查询失败(Text2SQL 生成失败/执行失败)不写记忆,错误上下文不会误导后续查询。
- 长会话不超预算:旧轮自动压缩成摘要,记忆上下文恒在 token 预算内,prompt 不会被对话历史撑爆。
- 可管理可清理:记忆对用户可见、可一键清空;每天凌晨自动清理 30 天前的旧记忆,不越攒越大。
限制与不足
以下是明确的边界,使用前先知道:
- 会话键是 user_id:当前 NLQ 链路无显式会话参数,每个用户一条记忆流;多人共用账号记忆会混在一起。
- 实体提及是正则启发式:只认 snake_case 与 CamelCase 模式,不识别中文业务实体,无 catalog 依赖。
- 旧轮压缩依赖 LLM:无 LLM 时摘要段降级跳过(截断),最近 6 轮原文不受影响。
- 原文只保最近 6 轮:更早轮次的原文被摘要替换,细节丢失;助手回答记忆文本最多 2000 字符。
- 保留期固定:30 天清理是常量与固定调度;记忆上下文 token 截断会丢最旧摘要,有省略提示。
场景故事
故事 1
连续追问:先查"上海的客户",再问"他们的金额呢"
场景:多轮追问
角色:业务分析师
耗时:约 3 分钟
- 背景
- 王姐在 /chat 先问"查询上海的客户的订单",拿到结果后又追问一句"他们的金额合计呢"。V5 之前每句查询独立处理,第二句必须把条件说全;现在会话记忆会在组装 Text2SQL 前把上一轮的对话注入上下文,她想验证省略句能不能接上。
- 传统做法对比
- 以前追问必须把条件说全("查询上海的客户的订单金额合计"),省略句经常掉到兜底分支(confidence 0.5);现在记忆上下文自动注入,第二句"他们的金额呢"也能自动带上上一轮的 WHERE 条件。
- 角色
- 业务分析师(登录即有记忆,会话键 = user_id)。
- 操作步骤
-
- 登录 /chat,第一句把条件说全:"查询上海的客户的订单"
- 拿到结果后追问省略句:"他们的金额合计呢"
- 查看第二句返回的 sql_query 是否自动带城市过滤
- 经 GET /chat/sessions/:sid/memory 验证两轮已落库
- 系统响应
- 第二句 NLQ 返回自动带上第一句的筛选条件:
{
"intent": "data_query",
"confidence": 0.8,
"sql_query": "SELECT SUM(o.sales_amount) AS total FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.city = '上海'",
"sql_explanation": "统计上海客户的订单金额合计(沿用了上一轮对话的城市条件)",
"query_result": { "columns": ["total"], "rows": [["2985"]] }
}
注入 Text2SQL 的 rag_context 中的"对话记忆"段(可经 GET /chat/sessions/u1/memory 验证):对话记忆:
最近对话:
用户: 查询上海的客户的订单
助手: SQL: SELECT ... WHERE c.city = '上海' ...
(更早轮次若存在则显示为"历史摘要"段)
- 结果洞察
- 每轮查询成功后 Append 两条记忆(user 原文 + assistant 的 SQL/解释/结果概要,截断 2000 防膨胀);Context 组装最近 6 轮原文 + 更早轮次摘要,作为 rag_context 的"对话记忆"段注入。Append 时正则抽取实体提及(snake_case 表/列名 + CamelCase 指标名)。王姐对比两次返回,确认第二句自动带上了 c.city='上海',总金额 2985 与第一句明细对得上。
- 调整建议
- 追问省条件虽然可行,关键口径仍建议说全以降低歧义;换话题时先清空记忆(DELETE /chat/sessions/:sid/memory)避免上一话题干扰。
- 动手试一试
- 登录:http://127.0.0.1:18080,admin / admin1。操作:/chat 先问"查询上海的客户的订单",再追问"他们的金额合计呢"。预期结果:第二句 SQL 自动带 c.city='上海',total=2985;GET /chat/sessions/admin 能看到该会话与消息数。
- 限制提示
- 记忆只回写成功的查询(失败轮次不写);无 LLM 时旧轮摘要降级截断,但最近 6 轮原文不受影响;会话键是 user_id,共用账号时记忆会串。
故事 2
记忆管理:查看系统"听懂"了什么,一键清空敏感会话
场景:记忆管理
角色:数据工程师 + 业务分析师
耗时:约 5 分钟
- 背景
- 陈工想看看系统实际记住了什么:表名、指标名抽得准不准。王姐则担心某次查询涉及敏感口径,想确认记忆内容并清掉。两人都到 /memory 页(MemoryManagePage.vue),先看会话概览,再逐条看记忆,最后清空。
- 传统做法对比
- 对话记录对用户不可见,只能猜系统记住了什么;现在记忆条目可读(含 entities 实体提及列)、可清空,数据工程师能据此校准"系统听懂的表/指标清单"。
- 角色
- 数据工程师(查看实体提及)+ 业务分析师(清空敏感记忆)。
- 操作步骤
-
- GET /chat/sessions 看会话概览(session_id/message_count/last_activity)
- GET /chat/sessions/:sid/memory?limit=50 看记忆条目
- 核对 entities 列的表名/指标名抽取
- DELETE /chat/sessions/:sid/memory 清空该会话记忆
- 系统响应
- 三个管理端点的返回:
// GET /chat/sessions
{ "code": 0, "data": [
{ "session_id": "u1", "user_id": "u1", "message_count": 24, "last_activity": "..." }
] }
// GET /chat/sessions/u1/memory?limit=50
{ "code": 0, "data": [
{ "id": "<uuid>", "user_id": "u1", "session_id": "u1", "role": "user",
"content": "查询 sales_data 表的 TotalRevenue 指标",
"summary": "", "entities": "[\"sales_data\",\"TotalRevenue\"]", "created_at": "..." },
{ "id": "<uuid>", "user_id": "u1", "session_id": "u1", "role": "assistant",
"content": "SQL: SELECT SUM(sales_amount) FROM sales_data ...",
"summary": "", "entities": "[\"sales_data\",\"sales_amount\"]", "created_at": "..." }
] }
// DELETE /chat/sessions/u1/memory
{ "code": 0, "data": { "deleted": 24 } }
- 结果洞察
- anl_conversation_memory 按 (session_id, created_at) 复合索引组织,GET 列表默认 limit 50、上限 500。entities 列是 Append 时正则抽取的实体提及数组(sales_data 命中 snake_case、TotalRevenue 命中 CamelCase 指标名),去重后按原文顺序返回。清空按 session_id + user_id 归属过滤——非匿名用户只能删自己的记忆,返回实际删除条数。
- 调整建议
- 定期看 entities 列能发现系统实际"听懂"了哪些表/指标,缺失的表名可检查是否是 snake_case 写法;敏感会话用 DELETE 手动清空比等 30 天清理更及时。
- 动手试一试
- 登录:admin / admin1。页面路径:/memory(或直接调 API)。操作:先查几轮数据,再 GET /chat/sessions 与 /chat/sessions/<sid>/memory。预期结果:概览显示 message_count;条目含 role/content/entities;DELETE 后 deleted 返回实际删除数,再查为空。
- 限制提示
- 实体提及是正则启发式,只认 snake_case 与 CamelCase 模式,中文业务实体不抽取;清单按 created_at 升序返回,超出 limit(默认 50、上限 500)会被截断。
故事 3
失败轮次不写记忆,凌晨 4 点自动清理 30 天前
场景:写回边界 + 清理
角色:管理员 + 业务分析师
耗时:约 5 分钟
- 背景
- 管理员想确认两件事:一是记忆会不会越攒越大、什么时候清理;二是查询失败的那一轮会不会被写进记忆、污染后面的上下文。王姐配合做了一次失败查询演示。
- 传统做法对比
- 有的系统把失败请求也记进上下文,后面越问越乱;且记忆只增不删越攒越大。现在失败轮次不写、每日定点清理 30 天前,记忆"只留成功、只留近期"。
- 角色
- 管理员(确认清理 job)+ 业务分析师(验证失败不写)。
- 操作步骤
-
- 查看调度任务 aip:memory-cleanup(0 4 * * *)注册情况
- 故意输入一个会失败的查询(如不存在的表/坏 SQL)
- GET /chat/sessions/:sid/memory 验证失败轮次未写入
- 核对最近成功的轮次仍保留在记忆里
- 系统响应
- 清理 job 与失败轮次行为说明:
// 清理 job(scheduler 注册)
job: aip:memory-cleanup
spec: 0 4 * * * // 每日凌晨 4 点执行
行为: 删除 created_at < now-30 天的记忆条目,返回删除条数
日志: "会话记忆清理完成" retention_days=30 deleted=N
// 失败轮次(Text2SQL 生成失败 / SQL 执行失败)
行为: 查询失败路径不调用 memory.Append
验证: GET /chat/sessions/u1/memory 中找不到该失败轮次的 user/assistant 条目
- 结果洞察
- 记忆回写位于查询成功路径(Text2SQL 生成 + 执行均成功后),失败轮次不写——避免把错误上下文写进记忆误导后续查询。清理 job 每日凌晨 4 点删除 30 天前的条目(retentionDays 默认 30),删除数大于 0 时记 Info 日志。记忆上下文还按 token 预算截断(默认 1200:CJK 1 字 1 token、其余 4 字符 1 token),超预算时优先保留最近原文、从最旧摘要开始丢弃,并追加"已按 token 预算截断"提示。
- 调整建议
- 保留期与清理 spec 是常量(MemoryRetentionDays=30、MemoryCleanupSpec="0 4 * * *"),要调需改代码;敏感/长会话建议用 DELETE 手动管理,不依赖 30 天兜底。
- 动手试一试
- 登录:admin / admin1。操作:输入一个必定失败的查询,然后 GET /chat/sessions/<sid>/memory 查看。预期结果:失败轮次没有对应条目;此前成功的轮次仍在;确认 scheduler 中 aip:memory-cleanup 已注册。
- 限制提示
- 记忆 Append 失败仅记日志不阻塞查询(不影响 NLQ 主链路);role 仅支持 user/assistant;超 30 天的记忆被清理后不可恢复;最近 6 轮原文外的旧轮只剩摘要。
常见问题
记忆存在哪里?
anl_conversation_memory 表(AIP 数据库):id/user_id/session_id/role/content/summary/entities/created_at,复合索引 (session_id, created_at),服务重启不丢。
为什么第二句不用带全条件了?
查询成功后会 Append 本轮对话(用户问题 + 助手回答摘要),下次查询前 Context 把"最近 6 轮原文 + 更早轮摘要"注入 Text2SQL 的 rag_context("对话记忆"段),模型能沿用上一轮的筛选条件。条件是"提示",关键口径仍建议说全。
记忆能关掉吗?
能。记忆服务是接线注入的(AttachConversationMemory),不注入时行为与旧版本完全一致;也可在注入处替换/移除。没有界面开关,属接线配置。
失败查询会写进记忆吗?
不会。记忆回写只在查询成功路径执行(Text2SQL 生成 + 执行均成功后),失败轮次不写,避免错误上下文误导后续查询。
30 天保留期怎么来的?
MemoryRetentionDays 默认 30,清理 job aip:memory-cleanup 的调度表达式是 0 4 * * *(每日凌晨 4 点)。保留期与调度都是常量,改需改代码。
记忆和 RAG 的 history 通道有什么区别?
RAG history 通道从 conversation_history 取最近 5 轮作为 RAG 一路召回;会话记忆是独立机制,落库 anl_conversation_memory,提供最近 6 轮原文 + 旧轮摘要并直接并入 rag_context 的"对话记忆"段。二者都服务多轮语境,但存储与组装路径不同。
主题小结
一句话:会话记忆 = 成功回写 + 最近 6 轮原文 + 旧轮摘要 + 30 天清理,让连续追问更自然、上下文不超预算。记住边界:会话键是 user_id、实体提及是正则启发式、旧轮压缩依赖 LLM、原文只保最近 6 轮、保留期固定。追问省着说,但关键口径还是说全更稳。