Appearance
🐣 小a的记性难题
循环跑起来了,小a能"干一步看一眼"了。但很快它发现两个麻烦:
- 聊久了,历史越来越长——迟早撑爆上下文窗口(第 1 章的伏笔)
- 关了程序再开,之前聊的都没了——它不记事
老z:"这两个问题,一个叫上下文管理,一个叫会话持久化。合起来,就是 Agent 的记忆。"
8.1 为什么需要记忆
🧙 模型本身是无状态的。
回忆第 1 章:模型有上下文窗口,但它不会自己记事——每次调用,你得把"历史对话"重新喂给它。
所以 Agent 必须维护一个外部的记忆——把所有 system、user、assistant、toolResult 消息存起来,每次调模型时带上。
老z打比方:模型是"金鱼",Agent 的记忆是"水族箱里贴的便利贴"——每次问它之前,把便利贴摊给它看。
8.2 记忆的三个层次
🧙 Agent 的记忆分三层,各有用途:
层次 是什么 例子 即时记忆 当前这一轮的对话 正在窗口里的消息 会话记忆 整个会话的历史 存成 JSONL 文件,重启能恢复 压缩记忆 旧对话的摘要 compaction 后的精华 pi 把这三层都做了。但本质上,记忆就是"把历史喂给无状态的模型"这件事的工程化。
8.3 上下文会满:记忆的敌人
第 1 章埋的伏笔,这里回收:
🐣 小a的烦恼(第 1 章):"聊着聊着窗口满了,怎么办?"
现在你明白了——循环每转一轮,消息就多几条(一个 assistant + 几个 toolResult)。转得越多,历史越长,窗口迟早被塞满。
窗口塞满的后果:
- 最早的对话被"挤出"记忆 → 模型忘了任务初衷
- 报 "context length exceeded" 错误 → 任务直接失败
8.4 窗口满了怎么办:三种策略
🧙 上下文超了,有三种处理策略,从简到繁:
策略一:截断(丢最早的)
[user] 改 config.ts
[assistant] 好,我读一下...
[toolResult] ... ← 最早的被删掉
[assistant] 找到 3 处...- 做法:删掉最早的消息
- 优点:简单、零成本
- 缺点:丢上下文——模型可能忘了"我一开始要干嘛"
策略二:滑动窗口 + 摘要
[user] 改 config.ts ← 保留第一条(初衷)
[summary] 之前的对话:读了 config.ts,改了3处...
[assistant] 找到 3 处... ← 最近的消息- 做法:保留第一条 + 最近的 N 条,中间的总结成摘要
- 优点:省空间但保信息,比截断聪明
- 缺点:总结本身要调一次 LLM(费 token)
策略三:分支总结(compaction)
- 做法:不只是"删中间",而是把旧对话总结成结构化摘要,甚至按主题分叉
- 优点:语义保留最完整
- 缺点:最复杂,工程量大
🐣 小a:"那 pi 用哪个?"
🧙 "pi 用的是 compaction(分支总结)——最聪明但也最复杂的那种。但对个人用,滑动窗口+摘要已经够好。选策略的原则:你的 Agent 多长会话?多在乎上下文?"
8.5 会话持久化:关了不丢
🧙 除了"窗口会满",记忆还有第二个问题:程序退出,记忆就没了。
解决办法是持久化——把会话存到磁盘,下次启动时加载。
最简单的方式:JSONL
# session.jsonl(每行一条消息)
{"role":"user","content":"改 config.ts"}
{"role":"assistant","content":"好,我读一下...","toolCalls":[...]}
{"role":"tool","toolCallId":"...","result":"config.ts 的内容是..."}🧙 为什么用 JSONL(每行一条 JSON)?
- append-only:加一条只追加一行,不用重写整个文件(快、安全)
- 崩溃友好:中途崩了,已写入的行不损坏
- 可读:每行一条,能 cat 看、能 grep
持久化的价值
- 断点续跑:任务跑到一半,关了,重启能继续
- 多轮对话:用户隔天回来,Agent 记得之前聊了什么
- 可追溯:回看 Agent 之前干过什么(排查问题)
8.6 记忆的边界与安全
🧙 记忆不是越多越好,有边界:
- 隐私:记忆里可能有敏感信息(密码、密钥)——持久化前要考虑加密/清理
- 污染:旧记忆里的错误结论,可能误导后续推理
- 成本:记忆越长,每次喂给模型的 token 越多,越贵
好的记忆管理 = 够用就行,不是越多越好。
8.7 长期记忆:记住"用户是谁、项目是什么"
"前三层记忆,都在讲这一次会话。"老z说,"但还有一类记忆,跨会话、长期有效——比如用户是谁、项目是什么。"
🧙 长期记忆(long-term memory)
长期记忆是独立于单次会话存储的知识与偏好,Agent 每次启动时加载。
记忆类型 内容 例子 用户偏好 用户的习惯与要求 "用中文回答""代码风格用单引号" 项目背景 项目的长期事实 "这个仓库是 monorepo""测试用 vitest" 积累的经验 之前踩过的坑 "上次改这模块,注意 X 依赖"
老z打了个比方:"会话记忆像聊天的便签,关灯就收走;长期记忆像墙上的挂历——每次进门都能看到。"
长期记忆怎么存、怎么用?
🧙 两种工程做法:
做法一:静态加载(偏好写进前缀)
把用户偏好拼进 System Prompt 的最前面——每次调用都带着。好处是这部分内容永远不变,完美命中 KV 缓存(1.12),几乎不增加成本。
做法二:动态检索(按需查知识库)
项目背景可能很大,塞不进 prompt。那就把它存成知识库,模型需要时检索相关片段、动态插入——这就是第 13 章的 RAG。长期记忆与 RAG 的区别:记忆存"关于用户/项目的稳定事实",RAG 存"可检索的外部资料",做法相通。
🐣 小a:"那记忆要存多深?"
🧙 "取决于服务周期。 本地工具型 agent(比如 pi)主要服务"这一次任务",长期记忆轻;长期陪伴的云端助手(比如智能客服、虚拟助理)服务"同一个用户很多年",长期记忆就必须重。记忆的深度,由你和用户的关系长度决定——别为了记而记。
长期记忆的边界
⚠️ 长期记忆更敏感:
- 隐私风险更高:跨会话保留的信息,泄露面更大——存之前问一句"这个该记吗?"
- 容易过期:用户偏好会变,要能忘、能改,不能"记死了"
- 污染更持久:一次记错,可能误导未来很多次会话——长期记忆比会话记忆更需要"可编辑、可清除"
本章小结
🐣 小a的笔记本
┌──────── 记忆与上下文 ────────┐ │ │ │ • 模型无状态 → 需要外部记忆 │ │ │ │ • 记忆三层:即时/会话/压缩 │ │ │ │ • 窗口会满,三种策略: │ │ 截断(丢信息) │ │ 滑动窗口+摘要(折中) │ │ compaction(最聪明最复杂) │ │ │ │ • 持久化:JSONL 存盘 │ │ → 关了不丢、可断点续跑 │ │ │ │ • 长期记忆:偏好/项目/经验 │ │ → 静态前缀(省缓存)或 RAG │ │ │ │ • 记忆有边界:隐私/污染/成本 │ └──────────────────────────────┘
关键认知:记忆让无状态的模型"记得"发生过什么。核心是两层工程——窗口会满(要压缩)、退出会丢(要持久化);再往上是长期记忆(记住用户与项目),深度取决于服务周期。记忆管理的好坏,决定 Agent 能陪你聊多久、干多复杂的活。
课后实验
- 感受记忆膨胀:让 agent 连续做几个任务,观察它的对话历史越来越长。
- 比较三种策略:如果窗口满了,截断 vs 摘要,哪个会让模型"忘事"?验证一下。
- 做持久化:给你的 Agent 加 JSONL 存盘,关闭重开,问"我们之前聊了啥",看它记不记得。
- 设计长期记忆:给你的 Agent 设计一条用户偏好("用中文回答"),分别用"拼进 system prompt 前缀"和"检索式"两种方式实现,对比成本与效果。
下一章:小a还想在循环里"插手"——工具执行前拦一下、执行后改一下。这就要用到 Hooks。 → 第 9 章 · Hooks