Files
zclaw_openfang/docs/superpowers/specs/2026-03-26-agent-growth-design.md
iven b7f3d94950
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
fix(presentation): 修复 presentation 模块类型错误和语法问题
- 创建 types.ts 定义完整的类型系统
- 重写 DocumentRenderer.tsx 修复语法错误
- 重写 QuizRenderer.tsx 修复语法错误
- 重写 PresentationContainer.tsx 添加类型守卫
- 重写 TypeSwitcher.tsx 修复类型引用
- 更新 index.ts 移除不存在的 ChartRenderer 导出

审计结果:
- 类型检查: 通过
- 单元测试: 222 passed
- 构建: 成功
2026-03-26 17:19:28 +08:00

24 KiB
Raw Blame History

ZCLAW Agent 成长功能设计规格

版本: 1.0 日期: 2026-03-26 状态: 已批准 作者: Claude + 用户协作设计


一、概述

1.1 背景

ZCLAW 当前的学习系统存在前后端分离问题

  • 前端有完整的学习逻辑 (active-learning.ts, memory-extractor.ts)
  • 但这些学习结果存储在 localStorage/IndexedDB
  • 后端执行系统 (Rust) 无法获取这些学习结果
  • 导致 Agent 无法真正"成长"

1.2 目标

设计并实现完整的 Agent 成长功能,让 Agent 像个人管家一样:

  • 记住偏好:用户的沟通风格、回复格式、语言偏好等
  • 积累知识:从对话中学习用户相关事实、领域知识、经验教训
  • 掌握技能:记录技能/Hand 的使用模式,优化执行效率

1.3 需求决策

维度 决策 理由
成长维度 偏好 + 知识 + 技能(全部) 完整的管家式成长体验
整合策略 完全后端化Rust 重写 避免前后端数据隔离问题
存储架构 OpenViking 作为完整记忆层 利用现有的 L0/L1/L2 分层 + 语义搜索
学习触发 对话后自动 + 用户显式触发 平衡自动化和可控性
行为影响 智能检索 + Token 预算控制 解决长期使用后数据量过大的问题

二、系统架构

2.1 整体架构图

┌─────────────────────────────────────────────────────────────────┐
│                    ZCLAW Agent 成长系统                          │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                   zclaw-growth (新 Crate)                │   │
│  │  ──────────────────────────────────────────────────────  │   │
│  │  • MemoryExtractor - 从对话中提取偏好/知识/经验          │   │
│  │  • MemoryRetriever - 语义检索相关记忆                    │   │
│  │  • PromptInjector - 动态构建 system_prompt              │   │
│  │  • GrowthTracker - 追踪成长指标和演化                    │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                   OpenViking (记忆层)                    │   │
│  │  ──────────────────────────────────────────────────────  │   │
│  │  URI 结构:                                               │   │
│  │  • agent://{id}/preferences/{category}  - 用户偏好       │   │
│  │  • agent://{id}/knowledge/{domain}      - 知识积累       │   │
│  │  • agent://{id}/experience/{skill}      - 技能经验       │   │
│  │  • agent://{id}/sessions/{sid}          - 对话历史       │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                   zclaw-runtime (修改)                   │   │
│  │  ──────────────────────────────────────────────────────  │   │
│  │  AgentLoop 集成:                                         │   │
│  │  1. 对话前 → MemoryRetriever 检索相关记忆               │   │
│  │  2. 构建请求 → PromptInjector 注入记忆                   │   │
│  │  3. 对话后 → MemoryExtractor 提取新记忆                 │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

2.2 数据流

用户输入
    │
    ▼
┌─────────────────────────────────────────┐
│ 1. 记忆检索                              │
│    • 用当前输入查询 OpenViking           │
│    • 召回 Top-5 相关记忆                 │
│    • Token 预算控制 (500 tokens)         │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│ 2. Prompt 构建                          │
│    system_prompt = base +               │
│      "## 用户偏好\n" + preferences +     │
│      "## 相关知识\n" + knowledge         │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│ 3. LLM 对话                             │
│    • 正常的 AgentLoop 执行               │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│ 4. 记忆提取 (对话后)                     │
│    • 分析对话内容                        │
│    • 提取偏好/知识/经验                  │
│    • 写入 OpenViking (L0/L1/L2)          │
└─────────────────────────────────────────┘

2.3 OpenViking URI 结构

agent://{agent_id}/
├── preferences/
│   ├── communication-style    # 沟通风格偏好
│   ├── response-format        # 回复格式偏好
│   ├── language-preference    # 语言偏好
│   └── topic-interests        # 主题兴趣
├── knowledge/
│   ├── user-facts            # 用户相关事实
│   ├── domain-knowledge      # 领域知识
│   └── lessons-learned       # 经验教训
├── experience/
│   ├── skill-{id}            # 技能使用经验
│   └── hand-{id}             # Hand 使用经验
└── sessions/
    └── {session_id}/         # 对话历史
        ├── raw               # 原始对话 (L0)
        ├── summary           # 摘要 (L1)
        └── keywords          # 关键词 (L2)

三、详细设计

3.1 新 Crate 结构

crates/zclaw-growth/
├── Cargo.toml
├── src/
│   ├── lib.rs                 # 入口和公共 API
│   ├── extractor.rs           # 记忆提取器
│   ├── retriever.rs           # 记忆检索器
│   ├── injector.rs            # Prompt 注入器
│   ├── tracker.rs             # 成长追踪器
│   ├── types.rs               # 类型定义
│   └── viking_adapter.rs      # OpenViking 适配器

3.2 核心类型定义

// types.rs

/// 记忆类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MemoryType {
    Preference,    // 偏好
    Knowledge,     // 知识
    Experience,    // 经验
    Session,       // 对话
}

/// 记忆条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
    pub uri: String,
    pub memory_type: MemoryType,
    pub content: String,
    pub keywords: Vec<String>,
    pub importance: u8,           // 1-10
    pub access_count: u32,
    pub created_at: DateTime<Utc>,
    pub last_accessed: DateTime<Utc>,
}

/// 提取的记忆
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedMemory {
    pub memory_type: MemoryType,
    pub category: String,
    pub content: String,
    pub confidence: f32,          // 提取置信度 0.0-1.0
    pub source_session: SessionId,
}

/// 检索配置
#[derive(Debug, Clone)]
pub struct RetrievalConfig {
    pub max_tokens: usize,           // 总 Token 预算,默认 500
    pub preference_budget: usize,    // 偏好 Token 预算,默认 200
    pub knowledge_budget: usize,     // 知识 Token 预算,默认 200
    pub experience_budget: usize,    // 经验 Token 预算,默认 100
    pub min_similarity: f32,         // 最小相似度阈值,默认 0.7
    pub max_results: usize,          // 最大返回数量,默认 10
}

impl Default for RetrievalConfig {
    fn default() -> Self {
        Self {
            max_tokens: 500,
            preference_budget: 200,
            knowledge_budget: 200,
            experience_budget: 100,
            min_similarity: 0.7,
            max_results: 10,
        }
    }
}

/// 检索结果
#[derive(Debug, Clone, Default)]
pub struct RetrievalResult {
    pub preferences: Vec<MemoryEntry>,
    pub knowledge: Vec<MemoryEntry>,
    pub experience: Vec<MemoryEntry>,
    pub total_tokens: usize,
}

/// 提取配置
#[derive(Debug, Clone)]
pub struct ExtractionConfig {
    pub extract_preferences: bool,   // 是否提取偏好,默认 true
    pub extract_knowledge: bool,     // 是否提取知识,默认 true
    pub extract_experience: bool,    // 是否提取经验,默认 true
    pub min_confidence: f32,         // 最小置信度阈值,默认 0.6
}

impl Default for ExtractionConfig {
    fn default() -> Self {
        Self {
            extract_preferences: true,
            extract_knowledge: true,
            extract_experience: true,
            min_confidence: 0.6,
        }
    }
}

3.3 MemoryExtractor 接口

// extractor.rs

/// 记忆提取器 - 从对话中提取有价值的记忆
pub struct MemoryExtractor {
    llm_driver: Arc<dyn LlmDriver>,
}

impl MemoryExtractor {
    pub fn new(llm_driver: Arc<dyn LlmDriver>) -> Self {
        Self { llm_driver }
    }

    /// 从对话中提取记忆
    pub async fn extract(
        &self,
        messages: &[Message],
        config: &ExtractionConfig,
    ) -> Result<Vec<ExtractedMemory>> {
        let mut results = Vec::new();

        if config.extract_preferences {
            let prefs = self.extract_preferences(messages).await?;
            results.extend(prefs);
        }

        if config.extract_knowledge {
            let knowledge = self.extract_knowledge(messages).await?;
            results.extend(knowledge);
        }

        if config.extract_experience {
            let experience = self.extract_experience(messages).await?;
            results.extend(experience);
        }

        // 过滤低置信度结果
        results.retain(|m| m.confidence >= config.min_confidence);

        Ok(results)
    }

    /// 提取偏好
    async fn extract_preferences(
        &self,
        messages: &[Message],
    ) -> Result<Vec<ExtractedMemory>> {
        // 使用 LLM 分析对话,提取用户偏好
        // 例如:用户喜欢简洁的回复、用户偏好中文等
        // ...
    }

    /// 提取知识
    async fn extract_knowledge(
        &self,
        messages: &[Message],
    ) -> Result<Vec<ExtractedMemory>> {
        // 使用 LLM 分析对话,提取有价值的事实和知识
        // 例如:用户是程序员、用户在做一个 Rust 项目等
        // ...
    }

    /// 提取经验
    async fn extract_experience(
        &self,
        messages: &[Message],
    ) -> Result<Vec<ExtractedMemory>> {
        // 分析对话中的技能/工具使用,提取经验教训
        // 例如:某个技能执行失败、某个工具效果很好等
        // ...
    }
}

3.4 MemoryRetriever 接口

// retriever.rs

/// 记忆检索器 - 从 OpenViking 检索相关记忆
pub struct MemoryRetriever {
    viking: Arc<VikingAdapter>,
}

impl MemoryRetriever {
    pub fn new(viking: Arc<VikingAdapter>) -> Self {
        Self { viking }
    }

    /// 检索与当前输入相关的记忆
    pub async fn retrieve(
        &self,
        agent_id: &AgentId,
        query: &str,
        config: &RetrievalConfig,
    ) -> Result<RetrievalResult> {
        // 1. 检索偏好
        let preferences = self.retrieve_by_type(
            agent_id,
            MemoryType::Preference,
            query,
            config.max_results,
        ).await?;

        // 2. 检索知识
        let knowledge = self.retrieve_by_type(
            agent_id,
            MemoryType::Knowledge,
            query,
            config.max_results,
        ).await?;

        // 3. 检索经验
        let experience = self.retrieve_by_type(
            agent_id,
            MemoryType::Experience,
            query,
            config.max_results / 2,
        ).await?;

        // 4. 计算 Token 使用
        let total_tokens = self.estimate_tokens(&preferences, &knowledge, &experience);

        Ok(RetrievalResult {
            preferences,
            knowledge,
            experience,
            total_tokens,
        })
    }

    /// 按类型检索
    async fn retrieve_by_type(
        &self,
        agent_id: &AgentId,
        memory_type: MemoryType,
        query: &str,
        limit: usize,
    ) -> Result<Vec<MemoryEntry>> {
        let scope = format!("agent://{}/{}", agent_id, memory_type_to_scope(&memory_type));

        let results = self.viking.find(query, FindOptions {
            scope: Some(scope),
            limit: Some(limit),
            level: Some("L1"),  // 使用摘要级别
        }).await?;

        // 转换为 MemoryEntry
        // ...
    }

    fn estimate_tokens(
        &self,
        preferences: &[MemoryEntry],
        knowledge: &[MemoryEntry],
        experience: &[MemoryEntry],
    ) -> usize {
        // 简单估算:约 4 字符 = 1 token
        let total_chars: usize = preferences.iter()
            .chain(knowledge.iter())
            .chain(experience.iter())
            .map(|m| m.content.len())
            .sum();
        total_chars / 4
    }
}

fn memory_type_to_scope(ty: &MemoryType) -> &'static str {
    match ty {
        MemoryType::Preference => "preferences",
        MemoryType::Knowledge => "knowledge",
        MemoryType::Experience => "experience",
        MemoryType::Session => "sessions",
    }
}

3.5 PromptInjector 接口

// injector.rs

/// Prompt 注入器 - 将记忆动态注入 system_prompt
pub struct PromptInjector {
    config: RetrievalConfig,
}

impl PromptInjector {
    pub fn new(config: RetrievalConfig) -> Self {
        Self { config }
    }

    /// 构建增强的 system_prompt
    pub fn inject(
        &self,
        base_prompt: &str,
        memories: &RetrievalResult,
    ) -> String {
        let mut result = base_prompt.to_string();

        // 注入偏好
        if !memories.preferences.is_empty() {
            let prefs_section = self.format_preferences(
                &memories.preferences,
                self.config.preference_budget,
            );
            result.push_str("\n\n## 用户偏好\n");
            result.push_str(&prefs_section);
        }

        // 注入知识
        if !memories.knowledge.is_empty() {
            let knowledge_section = self.format_knowledge(
                &memories.knowledge,
                self.config.knowledge_budget,
            );
            result.push_str("\n\n## 相关知识\n");
            result.push_str(&knowledge_section);
        }

        // 注入经验
        if !memories.experience.is_empty() {
            let exp_section = self.format_experience(
                &memories.experience,
                self.config.experience_budget,
            );
            result.push_str("\n\n## 经验参考\n");
            result.push_str(&exp_section);
        }

        result
    }

    fn format_preferences(&self, entries: &[MemoryEntry], budget: usize) -> String {
        let mut result = String::new();
        let mut used = 0;

        for entry in entries.iter().take(5) {  // 最多 5 条偏好
            let line = format!("- {}\n", entry.content);
            let line_tokens = line.len() / 4;

            if used + line_tokens > budget {
                break;
            }

            result.push_str(&line);
            used += line_tokens;
        }

        result
    }

    fn format_knowledge(&self, entries: &[MemoryEntry], budget: usize) -> String {
        // 类似 format_preferences
        // ...
    }

    fn format_experience(&self, entries: &[MemoryEntry], budget: usize) -> String {
        // 类似 format_preferences
        // ...
    }
}

3.6 AgentLoop 集成

修改 crates/zclaw-runtime/src/loop_runner.rs

pub struct AgentLoop {
    agent_id: AgentId,
    driver: Arc<dyn LlmDriver>,
    tools: ToolRegistry,
    memory: Arc<MemoryStore>,
    model: String,
    system_prompt: Option<String>,
    max_tokens: u32,
    temperature: f32,
    skill_executor: Option<Arc<dyn SkillExecutor>>,

    // 新增:成长系统
    memory_retriever: Option<Arc<MemoryRetriever>>,
    memory_extractor: Option<Arc<MemoryExtractor>>,
    prompt_injector: Option<PromptInjector>,
    growth_enabled: bool,
}

impl AgentLoop {
    pub async fn run(&self, session_id: SessionId, input: String) -> Result<AgentLoopResult> {
        // 1. 检索相关记忆 (新增)
        let memories = if self.growth_enabled {
            if let Some(retriever) = &self.memory_retriever {
                retriever.retrieve(
                    &self.agent_id,
                    &input,
                    &RetrievalConfig::default(),
                ).await.unwrap_or_default()
            } else {
                RetrievalResult::default()
            }
        } else {
            RetrievalResult::default()
        };

        // 2. 构建增强的 system_prompt (修改)
        let enhanced_prompt = if self.growth_enabled {
            if let Some(injector) = &self.prompt_injector {
                injector.inject(
                    self.system_prompt.as_deref().unwrap_or(""),
                    &memories,
                )
            } else {
                self.system_prompt.clone().unwrap_or_default()
            }
        } else {
            self.system_prompt.clone().unwrap_or_default()
        };

        // 3. 添加用户消息
        let user_message = Message::user(input);
        self.memory.append_message(&session_id, &user_message).await?;

        // 4. 获取完整上下文
        let mut messages = self.memory.get_messages(&session_id).await?;

        // 5. 执行 LLM 循环 (使用增强的 prompt)
        let mut iterations = 0;
        let max_iterations = 10;

        loop {
            // ... 现有的 LLM 循环逻辑
            // 使用 enhanced_prompt 作为 system message
        }

        // 6. 对话结束后提取记忆 (新增)
        if self.growth_enabled {
            if let Some(extractor) = &self.memory_extractor {
                let final_messages = self.memory.get_messages(&session_id).await?;
                let extracted = extractor.extract(
                    &final_messages,
                    &ExtractionConfig::default(),
                ).await?;

                // 写入 OpenViking
                for memory in extracted {
                    // 通过 VikingAdapter 写入
                }
            }
        }

        Ok(result)
    }
}

四、前端变化

4.1 新增组件

// desktop/src/components/GrowthPanel.tsx

interface GrowthPanelProps {
  agentId: string;
}

export function GrowthPanel({ agentId }: GrowthPanelProps) {
  // 功能:
  // - 显示 Agent 成长指标
  // - 手动触发学习
  // - 查看/编辑记忆
  // - 配置学习参数
}

4.2 Store 扩展

// desktop/src/store/agentStore.ts

interface AgentState {
  // ... 现有字段

  // 新增:成长相关
  growthEnabled: boolean;
  memoryStats: {
    totalMemories: number;
    preferences: number;
    knowledge: number;
    experience: number;
    lastLearningTime: string | null;
  };
}

4.3 Tauri Commands

// desktop/src-tauri/src/growth_commands.rs

#[tauri::command]
async fn get_memory_stats(agent_id: String) -> Result<MemoryStats, String>;

#[tauri::command]
async fn trigger_learning(agent_id: String, session_id: String) -> Result<Vec<ExtractedMemory>, String>;

#[tauri::command]
async fn get_memories(agent_id: String, memory_type: Option<String>) -> Result<Vec<MemoryEntry>, String>;

#[tauri::command]
async fn delete_memory(agent_id: String, uri: String) -> Result<(), String>;

#[tauri::command]
async fn update_memory(agent_id: String, uri: String, content: String) -> Result<(), String>;

五、执行计划

5.1 Phase 1: Crate 骨架 (1-2 天)

  • 创建 crates/zclaw-growth/ 目录结构
  • 定义 types.rs 核心类型
  • 设置 Cargo.toml 依赖

5.2 Phase 2: 检索系统 (2-3 天)

  • 实现 VikingAdapter 封装
  • 实现 MemoryRetriever
  • 单元测试

5.3 Phase 3: 注入 + 集成 (2-3 天)

  • 实现 PromptInjector
  • 修改 AgentLoop 集成点
  • 集成测试

5.4 Phase 4: 提取系统 (3-4 天)

  • 实现 MemoryExtractor
  • 设计 LLM prompt 模板
  • 测试提取质量

5.5 Phase 5: 前端 UI (2-3 天)

  • 实现 GrowthPanel 组件
  • 扩展 Agent Store
  • 添加 Tauri Commands

5.6 Phase 6: 测试 + 优化 (2-3 天)

  • 端到端测试
  • 性能优化
  • 文档完善

总计: 约 12-18 天


六、关键文件路径

核心类型

  • crates/zclaw-types/src/agent.rs - AgentConfig
  • crates/zclaw-types/src/message.rs - Message
  • crates/zclaw-types/src/id.rs - AgentId, SessionId

存储层

  • crates/zclaw-memory/src/store.rs - MemoryStore
  • crates/zclaw-memory/src/schema.rs - SQLite Schema

运行时

  • crates/zclaw-runtime/src/loop_runner.rs - AgentLoop

OpenViking 集成

  • desktop/src/lib/viking-client.ts - 前端客户端
  • desktop/src-tauri/src/viking_commands.rs - Tauri 命令
  • docs/features/03-context-database/00-openviking-integration.md - 文档

前端学习系统(将被后端化)

  • desktop/src/lib/active-learning.ts
  • desktop/src/lib/memory-extractor.ts
  • desktop/src/store/activeLearningStore.ts

七、风险与缓解

风险 影响 缓解措施
OpenViking 不可用 实现 LocalStorageAdapter 降级方案
记忆提取质量低 可配置的置信度阈值 + 人工审核
Token 预算超限 严格的 Token 控制和截断
前端学习数据丢失 提供迁移脚本导入旧数据

八、新会话执行指南

在新会话中执行此方案时,请:

  1. 阅读本文档docs/superpowers/specs/2026-03-26-agent-growth-design.md
  2. 参考计划文件plans/crispy-spinning-reef.md(包含更多分析细节)
  3. 从 Phase 1 开始:创建 zclaw-growth crate 骨架
  4. 遵循设计:严格按照本文档的接口定义实现
  5. 保持沟通:如有疑问,与用户确认后再修改设计