Skip to content

Latest commit

 

History

History
1197 lines (938 loc) · 28.4 KB

File metadata and controls

1197 lines (938 loc) · 28.4 KB

设计原理与实现细节

版本: 1.0.0-rc
更新: 2026-02-24

本文档详细说明 MemoryOS-Rust 的设计原理、实现细节和关键决策。


📋 目录


🎯 核心设计原理

1. 3-Tier 记忆架构

设计理念

模拟人类记忆系统:短期记忆 → 工作记忆 → 长期记忆

STM (Short-Term Memory)
  ↓ 自动合并
MTM (Mid-Term Memory)
  ↓ 热度提升
LTM (Long-Term Memory)

为什么选择 Redis + Qdrant?

Redis (STM):

  • ✅ 极快的读写速度(< 1ms)
  • ✅ 支持 List 数据结构(FIFO 队列)
  • ✅ 支持 TTL(自动过期)
  • ✅ 支持分布式锁(并发控制)

Qdrant (MTM/LTM):

  • ✅ 高性能向量检索
  • ✅ 支持过滤和元数据
  • ✅ 支持批量操作
  • ✅ 现代 Rust API

记忆合并策略

STM → MTM:

// 触发条件
if stm.len() >= capacity {
    // 1. 获取分布式锁
    let lock = acquire_lock("consolidation");
    
    // 2. 读取所有 STM 消息
    let messages = stm.get_all();
    
    // 3. 生成 Embedding
    let embedding = generate_embedding(&messages);
    
    // 4. 存储到 MTM
    mtm.upsert(Segment {
        content: messages,
        embedding,
        heat: 0.0,
    });
    
    // 5. 清空 STM
    stm.clear();
    
    // 6. 释放锁
    release_lock(lock);
}

MTM → LTM:

// 触发条件
if segment.heat > threshold {
    // 1. 提取用户画像
    let profile = extract_profile(&segment);
    
    // 2. 提取知识
    let knowledge = extract_knowledge(&segment);
    
    // 3. 更新 LTM
    ltm.update_profile(user_id, profile);
    ltm.add_knowledge(user_id, knowledge);
}

2. 用户画像提取原理

规则提取 vs LLM 提取

MemoryOS-Rust 选择规则提取:

struct ExtractionRule {
    marker: String,      // "i like"
    target: RuleTarget,  // Preference
    format: Option<String>,
}

// 示例规则
"i like"Preference: "likes {value}"
"i work as"Background: "works as {value}"
"my name is"Background: "name is {value}"

优点:

  • ✅ 快速(< 1ms)
  • ✅ 确定性(相同输入 → 相同输出)
  • ✅ 无 LLM 成本
  • ✅ 可配置(环境变量)

缺点:

  • ❌ 灵活性较低
  • ❌ 需要预定义规则

Mem0 使用 LLM 提取:

  • ✅ 灵活、智能、准确
  • ❌ 慢(1-3s)
  • ❌ 成本高
  • ❌ 不确定性

设计决策: 优先性能和成本,牺牲部分灵活性


🏗️ 架构实现

1. 六边形架构(Hexagonal Architecture)

为什么选择六边形架构?

传统分层架构问题:

  • ❌ 层与层耦合
  • ❌ 难以测试
  • ❌ 难以替换实现

六边形架构优势:

  • ✅ 领域逻辑独立
  • ✅ 易于测试(Mock 适配器)
  • ✅ 易于替换实现(换数据库)
  • ✅ 清晰的依赖方向

实现结构

Core (领域层)
  ↑ 依赖
Ports (接口层)
  ↑ 实现
Adapters (适配器层)
  ↑ 调用
Gateway (网关层)

依赖倒置: Core 不依赖任何外部实现


2. 优雅降级机制

三层降级策略

match (redis_available, qdrant_available) {
    (true, true) => {
        // Full Mode: 完整功能
        DefaultMemoryManager::new(redis, qdrant, llm)
    }
    (true, false) | (false, true) => {
        // Degraded Mode: 部分功能
        DegradedMemoryManager::new(
            redis.map(Some),
            qdrant.map(Some),
            llm
        )
    }
    (false, false) => {
        // Noop Mode: 仅 LLM
        NoopMemoryManager::new(llm)
    }
}

降级行为

模式 Redis Qdrant 功能
Full STM + MTM + LTM
Degraded STM only
Degraded MTM + LTM only
Noop LLM only

关键: 单个后端故障不影响其他能力


🔧 关键机制

1. 配置热更新

⚠️ 限制: 当前实现中,AppState 持有启动时的配置快照。大部分配置变更需要重启服务。 详见 CONFIG_HOT_RELOAD_LIMITATION.md

设计意图(未完全实现)

// 设计目标:后台任务定期检查配置变更
// 当前状态:后台轮询任务已移除,配置在启动时加载一次
// 原因:AppState 持有 Arc<Config> 快照,运行时替换需要重构状态管理
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(5));
    loop {
        interval.tick().await;
        if config_manager.file_changed() {
            match config_manager.reload() {
                Ok(new_config) => {
                    config.store(Arc::new(new_config));
                    info!("✅ Config hot-reloaded");
                }
                Err(e) => warn!("⚠️  Config reload failed: {}", e),
            }
        }
    }
});

关键技术:

  • ArcSwap: 原子指针交换,无锁读取
  • tokio::spawn: 后台异步任务
  • SystemTime: 文件修改时间检测

当前状态:

  • ⚠️ 受限 — AppState 持有启动时快照,大部分配置变更需重启
  • ⚠️ 详见 docs/CONFIG_HOT_RELOAD_LIMITATION.md
  • ✅ 无锁读取(高性能)
  • ✅ 支持 K8s ConfigMap

2. 实时健康检查

实现原理

async fn current_health(&self) -> HealthStatus {
    // 1. 实时探测 Redis
    let redis_status = match self.redis_storage {
        Some(ref redis) => {
            match redis.ping().await {
                Ok(_) => "up",
                Err(_) => "down",
            }
        }
        None => "bypassed",
    };
    
    // 2. 实时探测 Qdrant
    let qdrant_status = match self.qdrant_storage {
        Some(ref qdrant) => {
            match qdrant.health_check().await {
                Ok(_) => "up",
                Err(_) => "down",
            }
        }
        None => "bypassed",
    };
    
    // 3. 计算模式
    match (redis_status, qdrant_status) {
        ("up", "up") => HealthStatus::Ready,
        ("up", "down") | ("down", "up") => HealthStatus::DegradedReady,
        _ => HealthStatus::NotReady,
    }
}

关键: 每次请求都实时探测,不使用缓存

优势:

  • ✅ 反映真实状态
  • ✅ 快速故障检测
  • ✅ 支持动态切换

3. 并发控制

Fencing Lock + CAS

问题: 多个实例同时合并 STM

解决方案:

// 1. Fencing Lock(分布式锁)
let lock_key = format!("lock:consolidation:{}", user_id);
let fencing_token = uuid::Uuid::new_v4().to_string();

// 2. 获取锁(SET NX + TTL)
let acquired = redis.set_nx_ex(
    &lock_key,
    &fencing_token,
    15  // 15 秒 TTL
).await?;

if !acquired {
    return Err(AppError::Conflict("Consolidation in progress"));
}

// 3. Lease Renewal(续租)
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(5));
    loop {
        interval.tick().await;
        redis.expire(&lock_key, 15).await;
    }
});

// 4. CAS 版本控制
let current_version = get_version(user_id).await?;
let new_version = current_version + 1;

// 5. 执行操作
consolidate_stm(user_id).await?;

// 6. CAS 更新版本
let success = redis.set_if_equal(
    &version_key,
    current_version,
    new_version
).await?;

if !success {
    return Err(AppError::Conflict("Version mismatch"));
}

// 7. 释放锁
redis.del(&lock_key).await?;

关键技术:

  • Fencing Lock: 防止多个实例同时操作
  • Lease Renewal: 防止锁过期
  • CAS: 防止并发修改冲突

4. 事件去重

实现原理

// 1. 生成事件 ID
let event_id = format!("{}:{}:{}", 
    user_id, 
    message.role, 
    hash(&message.content)
);

// 2. 检查是否已处理
let dedup_key = format!("dedup:{}", event_id);
let exists = redis.exists(&dedup_key).await?;

if exists {
    return Ok(()); // 已处理,跳过
}

// 3. 标记为已处理(TTL 2 小时)
redis.set_ex(&dedup_key, "1", 7200).await?;

// 4. 处理事件
process_event(event_id, message).await?;

优势:

  • ✅ 防止重复处理
  • ✅ 自动过期(TTL)
  • ✅ 高性能(Redis)

📊 数据流详解

1. 聊天请求完整流程

Client
  │
  │ POST /v1/chat/completions
  ▼
Gateway (Axum)
  │
  │ 1. 路由匹配
  │ 2. 中间件处理
  ▼
3-Tier Router
  │
  │ 3. 复杂度分析
  │ 4. 选择 LLM Tier
  ▼
Memory Manager
  │
  ├─► 5. 检索 STM (Redis)
  │     └─► LRANGE key 0 -1
  │
  ├─► 6. 检索 MTM (Qdrant)
  │     └─► search(embedding, limit=5)
  │
  └─► 7. 检索 LTM (Qdrant)
        ├─► get_profile(user_id)
        └─► search_knowledge(user_id, query)
        │
        ▼
8. 构建上下文
  │
  ▼
LLM Adapter
  │
  │ 9. 调用 LLM API
  ▼
External LLM
  │
  │ 10. 返回响应
  ▼
Memory Manager
  │
  │ 11. 存储新消息到 STM
  │ 12. 检查是否需要合并
  ▼
Gateway
  │
  │ 13. 返回响应
  ▼
Client

2. 记忆合并流程

详见 核心设计原理 - 记忆合并策略


⚡ 性能优化

1. Embedding 缓存

struct EmbeddingCache {
    cache: RwLock<HashMap<String, Vec<f32>>>,
    max_size: usize,  // 1000
}

// 缓存命中率:待测试
// 性能提升:待基准测试验证

2. 连接池

// Redis 连接池
let redis_pool = RedisPool::new(
    max_size: 100,
    min_idle: 10,
    timeout: Duration::from_secs(5),
);

// Qdrant 客户端复用
let qdrant_client = Arc::new(QdrantClient::new(...));

3. 异步处理

// 并行检索
let (stm, mtm, ltm) = tokio::join!(
    retrieve_stm(user_id),
    retrieve_mtm(user_id, query),
    retrieve_ltm(user_id, query),
);

🎯 设计决策

1. Rust vs Python

选择 Rust 的原因:

  • ✅ 高性能(10x+ vs Python)
  • ✅ 内存安全
  • ✅ 并发安全
  • ✅ 类型安全

代价:

  • ❌ 开发速度较慢
  • ❌ 学习曲线陡峭

2. 规则提取 vs LLM 提取

选择规则提取的原因:

  • ✅ 性能(< 1ms vs 1-3s)
  • ✅ 成本(免费 vs $0.001/次)
  • ✅ 确定性

代价:

  • ❌ 灵活性较低

3. Redis vs 内存

选择 Redis 的原因:

  • ✅ 持久化
  • ✅ 分布式支持
  • ✅ 丰富的数据结构

代价:

  • ❌ 网络延迟(~1ms)

4. Qdrant vs Chroma/Pinecone

选择 Qdrant 的原因:

  • ✅ 高性能
  • ✅ Rust 原生 API
  • ✅ 丰富的过滤功能
  • ✅ 开源


🔌 MCP Server 设计 (memoryos-mcp)

设计目标

将 MemoryOS 的全部记忆管理能力通过 MCP (Model Context Protocol) 标准协议暴露给 AI 助手,使 Claude Desktop、Cursor、自定义 Agent 等客户端无需手写 HTTP 集成即可调用。

为什么选择 MCP

对比维度 HTTP REST (Gateway) MCP Server
协议 HTTP/JSON JSON-RPC 2.0 over stdio/SSE
发现 需要阅读 API 文档 客户端自动发现 tools/resources
类型安全 手动校验 JSON Schema 自动生成 (schemars)
适用客户端 Web/Mobile/Backend AI Agent (Claude, Cursor, etc.)
集成成本 手写 HTTP client 零代码,配置文件即接入

MCP 是 HTTP API 的补充而非替代。两者共享同一套 Core/Ports/Adapters 业务逻辑。

为什么选择独立 crate (Approach A)

方案 A: 独立 crate memoryos-mcp (推荐)
  ✅ 独立二进制,按需部署
  ✅ stdio 传输支持(Claude Desktop 必需)
  ✅ 不影响 Gateway 代码
  ✅ 可独立测试和发布

方案 B: 嵌入 Gateway 的 /mcp 端点
  ❌ Gateway 启动时必须初始化 MCP
  ❌ 无法支持 stdio 传输
  ❌ 增加 Gateway 复杂度

技术栈选型

// Cargo.toml (memoryos-mcp) — 实际实现
[dependencies]
rmcp = { version = "0.3", features = ["server", "transport-io"] }
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
clap = { version = "4", features = ["derive"] }

rmcp 是官方 Rust MCP SDK,基于 tokio,与项目技术栈一致。实际使用 v0.3 版本。

Tool 实现模式

每个 MCP Tool 通过 #[tool] 宏注册,rmcp 自动生成 JSON Schema:

use rmcp::tool;
use schemars::JsonSchema;

#[derive(Debug, serde::Deserialize, JsonSchema)]
pub struct AddMemoryInput {
    pub user_id: String,
    pub content: String,
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
}

#[tool(
    name = "add_memory",
    description = "存储对话或事实到短期记忆 (STM)"
)]
async fn add_memory(input: AddMemoryInput) -> Result<String, McpError> {
    let storage = get_storage().await;
    storage.add_short_term_message(
        &input.user_id,
        &input.content,
        input.metadata,
    ).await?;
    Ok(format!("Memory stored for user {}", input.user_id))
}

Tool ↔ Gateway API 映射 (实际实现)

MCP Server 采用 Gateway 代理模式,所有 tool call 转发到 Gateway HTTP API:

MCP Tool Gateway 端点 功能
add_memory POST /v1/memory/add 存储消息到 STM
search_memories POST /v1/memory/retrieve 语义搜索记忆
get_memories POST /v1/memory/retrieve 获取用户全部记忆
delete_memory POST /v1/security/gdpr/delete 删除用户数据 (GDPR)
query_graph POST /v1/graph/query 知识图谱查询
chat POST /v1/chat 记忆增强对话
health_check GET /v1/health 系统健康检查

Transport 实现

// stdio 模式(本地部署)
async fn run_stdio() -> Result<()> {
    let transport = rmcp::transport::stdio();
    let server = McpServer::new(build_tools(), build_resources());
    server.serve(transport).await
}

// SSE 模式(远程部署)
async fn run_sse(addr: SocketAddr) -> Result<()> {
    let transport = rmcp::transport::sse::SseServer::new(addr);
    let server = McpServer::new(build_tools(), build_resources());
    server.serve(transport).await
}

// CLI 入口
#[derive(clap::Parser)]
struct Cli {
    #[arg(long, default_value = "stdio")]
    transport: TransportMode,

    #[arg(long, default_value = "127.0.0.1:3001")]
    sse_addr: SocketAddr,

    #[arg(long, default_value = "config.toml")]
    config: PathBuf,
}

资源 (Resources) 设计

MCP Resources 提供只读数据订阅,客户端可以 list + read:

use rmcp::resource;

#[resource(
    uri = "memory://{user_id}/profile",
    name = "User Profile",
    description = "用户长期画像数据",
    mime_type = "application/json"
)]
async fn user_profile(user_id: String) -> Result<String, McpError> {
    let storage = get_storage().await;
    let profile = storage.get_long_term(&user_id, "profile").await?;
    Ok(serde_json::to_string(&profile)?)
}

错误处理

MCP 错误映射到 JSON-RPC error codes:

场景 JSON-RPC Code 说明
参数缺失/非法 -32602 Invalid params
用户未找到 -32001 自定义业务错误
存储不可用 -32002 降级返回缓存数据
认证失败 -32003 API Key 无效
内部错误 -32603 兜底错误

并发与安全

  • 共享状态: 通过 Arc<AppState> 共享存储连接池,与 Gateway 相同模式
  • 认证: MCP 协议本身不含认证,stdio 模式依赖本地权限,SSE 模式通过配置的 API Key 在 tool handler 内校验
  • GDPR: delete_memory tool 调用与 Gateway 相同的 GdprManager,审计记录统一

部署架构 (实际实现)

场景 1: 开发者本地使用 (stdio)
┌──────────────┐     stdio      ┌──────────────┐     HTTP      ┌──────────────┐
│ Claude       │ ←────────────→ │ memoryos-mcp │ ──────────→  │ Gateway:8080 │
│ Desktop      │  stdin/stdout  │ (binary)     │              │              │
└──────────────┘                └──────────────┘              └──────┬───────┘
                                                                     │
                                                              ┌──────▼───────┐
                                                              │ Qdrant+Redis │
                                                              └──────────────┘

场景 2: Gateway + MCP 共存 (推荐生产部署)
┌──────────────┐                ┌──────────────┐
│ Web App      │──HTTP REST───→ │ Gateway:8080 │──→ Core+Adapters
└──────────────┘                └──────────────┘       ↑
                                                       │ HTTP
┌──────────────┐                ┌──────────────┐       │
│ AI Agent     │──MCP stdio───→ │ MCP Server   │───────┘
└──────────────┘                └──────────────┘

📚 参考资料


更新时间: 2026-02-20
版本: 0.13.0


🏢 企业级功能设计

以下设计文档中的版本号 (v0.x.0) 为历史开发标记。当前版本为 v1.0.0-rc

设计原则

服务分离: 业务 API (gateway) 和管理 API (admin) 物理隔离,部署在不同网络区域。

最小权限: RBAC 模型确保每个用户只能访问其角色允许的资源。

租户隔离: 所有数据操作自动按 tenant_id 过滤,防止跨租户数据泄漏。

1. RBAC 设计

角色定义

pub enum Role {
    SuperAdmin,  // 全局管理: 租户CRUD + 所有权限
    Admin,       // 租户管理: 用户管理 + 数据读写 + 审计
    User,        // 普通用户: 数据读写
    ReadOnly,    // 只读用户: 仅读取
}

权限定义

pub enum Permission {
    ReadMemory,     // 读取记忆数据
    WriteMemory,    // 写入记忆数据
    ManageUsers,    // 管理用户(分配角色)
    ManageTenants,  // 管理租户(CRUD)
    ViewAudit,      // 查看审计日志
    ManageConfig,   // 修改系统配置
}

权限检查流程

Request → Auth Middleware → 提取 API Key
    → 查询 RbacManager → 获取 Role
    → 检查 Permission → 允许/拒绝

2. 多租户设计

租户模型

pub struct Tenant {
    pub id: String,           // UUID
    pub name: String,
    pub description: String,
    pub max_users: u32,       // 最大用户数
    pub storage_quota_mb: u64, // 存储配额
    pub api_rate_limit: u32,  // API 调用限制/分钟
    pub enabled: bool,
    pub created_at: DateTime<Utc>,
}

数据隔离策略

注意: 以下为计划中的数据层隔离方案,当前版本 (v0.12.1) 已实现租户管理和 RBAC 权限控制, 数据层自动过滤将在后续版本实现。

  • Qdrant (计划): 所有查询添加 tenant_id payload filter
  • Redis (计划): key 前缀 {tenant_id}:{key}
  • 审计日志: 每条记录标记 tenant_id

3. 服务分离设计

为什么分离?

  • 安全: 管理 API 不暴露到公网
  • 独立认证: 管理员可用不同认证方式(LDAP/SSO)
  • 审计清晰: 管理操作和业务操作日志分离
  • 独立部署: 管理服务可以独立升级/重启

memoryos-admin API

端点 方法 说明
/api/v1/tenants GET/POST 租户列表/创建
/api/v1/tenants/:id GET/PUT/DELETE 租户详情/更新/删除
/api/v1/users GET/POST 用户列表/创建
/api/v1/users/:id GET/PUT/DELETE 用户详情/更新/删除
/api/v1/users/:id/roles PUT 分配角色
/api/v1/audit/logs GET 审计日志查询
/api/v1/system/stats GET 系统统计

🎯 FAQ 系统设计

设计目标

将高频问答自动提升为 FAQ:

  • ⚡ 极速响应(Router Tier 0 FAQ 直接命中,跳过 LLM,✅ v0.3.0 已实现)
  • 🎯 精准匹配(相似度阈值可配置)
  • 🔄 自动更新(基于访问频率和热度计算)
  • 📚 知识沉淀(本地 Markdown + S3 + Confluence 导出,✅ v0.3.0 已实现)

核心组件

1. HeatTracker (热度追踪)

职责: 记录和计算记忆热度

数据结构:

pub struct MidTermSegment {
    pub id: Uuid,
    pub user_id: String,
    pub summary: String,
    pub embedding: Vec<f32>,
    
    // FAQ 热度字段
    pub access_count: u32,        // 访问次数
    pub heat_score: f32,          // 热度分数
    pub last_accessed: Option<DateTime>,
    pub memory_type: MemoryType,  // QA/Candidate/FAQ
}

pub enum MemoryType {
    QA,              // 普通问答
    FaqCandidate,    // FAQ 候选
    Faq,             // 正式 FAQ
}

热度计算公式:

heat_score = (access_count * 10.0) 
           + (positive_feedback * 5.0) 
           - (days_since_created * 0.5)

设计考虑:

  • 访问次数权重最高(10.0)
  • 时间衰减防止过时内容
  • 预留反馈机制接口

2. AutoPromoter (自动提升)

职责: 扫描并提升高频问答

提升规则:

// QA → FaqCandidate
if access_count >= 10 && heat_score >= 50.0 {
    promote_to_candidate();
}

// FaqCandidate → Faq
if access_count >= 20 && heat_score >= 100.0 {
    promote_to_faq();
}

提升历史:

pub struct PromotionRecord {
    pub id: Uuid,
    pub memory_id: Uuid,
    pub from_type: MemoryType,
    pub to_type: MemoryType,
    pub reason: String,
    pub heat_score: f32,
    pub access_count: u32,
    pub promoted_at: DateTime,
}

后台任务:

// 每小时扫描一次
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(3600));
    loop {
        interval.tick().await;
        let result = promoter.scan_and_promote(&mut segments).await;
        tracing::info!("提升 {} 个候选", result.promoted_to_candidate.len());
    }
});

3. WikiExporter (Wiki 导出)

职责: 导出成熟 FAQ 为知识库

筛选条件:

fn filter_exportable(&self, segments: &[MidTermSegment]) -> Vec<&MidTermSegment> {
    segments.iter().filter(|s| {
        s.memory_type == MemoryType::Faq
            && s.access_count >= 10
            && (now - s.created_at).num_days() >= 30
    }).collect()
}

智能分类:

fn extract_category(&self, segment: &MidTermSegment) -> String {
    let summary_lower = segment.summary.to_lowercase();
    
    if summary_lower.contains("wifi") || summary_lower.contains("网络") {
        "网络问题"
    } else if summary_lower.contains("密码") {
        "账号密码"
    } else if summary_lower.contains("报销") {
        "财务报销"
    } else if summary_lower.contains("请假") {
        "考勤休假"
    } else {
        "其他"
    }
}

Markdown 生成:

# FAQ 知识库

**生成时间**: 2026-02-19 00:00:00

---

## 📑 目录

- [网络问题](#网络问题)
- [账号密码](#账号密码)
- [财务报销](#财务报销)

---

## 网络问题

### 1. WiFi 密码是多少?

**访问次数**: 25 | **热度**: 245.5

**创建时间**: 2026-01-15

**最后访问**: 2026-02-18

---

导出目标:

pub enum ExportTarget {
    Local(String),                    // 本地文件系统
    S3 { bucket, prefix, endpoint },  // S3/OSS
    Confluence { base_url, space },   // Confluence
}

数据流

用户提问
    ↓
检索相似记忆
    ↓
更新 access_count++
    ↓
计算 heat_score
    ↓
存回 Qdrant
    ↓
[后台任务]
    ↓
扫描高热度记忆
    ↓
提升为 FAQ
    ↓
[定时任务]
    ↓
导出为 Wiki

性能优化

1. 批量更新

// 不要每次访问都写 Qdrant
// 使用内存缓存,定期批量写入
let mut batch = Vec::new();
for segment in segments {
    tracker.record_access(&mut segment);
    batch.push(segment);
    
    if batch.len() >= 100 {
        storage.batch_update(&batch).await?;
        batch.clear();
    }
}

2. 异步导出

// Wiki 导出不阻塞主流程
tokio::spawn(async move {
    let result = exporter.export(markdown).await;
    tracing::info!("导出完成: {:?}", result);
});

3. 缓存热度分数

// 避免重复计算
pub struct HeatTracker {
    cache: Arc<RwLock<HashMap<Uuid, f32>>>,
}

设计权衡

为什么不用 Redis 存储 FAQ?

Redis 问题:

  • 重启后数据丢失
  • 不支持向量检索
  • 难以做复杂过滤

Qdrant 优势:

  • 持久化存储
  • 原生向量检索
  • 支持元数据过滤
  • 统一存储架构

为什么需要三种类型(QA/Candidate/FAQ)?

渐进式提升:

  • QA: 新问答,观察期
  • Candidate: 高频但需验证
  • FAQ: 确认高质量

防止误判:

  • 避免低质量内容直接成为 FAQ
  • 给人工审核留出空间
  • 支持降级机制

为什么本地导出优先?

实用性:

  • S3/Confluence 需要额外配置
  • 本地文件最简单
  • 可以手动上传

扩展性:

  • 接口已预留
  • 后续可轻松添加

测试策略

#[tokio::test]
async fn test_faq_lifecycle() {
    // 1. 创建普通问答
    let mut segment = create_qa("WiFi密码?", 0);
    assert_eq!(segment.memory_type, MemoryType::QA);
    
    // 2. 模拟 15 次访问
    for _ in 0..15 {
        tracker.record_access(&mut segment);
    }
    
    // 3. 检查提升为候选
    assert!(tracker.should_promote(&segment));
    promoter.scan_and_promote(&mut [segment]).await;
    assert_eq!(segment.memory_type, MemoryType::FaqCandidate);
    
    // 4. 继续访问,提升为 FAQ
    for _ in 0..10 {
        tracker.record_access(&mut segment);
    }
    promoter.scan_and_promote(&mut [segment]).await;
    assert_eq!(segment.memory_type, MemoryType::Faq);
    
    // 5. 导出为 Wiki
    let exportable = exporter.filter_exportable(&[segment]);
    assert_eq!(exportable.len(), 1);
}

未来优化

1. 机器学习分类

// 使用 LLM 自动分类
async fn classify_with_llm(&self, segment: &MidTermSegment) -> String {
    let prompt = format!("将以下问题分类:{}", segment.summary);
    let response = llm.complete(prompt).await?;
    response.category
}

2. 多语言支持

// 自动翻译 FAQ
if user_lang != faq_lang {
    let translated = translator.translate(faq.content, user_lang).await?;
    return translated;
}

3. A/B 测试

// 测试不同提升阈值
if experiment_group == "A" {
    threshold = 50.0;
} else {
    threshold = 30.0;
}

📊 总结

FAQ 系统通过三个核心组件实现了从问答到知识库的自动化流程:

  1. HeatTracker: 精确追踪访问热度
  2. AutoPromoter: 智能提升高频问答
  3. WikiExporter: 自动导出为知识库

当前状态 (v0.9.0):

  • 响应速度: FAQ 直接命中绕过 LLM,具体延迟待基准测试
  • 提升准确率: 基于访问次数和热度分数,待生产验证
  • 导出质量: 本地 Markdown + S3 + Confluence 全部可用

实现状态:

  • ✅ HeatTracker: 热度计算和追踪已实现
  • ✅ AutoPromoter: 自动提升逻辑已实现
  • ✅ WikiExporter: 本地 Markdown 导出已实现
  • ✅ WikiExporter: S3 导出已实现 (OpenDAL S3ExportBackend, v0.3.0)
  • ✅ WikiExporter: Confluence 导出已实现 (REST API ConfluenceExportBackend, v0.3.0)
  • ✅ Router Tier 0: FAQ 直接命中已集成到路由器 (v0.3.0)
  • ✅ FAQ 管理 API: candidates/promote/delete/history/stats (v0.3.0)
  • ✅ LLM FAQ 分类: LlmFaqClassifier + /v1/admin/faq/classify API (v0.10.0)
  • ✅ 错误处理和后台任务支持