【Eino 框架入门】用 JSONL 实现会话持久化
上一篇用内存存 history,程序退出对话就没了。这篇把对话存到文件里,下次还能接着聊。
跟上一篇的区别
| ch02 | ch03 | |
|---|---|---|
| history 存储 | 内存切片 | JSONL 文件 |
| 程序退出后 | 丢失 | 保留 |
| 会话恢复 | 不支持 | 支持 |
打个比方:ch02 像浏览器的隐身模式,关掉就没了;ch03 像正常模式,历史记录都在。
核心代码
// 1. 创建 Store(管理会话目录)
store, _ := mem.NewStore("./data/sessions")
// 2. 获取或创建会话
session, _ := store.GetOrCreate(sessionID)
// 3. 用户消息加入会话(自动写文件)
session.Append(schema.UserMessage(input))
// 4. 获取完整历史,运行 Agent
history := session.GetMessages()
events := runner.Run(ctx, history)
// 5. assistant 回复加入会话(自动写文件)
session.Append(schema.AssistantMessage(content, nil))
JSONL 文件格式
每个会话一个 .jsonl 文件,JSONL 就是每行一个 JSON:
{"type":"session","id":"xxx","created_at":"2026-03-29T10:00:00Z"}
{"role":"user","content":"你好"}
{"role":"assistant","content":"你好!有什么可以帮你的?"}
{"role":"user","content":"我刚才说了什么?"}
{"role":"assistant","content":"你说了你好"}
第一行是会话头,后面每行是一条消息。追加消息就是在文件末尾加一行。
Session 管理原理
Session ID 怎么来的
用 UUID 生成,保证全局唯一:
sessionID := uuid.New().String()
// 输出类似:8a918a6e-29e7-402f-9541-05673f64dc4d
UUID 是 128 位随机数,碰撞概率极低。不需要中心化分配,每台机器自己生成就行。
会话的生命周期
创建 → 使用 → 持久化 → 恢复 → 使用 → ...
- 创建:第一次调用
GetOrCreate(sessionID),文件不存在就新建 - 使用:
Append()追加消息,GetMessages()读取历史 - 恢复:程序重启,传入相同 sessionID,从文件加载历史
Store 的缓存机制
Store 内部有个 map[string]*Session 缓存:
type Store struct {
dir string
mu sync.Mutex
cache map[string]*Session // 缓存已加载的会话
}
调用 GetOrCreate(sessionID) 时:
- 先查缓存,有就直接返回
- 没有才读文件,加载后放入缓存
这样同一个会话多次访问不会重复读文件。
追加写入的性能优化
每次 Append() 都写文件,会不会慢?
其实很快。因为:
- 追加写:
os.O_APPEND模式,操作系统直接在文件末尾写,不需要移动指针 - 顺序 IO:机械硬盘顺序写比随机写快 100 倍
- 小数据:一条消息几百字节,写一次几乎不耗时
真正慢的是 GetMessages() 时读整个文件,但这也只发生在恢复会话时。
并发安全
Session 有 sync.Mutex 保护:
type Session struct {
mu sync.Mutex
messages []*schema.Message
// ...
}
func (s *Session) Append(msg *schema.Message) error {
s.mu.Lock()
defer s.mu.Unlock()
// ...
}
多个 goroutine 同时操作同一个 Session 不会出问题。
Store 和 Session 的关系
Store 管理目录,Session 管理单个会话:
./data/sessions/ ← Store 管这个目录
├── abc-123.jsonl ← Session 1
├── def-456.jsonl ← Session 2
└── ghi-789.jsonl ← Session 3
一个 Store 实例管理一个目录,多个目录就创建多个 Store。
为什么要持久化
断点续聊。用户关闭程序,下次打开还能接着聊。
多设备同步。会话文件存在服务器,手机和电脑都能看到同一份历史。
数据分析。对话记录都在,可以做统计、训练模型。
为什么用 JSONL 不用 SQLite
JSONL 够简单。追加就是 os.OpenFile(O_APPEND),读取就是 bufio.Scanner 逐行解析。
不需要索引、不需要事务、不需要复杂查询。会话就是顺序追加,JSONL 刚好合适。
如果以后要支持大量会话、复杂查询,再换 SQLite 或数据库也不迟。
用法
# 新建会话
go run ./cmd/ch03
# 输出:Created new session: 8a918a6e-29e7-402f-9541-05673f64dc4d
# 恢复会话
go run ./cmd/ch03 --session 8a918a6e-29e7-402f-9541-05673f64dc4d
# 输出:Resuming session: 8a918a6e-29e7-402f-9541-05673f64dc4d
这篇的局限
会话文件是本地的,不支持分布式。如果要部署多实例,得把会话存到 Redis 或数据库。