AI Agent 学习笔记(二):Memory 记忆,让 Agent 记住一切

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。窗口里要同时装下:

  1. 系统提示词(长期规则);
  2. 历史对话(记忆);
  3. 工具定义;
  4. 检索到的资料(RAG);
  5. 用户当前的问题。
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 deque

class 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", "可爱!")
# 超过 4 条后,最老的自动被丢掉
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 OpenAI

client = 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 json
import os

class 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", -- fact / preference / event
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 sqlite3

class 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 json
from openai import OpenAI

client = OpenAI()

def embed(text: str) -> list:
resp = client.embeddings.create(model="text-embedding-3-small", input=text)
return resp.data[0].embedding

# 1. 记忆库:文本 + 向量(演示用列表,真实项目用向量数据库)
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:
# 余弦相似度:越接近 1 越相似
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 chromadb

client = chromadb.Client()
collection = client.get_or_create_collection("agent_memory")

# 写入(embedding 可以自己传,也可以让 Chroma 用默认模型)
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 检索增强(下一篇)
存什么 与用户的交互:偏好、历史、进度 外部知识:文档、网页、数据库
来源 对话过程中产生 预先准备的资料库
更新 每轮对话都可能写入 定期/人工更新
典型问题 “我上次让你做什么来着?” “你们产品的退款政策是什么?”

九. 小结与作业

小结

  1. 大模型 API 无状态,记忆必须由我们自己实现;
  2. 短期记忆:滑动窗口、摘要压缩、关键信息抽取;
  3. 长期记忆:JSON / SQLite / Redis 等外部存储;
  4. 语义记忆:Embedding + 向量数据库,按“意思”检索;
  5. Memory 记“人”,RAG 存“知识”,两者配合使用。

作业

  1. 用 SQLite 给“用户偏好”建表,写一个增删查的 Memory 类;
  2. 装好 Chroma,存 5 条你自己的“学习偏好”,用 2 种不同说法检索,观察结果;
  3. 想一想:向量检索结果如果和问题完全无关,怎么兜底?(提示:相似度阈值)

下一篇:Tool 工具与 Function Calling——给 Agent 装上真正能干活的“手”。