Files
DevFlow/docs/02-架构设计/专项设计/上下文管理演进-执行计划-2026-07-20.md
T
lxy faa61a9ba5 新增: 上下文管理演进 Wave1(契约定义 + T1动态阈值 + T2命名空间)
Agent 3: 共享契约类型定义
- CompressedSummary(T3输出格式)
- WorkflowContextBlock(T4工作流DAG上下文)
- WorkingContext + GoalItem/DecisionItem/StepInfo(T5常驻上下文)
- 脏标记版本追踪 + reset_all 压缩联动

T1: 动态压缩阈值
- maybe_auto_compress 加 sys_tokens 参数,触发条件从 budget*0.6 改为 available*0.6
- 调用处传 sys_tokens(loop顶部已缓存)
- 3 行核心改动

T2: 工具命名空间存储
- crates/df-ai/src/namespace_store.rs: 完整实现 + 单元测试
- process_tool_calls: Low/Med/High 路径 namespace 路由
- save_conversation: namespace 引用展开为原文再落 DB
- AiSession 新增 namespace_store 字段
2026-07-20 01:05:20 +08:00

353 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 上下文管理演进 — 执行计划
> 关联: `上下文管理演进-任务技术设计-2026-07-20.md` | 状态: 待执行
---
## Wave 划分
3 个 Wave,每个 Wave 内 agent 可并行,Wave 间串行 gate。
```mermaid
graph LR
subgraph "Wave 1 (4 agent 并行)"
A1["Agent 1: T1 动态阈值"]
A2["Agent 2: T2 命名空间存储"]
A3["Agent 3: 接口契约定义"]
end
A3 -.->|"定义 downstream 接口"| A2
A1 --> G1["Gate 1: cargo check + 单测"]
A2 --> G1
G1 --> subgraph "Wave 2 (3 agent 并行)"
B1["Agent 4: T3 结构化摘要"]
B2["Agent 5: T4 工作流 DAG"]
end
B1 --> G2["Gate 2: cargo check + 集成验证"]
B2 --> G2
G2 --> subgraph "Wave 3 (2 agent 串行)"
C1["Agent 6: T5 WorkingContext"]
end
```
---
## Wave 1:基础改造(4 agent 并行)
### Agent 1T1 动态压缩阈值
**改动清单**:精确到行
| 文件 | 行 | 操作 |
|------|----|------|
| `src-tauri/src/commands/ai/agentic/context_lifecycle.rs` | 64 签名 | `maybe_auto_compress``sys_tokens: u32` 参数 |
| 同上 | 88-92 触发条件 | `let budget``let available = budget.saturating_sub(sys_tokens)`;比较用 `available` 替代 `budget` |
| `src-tauri/src/commands/ai/agentic/mod.rs` | ~1117 调用处 | `maybe_auto_compress(...)``sys_tokens` 实参(同作用域 line 964 已定义) |
**接口契约**`maybe_auto_compress` 签名变更——调用方需传入 `sys_tokens`。不影响其他模块。
**验证**`cargo check -p devflow`;观察日志 `available` 值在 system prompt 大时显著小于 `budget`
**回退**:删参数 + 恢复 `budget` 变量,2 行。
---
### Agent 2T2 工具命名空间存储
**接口契约**Wave 1 必须先执行):
```rust
// crates/df-ai/src/namespace_store.rs — 新增,其他 agent 依赖此接口
pub struct NamespaceStore { .. }
impl NamespaceStore {
pub fn new(max_bytes: usize) -> Self;
pub fn store(&mut self, key: &str, tool_name: &str, args: &Value, content: &str) -> String;
pub fn read(&mut self, full_path: &str) -> Option<&str>;
pub fn read_summary(&self, full_path: &str) -> Option<&str>;
}
pub fn should_use_namespace(content: &str, tool_name: &str) -> bool;
pub const NAMESPACE_REF_PREFIX: &str = "namespace://";
pub const NAMESPACE_BYTE_THRESHOLD: usize = 2048;
```
**接线改动**
| 文件 | 行 | 操作 |
|------|----|------|
| `crates/df-ai/src/namespace_store.rs` | 新增 | 完整实现 + 单元测试 |
| `crates/df-ai/src/lib.rs` | — | `pub mod namespace_store;` + `pub use` 重导出 |
| `crates/df-ai/src/context_helpers.rs` | — | 新增 `NAMESPACE_REF_PREFIX``should_use_namespace``is_namespace_ref` |
| `src-tauri/src/commands/ai/audit/mod.rs` | `process_tool_calls` ~380 行 | tool 执行完成后:`if should_use_namespace``ns.store(...)` → push 引用;else → push 原文 |
| `src-tauri/src/commands/ai/mod.rs``state.rs` | `AiSession` struct | 新增 `namespace_store: NamespaceStore` 字段 |
| `src-tauri/src/commands/ai/conversation.rs` | `save_conversation` | namespace 引用展开为原文再写 DB |
**验证**
1. `cargo test -p df-ai``namespace_store` 单元测试(store → read → LRU 淘汰)
2. 手动:`read_file("large.txt")` → DB 中 `tool_result` 内容为原文(引用已展开)
3. `history_tokens` 统计应显著下降(大工具结果只计引用 token)
**与 Agent 3 的边界**`namespace_store.rs` 实现不依赖 AiSession,纯数据结构的 `HashMap` 操作,零耦合,可独立开发和测试。接线到 AiSession 和 process_tool_calls 由本 agent 完成。
---
### Agent 3:接口契约定义(跨 Wave 共享)
定义 Wave 2/Wave 3 依赖的数据结构,**仅定义不实现**,确保下游 agent 可独立编码。
```rust
// ── Wave 2 共享契约 ──
/// 压缩摘要双格式(T3 输出,T5 消费)
pub struct CompressedSummary {
pub json_card: String, // JSON 卡片(主题/决策/待办)
pub nl_summary: String, // 自然语言摘要(向前兼容)
}
// ── Wave 3 共享契约 ──
/// 工作流 DAG 上下文块(T4 输出 → 注入 system prompt
pub struct WorkflowContextBlock {
pub workflow_id: String,
pub workflow_name: String,
pub current_node: Option<String>,
pub completed: usize,
pub total: usize,
pub next_steps: Vec<String>,
}
/// WorkingContext 常驻上下文(T5 实现)
pub struct WorkingContext {
pub goals: Vec<GoalItem>,
pub decisions: Vec<DecisionItem>,
pub unresolved: Vec<String>,
pub current_step: Option<StepInfo>,
pub version: WorkingContextVersion,
}
pub enum GoalStatus { Active, Completed, Cancelled }
pub struct GoalItem { pub id: String, pub text: String, pub status: GoalStatus }
pub struct DecisionItem { pub question: String, pub decision: String, pub reason: String }
pub struct StepInfo { pub step_name: String, pub status: StepStatus }
pub enum StepStatus { Pending, InProgress, Done, Blocked }
pub struct WorkingContextVersion {
pub goals_changed_at: u32,
pub decisions_changed_at: u32,
pub unresolved_changed_at: u32,
pub step_changed_at: u32,
pub current_turn: u32,
}
```
**验证**`cargo check` — 编译器验证所有 use 路径和类型引用正确,无需运行时测试。
**简化**:Agent 3 先把上述结构体定义放进 `crates/df-ai/src/context_helpers.rs``pub use` 重导出。Wave 2/3 的 agent 直接引用,不用等。
---
## Gate 1Wave 1 → Wave 2
```
条件:
□ cargo check --workspace 全部通过
□ cargo test -p df-ai 全部通过
□ cargo test -p devflow 全部通过(已有测试不受影响)
验证项:
□ 日志: compress 触发时 available 减去 sys_tokensT1
□ 手动: 大工具结果 DB 存原文(T2)
□ 手动: namespace 引用格式正确(T2
回滚:
单文件回退: T1 删参数,T2 删 `namespace_store.rs` + 恢复 process_tool_calls 原文 push
```
---
## Wave 2:结构化改进(3 agent 并行)
### Agent 4T3 L3 结构化摘要
**前置依赖**Agent 3 定义了 `CompressedSummary` 结构
**改动清单**
| 文件 | 行 | 操作 |
|------|----|------|
| `src-tauri/src/commands/ai/compress.rs` | 45-52 签名 | `compress_via_llm` 返回类型 `Result<String>``Result<CompressedSummary>` |
| 同上 | 82-86 prompt 构造 | 改 `compress_prompt` 模板要求输出 JSON+NL 双格式 |
| 同上 | 114-119 clean + 返回 | `clean_summary` 适配双格式:提取 JSON 段 + NL 段,分别验证退化 |
| 同上 | 新增 | `extract_between` 辅助函数(提取 `<<<JSON>>>...<<<END_JSON>>>` 之间的内容) |
| `src-tauri/src/commands/ai/prompt.rs` | 240-290 | `compress_prompt` 模板新增 JSON+NL 双格式输出要求 |
| `src-tauri/src/commands/ai/agentic/context_lifecycle.rs` | 145-155 消费处 | `Ok(Some(summary))` 分支中 `insert_at` 内容改为双格式嵌入 |
| 同上 178-210 兜底 | 关键词摘要降级 | 关键词降级不改,仍是纯文本(`keyword_fallback` 无 JSON |
**验证**
1. `cargo test -p devflow` — compress 单测通过
2. 手动:触发压缩 → DB 中 system 消息含 `<<<STRUCTURED_SUMMARY>>>` 标记
3. 手动:关键词摘要降级路径仍为纯文本(不受影响)
**与 Agent 5 无依赖**T3 只改 compress 路径,T4 只改 system prompt 注入,零耦合。
---
### Agent 5T4 工作流 DAG 注入
**前置依赖**Agent 3 定义了 `WorkflowContextBlock`Agent 2 的 namespace 接口已就绪(弱依赖——仅用于产出关联)
**改动清单**
| 文件 | 行 | 操作 |
|------|----|------|
| `src-tauri/src/commands/ai/workflow_context.rs` | 新增 | `load_dag()`, `build_active_path()`, `summarize_node()` |
| `src-tauri/src/commands/ai/agentic/mod.rs` | ~1110-1128 | `build_for_request` 之前:检测 `conv.workflow_id` → 加载 DAG → 构建 context block → 注入 system prompt |
| `crates/df-ai/src/context_helpers.rs` | 新增常量 | `WORKFLOW_DAG_MARKER` |
**核心逻辑**
```rust
// workflow_context.rs
pub fn build_active_path(dag: &Dag, current_node_id: &str) -> WorkflowContextBlock {
let path = dag.path_to_root(current_node_id); // 到根路径
let next = dag.children(current_node_id, 2); // 后续 2 层
WorkflowContextBlock {
workflow_id: dag.id.clone(),
workflow_name: dag.name.clone(),
total_nodes: dag.nodes.len(),
completed_nodes: dag.nodes.iter().filter(|n| n.status == "completed").count(),
current_node: summarize_node(dag.get_node(current_node_id)),
next_nodes: next.into_iter().map(|n| summarize_node(n)).collect(),
}
}
```
**关键安全**
```rust
// mod.rs: system_prompt 注入处
if let Some(wf_id) = &conv.workflow_id {
if let Ok(dag) = df_workflow::Dag::load(&db, wf_id) {
let block = build_active_path(&dag, &conv.current_node_id);
system_prompt = format!("[工作流] {}\n{}", block.to_system_text(), system_prompt);
}
// Dag::load 失败 → 静默跳过
}
// workflow_id == None → 行为完全不变
```
**验证**
1. `cargo check -p devflow` — 依赖 `df-workflow` crate 的 `Dag::load` 接口
2. 手动:创建带工作流的对话 → system prompt 含 `[工作流]`
3. 手动:自由对话(无 workflow)→ system prompt 不变
---
## Gate 2Wave 2 → Wave 3
```
条件:
□ cargo check --workspace 全部通过
□ cargo test --workspace 全部通过
□ 前端 vue-tsc --noEmit 通过
验证项:
□ 手动: 压缩后 DB system 消息含 JSON 标记(T3
□ 手动: 工作流对话注入 DAG 块(T4)
□ 手动: 自由对话不受影响(T4 安全)
□ 全量: history_tokens 在 T2 基础上再次下降(T3 摘要更精简)
回滚:
T3: 回退 compress.rs 签名 + prompt.rs 模板
T4: 删 workflow_context.rs + 恢复 mod.rs system_prompt 拼接
```
---
## Wave 3:常驻上下文(1 agent 串行)
### Agent 6T5 WorkingContext 分层注入
**前置依赖**
- Agent 3 的 `WorkingContext` 结构定义需在 `context_helpers.rs` 中(已在 Wave 1 Gate 前到位)
- Agent 4 的 `CompressedSummary` 用于 `reset_all_with_summary`(弱依赖——无 NL 摘要时可跳过)
**改动清单**
| 文件 | 行 | 操作 |
|------|----|------|
| `crates/df-ai/src/context_helpers.rs` | 已有定义 | `WorkingContext` / `WorkingContextVersion` 等已由 Agent 3 定义 |
| `crates/df-ai/src/context/mod.rs` | `ContextManager` struct | 新增字段 `pub working_context: Option<WorkingContext>` |
| `src-tauri/src/commands/ai/agentic/mod.rs` | ~960 system_prompt 拼接处 | 在 `build_for_request` 之前调 `WorkingContext::build_injection()` 注入 L1a+L1b |
| 同上 | tool_registry 注册 | 注册 `read_context` 工具(L1c |
| `src-tauri/src/commands/ai/agentic/context_lifecycle.rs` | ~145-155 压缩成功后 | 调 `ctx.version.reset_all_with_summary(&summary)` |
| `src-tauri/src/commands/ai/commands/chat.rs` | `ai_chat_clear_context` | 归档后 reset WorkingContext(归档 = 上下文状态变更) |
**脏标记核心逻辑**
```rust
impl WorkingContext {
pub fn build_injection(&self, recent_turns: u32) -> String {
let mut parts = vec![];
// L1a: 当前步骤(每轮必带,~200 tokens
if let Some(step) = &self.current_step {
parts.push(format!("当前步骤: {}", step.step_name));
}
// L1b: 过去 recent_turns 轮内有变更才注入
let threshold = self.version.current_turn.saturating_sub(recent_turns);
let has_active = self.goals.iter().any(|g| g.status == GoalStatus::Active);
if has_active && self.version.goals_changed_at >= threshold {
let active: Vec<_> = self.goals.iter()
.filter(|g| g.status == GoalStatus::Active)
.collect();
parts.push(format!("目标({}): {}", active.len(),
active.iter().map(|g| g.text.as_str()).collect::<Vec<_>>().join("; ")));
}
if self.version.unresolved_changed_at >= threshold && !self.unresolved.is_empty() {
parts.push(format!("待解: {}", self.unresolved.join("; ")));
}
parts.join("\n")
}
}
```
**验证**
1. `cargo test -p df-ai``WorkingContext::build_injection` 单元测试(脏标记正确跳过/注入)
2. 手动:观察 system prompt 含 L1a(当前步骤)和条件性的 L1b(有变更时)
3. 手动:压缩触发后,下轮 L1b 正常注入(`reset_all` 生效)
4. 开关:`GOAL_PIN_ENABLED=false`(或等价的 workincontext 开关)→ 行为恢复到改动前
---
## 汇总:执行序列
```
Week 1:
Day 1: Agent 3 定义契约(30 分钟) → 其余 3 agent 拿到接口开始编码
Day 1-2: Agent 1 T1 + Agent 2 T2 并行编码
Day 3: Gate 1 验证 + 修复
Week 2:
Day 4-5: Agent 4 T3 + Agent 5 T4 并行编码
Day 6: Gate 2 验证 + 修复
Week 3:
Day 7-9: Agent 6 T5 编码(由于串行依赖,此阶段无并行)
Day 10: 最终验证 + 全量回归测试
```
## 回滚总策略
| 场景 | 操作 |
|------|------|
| T1 有问题 | 删 `sys_tokens` 参数 + 恢复 `budget` 变量 → 2 行回退 |
| T2 有问题 | 删 `namespace_store.rs` + 恢复 `process_tool_calls` push 原文 → 1 文件回退 |
| T3 有问题 | 回退 `compress.rs` + `prompt.rs``Result<String>` → 2 文件回退 |
| T4 有问题 | 删 `workflow_context.rs` + 恢复 mod.rs system_prompt 拼接 → 1 文件回退 |
| T5 有问题 | `GOAL_PIN_ENABLED=false` 等效开关 → 配置回退,不改代码 |
| 全量回退 | `git revert <commit-hash>` |