Session 会话机制详解
本系列文章皆基于开源 Vibecoding 工具 Opencode 源码进行详细拆解。
源码链接:github.com/anomalyco/o…
1. 什么是 Session?
一句话概括:Session 管理一次 AI 对话的生命周期,包含用户输入、LLM 调用、工具执行、压缩处理等完整过程。
当你运行 opencode run "帮我写个 hello world" 时,一个 Session 就被创建了,直到对话结束。
Session 解决的问题
| 问题 | 说明 |
|---|---|
| 上下文记忆 | AI 需要记住之前的对话内容 |
| 状态管理 | 记录文件修改、命令执行结果 |
| 历史追溯 | 支持回顾、分享、继续会话 |
2. 核心概念:三层结构
Session 由三层组成:
Session (会话)
└── Message (消息) - user/assistant/system 角色
└── Part[] (部分) - text/tool/reasoning 等类型
2.1 Session
一个独立的对话上下文,有自己的:
- ID 和标题
- 工作目录
- 权限配置
- 时间戳
2.2 Message
一次对话中的一条消息,可以是:
- user - 用户的输入
- assistant - AI 的回复
- system - 系统消息
2.3 Part
Message 中的具体内容,一条 Message 可以包含多个 Part:
| Part 类型 | 说明 | 示例 |
|---|---|---|
text | 普通文本 | AI 的回复 |
reasoning | 思考过程 | Claude/DeepSeek 的推理 |
tool | 工具调用 | 调用 bash、read 等 |
file | 文件附件 | 用户上传的文件 |
step-start | 步骤开始 | 开始新步骤 |
step-finish | 步骤结束 | 包含 token 统计 |
compaction | 压缩标记 | 历史压缩标记 |
关系图
erDiagram
SESSION ||--o{ MESSAGE : "contains"
MESSAGE ||--o{ PART : "contains"
3. 数据结构
3.1 Session
{
id: SessionID, // 唯一标识
project_id: ProjectID, // 所属项目
parent_id: SessionID, // 父会话(fork 用)
directory: string, // 工作目录
title: string, // 会话标题
permission: Ruleset, // 权限配置
time: { created, updated, compacting, archived }
}
3.2 Message
{
id: MessageID,
role: "user" | "assistant" | "system",
parentID?: MessageID, // 父消息(对话树)
agent?: string, // 使用的 Agent
modelID?: string, // 使用的模型
summary?: true, // 是否为摘要消息
}
3.3 Part
// 文本类型
{ type: "text", text: "hello" }
// 工具类型
{ type: "tool", tool: "bash", state: { status, input, output } }
// 工具状态
type ToolState =
| { status: "pending", input }
| { status: "running", input, time: { start } }
| { status: "completed", input, output }
| { status: "error", input, error }
4. 完整生命周期
4.1 流程概览
flowchart TD
Start([用户输入]) --> CreateSession
CreateSession --> BuildPrompt[构建系统提示词]
BuildPrompt --> GetTools[获取可用工具]
GetTools --> CallLLM[调用 LLM]
CallLLM --> HasToolCall{AI 调用工具?}
HasToolCall -->|是| ExecuteTool[执行工具]
ExecuteTool --> ToolResult[返回结果]
ToolResult --> CallLLM
HasToolCall -->|否| Save[保存到数据库]
Save --> CheckOverflow{token 超限?}
CheckOverflow -->|是| Compress[触发压缩]
Compress --> CallLLM
CheckOverflow -->|否| End([完成])
4.2 详细步骤
| 步骤 | 操作 | 源码位置 |
|---|---|---|
| 1 | 创建/获取 Session | session/index.ts |
| 2 | 创建 User Message | session/index.ts:685 |
| 3 | 构建系统提示词 | session/system.ts |
| 4 | 获取可用工具 | session/llm.ts |
| 5 | 调用 LLM | session/llm.ts |
| 6 | 处理工具调用 | session/processor.ts |
| 7 | 保存到数据库 | session/index.ts:754 |
| 8 | 检查是否压缩 | session/compaction.ts |
5. 压缩机制(重点)
5.1 本质
压缩只作用于工具输出,文本消息永远不压缩。
| 内容类型 | 压缩后 |
|---|---|
| 用户文字问题 | ✅ 一直发送 |
| AI 文字回复 | ✅ 一直发送 |
| AI 推理过程 | ✅ 一直发送 |
| 工具输出 | ⚠️ 替换为 "[Old tool result content cleared]" |
5.2 触发条件
// tokens 超过模型可用空间时触发
const usable = context - reserved // 保留 ~20K 给输出
return count >= usable
5.3 压缩流程
token 超限
↓
1. 调用 LLM 生成摘要
(Goal/Instructions/Discoveries/Accomplished)
2. 保存摘要为新消息 (summary=true)
3. 给旧工具输出打标记 (compacted=true)
4. 重新发送消息
↓
空间够? → ✅ 继续聊
空间不够?
↓
渐进式压缩(replay)→ 只保留最近一个用户消息
↓
空间够? → ✅ 继续聊
空间不够?
↓
媒体剥离(图片→文字)
↓
空间够? → ✅ 继续聊
空间不够? → ❌ 报错停止
5.4 可以多次触发
| 轮次 | 操作 |
|---|---|
| 第4轮 | 触发压缩 → 工具输出被标记,生成摘要 |
| 第5-10轮 | 正常聊 |
| 第11轮 | 触发压缩 → 新的工具输出被标记 |
| ... | 继续压缩,直到上限 |
5.5 渐进式压缩
当普通压缩不够时,会尝试更激进方案:
// compaction.ts:113-129
// 只保留最后一个用户消息,之前的全删掉
messages = messages.slice(0, lastUserIndex)
5.6 媒体剥离
图片/PDF 转成文字:
// 1MB 图片 → 30 字符
"[Attached image/png: screenshot.png]"
5.7 什么时候彻底不能聊?
- 所有工具输出都已压缩
- 纯文本消息本身就超过上下文限制
5.8 举例说明
压缩前:
消息1 (user): "帮我重构 user.ts"
消息2 (assistant): [text: "好的"]
消息3 (assistant): [tool: read, output: "500行代码"]
消息4 (assistant): [text: "我读取了文件"]
消息5 (user): "改成箭头函数"
消息6 (assistant): [tool: edit, output: "已修改"]
消息7 (assistant): [text: "完成了"]
压缩后:
消息1 (user): "帮我重构 user.ts"
消息2 (assistant): [text: "好的"]
消息3 (assistant): [tool: read, output: "[Old tool result content cleared]"] ← 被替换
消息4 (assistant): [text: "我读取了文件"]
消息5 (user): "改成箭头函数"
消息6 (assistant): [tool: edit, output: "[Old tool result content cleared]"] ← 被替换
消息7 (assistant): [text: "完成了"]
消息8 (assistant): [summary: true, text: "## Goal\n用户想重构...\n## Accomplished\n..."] ← 新增摘要
6. 数据库存储
6.1 表结构
// Session 表
const SessionTable = sqliteTable("session", {
id: text().$type<SessionID>().primaryKey(),
project_id: text().notNull(),
// ...
})
// Message 表
const MessageTable = sqliteTable("message", {
id: text().$type<MessageID>().primaryKey(),
session_id: text().$type<SessionID>().notNull(),
data: text({ mode: "json" }).notNull(), // role, agent 等
})
// Part 表
const PartTable = sqliteTable("part", {
id: text().$type<PartID>().primaryKey(),
message_id: text().$type<MessageID>().notNull(),
session_id: text().$type<SessionID>().notNull(),
data: text({ mode: "json" }).notNull(), // type, text, state 等
})
6.2 存储示例
Message 表:
| id | session_id | data |
|---|---|---|
| msg_001 | sess_001 | {"role":"assistant","agent":"build"} |
Part 表:
| id | message_id | data |
|---|---|---|
| part_001 | msg_001 | {"type":"text","text":"好的"} |
| part_002 | msg_001 | {"type":"tool","tool":"read","state":{...}} |
7. 关键特性
7.1 Fork(分叉)
从任意历史点创建分支:
opencode run -c --fork "尝试另一种方案"
7.2 分享
生成公开链接:
await Session.share(sessionID)
// 返回 share.opencode.ai/xxx
7.3 权限控制
每个会话可独立配置权限:
{
"permission": {
"bash": "allow",
"write": "ask",
"rm": "deny"
}
}
8. 总结
核心要点
- 三层结构:Session → Message → Part
- 文本不压缩:只有工具输出会被压缩
- 可多次触发:直到没有工具可压缩
- 渐进式压缩:从轻量到激进
一句话总结
Session 管理一次 AI 对话的生命周期,Message 包含多 Part,压缩只作用于工具输出保留文本,直到没有工具可压缩且文本仍超限时会话结束。
相关源码
| 文件 | 作用 |
|---|---|
session/index.ts | Session 核心逻辑 |
session/message-v2.ts | Message 和 Part 定义 |
session/llm.ts | LLM 调用 |
session/processor.ts | 消息处理器 |
session/compaction.ts | 压缩机制 |
session/session.sql.ts | 数据库 Schema |