10 KiB
tags, create time, title, weight
| tags | create time | title | weight | ||||||
|---|---|---|---|---|---|---|---|---|---|
|
2026-04-29 15:30 | 第三章:Memory 与 Session(持久化对话) | 3 |
概述
在第二章掌握了多轮对话的实现后,我们面临一个关键问题:对话历史只存在于内存中,进程退出后一切归零。本章引入 Memory 与 Session 机制,让对话历史能够持久化保存并跨进程恢复,为构建真正的智能助手奠定基础。
[!warning] 重要概念区分:业务层 vs 框架层 本章介绍的 Memory、Session、Store 是业务层概念,不是 Eino 框架的核心组件。Eino 框架只负责"如何处理消息",而"如何存储消息"完全由业务层决定。本章提供的实现只是一个参考示例,你可以根据自己的需求选择数据库、Redis、云存储等方案。
代码位置
- 入口代码:cmd/ch03/main.go
- Store 实现:mem/store.go
前置条件
与第二章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。
运行
在 examples/quickstart/chatwitheino 目录下执行:
# 创建新会话
go run ./cmd/ch03
# 恢复已有会话
go run ./cmd/ch03 --session <session-id>
输出示例:
Created new session: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
Session title: New Session
Enter your message (empty line to exit):
you> 你好,我是张三
[assistant] 你好张三!很高兴认识你...
you> 我叫什么名字?
[assistant] 你叫张三...
Session saved: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
Resume with: go run ./cmd/ch03 --session 083d16da-6b13-4fe6-afb0-c45d8f490ce1
从内存到持久化:为什么需要 Memory
[!question] 思考一下 第二章我们实现了多轮对话,但存在一个问题——如果进程退出、机器重启,之前聊的内容还会在吗?
内存存储的局限:
- 进程退出后,对话历史丢失
- 无法跨设备、跨进程恢复会话
- 无法实现会话管理(列表、删除、搜索等)
Memory 的定位:
- Memory 是对话历史的持久化存储:将对话保存到磁盘或数据库
- Memory 支持 Session 管理:每个 Session 代表一次完整的对话
- Memory 与 Agent 解耦:Agent 不关心存储细节,只关心消息列表
简单类比:
| 方式 | 比喻 | 特点 |
|---|---|---|
| 内存存储 | "草稿纸" | 进程退出就没了 |
| Memory | "笔记本" | 永久保存,随时翻阅 |
关键概念
[!tip] 重要提示 以下 Session、Store 等概念都是业务层实现,用于管理对话历史的存储。Eino 框架本身不提供这些组件,而是由业务层负责管理消息列表,然后将消息传递给
adk.Runner进行处理。
Session(业务层概念)
Session 代表一次完整的对话会话:
type Session struct {
ID string
CreatedAt time.Time
messages []*schema.Message // 对话历史
// ...
}
核心方法:
Append(msg):追加消息到会话,并持久化GetMessages():获取所有消息Title():从第一条用户消息生成会话标题
Store(业务层概念)
Store 管理多个 Session 的持久化存储:
type Store struct {
dir string // 存储目录
cache map[string]*Session // 内存缓存
}
核心方法:
GetOrCreate(id):获取或创建 SessionList():列出所有 SessionDelete(id):删除 Session
JSONL 文件格式
每个 Session 存储为一个 .jsonl 文件:
{"type":"session","id":"083d16da-...","created_at":"2026-03-11T10:00:00Z"}
{"role":"user","content":"你好,我是谁?"}
{"role":"assistant","content":"你好!我暂时不知道你是谁..."}
{"role":"user","content":"我叫张三"}
{"role":"assistant","content":"好的,张三,很高兴认识你!"}
为什么用 JSONL?
| 特性 | 说明 |
|---|---|
| 简单 | 每行一个 JSON 对象,易于读写 |
| 可扩展 | 可以追加新消息,无需重写整个文件 |
| 可读性好 | 可以用文本编辑器直接查看 |
| 容错性强 | 单行损坏不影响其他行 |
Memory 的实现(业务层示例)
以下是一个简单的业务层实现示例,使用 JSONL 文件存储对话历史。这只是众多可能实现中的一种,你可以根据实际需求选择数据库、Redis 等其他存储方案。
1. 创建 Store
sessionDir := "./data/sessions"
store, err := mem.NewStore(sessionDir)
if err != nil {
log.Fatal(err)
}
2. 获取或创建 Session
sessionID := "083d16da-6b13-4fe6-afb0-c45d8f490ce1"
session, err := store.GetOrCreate(sessionID)
if err != nil {
log.Fatal(err)
}
3. 追加用户消息
userMsg := schema.UserMessage("你好")
if err := session.Append(userMsg); err != nil {
log.Fatal(err)
}
4. 获取历史并调用 Agent
history := session.GetMessages()
events := runner.Run(ctx, history)
content := collectAssistantFromEvents(events)
5. 追加助手消息
assistantMsg := schema.AssistantMessage(content, nil)
if err := session.Append(assistantMsg); err != nil {
log.Fatal(err)
}
关键代码解析
完整流程可以浓缩为以下核心片段:
// 1. 创建或恢复 Session
session, err := store.GetOrCreate(sessionID)
if err != nil {
log.Fatal(err)
}
// 2. 读取用户输入并追加到会话
userMsg := schema.UserMessage(line)
if err := session.Append(userMsg); err != nil {
log.Fatal(err)
}
// 3. 获取全部历史,送入 Agent 处理
history := session.GetMessages()
events := runner.Run(ctx, history)
content := collectAssistantFromEvents(events)
// 4. 收集回复并存回会话
assistantMsg := schema.AssistantMessage(content, nil)
if err := session.Append(assistantMsg); err != nil {
log.Fatal(err)
}
[!note] 简化说明 以上代码已省略错误处理之外的细节(如命令行参数解析、事件流消费等),不能直接运行。完整代码请参考 cmd/ch03/main.go。
Session 与 Agent 的关系:业务层与框架层的协作
[!tip] 理解要点
- Session 是业务层概念:由你的代码实现和管理,负责存储和加载对话历史
- Agent(Runner)是框架层概念:由 Eino 框架提供,负责处理消息并生成回复
- 两者的交互点:业务层通过
session.GetMessages()获取消息列表,传递给runner.Run(ctx, history)进行处理
数据流示意图:
flowchart TD
A["用户输入"] --> B["session.Append()<br/>保存用户消息"]
B --> C["session.GetMessages()<br/>获取完整历史"]
C --> D["runner.Run(history)<br/>Agent 处理消息"]
D --> E["收集助手回复"]
E --> F["session.Append()<br/>保存助手消息"]
style B fill:#e8f5e9
style C fill:#e8f5e9
style F fill:#e8f5e9
style D fill:#fff3e0
分层面貌:
graph LR
subgraph biz_layer["业务层 — 你的代码"]
S1["Session<br/>持久化存储"]
S2["GetMessages()"]
S3["Append()<br/>保存消息"]
S1 --> S2
S3 -.->|"回写"| S1
end
subgraph frame_layer["框架层 — Eino"]
R1["runner.Run()"]
end
S2 --> R1
R1 -->|"助手回复"| S3
style S1 fill:#e3f2fd
style S2 fill:#e3f2fd
style S3 fill:#e3f2fd
style R1 fill:#fff3e0
style biz_layer fill:none,stroke:#90caf9
style frame_layer fill:none,stroke:#ffcc80
本章小结
| 核心概念 | 定位 | 说明 |
|---|---|---|
| Memory | 业务层 | 对话历史的持久化存储,支持跨进程恢复 |
| Session | 业务层 | 一次完整的对话会话,包含 ID、创建时间、消息列表 |
| Store | 业务层 | 管理多个 Session 的存储,支持创建、获取、列表、删除 |
| JSONL 格式 | 业务层 | 简单的文件格式,易于读写和扩展 |
| adk.Runner | 框架层 | 接收消息列表,调用 ChatModel,返回回复 |
[!success] 学习成果 完成本章后,你应该能够:
- 理解 Memory/Session/Store 的业务层职责
- 知道如何将对话历史持久化为 JSONL 文件
- 明白业务层与框架层的协作边界
- 根据业务需求选择合适的存储方案
扩展思考:业务层存储方案的选择
本章提供的 JSONL 文件存储方案适合简单的单机应用。在实际业务中,你可能需要考虑其他存储方案:
| 存储方案 | 适用场景 | 优势 | 劣势 |
| JSONL 文件 | 单机应用、开发调试 | 零依赖,简单直观 | 不支持并发、分布式 |
| SQLite / LevelDB | 桌面端应用 | 轻量级嵌入式数据库 | 不适合高并发写入 |
| MySQL / PostgreSQL | 服务端部署 | 成熟稳定,功能丰富 | 运维成本较高 |
| Redis | 分布式、高频访问 | 性能极高,支持过期策略 | 数据需额外持久化 |
| S3 / OSS | 海量冷数据归档 | 成本极低,无限扩展 | 不适合频繁查询 |
高级功能展望:
- 会话过期清理(TTL 自动删除)
- 会话全文搜索
- 会话导出 / 导入
- 会话分享(生成公开链接)
[!tip] Middleware 联动 当对话非常长时,单纯增加存储容量是不够的。第五章介绍的 Summarization Middleware 可以在调用 Agent 之前自动压缩历史消息,有效控制 Token 消耗。
下一章预告
Eino/quick_start/chapter_04_tool_and_filesystem 将为 Agent 添加文件访问能力,让智能助手能够读取代码仓库中的真实内容。