Skip to content

🐣 小a的记性难题

循环跑起来了,小a能"干一步看一眼"了。但很快它发现两个麻烦:

  1. 聊久了,历史越来越长——迟早撑爆上下文窗口(第 1 章的伏笔)
  2. 关了程序再开,之前聊的都没了——它不记事

老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 记忆的边界与安全

🧙 记忆不是越多越好,有边界:

  1. 隐私:记忆里可能有敏感信息(密码、密钥)——持久化前要考虑加密/清理
  2. 污染:旧记忆里的错误结论,可能误导后续推理
  3. 成本:记忆越长,每次喂给模型的 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 能陪你聊多久、干多复杂的活。


课后实验

  1. 感受记忆膨胀:让 agent 连续做几个任务,观察它的对话历史越来越长。
  2. 比较三种策略:如果窗口满了,截断 vs 摘要,哪个会让模型"忘事"?验证一下。
  3. 做持久化:给你的 Agent 加 JSONL 存盘,关闭重开,问"我们之前聊了啥",看它记不记得。
  4. 设计长期记忆:给你的 Agent 设计一条用户偏好("用中文回答"),分别用"拼进 system prompt 前缀"和"检索式"两种方式实现,对比成本与效果。

下一章:小a还想在循环里"插手"——工具执行前拦一下、执行后改一下。这就要用到 Hooks。 → 第 9 章 · Hooks