Appearance
第10章 记住什么,忘掉什么——Memory
小a的 Agent 能循环了,但跑久之后出现两个毛病:聊到一半,模型“忘了”最开始的任务;程序关掉再开,之前的过程全没了。他有点泄气地把这两件事讲给老z听。
“它是不是记性不好?”小a问。
“不是‘记性不好’,是‘没人替它记’。”老z说,“循环让任务能跨多步推进,也让历史不断增长。你发现的其实是两个不同的问题:模型的上下文会满,进程退出后又无法继续工作。”
“那我把所有历史都存下来,问题不就都解决了吗?”小a问。
“先别急着存。”老z说,“存什么、存多久、谁有权访问、什么时候删除——每一问都是设计决策。你以为在解决‘记不住’,实际要面对的是‘记太多’:上下文会满,存储会膨胀,敏感信息会滞留。这一章不教你存多少,而是帮你分清每一层记忆各自的责任与代价。”
本章区分上下文、会话持久化、压缩和长期记忆,重点是它们的代价与隐私边界。
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| 上下文 | context | 当前一次模型请求可见的材料集合 | 数据库中的完整历史 |
| 会话 | session | 跨多个回合组织消息、调用与状态的应用记录 | 模型天然持有的记忆 |
| 短期/长期记忆 | short-term/long-term memory | 当前任务材料/经确认可跨会话复用的信息 | 任何历史都永久保存 |
| 截断/压缩 | truncation/compaction | 删除部分材料/以摘要替代旧材料 | 无损存档 |
| 检索 | retrieval | 按查询从受控存储取回材料 | 自动相信命中内容 |
| 保留策略 | retention | 数据保留、访问、更新与删除的规则 | “以后也许有用” |
10.1 模型无状态,应用必须管理状态
“那模型自己不能记住吗?”小a问。
“不能。每次模型请求只看到应用带入的内容。”老z说,“应用通常维护完整会话记录,再选择其中一部分放入本轮上下文。不要把‘上下文窗口’与‘持久化存储’混为一谈:前者是一次请求可见的有限预算,后者是跨进程保存记录的机制。”
比喻:顾问与助理
模型是每次提问都得重新把资料递到桌前的顾问,而你是负责整理档案的助理。顾问记不住,是助理在记。
“那模型厂商不是宣传过‘长上下文’?窗口一大不就都解决了?”小a追问。
“窗口变长只是放宽了单次请求的预算上限,并没有改变‘每次请求只看应用带入的内容’这一前提。”老z说,“窗口再大也是按 token 计费、按容量截断的有限预算,而且放进去不等于模型会同等关注:长上下文里靠前的关键约束常常被冲淡。窗口长不能替代你决定放什么、不放什么,更不能替代进程退出后的恢复机制。”
“那是不是把窗口填满,模型就一定能正确推理?”小a又问。
“不一定。”老z说,“填得越满,单次成本越高、延迟越大,而且重要约束被淹没的风险也越高。窗口是预算,不是保险。决定可靠性的,是应用如何挑选、组织和标注那些材料,而不是材料堆得有多高。”
“材料怎么摆,会影响模型关注吗?”小a追问。
“会,这是长上下文最实际的坑。”老z说,“靠近开头和结尾的指令通常被遵循得更好,埋在中间的长篇资料常常被当作背景略过。同一个约束,放在系统指令里和埋在第五百条历史消息里,效力完全不同。所以常见的做法是:最关键的规则放前面,动态内容放后面,中间塞参考资料。这当然不是定理,只是工程上常见的失效模式——别指望模型平等对待窗口里每一段 token。”
“那同样的材料,放前面和放中间真的差很多?”小a继续问。
“具体差多少取决于模型和任务,没法给通用数字。”老z说,“但设计上有一条有用的默认:越重要的越往前放,越动态的越往后放。位置不是记忆策略的全部,但它是零成本可调的一环——调整材料的摆放顺序,比扩大窗口便宜得多,也比反复改写提示词更可预测。”
10.2 四类记忆,四种责任
“那‘记忆’有几种?”小a问。
“工程上常分四类,各自责任不同。”老z列了一张表:
| 类型 | 保存什么 | 主要用途 | 主要风险 |
|---|---|---|---|
| 工作上下文 | 当前请求所需消息和工具结果 | 让模型完成本轮任务 | 超限、成本、提示注入 |
| 会话记录 | 可恢复的事件与消息 | 续接、审计、排障 | 敏感数据滞留 |
| 压缩摘要 | 旧记录的有损概括 | 释放上下文预算 | 丢失约束或错误固化 |
| 长期记忆 | 经确认的稳定偏好或项目事实 | 跨会话个性化 | 过期、越权保存、污染 |
“它们能放一起吗?”小a问。
“这是设计分类。实际系统可以将它们存于同一介质,但不应让它们采用相同的保留期限和访问权限。”
“同样的介质、不同的策略,具体差在哪?”小a追问。
“差在三处:保留期限、访问范围、清除入口。”老z说,“工作上下文随请求结束就该清掉;会话记录要为审计留一段时间;压缩摘要的失效时间应和它概括的原始记录挂钩;长期记忆则必须给用户提供查看与删除入口。如果你把它们都塞进同一张表、共用同一个删除脚本,那要么会话被过早清除、无法排障,要么摘要和偏好被无限期滞留、扩大隐私面。”
“长期记忆和工作上下文的边界会不会模糊?”小a又问。
“会,这是常踩的坑。”老z说,“模型在某轮随口说的‘用户似乎偏好简洁’,若被直接写进长期记忆,就成了来源不明、用户不知情的固化判断。进入长期记忆的前提是来源明确、用途正当、用户允许;未经确认的模型推断,最多停留在工作上下文里随本轮结束消散。”
“那长期记忆和工作上下文,还差在哪?”小a问。
“差在哪些信息可以凭直觉进、哪些要过审核。”老z说,“工作上下文里的信息是临时的,错了下一轮就冲掉了,模型推断放进去影响有限;长期记忆里的信息是跨会话复用的,一条错误固化下来,会让之后每一次会话都带着它行动。审核门槛是应随层级的持久性递增的:越是长期的信息,越要经确认才写入。这不是保守,是把变更风险压在它影响面最小的地方。”
“四类记忆各自的时间跨度、放行条件和主要风险,能并排看看吗?”小a问。
老z列了一张维度表:
| 维度 | 工作上下文 | 会话记录 | 压缩摘要 | 长期记忆 |
|---|---|---|---|---|
| 时间跨度 | 当前请求 | 会话期间 | 随原始记录 | 跨会话 |
| 写入门槛 | 低,随请求组织 | 自动记录 | 派生生成 | 需确认 |
| 访问范围 | 模型可见 | 应用、审计 | 续接用 | 用户可控 |
| 清除入口 | 请求结束即清 | 会话删除 | 随原文失效 | 必须提供 |
“这张表不是让你照着建四套存储,”老z补充,“而是让你在设计时逐一回答每一列。答不出来的那一格,就是将来出问题的地方。”
10.3 上下文满了如何处理
“上下文快满的时候怎么办?”小a问。
“常见策略有截断、滑动窗口、摘要和按需检索。”老z说,“截断简单却可能丢掉任务目标;摘要保留更多语义却有额外成本和失真风险;按需检索适合资料量大但需要可靠索引、权限过滤和来源标注。不存在对所有会话最优的策略。”
“有没有一个稳妥的顺序?”小a问。
“一个稳健流程是先固定不可丢失项(安全规则、用户当前目标、未完成动作、关键约束),再对可重取的工具输出做截断或引用,对旧对话生成带来源的摘要。”老z说,“摘要应标记为摘要,不能伪装成原始记录。”
“截断和摘要,哪个更安全?”小a追问。
“两者各有失败模式。”老z列了个对照:
| 策略 | 释放的预算 | 主要代价 | 典型失败 |
|---|---|---|---|
| 截断 | 直接、确定 | 丢掉被截断的内容 | 首轮的目标或约束被切走 |
| 摘要 | 通常更大、更省 token | 摘要本身的成本与失真 | 关键约束被概括掉或改写 |
“截断的失败是‘显式丢’——你看得见少了什么;摘要的失败是‘静默变’——内容看起来还在,但语义已被改写或冲淡。”老z说,“所以摘要只能标为派生物,必要时要能回查原文。把一条写着‘不要删除文件’的约束概括成‘讨论了文件操作’,就是典型的摘要失真。”
“这两种失败,能用图对比吗?”小a问。
老z画了两行:
text
截断: [A][B][C][D][E] → 移除尾部 → [A][B]
(约束在这) 约束已消失,但"消失"是可见的
摘要: [A][B][C][D][E] → 替换为摘要 → [sum(A..E)]
(约束在这) 约束仍在文本里,但语义已被改写“注意两行的差别。”老z说,“截断后你数得出来少了三块;摘要后表面上东西还在,只是‘变了’——而模型分不清它读到的是原文还是概括。所以摘要要能回查:留一条从摘要指向原始消息的引用,失真时可逆着查回去。截断的错误是量上的,摘要的错误是质上的,后者更隐蔽。”
“那滑动窗口算截断还是摘要?”小a又问。
“算有结构定向截断:保留最近 N 轮、丢弃更早的部分。”老z说,“它的好处是行为可预测、无失真;代价是它对‘靠前但重要’的约束不友好——一旦目标在第 1 轮、当前在第 20 轮,目标就被切走了。所以滑动窗口通常要配合‘固定保留区’:把不可丢失项钉在窗口里不被滚动,再让其余部分按时间截断。”
“那固定保留区,示意一下?”小a问。
老z画道:
text
窗口(保持最近 8 轮)
┌─────────┬─────────────────────────────┐
│ 固定保留区│ 滚动区 │
│ 目标、安全 │ 第 N-7 ... 第 N 轮消息 │
│ 规则、未完成│ (随新轮次滚动截断) │
└─────────┴─────────────────────────────┘“注意固定保留区的特点:它不按时间滚动,按重要程度保留。”老z说,“代价是这部分内容一直占着预算,不会因为时间流逝而让位。所以放进固定保留区的内容要少而精——放进去了就长期在场,放错了就长期占位。固定保留区是‘钉住’,不是‘缓存’,设计时要想清楚它该服务多久。”
“那按需检索呢?资料量大的时候它最合适吧?”小a追问。
“它合适的前提是有可靠索引、权限过滤和来源标注。”老z说,“检索不是‘存一堆、查一下’这么简单:没索引就是全量扫描,比直接塞上下文还慢;没权限过滤就是把不该见的数据放到了模型眼前;没来源标注,检索命中就退化成一句无凭无据的摘录。而且检索本身有失败——查不到、查错、查到过期版本。所以检索适合资料量大、需要精确取用的场景;小会话硬上检索,属于用贵方案解决便宜问题。”
“四种策略的代价,能不能列在一张表里?”小a问。
| 策略 | 实现成本 | 推理成本 | 主要失效 | 适合场景 |
|---|---|---|---|---|
| 截断 | 低 | 低 | 丢失任务目标 | 历史短、可重取 |
| 滑动窗口 | 低 | 低 | 丢弃靠前约束 | 对话长、行为要可预测 |
| 摘要 | 中 | 中 | 失真、约束被概括掉 | 历史长、需保语义 |
| 按需检索 | 高 | 中 | 索引、权限、来源问题 | 资料量大、需精确取用 |
“这张表里,‘实现成本’和‘推理成本’是会变的。”老z提醒,“比如摘要是离线生成的,成本摊在会话间隙;按需检索如果每次都查全库,成本就会随资料量涨。任何策略都不是免费的——它要么现在付钱(生成摘要、建索引),要么将来付钱(失败、失真、误伤)。选择策略,本质上是在挑‘付哪种代价’。”
10.4 会话持久化与恢复
“那‘关掉再开还能继续’是怎么做到的?”小a问。
“追加式事件记录(例如每行一个结构化事件)便于在崩溃后恢复,也便于审计工具副作用。”老z说,“记录至少应包含消息、工具调用 ID、执行状态、时间信息和版本;写入失败或半条记录是必须演练的错误路径。”
“恢复的时候,把所有历史塞回去就行?”小a问。
“不行。恢复时不能只把所有历史原样塞回模型。”老z说,“应用应重建状态、识别未完成调用、确认是否允许重试,并根据当前上下文预算重新选择材料。对有外部副作用的未完成调用,默认应要求幂等证明或人工确认。”
“半条记录是怎么产生的,又要怎么处理?”小a追问。
“进程在写入事件流的中途被杀,最后那行就是半条记录——可能是截断的 JSON,也可能是状态字段缺失的调用记录。”老z说,“恢复程序不能假定上一条已完整:要么按行边界容忍尾部、丢弃不完整的那行;要么为每条记录加校验和与结束标记,校验失败即视为未完成。更关键的是,半条记录对应的工具调用是否已对外执行,光看记录是判断不出来的——这才是‘待确认’状态的由来。”
“那崩溃前刚发起的工具调用,重启后算成功还是失败?”小a又问。
“记录无法回答这个问题,只有外部世界的可验证状态能回答。”老z说,“比如写文件要看文件是否真的存在、内容是否匹配;发邮件要看收件方是否有去重或幂等键。持久化记录的是‘应用想做什么’,不是‘外部世界发生了什么’;两者之间的缝隙,要靠重查和确认来填,而不是靠盲目重放或盲目跳过。”
“那‘重放’和‘恢复’的区别在哪?”小a追问。
“重放是机械地把所有事件重新执行一遍,恢复是重建状态但不重复动作。”老z说,“事件流里既有‘状态变化’也有‘副作用动作’。重放状态变化是安全的——内存里的状态可以被推演回任何历史点;重放副作用动作是危险的——写文件、发消息这类动作在外部世界已经发生了,再放一遍等于重复执行。所以恢复程序要能区分两类事件:前者重放,后者只标记、不自动重放,等确认或幂等证明。”
“怎么判断一个事件是状态变化还是副作用动作?”小a又问。
“靠事件类型本身无法完全判断,要靠外部状态核对。”老z说,“同一个‘写文件’事件,在恢复程序眼里是副作用动作;但它的结果字段——‘文件已写入’——又反映了一次状态变化。所以实践上是‘副作用动作先确认再重放,确认过的结果才视为状态’。用外部世界的可验证状态来判断,而不是用事件名称来猜——这是恢复程序唯一可靠的依据。”
“还有一点,”老z补充,“崩溃点不同,需要核对的深度也不同。在动作前崩溃,事件可能根本没对外发生;在动作后、记录前崩溃,动作可能已经完成但没留下结果——这两种情况恢复动作完全不同。恢复程序要按‘动作前/动作后/记录前/记录后’分情况处理,而不是对每条记录套同一套逻辑。”
“那恢复时碰到的待确认动作,具体分几类?”小a问。
| 待确认动作 | 可安全恢复的动作 | 必须人工确认的动作 | 绝不应自动重试的动作 |
|---|---|---|---|
| 读文件、查状态 | 重新读取 | 无 | 无 |
| 写文件 | 先查文件是否已存在 | 存在则核对内容 | 无 |
| 发消息/发邮件 | 无 | 核对是否已发出 | 确认后再判断 |
| 扣费、转账 | 无 | 核对外部账单状态 | 确认前不重放 |
“这张表按动作类型分,是示意,不是给你照抄的清单。”老z说,“每类动作的‘可验证状态’不同——文件看存在性和内容,消息看对方是否收到,账单看外部流水。设计恢复程序时,先为每种副作用动作定义它的可验证状态,再决定‘安全恢复/人工确认/绝不重试’三档怎么分。分档的依据是外部世界的可验证状态,不是事件名称好听不好听。”
10.5 长期记忆要能更正和删除
“那‘长期记忆’是不是存得越多越好?”小a问。
“不是。长期保存‘用户偏好’或‘项目约定’看似方便,却扩大隐私和误导面。”老z说,“只有来源明确、用途正当且用户允许的信息才应进入长期记忆;每条记录应有来源、更新时间、可见范围和删除机制。旧偏好、过期项目事实和模型自行推断的性格标签,都不该被永久化。”
“能不能把所有历史都存起来,以后再检索?”小a问。
“这是保留策略问题,不是纯技术选择。”老z说,“数据最小化、加密、访问控制、保留期限和导出删除能力应在设计时确定;‘以后或许有用’通常不是充分理由。”
“怎么判断一条信息够不够格进长期记忆?”小a追问。
“可以问三个问题:来源是谁、用途是什么、用户是否知情。”老z说,“来源是用户明确说出的、用途是为下次会话复用、用户有查看和删除入口——三者都满足才考虑长期化。只要有一项含糊,比如来源是模型自行推断、用途模糊、或用户根本不知道这条记录存在,它就只该停留在工作上下文,随任务结束消散。”
“删除入口是不是给个按钮就够了?”小a又问。
“不够。删除要分单条删除、按来源批量删除、整体导出后清空三种粒度。”老z说,“还要回答一个边界问题:删除长期记忆时,已经被压缩进摘要里的同一条信息怎么办?如果摘要不随之更新,删除就是表面的。所以长期记忆的更新和删除要沿着来源链传播,而不是只改一张表。”
“那删除到底‘删了什么’,也有讲究?”小a追问。
“有,分两种:逻辑删除和物理删除。”老z说,“逻辑删除是打一个‘已删除’标记,数据还在盘上——好处是能恢复误删,坏处是敏感数据仍滞留在存储里;物理删除是把数据抹掉——代价是彻底、不可恢复。对用户主动发起的删除,通常期望物理删除;对系统内部的自动清理,逻辑删除加定期物理清理更稳妥。删除是产品语义,不是一条 SQL,用户以为删了、数据还在,比不删更麻烦。”
“删除之后,模型缓存里会不会还留着旧信息?”小a又问。
“这是删除最难覆盖的地方。”老z说,“长期记忆更新、删除时,本轮的上下文由应用重新组织,一般不会带上被删的内容;但如果应用对历史消息做过缓存,旧快照里可能仍含已删除信息。所以删除长期记忆时,要把上下文缓存、摘要、检索索引一起标为失效,而不是只清理主表。任何一层漏了,删除都只做了一半。”
10.6 恢复不是把历史重新发送
“那正确的恢复流程是什么?”小a问。
“持久化后的恢复应先重放事件以重建应用状态,再决定哪些内容进入下一次上下文。”老z说,“消息历史是证据源,摘要是派生物;二者都应带版本、来源与保留策略。若摘要覆盖了原始消息,它就无法证明自己是否遗漏了关键约束。”
text
追加事件 → 崩溃 → 读取有效记录 → 重建调用状态
↓
选择上下文 / 标记待确认动作“摘要和长期记忆会过期吗?”小a问。
“会,所以都需要失效机制。”老z说,“可以给项目事实设置更新时间和来源,给用户偏好提供查看、修改和删除入口;检索命中也应标注出处。更多存储会提高恢复能力,却会增加隐私暴露、迁移成本和错误长期化风险。”
“检索命中和摘要,到底谁是证据?”小a追问。
“两者都不是原始证据,都是派生物。”老z说,“原始证据是消息历史本身。检索命中是从历史或外部库取回的片段,摘要是对历史的概括;它们都可能过期、越权或被污染。模型不应把检索命中当作已验证事实直接相信,应用应在送入上下文时标注来源、时间与访问范围,让模型把它们当作‘待核对的引用’而不是‘既定结论’。”
“那原始历史就一定可靠吗?”小a又问。
“原始历史只是‘最早被记录下来’的那一层,它可能含错误调用、被污染的输入或半条记录。”老z说,“但比起摘要和检索结果,它最接近真相、最少被改写,所以排障和审计要回查到这一层。这是一个优先级问题:派生物先服务于流畅续接,证据链出问题时再回查原始层。”
“那哪些内容该回查、哪些不用,有标准吗?”小a追问。
“标准是‘这句话被哪个环节改写或概括过’。”老z说,“模型引用了检索命中,要回查原文;摘要里说‘讨论了文件操作’,要回查它概括了哪几轮;用户偏好被固化进长期记忆,要回查它最初的来源。凡是经过摘录、概括、推断的内容,都带一个派生链;原始层是这条链的终点,回查就是在链条上逆着走。没改写过的话,比如某条用户直接说出的指令,就不必回查。”
“派生链要存多少级,够用就行吗?”小a问。
“至少存‘原始—第一层派生’这一级。”老z说,“摘要标出它概括了哪几条原始消息,检索命中标出它取回自哪个文件,长期记忆标出它来自哪轮会话。存这一级的成本不高,但能让排障时直接跳到原始层。存得越浅,回查越要翻整库;存得越深,每层都得多维护一条引用关系。先存一级,够了再加。”
示意: 工具已写入文件但尚未记录结果时进程崩溃。恢复程序不能假定写入成功或失败;应检查可验证的文件状态、匹配调用 ID,并将不能判断的外部动作转为待确认,而不是自动重放。
10.7 恢复要区分消息、世界与权限
“能不能把这条链路画出来?”小a问。
“可以。”老z画了一张:
text
追加 session event → 进程中断 → 读有效记录 → 重建内部状态
→ 选择 context/检索材料
→ 标记未确认的外部动作“反例呢?”小a问。
“正常路径是 session 记录调用 ID、状态和时间,恢复后只把当前目标、关键约束和可验证证据装入 context。”老z说,“反例一是压缩摘要漏掉‘不要删除文件’的约束:摘要只能标为派生物,必要时可回查来源,不能伪装为原文。反例二是工具写文件后、结果落盘前崩溃:持久化不会自动说明外部写入成功或失败,应检查可验证状态并要求幂等证明或人工确认。”
“那检索命中就一定可信吗?”小a问。
“不一定。检索命中也可能过期、越权或被污染,因此要保留来源、更新时间、访问范围和删除入口。”老z说,“更多 retention 的收益是续接与审计;代价是隐私暴露、迁移成本和错误长期化。压缩不是无损,持久化不是外部副作用的事务恢复,这是本章的两条硬边界。”
“这两条边界在实际设计里会怎么冲突?”小a追问。
“最常见的冲突是‘为了恢复完整,把所有东西都留着’。”老z说,“保留得越多,续接和审计能力越强,但隐私暴露面、迁移成本和错误长期化风险也同步上升。比如一条早期会话里混入的敏感路径或令牌,如果按‘完整恢复’无限期保留,就成了一颗埋着的雷。所以 retention 不是‘能存就存’,而是给每一层都定下保留期限和清除入口,到期就清。”
“那隐私和可恢复性,能不能两全?”小a又问。
“靠分层来逼近,没法靠单一手段两全。”老z说,“原始历史短期保留、用于排障;摘要中期保留、用于续接;长期记忆只保留用户确认过的稳定信息,且提供删除入口。三层各自的保留期限和访问权限不同,隐私暴露集中在最长期那层,而最长期那层的体量被压到最小。用体量换隐私面:越是长期的信息,越要严格筛选。”
“那用户要是要求‘全部删光’,这几层怎么联动?”小a追问。
“三层联动,不只是删一张表。”老z说,“删原始历史,引用它的摘要和长期记忆就该一起标为失效,否则摘要还在引用已删除的原文;删长期记忆,同步清掉上下文缓存里可能的旧快照。一层删了、别的层还留着,用户看到的‘已删除’就是假的。删除要沿着来源链传播——这在 10.5 说过一次,放在恢复场景里也一样成立:恢复不是只恢复状态,还要恢复‘删除’这个状态。”
“那权限呢?恢复出来的内容,谁有资格看?”小a又问。
“访问权限要跟着来源走,不是跟着‘恢复了’走。”老z说,“恢复进程读到的材料,如果来源只授权给当前用户,就不该被另一个会话或另一个 Agent 取用。恢复本身不改变访问范围——它只是把记录从盘上捞回来,该有的过滤一样不能省。权限过滤在每一层读入口都要做一遍,恢复路径尤其容易漏:因为它看似是‘内部流程’,最容易被当成不需要鉴权的地方。”
“那证据链的三层,优先级怎么排?”小a问。
老z画道:
text
原始消息历史 ── 排障与审计回查的终点(最接近真相,最少被改写)
↑ 引用
摘要 ── 供续接使用(派生物,可标失效、可回查来源)
↑ 引用
长期记忆 ── 跨会话复用(最严格筛选、必须可删除)“三条链,三层用途。”老z说,“排障回查原始层,续接读摘要层,个性化用长期记忆层。每向上一层,信息被改写的程度就高一分,筛选的门槛也严一分——越往上越派生物,越要标注和删除能力。这条优先级不是让你只在出问题时才想起原始层,而是让你设计时就想清楚:每一层服务于谁、谁有权改它、谁有权删它。”
小结
记忆不是模型自带的能力,而是应用层精心设计的几层存储。最常被混淆的是上下文和会话:上下文是当前这一次请求里模型能看到的全部材料,它服务的是当下的推理;会话是跨多次请求保存下来的记录,它服务的是恢复和审计。两者完全不同——上下文随请求结束而消失,会话留在磁盘上等你回看。除此之外还有摘要,它用更少的空间换一个语义上的近似,牺牲细节来换取窗口里能塞下更多轮次;以及长期记忆,用来保存那些跨会话都稳定的偏好和信息。每一层都要回答同样一组问题:数据从哪来、能存多少、谁有权访问、保留多久、何时删除。
这一章要打掉的错觉是"记得越多越可靠"。更多记录并不自动带来更可靠的 Agent,反而可能带来更多噪声、更高的成本和更大的隐私风险。一个把所有对话都塞进上下文的系统,很快就会撞上窗口上限;一个永久保留所有会话的系统,要面对数据残留和合规问题。记忆设计的核心是取舍:每一层保留什么、丢弃什么,都要有明确的策略,而不是无脑地全部记住。
上下文满了的时候,截断、滑动窗口、摘要和按需检索四条路各付各的代价。截断便宜但丢掉任务目标,而且丢得明明白白;摘要省 token 但可能悄悄改写约束,而且改写得无声无息;滑动窗口可预测但对"靠前但重要"的内容不友好;按需检索省上下文却要背负索引、权限和来源三座成本。选择策略,本质上是在挑"付哪种代价"。恢复也同理——持久化记录的是"应用想做什么",不是"外部世界发生了什么",所以恢复必须区分状态变化与副作用动作:前者重放,后者先查外部可验证状态,再决定安全恢复、人工确认还是绝不重试。
层与层之间还有三条贯穿规则。一是派生物规则:摘要、检索命中、长期记忆都是从原始历史派生出来的,都要带来源、版本和失效机制,都要能被回查到原始层。二是删除传播规则:删除要沿来源链传播,删一层就要让引用它的摘要、缓存、索引一起失效,还要分清逻辑删除和物理删除。三是权限随来源走:恢复路径读到的材料,访问权限跟着来源走,而不是跟着"恢复了"走。把这三条贯穿到每一层,记忆系统才不是一堆堆着的数据,而是有边界、有取舍、能清理的存储设计。
动手核验
为一次包含工具调用的会话设计一份事件记录,模拟在工具执行后、结果写入前崩溃。重启时列出可安全恢复、必须人工确认和绝不应自动重试的动作。再检查记录中是否含有不应长期保存的路径、令牌或用户内容,并为它们定义清除策略。