13.7 SkillMemory 记忆系统

`SkillMemory` 是 RAF 的持久化跨会话记忆系统。它通过后台 `MemoryConsolidationWorker` 定期触发 `MemoryAgent` 进行记忆整合,并将记忆持久化到文件系统,使 Agent 在多次对话之间保持上下文。

架构概览

sequenceDiagram
    participant U as 用户
    participant A as 主 Agent
    participant SMP as SkillMemoryContextProvider
    participant CW as MemoryConsolidationWorker
    participant MA as MemoryAgent
    participant FS as 文件系统

    U->>A: 发送消息
    A->>SMP: on_invoking (Pre)
    SMP->>A: 注入记忆指令 + 记忆工具
    A->>A: 执行

    A->>SMP: on_invoked (Post)
    Note over SMP: 累计轮次计数
    alt 达到整合间隔
        SMP->>CW: enqueue ConsolidationJob
        CW->>MA: 运行 MemoryAgent
        MA->>FS: 写入整合后的记忆文件
    else 未达间隔
        SMP->>SMP: 递增计数
    end

SkillMemoryContextProvider

核心上下文提供器,负责:

1. **Pre-invocation**:注入记忆检索指令和工具 2. **Post-invocation**:触发后台记忆整合

pub struct SkillMemoryContextProvider {
    enabled: bool,
    memory_dir: PathBuf,
    skills_provider: Option<AgentSkillsProvider>,
    memory_agent_client: Option<Arc<dyn IChatClient>>,
    consolidation_interval: usize,       // 整合间隔(默认 3 轮)
    worker: Arc<MemoryConsolidationWorker>,
}

配置

let memory = SkillMemoryContextProvider::new("./memory")
    .with_enabled(true)                    // 启用记忆
    .with_memory_agent(memory_client)      // 指定 MemoryAgent 的客户端
    .with_consolidation_interval(5);       // 每 5 轮整合一次

Pre-invocation 注入

每次调用时,`SkillMemoryContextProvider` 向 Agent 注入记忆检索指令:

fn build_advertise(&self) -> String {
    concat!(
        "## PERSISTENT MEMORY\n\n",
        "You have a persistent, cross-session memory system. Memory files exist even ",
        "when a conversation is brand new — do NOT assume 'no history' just because ",
        "the current conversation just started. Your training data does NOT contain ",
        "these memory files; their contents are the authoritative source.\n\n",
        "**When to retrieve memory (MANDATORY):**\n",
        "- Identity: your name/role, the user's identity, shared goals\n",
        "- Constraints: behavioral rules, user preferences, past lessons\n",
        "- Domain knowledge: professional material previously studied\n\n",
        "**How to retrieve:**\n",
        "1. Call `load_skill(\"skill-memory\")` to get the full retrieval guide.\n",
        "2. Follow that guide to read the correct memory files.\n\n",
        "Do NOT use training-data defaults for any identity-related question."
    )
}

Post-invocation 整合触发

async fn on_invoked(&self, agent, session, request_messages, response, error) -> Result<()> {
    // 递增轮次计数
    let key = format!("{}_turn_count", self.name());
    let current_count = session.get_provider_state(&key).unwrap_or(0) as usize;
    let new_count = current_count + 1;

    if new_count >= self.consolidation_interval {
        // 重置计数
        session.set_provider_state(&key, Value::Number(0.into()))?;

        // 准备整合消息
        let turn_transcript = response.map(|r| r.turn_transcript.clone()).unwrap_or_default();
        let memory_projection = load_memory_projection(session);
        let consolidation = prepare_consolidation_messages(&memory_projection, &turn_transcript);

        // 保存当前记忆投影
        save_memory_projection(session, &consolidation)?;

        // 入队后台整合任务
        self.worker.enqueue_latest(ConsolidationJob {
            memory_dir: self.memory_dir.clone(),
            client: self.resolve_client(agent)?,
            messages: consolidation,
            session_id: Some(session.session_id().to_string()),
            coalesced_dropped: 0,
        });
    } else {
        session.set_provider_state(&key, Value::Number(new_count.into()))?;
    }

    Ok(())
}

客户端解析

`SkillMemoryContextProvider` 自动从主 Agent 发现或通过 `memory_agent_client` 指定 ChatClient,用于生成 MemoryAgent:

fn resolve_client(&self, agent: &dyn IAgent) -> Option<Arc<dyn IChatClient>> {
    // 1. 优先使用显式配置的 memory_agent_client
    if let Some(c) = &self.memory_agent_client {
        return Some(Arc::clone(c));
    }

    // 2. 检查缓存的自动发现客户端
    if let Some(c) = &*self.auto_client.lock().unwrap() {
        return Some(Arc::clone(c));
    }

    // 3. 从主 Agent 获取 ChatClient
    let main_client = agent.chat_client()?;

    // 4. 解包装饰器链,获取原始客户端
    let raw = unwrap_to_raw(main_client);

    // 5. 包装为 MemoryAgentChatClient
    let wrapped: Arc<dyn IChatClient> = 
        Arc::new(MemoryAgentChatClient::new(Arc::clone(raw)));

    // 6. 缓存
    *self.auto_client.lock().unwrap() = Some(Arc::clone(&wrapped));

    Some(wrapped)
}

MemoryConsolidationWorker

后台工作线程,负责合并和串行化整合任务:

pub struct MemoryConsolidationWorker {
    // 内部维护任务队列和处理循环
}

impl MemoryConsolidationWorker {
    pub fn spawn() -> Arc<Self>;
    pub fn enqueue_latest(&self, job: ConsolidationJob);
    pub fn stats(&self) -> WorkerStats;
}

使用 `enqueue_latest` 合并策略——如果前一个整合任务尚未完成,新任务会替换旧任务(`coalesced_dropped` 计数递增)。

WorkerStats 统计

pub struct WorkerStats {
    pub total_completed: u64,      // 已完成整合数
    pub total_failed: u64,         // 失败整合数
    pub queued: u64,               // 当前队列数
    pub coalesced_dropped: u64,   // 合并丢弃数
}

记忆目录结构

memory/
├── SKILL.md              # 记忆系统的指令文件
├── identity.md           # Agent 身份记忆
├── user.md               # 用户信息记忆
├── preferences.md        # 用户偏好记忆
├── knowledge/            # 领域知识记忆
│   └── ...
├── sessions/             # 会话总结记忆
│   └── ...
└── constraints.md        # 行为约束记忆

种子初始化

首次使用时,`memory_seed::seed_memory_dir()` 自动创建目录结构和默认文件:

pub fn seed_memory_dir(memory_dir: &Path) {
    // 创建目录结构
    std::fs::create_dir_all(memory_dir.join("knowledge")).ok();
    std::fs::create_dir_all(memory_dir.join("sessions")).ok();

    // 创建默认 SKILL.md
    write_if_missing(memory_dir.join("SKILL.md"), DEFAULT_SKILL_MD);

    // 创建空记忆文件
    write_if_missing(memory_dir.join("identity.md"), "");
    write_if_missing(memory_dir.join("user.md"), "");
    write_if_missing(memory_dir.join("preferences.md"), "");
    write_if_missing(memory_dir.join("constraints.md"), "");
}

完整示例

use rust_agent_framework::{
    AgentBuilder,
    memory::SkillMemoryContextProvider,
};
use std::path::Path;

async fn agent_with_memory() -> anyhow::Result<()> {
    let client = DeepSeekChatClient::new(/* ... */)?;

    // 1. 配置持久记忆
    let memory = SkillMemoryContextProvider::new("./agent_memory")
        .with_consolidation_interval(3); // 每 3 轮对话触发记忆整合

    // 2. 构建 Agent
    let agent = AgentBuilder::new("assistant_with_memory")
        .chat_client(client)
        .instructions("你是个人 AI 助手。使用 load_skill('skill-memory') 查看记忆检索指南。")
        .with_context_provider(memory)
        .with_tool(ReadFile::default())
        .with_tool(WriteFile::default())
        .max_tool_rounds(10)
        .build()?;

    // 3. 第一轮对话
    let input1 = vec![ChatMessage::user("我叫张三,是一名 Rust 开发者。")];
    agent.run(input1, None, None).await?;

    // 4. 第二轮对话
    let input2 = vec![ChatMessage::user("我喜欢简洁的代码风格。")];
    agent.run(input2, None, None).await?;

    // 5. 第三轮对话(触发记忆整合)
    let input3 = vec![ChatMessage::user("请记住我的偏好。")];
    agent.run(input3, None, None).await?;

    // 等待后台整合完成(生产环境中由 Worker 异步处理)
    tokio::time::sleep(Duration::from_secs(5)).await;

    // 6. 新会话 — Agent 应该能从记忆中回忆用户信息
    let input4 = vec![ChatMessage::user("我叫什么名字?我的编程偏好是什么?")];
    let mut stream = agent.run(input4, None, None).await?;
    // Agent 应从 memory 目录读取 identity.md 和 preferences.md

    Ok(())
}

注意事项

1. **异步整合**:记忆整合是异步后台任务,写入可能在几秒到几十秒后完成 2. **客户端解包**:`unwrap_to_raw` 会递归展开装饰器链以获取原始 ChatClient 3. **文件锁安全**:`MemoryAgent` 使用独立的文件写入逻辑,避免与主 Agent 冲突 4. **token 消耗**:每次对话前会注入 ~200 tokens 的记忆检索指令 5. **整合频率**:默认每 3 轮触发整合,可通过 `with_consolidation_interval()` 调整 6. **合并策略**:使用 `enqueue_latest` 而非 FIFO 队列,避免积压