docs(agent-runtime): 补充 SessionMemory 设计与多 context 切换备忘

- 在方案文档中新增 `SessionMemory` 作为会话级记忆桥接组件
- 将 AgentSession 的 `agent_name: String` 改为 `agent: Arc<dyn Agent>`
- RuntimeBundle 新增 `session_memory_backend` 可选字段
- 新增 `docs/note-context-switch-design.md` 记录多 context 切换方案
- 更新 Roadmap 状态并补充 Phase 4 范围界定
- Phase 4 仅实现 SessionMemory 数据结构与 API,ContextManager 延后至 v0.2+
This commit is contained in:
徐涛
2026-06-10 22:22:18 +08:00
parent 75f8736931
commit be595a6771
3 changed files with 364 additions and 47 deletions
+222
View File
@@ -0,0 +1,222 @@
# Context 切换方案设计备忘
> 创建日期:2026-06-10
> 状态:备忘(Phase 4 不实现)
> 关联文档:
> - `docs/7-agent-runtime.md` — Phase 4 方案(含 SessionMemory 设计)
> - `docs/note-opencode-agent-switching.md` — OpenCode 切换机制调研
> - `docs/roadmap.md` — 项目总 Roadmap
---
## 1. 背景
### 1.1 问题
在调研 OpenCode 的 Agent 切换机制后(详见 `docs/note-opencode-agent-switching.md`),发现其做法是:
- 切换 agent 时**不动消息历史**
- 在 user message 末尾追加 `synthetic: true``<system-reminder>` 提醒
- 同时**完全重新计算** system prompt
这个方法的问题是:**长上下文中频繁切换 agent 容易给 LLM 造成身份困惑**。同一个消息列表里有 `system: build` 的 identity,又出现 `system: plan` 的 identityLLM 容易"串味"。
### 1.2 核心思路
以一个 session 里存在**多个独立的 context** 来解决,每个 context 有自己独立的 system prompt + 消息列表:
```
OpenCode 模式(一条流):
[system: build, user: A, ass: A', user: <切plan>, system: plan, user: B]
↑ 身份困惑
建议的多 context 模式:
session {
context_a: [system: build, user: A, ass: A'] ← 只有 build 的 identity
context_b: [system: plan, user: B, ass: B'] ← 只有 plan 的 identity
}
```
### 1.3 适用范围
| 场景 | 适用性 | 说明 |
|------|--------|------|
| Agent 切换(build ↔ plan | ✅ 核心场景 | 同一 session 内更换角色 |
| 主从 Agent 协作 | ✅ 核心场景 | primary 委派子任务给 subagentsubagent 独立运作 |
| 长 session 上下文压缩 | ✅ 附带收益 | 拆分 context 后,每个 context 独立累积消息,不会互相拖长 |
| 并行 context 执行 | ⚠️ 拓展场景 | context_a 和 context_b 可各自独立推进 |
---
## 2. 三个候选方案
### 方案 AOpenCode 式(system prompt 重算 + synthetic 追加)
**做法**
- 单一消息列表
- 切换时重算 system prompt
- user message 末尾追加 `<system-reminder>` 标签
**优点**
- 实现简单
- 消息历史完整可见
**缺点**
- 长上下文身份困惑
- context 互相污染(每个 context 都要看全部历史)
**结论**:❌ 否决。不解决身份困惑问题。
### 方案 B:信息池 + 切换不重置(借鉴 OpenCode + 增强)
**做法**
- 切换时保留历史
- 使用 `<system-reminder>` 标签
- 靠 prompt 工程让 LLM 理解身份变更
**优点**
- 历史连贯
- 改动最小
**缺点**
- 仍然有身份困惑风险
- 上下文不受控增长
**结论**:❌ 否决。治标不治本。
### 方案 C:多 context 隔离 + SessionMemory 桥接(推荐)
**做法**
- 每个 agent 切换创建一个新的 context(独立消息列表 + 独立 system prompt
- context 之间通过 `SessionMemory` 桥接关键信息
- 切换时新 context 的 system prompt 末尾注入 `SessionMemory::snapshot()`
```
context_a (build)
→ 对话 50 轮
→ 写入 SessionMemory: {"design_decision": "用 PostgreSQL",
"files_changed": "src/db.rs"}
→ 销毁(或沉睡)
创建 context_b (plan)
→ system_prompt += snapshot()
→ "<session-context>
design_decision: 用 PostgreSQL
files_changed: src/db.rs
</session-context>"
→ 对话 10 轮(不需要看 context_a 的 50 轮历史)
→ 读 SessionMemory: get("design_decision") → "用 PostgreSQL"
```
**优点**
- ✅ 身份稳定:每个 context 只有一套 system prompt
- ✅ 上下文隔离:context_b 不受 context_a 的消息量影响
- ✅ 信息桥接:关键结论通过 SessionMemory 显式传递
- ✅ 并行潜力:两个 context 可各自运行
**缺点**
- ❌ 实现复杂度:从"一个消息列表"到"多个消息列表 + 桥接"
- ❌ 信息完整性:LLM 自主决定"什么值得记",可能遗漏细节
- ❌ 上层理解成本:应用层需要理解 context 概念
**结论**:✅ 推荐。架构上最干净,但 Phase 4 不做全部实现。
---
## 3. SessionMemory 桥接机制(方案 C 的核心)
### 3.1 设计决策
| 决策 | 结论 | 理由 |
|------|------|------|
| 复用 Phase 3 `MemoryStore` | ✅ 是 | 不引入新存储机制 |
| 跨进程支持 | ✅ 是 | 换后端即可(Redis / SQLite),`InMemoryStore` 兜底 |
| namespace 隔离 | ✅ 是 | `_session_{session_id}` 命名空间 |
| 谁写 SessionMemory | LLM 通过 tool 显式写(v0.2+)或上层应用 API 写 | 不支持自动写——避免 "写太多 = 噪音,写太少 = 遗漏" |
| snapshot 格式 | `<session-context>` XML 风格 | 专为注入 system prompt 设计 |
### 3.2 谁写 SessionMemory 的三种选项
| 选项 | 描述 | 评估 |
|------|------|------|
| **选项 1AgentSession 自动写** | 每轮对话后自动摘录关键信息 | ❌ 摘录什么?容易变成精简版对话历史,失去"关键信息"的定位 |
| **选项 2LLM 通过 tool 显式写** | 把 `SessionMemory::set` 暴露为 Tool 供 LLM 调用 | ✅ LLM 自主决定什么值得记;v0.2+ 实现自动注册 |
| **选项 3:上层应用 API 写** | `agent_session.session_memory.set("k", "v")` | ✅ Phase 4 即可用,最透明 |
**Phase 4 实现选项 3**v0.2+ 补充选项 2(tool 自动注册)。
### 3.3 三层记忆体系
```
持久层(Phase 3 MemoryStore / KnowledgeStore ── 跨 session 持久,长期知识
会话层(Phase 4 SessionMemory ── 单 session 内共享,context 桥接
对话层(Phase 3 ConversationMemory ── 单 context 内消息历史
```
---
## 4. Phase 4 范围 vs v0.2+ 范围
### ✅ Phase 4 做
| 组件 | 状态 | 行数 |
|------|------|------|
| `SessionMemory` struct | ✅ 做 | ~40 行 |
| `AgentSession` + `session_memory` 字段 | ✅ 做 | ~3 行 |
| `AgentSession``Arc<dyn Agent>` 替代 `agent_name: String` | ✅ 做 | ~3 行 |
| `RuntimeBundle` + `session_memory_backend` 字段 | ✅ 做 | ~1 行 |
| `AgentBuilder` + `.session_memory_backend()` | ✅ 做 | ~3 行 |
### ❌ 延后到 v0.2+
| 组件 | 状态 | 说明 |
|------|------|------|
| Context 切换管理(`switch_context` / `create_context` | ❌ 延后 | 需要 `ContextManager` 包装 |
| 多 context 生命周期管理 | ❌ 延后 | context 的创建/销毁/切换策略 |
| `"session_memory_set"` tool 自动注册 | ❌ 延后 | 在 `ToolRegistry` 里注册特殊 tool |
| Context 级别的 `ConversationMemory` 自动管理 | ❌ 延后 | 每个 context 独立消息历史 |
### 延后的理由
1. **最小范围原则**Phase 4 定位是"薄胶水层 + trait 抽象",多 context 管理属于业务编排的范畴
2. **稳定 API 优先**:先把 `AgentSession` / `RuntimeBundle` / `SessionMemory` 的 API 定稳,v0.2+ 在上面搭建 context 切换
3. **降低实施风险**:Phase 4 已有 13 个交付任务,加 context 切换会增加 2-3 倍复杂度
---
## 5. v0.2+ Context 切换的设想接口
> 以下为未来实现的草案,非承诺。记录在这里避免 v0.2+ 重新设计时丢失上下文。
```rust
pub struct ContextManager {
contexts: HashMap<String, AgentSession>,
active_context: String,
session_memory: SessionMemory,
}
impl ContextManager {
/// 创建一个新的 context,绑定指定 agent
pub fn create_context(&mut self, id: &str, agent: Arc<dyn Agent>) -> Result<(), AgentError>;
/// 切换到已有 context
pub fn switch_context(&mut self, id: &str) -> Result<&mut AgentSession, AgentError>;
/// 销毁 context
pub fn destroy_context(&mut self, id: &str) -> Result<(), AgentError>;
/// 从 context_a 桥接关键信息到 context_b 的 system prompt
pub fn bridge(&mut self, from: &str, to: &str) -> Result<(), AgentError>;
}
```
切换流程:
1. `context_manager.create_context("plan", plan_agent)` — 新 context 的 system prompt 自动附加 `session_memory.snapshot()`
2. `context_manager.switch_context("plan")` — 返回 context 的 `AgentSession`,应用层调 `submit_turn`
3. context 销毁时,关键信息经由 LLM 或上层应用写入 `SessionMemory`
---
## 6. 一句话总结
> **多 context 切换方案 = `SessionMemory`Phase 4 做信息桥接基础) + `ContextManager`v0.2+ 做切换管理)。Phase 4 只铺"水管接口",不装"水循环系统"。**