本章目录
阅读过程中可按 Ctrl + Z 快速返回当前页面的本章目录;也可以点击右下角按钮。
本章思维导图
知识流向:先完成左侧基础,再进入中间机制,最后用右侧工程控制把它变成可上线能力。节点可直接点击跳转。
状态与记忆边界
按照本模块顺序完成以下知识点,再进入下一模块。
M01状态、上下文、历史与记忆的边界
| 概念 | 存在哪里 | 作用 |
|---|---|---|
| State 状态 | 应用数据库/状态存储 | 任务当前做到哪一步,哪些操作已完成 |
| Context 上下文 | 本次模型请求 | 模型这一轮实际能看到的信息 |
| History 历史 | 聊天消息数据库 | 保存过去消息,按需选取进入上下文 |
| Memory 记忆 | 受控长期存储 | 跨会话保存经确认的偏好或事实 |
M02多轮请求状态恢复
模型通常不会自行访问你的历史数据库;是后端在每次请求前选择材料并发送。session_id由应用生成和校验,不能信任用户随便传入别人的会话ID。
M03短期上下文裁剪
上下文窗口有限,不能无限发送全部历史。默认优先级:System安全规则 → 当前任务状态 → 当前问题 → 相关工具结果/RAG证据 → 最近必要对话 → 较早对话摘要。
摘要也是模型输出,可能漏掉否定词或金额;高风险事实必须从业务数据库重查。
长期记忆管理
按照本模块顺序完成以下知识点,再进入下一模块。
M04长期记忆的保存边界
适合保存
用户明确确认的语言偏好、常用格式、稳定业务配置,并记录来源、创建时间、置信来源和删除入口。
默认不保存
模型猜测的身份、临时情绪、密码、银行卡、一次性验证码、未经同意的敏感信息。
写入原则:长期记忆由应用策略控制;模型可以提议“这可能值得记住”,程序按同意、敏感级别和有效期决定是否写入。
M05记忆的数据结构与来源追踪
{
"memory_id":"mem-21",
"user_id":"u-7",
"type":"response_preference",
"value":"喜欢先看结论再看原理",
"source_message_id":"msg-99",
"confirmed_by_user":true,
"created_at":"2026-09-15",
"expires_at":null
}每条记忆需要稳定ID、归属用户、类型、来源、确认状态、创建与过期时间。这样才能解释“为什么记住”、修改和删除。
M06TTL、过期和遗忘机制
数据生命周期设置 TTL是Time To Live,表示数据还能存活多久。登录会话、临时任务和缓存适合TTL;稳定偏好可能无固定TTL,但也要允许用户删除。
会话状态:30分钟无活动后过期 Agent临时草稿:任务结束后7天删除 缓存答案:5分钟或知识版本改变立即失效 用户偏好:用户可查看、修改、删除
TTL到期是程序或存储系统执行删除,不是让模型“忘掉”。
一致性、隔离与隐私
按照本模块顺序完成以下知识点,再进入下一模块。
M07并发状态覆盖问题
用户快速发送两条消息,两个后端进程可能同时读取版本10,各自写入版本11,后写入的会覆盖前一个结果。这叫Lost Update,丢失更新。
默认做法:同一会话使用队列串行处理,或使用version字段做乐观锁。
UPDATE session SET state=?, version=11 WHERE session_id=? AND version=10 影响0行:说明别人已经更新,重新读取再决定,而不是强行覆盖。
M08记忆究竟放在哪里:默认存储架构与读写过程
先给结论:记忆通常不保存在大模型“脑子里”,而是保存在你自己的应用数据库中。退款客服项目默认用 PostgreSQL 保存权威记录,用 Redis 保存短期会话和缓存副本,只有需要“按意思找回”时才把记忆的向量与 memory_id 写进 向量库。每次调用模型前,由后端把需要的记忆读出来放进本轮 Prompt。
1. 四个位置分别保存什么
| 位置 | 物理上是什么 | 保存内容 | 是否权威 | 典型生命周期 |
|---|---|---|---|---|
| PostgreSQL等关系数据库 | 后端连接的持久化数据库服务 | 用户、会话、消息、任务状态、长期记忆正文、来源、确认状态、版本、过期时间和审计记录 | 是,默认真相来源 | 按业务政策长期保存,支持事务、修改和删除 |
| Redis | 独立运行的高速内存数据服务 | 当前会话状态、最近消息缓存、分布式锁、幂等键、流式进度 | 通常不是 | 几分钟到几小时,通过TTL自动过期 |
| 向量数据库 | Milvus、Qdrant或pgvector等语义检索存储 | 记忆向量、memory_id、user_id/tenant_id和少量过滤字段;可选保存短文本 | 通常不是,属于检索索引 | 跟随PostgreSQL中的权威记忆创建、更新和删除 |
| 本轮模型请求 | 后端发送给模型API的messages/prompt | 本轮选中的少量历史、摘要、记忆和当前问题 | 不是存储库 | 只服务这次调用;下一轮要由后端重新读取和发送 |
2. 一条长期记忆在数据库里的真实形状
用户说“以后回答先给结论”,并确认希望长期保存。后端可以在 PostgreSQL 的 user_memories 表写入:
user_memories ┌──────────────────┬────────────────────────────────┐ │ memory_id │ mem-21 │ │ tenant_id │ company-a │ │ user_id │ u-7 │ │ type │ response_preference │ │ value │ 喜欢先看结论再看原理 │ │ source_message_id│ msg-99 │ │ confirmed │ true │ │ version │ 1 │ │ status │ active │ │ created_at │ 2026-09-21 10:30:00 │ │ expires_at │ null │ └──────────────────┴────────────────────────────────┘
value 是记忆正文;source_message_id 证明从哪句话得来;confirmed 表示是否由用户确认;status/version 用于修改和并发控制;expires_at 决定是否过期。这些不能只放在向量库里,因为还要可靠地修改、审计和删除。
3. 为什么向量库里还会有一份
如果只有“语言=中文”“回复格式=先结论”这类固定字段,可以直接按 user_id 从 PostgreSQL 查询,不需要向量库。只有记忆数量很多、用户问法不固定,需要按语义寻找时,才增加向量索引:
向量库中的索引记录
{
"memory_id": "mem-21",
"tenant_id": "company-a",
"user_id": "u-7",
"embedding": [0.018, -0.227, ...],
"status": "active"
}
搜索命中 mem-21 后,后端最好再回 PostgreSQL 检查它是否仍为 active、有没有过期、用户是否仍有权限,再取得权威正文。向量库中的记录更像索引,不应成为唯一真相来源。
4. 写入记忆时,程序实际做什么
并清理Redis缓存
memory_id。memory_id 写入向量库。模型没有数据库写权限:模型最多提出“值得记住”,真正写入由后端工具和确定性策略执行。否则Prompt注入可能诱导系统保存错误或敏感信息。
5. 下一轮对话时,记忆怎样回到模型眼前
tenant_id/user_id
读PostgreSQL固定偏好
长期记忆候选
调用模型
# 通用伪代码:运行在AI应用后端,不在模型内部
identity = authenticate(request)
session = redis.get(f"session:{identity.user_id}:{session_id}")
preferences = postgres.load_active_preferences(
tenant_id=identity.tenant_id,
user_id=identity.user_id,
)
memory_candidates = vector_store.search(
query=current_question,
filter={
"tenant_id": identity.tenant_id,
"user_id": identity.user_id,
"status": "active",
},
)
verified_memories = postgres.verify_and_load(memory_candidates)
messages = build_prompt(
system_rules=SYSTEM_RULES,
session_state=session,
preferences=preferences,
memories=verified_memories,
current_question=current_question,
)
answer = llm.chat(messages=messages)
模型为什么表现得像“记得”?因为后端在这次 API 请求的 messages 中重新放入了“用户喜欢先看结论”等内容。模型如果没收到这些内容,通常就不知道。
6. 系统究竟怎样判断“当前需要哪几条记忆”
关键不是让模型先阅读全部记忆再挑选。那仍然等于全量加载。正确做法是在调用聊天模型之前,由后端程序和数据库完成分层检索,只把最终入选的少量文字放入 Prompt。
要做到这一点,写入记忆时就不能只存一句正文,还要同时保存便于筛选的字段:
| 字段 | 含义 | 选择记忆时怎样使用 |
|---|---|---|
scope | 作用范围,如 global、project、session | 判断是每轮固定偏好、某项目记忆,还是只属于当前会话 |
type | preference、task_fact、decision等类型 | 决定使用精确查询还是语义检索 |
subject_key | 同一事实的稳定键 | 例如 refund_project.framework,用于发现新旧冲突 |
project_id | 归属哪个项目 | 当前正在退款项目时,优先限制到该项目 |
importance | 业务设定的重要级别 | 安全约束、用户明确决定比闲聊摘要优先 |
version/status | 版本与是否有效 | 同一subject只取最新active版本 |
created_at/last_used_at | 创建和最近使用时间 | 在相关度接近时,可让较新或常用记忆优先 |
embedding | 记忆正文的语义向量 | 与当前问题向量比较,召回意思相关的任务记忆 |
7. 第一步:先分三条通道,不是所有记忆都用同一种查法
A. 固定记忆
语言、回复格式、无障碍需求等少量稳定偏好。用SQL按 user_id + type 精确读取,通常每轮带上,不需要向量搜索。
B. 当前会话状态
正在处理的订单号、工作流步骤、刚确认的选择。用 session_id 从Redis或状态表精确读取,只属于当前会话。
C. 大量长期经历
以前讨论过的项目、决定和任务事实。数量可能很多,才使用“metadata过滤 + 向量召回 + 重排”。
8. 第二步:把当前问题变成“检索条件”
假设用户问:“继续修改上周的退款客服方案,把回答改成先给结论。”后端从登录状态和当前会话中已经知道:
tenant_id = "company-a" user_id = "u-7" project_id = "refund-assistant" session_id = "session-88" current_question = "继续修改上周的退款客服方案,把回答改成先给结论"
这些确定性ID直接用作数据库过滤;当前问题和必要的任务摘要则用于生成查询向量。不能让向量相似度代替身份权限过滤。
# 在Embedding API或本地Embedding模型中执行 memory_query_text = """ 当前项目:退款客服助手 当前任务:修改上周方案 用户问题:继续修改上周的退款客服方案,把回答改成先给结论 """ query_vector = embedding_model.embed(memory_query_text)
9. 第三步:先做硬过滤,再做向量Top-k
向量库不是在全公司的所有记忆中搜索。程序同时传入查询向量和确定性过滤条件:
candidates = memory_vector_store.search(
vector=query_vector,
filter={
"tenant_id": "company-a",
"user_id": "u-7",
"project_id": "refund-assistant",
"status": "active",
},
top_k=20,
)
数据库先排除其他用户、其他项目、已删除和失效记忆,再在剩余集合中比较语义相似度。top_k=20 表示只拿20条候选供下一步精排,不代表这20条都会进入Prompt。
10. 第四步:候选怎样重新打分
向量相似度只回答“意思像不像”,还要结合重要性、新鲜度和明确的业务规则。下面的总分公式只是一个可解释起点,权重必须用真实评测调整:
final_score =
0.65 × semantic_relevance # 与当前问题的语义相关度
+ 0.20 × importance # 用户确认决定/安全约束更重要
+ 0.15 × recency # 同等相关时较新内容优先
| 候选记忆 | 语义相关 | 重要性 | 新鲜度 | 规则处理 | 结果 |
|---|---|---|---|---|---|
| A:退款客服采用RAG方案 | 0.93 | 0.90 | 0.80 | active | 保留,约0.90 |
| B:用户喜欢先给结论 | 0.68 | 1.00 | 0.95 | 固定偏好,强制通道已读取 | 去重后保留一份 |
| C:旧方案使用纯关键词搜索 | 0.88 | 0.70 | 0.20 | 被新版本 superseded | 淘汰 |
| D:用户上周讨论健身计划 | 0.15 | 0.50 | 0.90 | 低于相关阈值 | 淘汰 |
| E:退款操作必须二次确认 | 0.76 | 1.00 | 0.85 | 安全约束 | 保留 |
实际项目可以用什么做重排?简单项目用普通程序公式和规则即可;记忆正文较复杂时,可以把20条候选交给第5章讲过的Reranker,根据“当前问题—记忆”相关性重新排序。无论哪种方式,都不是让最终聊天模型先看1000条。
11. 第五步:新旧记忆冲突怎样处理
同一个事实必须有稳定的 subject_key。例如:
旧记忆: subject_key = "refund_project.retrieval_strategy" version = 1 value = "只使用关键词搜索" status = "superseded" 新记忆: subject_key = "refund_project.retrieval_strategy" version = 2 value = "使用向量+BM25+Rerank" status = "active"
后端按 subject_key 分组,只留下最高的active版本。不能仅凭“新记忆更相似”解决冲突,因为旧事实也可能与当前问题非常相似。
12. 第六步:用Token预算把候选装进Prompt
经过过滤和重排后可能还剩8条,但本轮给“记忆区”只分配800 Token。程序按优先级逐条加入,下一条放不下就跳过、截短或使用已有摘要;绝不能挤掉System安全规则和当前问题。
记忆区预算:800 Token 固定偏好: - 使用中文 8 Token - 回答先给结论 12 Token 任务记忆候选: - RAG方案决定 120 Token - 工具操作必须二次确认 80 Token - 上周未完成事项 150 Token - 页面风格决定 90 Token - 很长的历史讨论摘要 600 Token ← 放不下,跳过或压缩 最终只发送前面6条,总计约460 Token。
为什么不是固定“取5条”就结束?五条短记忆和五条长记忆占用完全不同。工程上要同时设置候选数、最低相关阈值和Token预算。
13. 把整套逻辑写成接近真实的代码
def select_memories(identity, session_id, project_id, question):
# 1. 精确读取:少量固定偏好
fixed = postgres.query_memories(
tenant_id=identity.tenant_id,
user_id=identity.user_id,
scope="global",
type="preference",
status="active",
)
# 2. 精确读取:当前会话状态
session_state = redis.get(
f"session:{identity.tenant_id}:{identity.user_id}:{session_id}"
)
# 3. 当前问题变成向量
query_text = build_memory_query(
project_id=project_id,
session_state=session_state,
question=question,
)
query_vector = embedding_model.embed(query_text)
# 4. 硬过滤后做语义召回
candidates = vector_store.search(
vector=query_vector,
filter={
"tenant_id": identity.tenant_id,
"user_id": identity.user_id,
"project_id": project_id,
"status": "active",
},
top_k=20,
)
# 5. 回权威库校验版本、过期、来源和权限
verified = postgres.verify_memory_candidates(candidates)
# 6. 去重、消除新旧冲突并重新排序
ranked = rank_and_resolve_conflicts(
verified,
question=question,
)
# 7. 固定偏好 + 相关记忆,在预算内装入Prompt
return pack_by_token_budget(
fixed=fixed,
ranked=ranked,
max_memory_tokens=800,
)
真正的“需要”是谁判断的?不是某一个神奇模型一次决定,而是多层共同决定:身份和状态字段做硬过滤,Embedding做粗召回,规则或Reranker做精排,版本字段解决冲突,Token预算决定最终能装多少。
14. 如何证明这套选择逻辑有效
准备记忆检索评测集:每条包含当前问题、身份、项目、当时有效记忆和人工标注的应召回/不应召回 memory_id。比较 Recall@k、错误用户记忆泄露数、旧版本命中数、最终Prompt Token、延迟和答案质量。调 top_k、阈值和权重时用开发集,最终用独立测试集验收。
默认推荐:固定偏好用结构化SQL精确读取;当前会话用session_id精确读取;大量任务记忆用“metadata硬过滤 → 向量Top-20 → 规则/重排 → 冲突消解 → 800 Token预算”。数字只是教学起点,必须经过真实评测调整。
15. 一个项目中的默认数据分工
推荐
PostgreSQL:永久消息、长期记忆、状态和审计。
Redis:30分钟会话、锁、幂等和缓存。
向量库:大量长期记忆的语义索引。
对象存储:用户上传的图片、音频和大附件。
不推荐
所有聊天永远塞进Prompt;把Redis当唯一永久库;只存向量不存原始记忆;让LLM自行决定读写;删除PostgreSQL后忘记清理向量索引和缓存。
16. 删除一条记忆时要删哪些地方
删除不能只隐藏网页上的文字。后端应先把 PostgreSQL 权威记录标记删除或按政策物理删除,再删除对应 memory_id 的向量索引、清除 Redis 缓存,并按照备份保留政策完成后续清理。各副本通过同一个 memory_id 关联。
最终原则:权威内容只认一个来源,其他位置是缓存或索引。否则PostgreSQL说“用户喜欢中文”,向量库却残留“用户喜欢英文”,系统无法判断谁是真的。
M09隐私、租户隔离和删除
读取记忆前必须用当前登录user_id和tenant_id过滤;导出、删除、保留期限和用途要符合产品政策。日志中避免记录原始敏感内容,可保存脱敏摘要和引用ID。
删除必须贯穿副本:主数据库、向量索引、缓存、搜索索引和依法应删除的备份流程都要考虑。只删聊天界面不等于真正删除。
M10本章完整记忆链
数据库保存,程序选择,模型只看到本轮被选中的部分;长期记忆必须可追踪、可修改、可删除。
本章总结:从哪里来,解决什么,为什么继续
本章参考的权威资料
课程讲的是跨厂商通用工程方法;具体参数会更新,真正开发时再以所选厂商最新官方文档为准。