会话(Session)详解
一句话概括
会话管理就是让 AI Agent 记住"上次聊到哪了",实现跨请求、跨进程、多用户的状态恢复。
你能学到什么
- 理解为什么 Agent 需要会话管理
- 掌握双轨存储的设计思想(快照 + 日志)
- 学会使用
RuntimeContext配置会话 - 理解多用户隔离的实现原理
前置知识
详见 学习指南。此外需了解 工作空间(Workspace) 的文件存储基本结构和 JSONL 文件格式。
核心概念
会话(Session)--- 餐厅的点餐单
生活类比:想象你去一家餐厅吃饭,每次来都会有一张专属的点餐单。
- 点餐单 = 会话(Session)
- 你的名字 = sessionId(会话ID)
- 记录的菜品 = 对话历史
- 下次来继续点菜 = 恢复会话状态
如果餐厅不保存点餐单,你每次去都要重新说一遍"我要一份宫保鸡丁,不要花生"。会话管理就是帮 Agent 准备好这张"点餐单",让它记得之前的对话内容。
双轨存储 --- 快照 + 日志
生活类比:就像你备份手机数据有两种方式:
- 快照(Snapshot):定期给手机拍个"全家福",某一时刻的完整状态
- 日志(Log):记流水账,每发生一件事就记一笔,永不删除
| 方式 | 类比 | 特点 | Agent 中的用途 |
|---|---|---|---|
| 快照 | 手机备份 | 定期保存完整状态 | 保存 Memory、ToolExecutionContext 等状态 |
| 日志 | 记账本 | 逐条追加,永不修改 | 保存完整对话历史,用于审计 |
为什么需要两种?
- 快照:恢复快,直接加载就能用
- 日志:完整记录,不会丢失任何对话细节,可以追溯历史
RuntimeContext --- 餐厅的预订信息
生活类比:打电话预订餐厅时,你要告诉对方:
- "我叫张三" ---
userId(用户身份) - "预订今晚8点的桌子" ---
sessionId(这顿饭的标识) - "我要靠窗的位置" --- 其他上下文信息
java
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("sess-001") // 这顿"饭"的编号
.userId("alice") // "吃饭"的人是谁
.build();
多用户隔离 --- 餐厅的分区座位
生活类比:一家餐厅要服务很多客人,每个人的点餐单要分开存放:
- 张三的点餐单放 A 区
- 李四的点餐单放 B 区
- 互不干扰,隐私得到保护
在代码中:
java
// Alice 和 Bob 用同一个 Agent,但各自独立
agent.call(msg, RuntimeContext.builder()
.sessionId("alice-1")
.userId("alice")
.build());
// 会话文件在:workspace/agents/MyAgent/context/alice-1/
agent.call(msg, RuntimeContext.builder()
.sessionId("bob-1")
.userId("bob")
.build());
// 会话文件在:workspace/agents/MyAgent/context/bob-1/
关键代码解读
1. 创建 RuntimeContext
java
// 创建运行时上下文,告诉 Agent "我是谁,这是哪次对话"
RuntimeContext ctx = RuntimeContext.builder()
.sessionId("sess-001") // 设置会话 ID,相当于"这顿饭的编号"
.userId("alice") // 设置用户 ID,相当于"谁在吃饭"
.build();
// 用这个上下文调用 Agent
// block() 表示阻塞等待结果(响应式编程)
agent.call(msg, ctx).block();
2. 默认会话与自定义会话
java
// 方式一:使用默认会话(最简单)
// HarnessAgent 会自动创建 WorkspaceSession
HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(workspace)
.build();
// 会话文件存在:workspace/agents/MyAgent/context/
// 方式二:使用自定义路径的会话
HarnessAgent.builder()
.name("MyAgent")
.model(model)
.workspace(workspace)
.session(new JsonSession(Path.of("/custom/sessions"))) // 指定会话存储位置
.build();
// 会话文件存在:/custom/sessions/
// 方式三:在调用时临时覆盖会话设置
agent.call(msg, RuntimeContext.builder()
.sessionId("sess-001") // 临时指定会话 ID
.session(customSession) // 临时使用自定义会话
.sessionKey(SimpleSessionKey.of("sess-001")) // 会话的键名
.build())
.block();
3. 多用户隔离示例
java
// 同一个 Agent 实例服务不同用户
// alice 的会话
agent.call(msg, RuntimeContext.builder()
.sessionId("alice-1") // alice 的第一个会话
.userId("alice") // 用户 ID
.build())
.block();
// bob 的会话
agent.call(msg, RuntimeContext.builder()
.sessionId("bob-1") // bob 的第一个会话
.userId("bob") // 用户 ID
.build())
.block();
// 结果:两个用户的会话状态、文件路径完全隔离
// alice 的数据在:context/alice-1/
// bob 的数据在:context/bob-1/
4. bindRuntimeContext 的处理逻辑
java
// HarnessAgent.bindRuntimeContext 内部逻辑(简化版)
public void bindRuntimeContext(RuntimeContext ctx) {
// 1. 补默认值
// 如果没传 session,就用 defaultSession(默认是 WorkspaceSession)
Session session = ctx.getSession() != null
? ctx.getSession()
: this.defaultSession;
// 2. 确定 sessionKey
// 优先级:传入的 sessionKey > sessionId > agentName
SessionKey key = ctx.getSessionKey() != null
? ctx.getSessionKey()
: SimpleSessionKey.of(ctx.getSessionId());
// 3. 预加载状态
// 如果 session 和 sessionKey 都有,尝试从磁盘恢复之前的状态
if (session != null && key != null) {
delegate.loadIfExists(session, key);
}
// 4. 同步 userId(用于多租户文件隔离)
this.userIdRef.set(ctx.getUserId());
}
整体流程图
┌─────────────────────────────────────────────────────────────────────┐
│ 用户发起请求 │
│ agent.call(msg, ctx) │
└─────────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 1. bindRuntimeContext │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ • 补默认 session/sessionKey │ │
│ │ • 调用 loadIfExists 恢复之前保存的状态 │ │
│ │ • 设置 userId 用于多用户隔离 │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 2. 执行 Agent 逻辑 │
│ (调用 LLM、执行工具等) │
└─────────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 3. 触发 PostCallEvent / ErrorEvent │
└─────────────────────────┬───────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ SessionPersistenceHook │ │ MemoryFlushManager │
│ (priority: 900) │ │ (压缩/flush 时触发) │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ saveTo(session) │ │ offloadMessages() │
│ 保存状态快照 │ │ 追写对话日志 │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ context/<sessionId>/│ │ sessions/<sessionId> │
│ ├── memory.json │ │ ├── .jsonl (压缩上下文) │
│ └── *.json │ │ └── .log.jsonl (完整) │
│ (StateModule快照) │ │ (JSONL双文件) │
└──────────────────────┘ └──────────────────────┘
文件存储结构
workspace/
└── agents/
└── <agentId>/
├── context/ ← WorkspaceSession 负责
│ └── <sessionId>/ ← 每个会话独立目录
│ ├── memory.json ← Agent 的记忆快照
│ └── *.json ← 其他 StateModule 快照
│
└── sessions/ ← SessionTree + WorkspaceManager 负责
├── sessions.json ← 会话索引(所有会话概览)
├── <sessionId>.jsonl ← LLM 可见的压缩上下文
└── <sessionId>.log.jsonl ← 完整对话日志(永不压缩)
关键区别:
| 目录 | 内容 | 谁负责 | 特点 |
|---|---|---|---|
context/ |
状态快照 | WorkspaceSession | 恢复快,覆盖写 |
sessions/ |
对话日志 | SessionTree | 完整记录,追加写 |
学习要点
1. 会话解决的核心问题
"上次聊到哪了?" --- 这是会话管理要解决的根本问题。没有会话管理,Agent 就像金鱼记忆,每次对话都从零开始。
2. 双轨存储的设计智慧
| 存储方式 | 类比 | 优点 | 缺点 |
|---|---|---|---|
| 快照 | 拍照纪念 | 恢复快,直接可用 | 会丢失快照后的变化 |
| 日志 | 记流水账 | 完整记录,可追溯 | 恢复慢,需要回放 |
两者结合,取长补短 --- 这就是工程设计的智慧。
3. RuntimeContext 的三个关键字段
java
RuntimeContext.builder()
.sessionId("xxx") // 会话唯一标识,决定文件存哪
.userId("xxx") // 用户标识,实现多用户隔离
.session(custom) // 自定义会话实现(可选)
.build();
4. 多用户隔离的两个层面
- 会话层隔离 :不同
sessionId的文件放在不同目录 - 文件层隔离 :不同
userId的文件操作有不同路径前缀
5. 常见使用场景
java
// 场景一:单用户简单使用(使用默认设置)
agent.call(msg).block();
// 场景二:多用户服务(必须设置 userId)
agent.call(msg, RuntimeContext.builder()
.sessionId("chat-001")
.userId("user-alice")
.build()).block();
// 场景三:恢复历史会话(使用已有 sessionId)
agent.call(msg, RuntimeContext.builder()
.sessionId("previous-session-id") // 之前的会话 ID
.userId("user-alice")
.build()).block();
与其他模块的关系
⬅️ 上一篇:03-workspace | 📖 学习指南 | ➡️ 下一篇:05-memory