新增: 上下文管理演进 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 字段
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
# 上下文管理演进 — 执行计划
|
||||
|
||||
> 关联: `上下文管理演进-任务技术设计-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 1:T1 动态压缩阈值
|
||||
|
||||
**改动清单**:精确到行
|
||||
|
||||
| 文件 | 行 | 操作 |
|
||||
|------|----|------|
|
||||
| `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 2:T2 工具命名空间存储
|
||||
|
||||
**接口契约**(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 1(Wave 1 → Wave 2)
|
||||
|
||||
```
|
||||
条件:
|
||||
□ cargo check --workspace 全部通过
|
||||
□ cargo test -p df-ai 全部通过
|
||||
□ cargo test -p devflow 全部通过(已有测试不受影响)
|
||||
|
||||
验证项:
|
||||
□ 日志: compress 触发时 available 减去 sys_tokens(T1)
|
||||
□ 手动: 大工具结果 DB 存原文(T2)
|
||||
□ 手动: namespace 引用格式正确(T2)
|
||||
|
||||
回滚:
|
||||
单文件回退: T1 删参数,T2 删 `namespace_store.rs` + 恢复 process_tool_calls 原文 push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wave 2:结构化改进(3 agent 并行)
|
||||
|
||||
### Agent 4:T3 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 5:T4 工作流 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 2(Wave 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 6:T5 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>` |
|
||||
Reference in New Issue
Block a user