Files
DevFlow/docs/02-架构设计/专项设计/上下文管理演进-执行计划-2026-07-20.md
T
lxy dfa38f6259 修复: 上下文记忆与工作流注入生产侧空壳接通
WorkingContext 写入链落地(用户轮目标钉扎+压缩后 JSON 卡片回填重锚定,含 6 单测);
工作流 DAG 摘要注入生产侧接通(AI 路径启动写入+终态回写,审批调用点透传会话)。
附知识库与记忆走查诊断(「笨/丢历史」根因:消息压缩视图丢失+知识检索四重收口),
todo 登记修复分档(含随批补登 09-28 交互走查索引条目)。
2026-09-29 13:28:56 +08:00

14 KiB
Raw Blame History

上下文管理演进 — 执行计划

关联: 上下文管理演进-任务技术设计-2026-07-20.md | 状态: T1-T3 已落地;T4/T5 生产侧 2026-09-29 补齐落地(消费侧此前已接,本次补 WorkingContext 写入链+工作流摘要产出,见 05-代码审查/aichat知识库与记忆走查-2026-09-29.md 第四章)


Wave 划分

3 个 Wave,每个 Wave 内 agent 可并行,Wave 间串行 gate。

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 必须先执行):

// 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 可独立编码。

// ── 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

核心逻辑:

// 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(),
    }
}

关键安全:

// 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(归档 = 上下文状态变更)

脏标记核心逻辑:

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>