进阶20 分钟
记忆与多轮对话
从无状态到会话持久化:短期记忆、长期记忆、项目记忆三层体系;参考 easy-agent 的 session + memory 实现。
模型没有记忆——你以为的「它记得我」,只是把历史全量重发了一遍。这一章讲怎么管理这些历史。
先破一个幻觉
你跟 ChatGPT 说「刚才那个文件名是什么」,它答对了。你觉得它「记得」。
实际上,它每次调用都收到了从第一句到现在的完整对话历史。「记得」不是存储在模型里,是存储在你发过去的消息数组里。
三层记忆体系
实际的 Agent 产品不是把全部历史无限重发,而是分层管理:
easy-agent 的会话持久化
easy-agent Step 9 实现了项目级的会话持久化。核心思路出奇地简单:用 JSONL 文件(每行一个 JSON 对象)追加写入。
为什么选 JSONL?
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSONL(追加写) | 写入零成本、天然按时间排序、崩溃不丢已写数据 | 读取时要逐行解析 |
| SQLite | 结构化查询、事务 | 引入依赖、过度设计 |
| 内存 → 定期刷盘 | 简单 | 崩溃丢数据 |
对个人 Agent 来说,JSONL 是刚好够用的方案。
存储结构
~/.easy-agent/
projects/
a3f8b2c1d4e5f6a7/ ← 项目路径的 hash
session-uuid-1.jsonl ← 每个会话一个文件
session-uuid-2.jsonl
latest ← 记录最近的 session ID
每行一个 JSON 对象,五种类型:
// 会话元信息(第一行)
{ type: "session_meta", sessionId, cwd, startedAt, model }
// 对话消息
{ type: "message", timestamp, role: "user", message: { … } }
// 工具事件(记录但不重放)
{ type: "tool_event", timestamp, name: "Read", phase: "start" }
// Token 用量
{ type: "usage", timestamp, turn: { input: 500, output: 100 }, total: { … } }
// 系统日志
{ type: "system", timestamp, level: "info", message: "…" }
恢复:从文件读回消息数组
async function restoreSession(cwd, sessionId) {
// 1. 读 JSONL 文件
const entries = await readTranscriptEntries(transcriptPath);
// 2. 只取 message 类型的条目
const messages = entries
.filter(e => e.type === "message")
.map(e => e.message);
// 3. 返回可用的消息数组 + 统计信息
return { messages, summary: { messageCount, totalUsage } };
}
重启后 restoreSession 读回消息数组,Agent 就「记得」上次聊到哪了。
关键设计决策
追加写,不覆盖写
await fs.appendFile(transcriptPath, JSON.stringify(entry) + "\n");
appendFile 是原子操作——进程崩溃时最多丢最后一行,不会损坏整个文件。这是所有日志系统的标准做法。
项目隔离
function getProjectHash(cwd) {
return crypto.createHash("sha256")
.update(path.resolve(cwd))
.digest("hex")
.slice(0, 16);
}
不同项目(不同目录)的会话完全隔离,不会串。Claude Code 也是这么做的。
工具事件不重放
JSONL 里记录了 tool_event(工具什么时候被调用),但恢复时只取 message——工具执行是有副作用的(读文件、跑命令),不能恢复时重新执行。
LangChain 的记忆抽象
LangChain 把这些模式包装了:
import { ChatMessageHistory } from "@langchain/community/stores/message/chat_message_history";
const history = new ChatMessageHistory();
await history.addUserMessage("鸡胸肉怎么做嫩?");
await history.addAIMessage("低温慢煮 60°C…");
const messages = await history.getMessages(); // 恢复时用
| easy-agent 的裸实现 | LangChain |
|---|---|
| JSONL 文件 + appendFile | ChatMessageHistory(可接 Redis/Postgres) |
| 手动过滤 message 条目 | getMessages() 直接返回 |
| 手动算 projectHash | sessionId 参数 |
底层逻辑完全一样,LangChain 只是帮你接好了后端存储。
本章要点
- 模型没有记忆——「记得」= 全量重发消息数组
- 三层记忆:工作记忆(当前对话)→ 项目记忆(跨会话)→ 长期记忆(全局偏好)
- JSONL 追加写是个人 Agent 的最佳持久化方案:零成本、崩溃安全
- 工具事件记录但不重放——工具执行有副作用
- LangChain 的 ChatMessageHistory 是同一思想的框架包装
→ 下一章:RAG 代码化——拆开 Dify 的黑盒