Opencode 核心设计-Session会话机制

0 阅读5分钟

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创建/获取 Sessionsession/index.ts
2创建 User Messagesession/index.ts:685
3构建系统提示词session/system.ts
4获取可用工具session/llm.ts
5调用 LLMsession/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 表

idsession_iddata
msg_001sess_001{"role":"assistant","agent":"build"}

Part 表

idmessage_iddata
part_001msg_001{"type":"text","text":"好的"}
part_002msg_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. 总结

核心要点

  1. 三层结构:Session → Message → Part
  2. 文本不压缩:只有工具输出会被压缩
  3. 可多次触发:直到没有工具可压缩
  4. 渐进式压缩:从轻量到激进

一句话总结

Session 管理一次 AI 对话的生命周期,Message 包含多 Part,压缩只作用于工具输出保留文本,直到没有工具可压缩且文本仍超限时会话结束。


相关源码

文件作用
session/index.tsSession 核心逻辑
session/message-v2.tsMessage 和 Part 定义
session/llm.tsLLM 调用
session/processor.ts消息处理器
session/compaction.ts压缩机制
session/session.sql.ts数据库 Schema