论文查重检测
80.49M · 2026-03-22
本系列文章皆基于开源 Vibecoding 工具 Opencode 源码进行详细拆解。
源码链接:github.com/anomalyco/o…
相信大家在日常使用各种 AI 编程工具的时候,有没有想过这个问题:AI 是怎么记住我们之前的对话的?
你说一句"帮我写个登录功能",AI 噼里啪啦写完了;然后你又说"再加上验证码",AI 居然知道你在登录功能基础上继续写,而不是重新从头开始写一个完整功能——它是怎么做到的?
这就是咱们今天要聊的话题:Session 会话机制。不夸张地说,理解了这个,你对 AI 编程工具的理解能上一个台阶!
Session 这个词儿相信大家不陌生,但具体是干啥的呢?
Session = 一次 AI 对话从开始到结束的完整生命周期。
当你运行 opencode run "帮我写个 hello world" 时,一个 Session 就被创建了,一直到你关闭对话为止,这个过程都叫做一个 Session。
| 问题 | 说明 |
|---|---|
| 上下文记忆 | AI 需要记住之前的对话内容,不然它就是个金鱼 |
| 状态管理 | 记录文件修改、命令执行结果,别下次又给你整重复了 |
| 历史追溯 | 支持回顾、分享、继续会话,像 git 一样管理对话 |
你说这些功能重要吗?简直不要太重要!没有 Session,你每次对话都是"从零开始",那 AI 编程工具的效率得大打折扣!
Session 这个东西啊,它是分层!分层的!重要的事情说两遍。
Session (会话)
└── Message (消息) - user/assistant/system 角色
└── Part[] (部分) - text/tool/reasoning 等类型
一个独立的对话上下文,有自己的:
一次对话中的一条消息,可以是三种角色:
| 角色 | 说明 | 谁发的 |
|---|---|---|
| user | 用户的输入 | 你 |
| assistant | AI 的回复 | AI |
| system | 系统消息 | 系统 |
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"
{
id: SessionID, // 唯一标识
project_id: ProjectID, // 所属项目
parent_id: SessionID, // 父会话(fork 用)
directory: string, // 工作目录
title: string, // 会话标题
permission: Ruleset, // 权限配置
time: { created, updated, compacting, archived }
}
{
id: MessageID,
role: "user" | "assistant" | "system",
parentID?: MessageID, // 父消息(对话树)
agent?: string, // 用的哪个 Agent
modelID?: string, // 用的哪个模型
summary?: true, // 是不是摘要消息
}
// 文本类型
{ 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 } // 执行出错
重头戏来了!咱们来看看一个 Session 到底是咋工作的:
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([完成])
| 步骤 | 操作 | 源码位置 |
|---|---|---|
| 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 |
简单说:用户输入 → 构建提示 → 获取工具 → 调用 LLM → 工具循环 → 保存 → 检查压缩 → 完事儿!
这个章节太重要了,面试经常问!
压缩这玩意儿,只作用于工具输出,文本消息永远不压缩!
| 内容类型 | 压缩后 |
|---|---|
| 用户文字问题 | 一直发送 |
| AI 文字回复 | 一直发送 |
| AI 推理过程 | 一直发送 |
| 工具输出 | 替换为 "[Old tool result content cleared]" |
// tokens 超过模型可用空间时触发
const usable = context - reserved // 保留 ~20K 给输出
return count >= usable
简单说就是:对话太长了,塞不下了!
token 超限
↓
1. 调用 LLM 生成摘要
(Goal/Instructions/Discoveries/Accomplished)
2. 保存摘要为新消息 (summary=true)
3. 给旧工具输出打标记 (compacted=true)
4. 重新发送消息
↓
空间够? → 继续聊
空间不够?
↓
渐进式压缩(replay)→ 只保留最近一个用户消息
↓
空间够? → 继续聊
空间不够?
↓
媒体剥离(图片→文字)
↓
空间够? → 继续聊
空间不够? → 报错停止
| 轮次 | 操作 |
|---|---|
| 第4轮 | 触发压缩 → 工具输出被标记,生成摘要 |
| 第5-10轮 | 正常聊 |
| 第11轮 | 触发压缩 → 新的工具输出被标记 |
| ... | 继续压缩,直到上限 |
当普通压缩不够时,会尝试更激进方案:
// compaction.ts:113-129
// 只保留最后一个用户消息,之前的全删掉
messages = messages.slice(0, lastUserIndex)
图片/PDF 转成文字:
// 1MB 图片 → 30 字符
"[Attached image/png: screenshot.png]"
压缩前:
消息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: "## Goaln用户想重构...n## Accomplishedn..."]
// 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 等
})
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":{...}} |
从任意历史点创建分支:
opencode run -c --fork "尝试另一种方案"
生成公开链接:
await Session.share(sessionID)
// 返回 share.opencode.ai/xxx
每个会话可独立配置权限:
{
"permission": {
"bash": "allow",
"write": "ask",
"rm": "deny"
}
}
面试官:说说 Cookie 和 Token 的区别?
Vue3 原理解析之响应系统的实现
# 面试官: 能不能手写 Vue 响应式?(Vue2 响应式原理【完整版】)
看到这里,不得不说一句:Session 这玩意儿真是太重要了!
理解了这个,你才能真正搞懂 AI 编程工具是怎么工作的,以后面试问到这方面的问题,也能说得头头是道!
如果文章对您有帮助麻烦亲点赞、收藏 + 关注和博主一起成长哟!!️️️
有问题欢迎评论区聊聊!
| 文件 | 作用 |
|---|---|
| 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 |