AI Agent 学习笔记(二):Memory 记忆,让 Agent 记住一切
上一篇我们学会了让模型“听话”。这篇解决另一个问题:模型天生没有记忆 ,每次调用都是“失忆”的,我们需要自己动手给它装上记忆。
一. 为什么 Agent 需要记忆 大模型的 API 是**无状态(stateless)**的:每次调用,模型只看得到你这次传入的 messages,之前聊过什么,它一概不知。
1 2 3 4 5 messages = [{"role" : "user" , "content" : "我叫小明,我喜欢喝咖啡" }] messages = [{"role" : "user" , "content" : "我叫什么名字?" }]
所以 Agent 需要记忆系统 :把历史对话、用户偏好、任务进度存下来,在需要时重新注入到 Prompt 里。
二. 记忆的四种类型
类型
英文
存什么
生命周期
例子
短期记忆
Short-term
当前任务相关的上下文
几分钟~几小时
本轮对话历史
长期记忆
Long-term
用户偏好、长期事实
几天~几年
“用户喜欢简洁回答”
情景记忆
Episodic
发生过的事件
长期
“上周问过 MySQL 优化”
语义记忆
Semantic
通用知识
长期
产品文档、公司制度
工程上不用记这些名词,记住两条主线即可:短期记忆 = 对话历史管理 ,长期记忆 = 外部存储(数据库/文件/向量库) 。
三. 先搞懂 Token 和上下文窗口 模型按 Token(词元) 计费和处理文本。中文大致 1 个汉字 ≈ 1~2 个 Token,英文 1 个单词 ≈ 1.3 个 Token。
每个模型有一个 上下文窗口(context window) ,比如 8K、32K、128K、1M Token。窗口里要同时装下:
系统提示词(长期规则);
历史对话(记忆);
工具定义;
检索到的资料(RAG);
用户当前的问题。
1 2 3 4 5 6 7 8 9 10 11 ┌─────────────────────────────────────────────┐ │ 上下文窗口(假设 8000 Token) │ │ │ │ system 提示词 500 Token │ │ 工具定义 1000 Token │ │ 历史对话 4000 Token ← 记忆占大头│ │ 检索资料 1000 Token (RAG) │ │ 用户问题 500 Token │ │ ─────────────────────────────────────── │ │ 合计 7000 / 8000,还剩 1000 给模型输出 │ └─────────────────────────────────────────────┘
窗口装不下怎么办?这就是记忆管理要解决的三个问题:存什么、丢什么、怎么压缩 。
四. 短期记忆:对话历史管理 4.1 方案一:滑动窗口(最简单) 只保留最近 N 轮消息,最早的丢掉:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from collections import dequeclass SlidingWindowMemory : """只保留最近 max_messages 条消息""" def __init__ (self, max_messages: int = 10 ): self .history = deque(maxlen=max_messages) def add (self, role: str , content: str ): self .history.append({"role" : role, "content" : content}) def get_messages (self ): return list (self .history) memory = SlidingWindowMemory(max_messages=4 ) memory.add("user" , "我喜欢喝咖啡" ) memory.add("assistant" , "好的,记住了" ) memory.add("user" , "我的猫叫咪咪" ) memory.add("assistant" , "可爱!" ) memory.add("user" , "刚才说的两个信息是什么?" ) print (memory.get_messages())
优点:实现简单、省 Token;缺点:早期的重要信息可能被丢掉。
4.2 方案二:摘要压缩(更聪明) 当历史太长时,让模型把旧对话总结成摘要 ,只保留摘要 + 最近几轮原文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from openai import OpenAIclient = OpenAI() def summarize (history: list ) -> str : """把对话历史压缩成一段摘要""" text = "\n" .join(f"{m['role' ]} : {m['content' ]} " for m in history) resp = client.chat.completions.create( model="gpt-4o-mini" , messages=[ {"role" : "system" , "content" : "你是对话摘要助手,用 100 字以内总结对话中的关键事实和用户偏好,只输出摘要。" }, {"role" : "user" , "content" : text}, ], ) return resp.choices[0 ].message.content class SummaryMemory : def __init__ (self, keep_raw: int = 6 ): self .raw = [] self .summary = "" self .keep_raw = keep_raw def add (self, role: str , content: str ): self .raw.append({"role" : role, "content" : content}) if len (self .raw) > self .keep_raw: old = self .raw[: len (self .raw) - self .keep_raw] self .summary = summarize(old) self .raw = self .raw[-self .keep_raw:] def get_messages (self ): base = [{"role" : "system" , "content" : f"历史摘要:{self.summary} " }] if self .summary else [] return base + self .raw
4.3 方案三:关键信息抽取(面向长期记忆) 每次对话后,让模型抽出值得长期记住的事实 (用户偏好、重要决定),存进长期记忆库:
1 2 3 4 5 6 7 8 9 10 def extract_facts (dialog: str ) -> list : resp = client.chat.completions.create( model="gpt-4o-mini" , messages=[ {"role" : "system" , "content" : "从对话中抽取值得长期记住的事实,输出 JSON 数组,例如 [{\"fact\": \"用户叫小明\", \"type\": \"preference\"}]。没有就输出 []。" }, {"role" : "user" , "content" : dialog}, ], response_format={"type" : "json_object" }, ) return json.loads(resp.choices[0 ].message.content).get("facts" , [])
五. 长期记忆:外部存储 5.1 最简单的方案:JSON 文件 适合个人项目、原型验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import jsonimport osclass FileMemory : def __init__ (self, path: str = "memory.json" ): self .path = path self .data = {"facts" : [], "history" : []} if os.path.exists(path): with open (path, "r" , encoding="utf-8" ) as f: self .data = json.load(f) def save (self ): with open (self .path, "w" , encoding="utf-8" ) as f: json.dump(self .data, f, ensure_ascii=False , indent=2 ) def add_fact (self, fact: str , type_: str = "general" ): self .data["facts" ].append({"fact" : fact, "type" : type_}) self .save() def get_facts (self ): return [item["fact" ] for item in self .data["facts" ]] mem = FileMemory() mem.add_fact("用户叫小明" , "preference" ) print (mem.get_facts())
5.2 结构化方案:SQLite 适合需要按用户、按时间查询的场景:
1 2 3 4 5 6 7 CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL , content TEXT NOT NULL , kind TEXT DEFAULT "fact", created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import sqlite3class SqliteMemory : def __init__ (self, db_path: str = "agent.db" ): self .conn = sqlite3.connect(db_path) self .conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, kind TEXT DEFAULT "fact", created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """ ) self .conn.commit() def add (self, user_id: str , content: str , kind: str = "fact" ): self .conn.execute( "INSERT INTO memories (user_id, content, kind) VALUES (?, ?, ?)" , (user_id, content, kind), ) self .conn.commit() def search (self, user_id: str , keyword: str ): cur = self .conn.execute( "SELECT content, kind FROM memories WHERE user_id=? AND content LIKE ? ORDER BY id DESC LIMIT 10" , (user_id, f"%{keyword} %" ), ) return cur.fetchall() mem = SqliteMemory() mem.add("u-001" , "用户喜欢简洁的回答" , "preference" ) print (mem.search("u-001" , "简洁" ))
生产环境还可以用 Redis(快、支持过期时间)、PostgreSQL 等。对初学者,先把 JSON 和 SQLite 玩熟 就够了。
六. 语义记忆:向量数据库 6.1 问题:关键词匹配不够聪明 SQLite 的 LIKE 只能做字面匹配 :用户说“我讨厌啰嗦”,你搜“简洁”就搜不到。人的记忆是按意思联想 的,所以需要语义检索。
6.2 Embedding:把文字变成向量 Embedding(向量化) 就是把一句话变成一串数字(比如 1536 维),让意思相近的句子在向量空间里距离也近 :
1 2 3 "我喜欢喝咖啡" → [0.12, -0.34, 0.87, ...] "我爱喝拿铁" → [0.11, -0.33, 0.86, ...] ← 距离很近 "今天天气不错" → [0.95, 0.21, -0.03, ...] ← 距离很远
6.3 完整流程:写入 + 检索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import jsonfrom openai import OpenAIclient = OpenAI() def embed (text: str ) -> list : resp = client.embeddings.create(model="text-embedding-3-small" , input =text) return resp.data[0 ].embedding memory_store = [] def remember (text: str , kind: str = "fact" ): memory_store.append({"text" : text, "kind" : kind, "vector" : embed(text)}) def recall (query: str , top_k: int = 3 ): """找和 query 意思最接近的 top_k 条记忆""" q_vec = embed(query) scored = [] for item in memory_store: a, b = q_vec, item["vector" ] dot = sum (x * y for x, y in zip (a, b)) norm = (sum (x * x for x in a) ** 0.5 ) * (sum (y * y for y in b) ** 0.5 ) scored.append((dot / norm, item["text" ], item["kind" ])) scored.sort(reverse=True ) return scored[:top_k] remember("用户喜欢简洁的回答" ) remember("用户早上 9 点上班" ) remember("用户养了一只叫咪咪的猫" ) for score, text, kind in recall("这个人讨厌长篇大论" ): print (score, text, kind)
6.4 真实项目用现成向量数据库 自己算相似度只适合学习。真实项目直接用向量数据库:
工具
特点
适合
Chroma
轻量、纯 Python、易上手
学习、小项目
FAISS
Meta 开源、快
大规模向量检索
Milvus
分布式、功能全
企业级
Qdrant
Rust 实现、性能好
生产环境
pgvector
PostgreSQL 插件
已有 PG 的项目
Chroma 示例(pip install chromadb):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import chromadbclient = chromadb.Client() collection = client.get_or_create_collection("agent_memory" ) collection.add( ids=["1" , "2" ], documents=["用户喜欢简洁的回答" , "用户养了一只猫" ], metadatas=[{"kind" : "preference" }, {"kind" : "fact" }], ) results = collection.query(query_texts=["这个人讨厌啰嗦" ], n_results=2 ) print (results["documents" ])
七. Memory 在 Agent 循环里的位置 1 2 3 4 5 6 7 8 9 10 11 12 用户输入 │ ▼ ┌──────────────┐ 生成回答时注入 ┌──────────────┐ │ 短期记忆 │ ─────────────────────►│ │ │ (最近对话) │ │ LLM │ ├──────────────┤ │ │ │ 长期记忆 │ ──检索相关事实────────►│ │ │ (SQLite/向量) │ └──────┬───────┘ └──────────────┘ │ 回答 ▲ ▼ └────────── 对话结束后抽取新事实存下来 ◄──── 完成
八. Memory 和 RAG 的区别(初学者必看) 很多人把两者搞混:
Memory 记忆
RAG 检索增强(下一篇)
存什么
与用户的交互:偏好、历史、进度
外部知识:文档、网页、数据库
来源
对话过程中产生
预先准备的资料库
更新
每轮对话都可能写入
定期/人工更新
典型问题
“我上次让你做什么来着?”
“你们产品的退款政策是什么?”
九. 小结与作业 小结
大模型 API 无状态,记忆必须由我们自己实现;
短期记忆:滑动窗口、摘要压缩、关键信息抽取;
长期记忆:JSON / SQLite / Redis 等外部存储;
语义记忆:Embedding + 向量数据库,按“意思”检索;
Memory 记“人”,RAG 存“知识”,两者配合使用。
作业
用 SQLite 给“用户偏好”建表,写一个增删查的 Memory 类;
装好 Chroma,存 5 条你自己的“学习偏好”,用 2 种不同说法检索,观察结果;
想一想:向量检索结果如果和问题完全无关,怎么兜底?(提示:相似度阈值)
下一篇:Tool 工具与 Function Calling ——给 Agent 装上真正能干活的“手”。