【Eino 框架入门】用 JSONL 实现会话持久化

【Eino 框架入门】用 JSONL 实现会话持久化

上一篇用内存存 history,程序退出对话就没了。这篇把对话存到文件里,下次还能接着聊。

跟上一篇的区别

ch02 ch03
history 存储 内存切片 JSONL 文件
程序退出后 丢失 保留
会话恢复 不支持 支持

打个比方:ch02 像浏览器的隐身模式,关掉就没了;ch03 像正常模式,历史记录都在。

核心代码

go 复制代码
// 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:

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 生成,保证全局唯一:

go 复制代码
sessionID := uuid.New().String()
// 输出类似:8a918a6e-29e7-402f-9541-05673f64dc4d

UUID 是 128 位随机数,碰撞概率极低。不需要中心化分配,每台机器自己生成就行。

会话的生命周期

erlang 复制代码
创建 → 使用 → 持久化 → 恢复 → 使用 → ...
  1. 创建 :第一次调用 GetOrCreate(sessionID),文件不存在就新建
  2. 使用Append() 追加消息,GetMessages() 读取历史
  3. 恢复:程序重启,传入相同 sessionID,从文件加载历史

Store 的缓存机制

Store 内部有个 map[string]*Session 缓存:

go 复制代码
type Store struct {
    dir   string
    mu    sync.Mutex
    cache map[string]*Session  // 缓存已加载的会话
}

调用 GetOrCreate(sessionID) 时:

  1. 先查缓存,有就直接返回
  2. 没有才读文件,加载后放入缓存

这样同一个会话多次访问不会重复读文件。

追加写入的性能优化

每次 Append() 都写文件,会不会慢?

其实很快。因为:

  • 追加写os.O_APPEND 模式,操作系统直接在文件末尾写,不需要移动指针
  • 顺序 IO:机械硬盘顺序写比随机写快 100 倍
  • 小数据:一条消息几百字节,写一次几乎不耗时

真正慢的是 GetMessages() 时读整个文件,但这也只发生在恢复会话时。

并发安全

Session 有 sync.Mutex 保护:

go 复制代码
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 管理单个会话:

bash 复制代码
./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 或数据库也不迟。

用法

bash 复制代码
# 新建会话
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 或数据库。

相关推荐
Memory_荒年2 小时前
Netty面试终极指南:从“Hello World”到源码深处
java·后端
0xDevNull2 小时前
Java IO流教程:从入门到最佳实践
java·后端
Memory_荒年2 小时前
Netty深度解构:高性能背后的核心机制与实战精要
java·后端
平平无奇的开发仔2 小时前
通过@Transational注解的对象,简单了解SpringAop是如何执行的
后端
元俭2 小时前
【Eino 框架入门】用 Agent 实现多轮对话
后端
斯瓦辛武2 小时前
linux系统安装skywalking
后端
学习指针路上的小学渣2 小时前
requests笔记
后端·python
CodeSheep2 小时前
JetBrains又一知名软件宣布倒下,五味杂陈
前端·后端·程序员
SimonKing2 小时前
GitHub热榜1k星影视壳(OuonnkiTV)遇上AI影视源
java·后端·程序员