【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 或数据库。

相关推荐
AI绘画哇哒哒11 小时前
【建议收藏!】35岁后端血泪忠告,这3类人别硬转Agent(过来人亲述)
java·人工智能·后端·ai·程序员·大模型·agent
唐青枫12 小时前
一个值多种形态:Zig union、Tagged Union 与内存布局实战
后端
卷无止境12 小时前
Windows 上丝滑开发 Python,并稳定构建 Docker 镜像
后端·python·docker
Nturmoils12 小时前
备份完不算完,先还原到临时库验一遍
后端
Csvn13 小时前
📊 SQL 入门 Day 22:视图与物化视图
后端·sql
Csvn13 小时前
🐍 Day 4: Python 控制流 — 条件、循环与推导式的艺术
后端·python
2601_9537208214 小时前
【计算机毕业设计】基于Vue与Spring Boot的高校兼职信息服务平台设计与实现
spring boot·后端·课程设计
再吃一根胡萝卜15 小时前
用 Rust 写一个桌面悬浮图标:为什么它比 Python 更适合 AI 桌面工具?
后端
IT_陈寒15 小时前
Vue的computed属性把我坑惨了,原来我一直用错姿势
前端·人工智能·后端
evans在进步16 小时前
Spring Boot 核心机制详解:可执行 JAR、CORS、静态资源与配置绑定
spring boot·后端·jar