本节摘要:本节补上跨会话记忆的两端手艺。写入端三件事:什么值得记——门槛三问一票否决(下次还成立吗 / 影响未来交互吗 / 能溯源吗),一次性事实、易变状态、可重取数据、会话过程一律不进库;何时写入——三种时机(用户显式信号的立刻写、轮末评估的择优写、会话结束的批量写);记成什么形态——原文引用、抽取事实、结构化笔记三档,Anthropic 把"让 Agent 在任务中维护结构化笔记"列为上下文工程的基本手段(官方)。召回端也是三件事:何时召回(会话开始一次 + 实体触及增量,别每轮都召)、召回多少(预算倒推,宁缺勿滥)、注入哪里(系统提示之后的专门记忆区块,带时间戳与来源,绝不住中部——2.2 节位置效应)。附
ce_memory.py最小分层记忆示意;最后讲记忆卫生:过期、冲突与遗忘。这两端恰好是 LangChain 四操作中 write 与 select 在记忆成分上的落地(7.1 节回收)。
阅读完本节,你应当能够:
ce_memory.py,说出它与预算口径的衔接点。跨会话记忆的第一个设计错误总是"记太多"。每一条入库记录都是未来每次召回的候选——库里噪声越多,召回越难命中,还挤占那份可怜的 1%~5% 预算(4.1 节)。写入门槛三问,任一答"否"即拒收:
| 门槛 | 问的是 | 拒收的反例 |
|---|---|---|
| 稳定吗 | 下次会话它还成立吗? | "用户当前在看的订单 88880"(易变状态) |
| 有用吗 | 它会影响未来的交互吗? | 本轮的一次寒暄、已完结工单的过程细节 |
| 可溯源吗 | 能注明它来自哪一轮、哪个来源吗? | 模型猜测出的"用户可能喜欢……"(推测不是事实) |
反例清单再加两条工程常见的:可重取数据不记(政策条文、库存状态——知识库和查询接口随时能给,4.1 节事故一);会话过程不记(试错与中间推理——那是压缩(6.1)用任务简报处理的对象,不是跨会话记忆的对象)。留下来的合格者恰好就是 4.1 节那三样:偏好、事实、约定。
⚠️ 记错比不记更贵:一条错误偏好("用户不要发票")一旦入库,之后每个会话都被召回、被放大,直到某天用户忍无可忍纠正它——而你可能根本查不到它是哪轮被误抽的。这就是第三问"可溯源"存在的理由:每条记忆必须带来源轮次,错了能删。
| 时机 | 触发 | 成本 | 特点 |
|---|---|---|---|
| 显式信号 | 用户说"记住……""以后叫我……" | 零额外调用 | 可靠度最高,应立刻写,别等会话结束 |
| 轮末评估 | 每轮(或每 N 轮)跑一次轻量判定:这轮出现了值得记的信息吗 | 每轮一次小模型调用(示意) | 及时,但要防写入过滥——判定 prompt 里放门槛三问 |
| 会话结束 | 会话关闭时生成摘要、抽取事实入库 | 一次批处理 | 成本最友好,牺牲及时性;长会话可降频为"每 20 轮一次"(示意) |
写入形态三档,按需选用:
{"key": "称呼", "value": "王工"}。紧凑、可校验,是首选。何时召回——两个时机,够用且别多:会话开始时按用户 ID 一次性召回;会话中用户提及新实体 / 新话题时增量召回("我是不是说过我常用杭州仓?"——按话题补查)。反面教训是每轮全量召回:多一跳延迟,还把无关记忆反复灌进窗口(注意力稀释,1.1 节)。
召回多少——预算倒推,不是"库里有什么就带什么"。2.1 节分配表给跨会话召回留 3%(128k 窗口即约 3.8k token,示意),按每条 50~150 token 计,上限就是二三十条。相关性排序后装到预算为止,宁缺勿滥:一条无关记忆不是中性的,它在稀释真正相关的那几条。
注入哪里——专门的记忆区块,位置在系统提示之后、对话历史之前:
[系统提示 v12(指令成分)] [记忆区块] ← 跨会话记忆住这里 - (2026-08-12,偏好)回复控制在三句话内 - (2026-07-30,事实)企业账户,发票抬头:某科技公司 [对话历史 / 素材 / 本轮输入] ← 中部在后面
三个格式要点:每条带时间戳与来源——模型由此能自判新旧(见下一节的冲突处理);类型标注——偏好 / 事实 / 约定分行列出,比一大段散文可读;关键偏好末尾复述——少数红线级偏好("禁止向我推销")按 2.2 节推论一,在本轮用户输入之前再复述一次,首尾双保险。
💡 召回的记忆仍是预算恒等式里"记忆"科目的一行——它不是免费午餐。记不住这条的团队常见症状:召回额度从 3% 悄悄涨到 15%,素材被挤爆,然后回头去砍输出预留。
ce_memory.py:最小分层记忆示意# ce_memory.py —— 最小跨会话记忆:写入门控与预算内召回(写法示意,以实际工程为准) import time def est_tokens(text: str) -> int: # 与 ce_budget.py 同口径 cn = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff") return cn + (len(text) - cn) // 4 RECALL_BUDGET = 3_800 # 单次召回的 token 上限(示意:128k 窗口 × 3%) MAX_AGE_DAYS = 180 # 条目过期年龄(示意:偏好类长期,状态类应另设短 TTL) def worth_writing(item: dict) -> bool: """写入门槛三问,一票否决:不稳定 / 下次没用 / 无法溯源的都不进库。""" return bool(item.get("stable") and item.get("useful_next") and item.get("source")) def write(store: dict, item: dict) -> bool: """写入即合并:同键新事实覆盖旧事实,时间戳取新。返回是否真的写入。""" if not worth_writing(item): return False # 拒收:门槛三问未过 key = item["key"] # 如 ("偏好", "回复长度") store[key] = {**item, "ts": time.time()} # 冲突策略:新覆旧 return True def recall(store: dict, query: str, top_k: int = 20) -> list[dict]: """召回:过期过滤 → 相关性粗排 → 装到预算为止。""" now = time.time() items = [v for v in store.values() if now - v["ts"] < MAX_AGE_DAYS * 86_400] scored = sorted(items, key=lambda v: -overlap(query, v["text"]))[:top_k] out, used = [], 0 for v in scored: # 召回也过预算恒等式 cost = est_tokens(v["text"]) if used + cost > RECALL_BUDGET: break out.append(v); used += cost return out def overlap(a: str, b: str) -> float: """相关性粗排的占位实现:字符二元组重合度。生产应换向量检索。""" grams = lambda s: {s[i:i + 2] for i in range(len(s) - 1)} ga, gb = grams(a), grams(b) return len(ga & gb) / max(1, len(ga | gb))
三个实现要点:门控在写入侧——worth_writing 是三问的代码化,脏数据进不来比召回侧过滤便宜一个量级;召回带预算——RECALL_BUDGET 对应分配表的记忆-召回行,与 ce_budget.py 的 audit() 口径一致;粗排是占位——overlap 只保证教学可跑,生产的检索(向量、混合)属于实现层,深潜见 《腾讯 Agent Memory 记忆系统》与《memU 记忆框架》教程。
记忆不是只进不出的收藏夹,三件卫生事:
MAX_AGE_DAYS = 180,示意),状态类短或干脆不进库(4.1 节事故三)。会员等级这类"会变的事实",TTL 到期或来源事件出现(用户升级)时刷新。write 的策略),并保留时间戳——召回时把新旧都带上的"带时间戳让模型自裁"是另一派做法,适合事实类;偏好类直接新覆旧更干净。⚠️ 隐私是跨会话记忆的合规红线:入库内容是最小必要(只要影响交互的事实,不要对话原文全存)、可导出(用户行使"查看我的数据"权利)、可删除(用户说"忘了我说的"必须能物理删除)。本节只做工程层提示,合规评估请在部署前完成(0.2 节约定)。
ce_memory.py:门控在写入侧、召回带预算、粗排是占位——决策层自建,实现层外委。记忆成分到此完整:三层地图(4.1)加两端管道(4.2)。动态层还剩另一半——素材:总量最大、增长最快、时效最短的成分。它的供给哲学与记忆相反:不是"沉淀下来反复使用",而是"用时才取、用完即走"。下一章讲按需供给的两种形态与取回之后的组装手艺。