没有记忆的助手,每天早上都重新认识你一遍。本篇讲知了的三层记忆设计:会话历史、滚动摘要、跨会话长期记忆。代码对应
zhiliao/agent/memory.py。
一、为什么"记忆"是个问题?
两个残酷的事实:
- 上下文窗口有限且按 token 收费。历史无限堆下去,第一请求会被拒(超窗),第二即使不超,每轮都把全部历史重发一遍,账单线性上涨、速度线性变慢。
- 模型天生没有跨会话记忆。今天聊的内容,明天新开会话它一无所知。
所以记忆系统的目标很明确:在"记得住"和"装得下、付得起"之间做工程权衡。
二、三层记忆:各司其职
┌──────────────────────────────────────────────────┐
│ 1. 工作记忆(本轮 messages) │
│ 工具调用的中间过程,任务结束即弃 │
│ → 第 3 篇讲过:又长又贵,不进历史 │
├──────────────────────────────────────────────────┤
│ 2. 会话记忆(history + 滚动摘要) │
│ 用户/助手的完整往来。超过上限 → 旧的一半浓缩成摘要 │
│ → "我不记得每个字,但我记得我们在忙什么" │
├──────────────────────────────────────────────────┤
│ 3. 长期记忆(facts,存 JSON 文件) │
│ 用户偏好、重要事实。跨会话、跨重启 │
│ → Agent 用 remember_fact/recall_facts 主动读写 │
└──────────────────────────────────────────────────┘
一个真实的对话感受:
(周三)你 > 记住,我在做一款宠物社区 App,叫毛球
知了 > 已记住:用户在做宠物社区App「毛球」
(周五,新会话)
你 > 帮我想三个新功能
知了 > 围绕毛球的用户(铲屎官们),我想到:① ……
周五那次回答的依据,就是长期记忆里的一条 fact 被检索出来、注入了本轮上下文。
三、会话记忆 + 滚动摘要
存储结构
self.history: list = [] # [{"role":"user","content":"..."}, ...]
self.summary: str = "" # 被浓缩掉的旧对话的摘要
取的时候:摘要在前,最近的在后
def view(self):
messages = []
if self.summary:
messages.append({"role": "system",
"content": f"【此前对话的摘要】{self.summary}"})
return messages + self.history[-self.history_max:] # 默认保留最近 24 条
突破口:摘要这件事,模型自己就能干
历史一超限,把旧的一半"请"大模型浓缩:
def maybe_summarize(self, llm):
if not self.needs_summarize():
return
cut = len(self.history) // 2
old_part, self.history = self.history[:cut], self.history[cut:]
transcript = "\n".join(f"{m['role']}: {m['content']}" for m in old_part)
new_summary = llm.complete(
f"请把下面这段人机对话浓缩成 200 字以内的要点,"
f"保留用户的偏好、目标、重要事实,丢弃寒暄和过程细节:\n\n{transcript}")
self.summary = f"{self.summary}\n{new_summary}".strip() ...
注意提示词里"保留偏好、目标、重要事实,丢弃寒暄和过程细节"——摘要的质量完全取决于你告诉模型扔什么留什么。还有个细节:新旧摘要是拼接的(摘要的摘要),多次滚动后仍能保留最早的线索。
成本直觉:摘要是"一次付费,永久省钱"的买卖——花几十 token 做摘要,省下之后每一轮重复发送几千 token。
四、长期记忆:一个 JSON 文件 + 主动读写
存储: humans 能直接读的 JSON
# storage/memory.json 长这样
[{"text": "用户在做宠物社区App「毛球」", "ts": "2026-09-30 14:22"}]
写入:让模型自己决定"什么值得记"
remember_fact 被注册成工具,系统提示里写明触发条件(用户说"记住…"、表达偏好时)。记什么不该由代码硬编码,而该由模型按对话语境判断——这就是"给 Agent 权限"的设计。
读取:两条路,都要有
路 1:每轮自动检索注入。 core.py 每轮开头拿用户输入去匹配:
facts = self.memory.search_facts(user_input, top=3)
if facts:
context_blocks.append("【相关长期记忆】\n" + "\n".join(f"- {t}" for t in facts))
好处是用户不用主动提,Agent 就能"想起"相关的事。
路 2:模型主动调 recall_facts。 当它觉得需要回忆时自己查。两条路互补:自动注入覆盖"明显相关",主动检索覆盖"隐藏相关"。
检索算法:朴素关键词打分
def search_facts(self, query, top=3):
query = query.lower()
scored = []
for f in self.facts:
text = f["text"].lower()
score = sum(1 for ch in query if ch.isalnum() and ch in text) # 逐字符命中
if query in text:
score += 2 # 整串命中加权
if score > 0:
scored.append((score, f["text"]))
scored.sort(key=lambda x: -x[0])
return [t for _, t in scored[:top]]
对十几条记忆足够好了。记忆上了几百条?把打分函数换成第 6 篇的 TF-IDF,甚至 embedding——接口不变,换算法即可,这正是当初把检索写成独立方法的好处。
五、会话持久化:重启不丢
def save_session(self, path): # 每轮对话结束自动存
...json.dumps({"summary": self.summary, "history": self.history}, ...)
Memory.load_session(path, ...) # 下次启动自动恢复
CLI 里 /new 会先把旧会话按时间戳存档再清空——用户数据要温柔对待,永远先存档、不直接删。
六、进阶方向(本篇留的窗口)
- 语义记忆:关键词检索换成 embedding 向量检索,"我在做宠物App"能被"毛球进展如何"召回
- 情景记忆:除了"事实",还存"事件"(哪天干了什么),按时间线回放
- 记忆巩固:定期让模型整理 facts——合并重复、淘汰过时、提炼规律
- 遗忘:给每条 fact 加访问次数/最后访问时间,长期不用的降权——和人类记忆一样,遗忘是特性不是缺陷
本篇作业
- 对知了说"记住:我喜欢简洁的回复",重启程序,验证它还记得。
- 把
history_max_messages改成 4,连续聊 6 轮,打开storage/session_current.json亲眼看看摘要长什么样。 - 给
search_facts加一个"ts 越新加分越多"的时间衰减,体会权衡。
下一篇预告:《06 · RAG 知识库》——记忆解决"记住你",RAG 解决"读过书"。不装向量数据库、不装任何依赖,用纯 Python 实现中文检索,让 Agent 能回答你私有文档里的问题。