GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-26-memory-management-design.md
2026-05-20 21:39:12 +08:00

15 KiB
Raw Permalink Blame History

记忆管理模块设计

日期2026-04-26 状态:待实施 目标:为 Agent 系统提供可视化的记忆管理能力,让 Agent 越用越好用

1. 背景与目标

现有记忆系统已支持 MySQL/SQLite 双后端存储Agent 可通过 memory_save / memory_recall 工具自动积累用户信息。但存在以下不足:

  • 记忆类型固定为 4 种user/project/feedback/reference无法扩展
  • 没有管理页面,管理员无法查看、编辑、预设记忆数据
  • Agent 工具缺少类型感知和 metadata 支持
  • 管理员无法直观了解各 Agent 各存储后端的记忆分布

本次实现一个记忆管理模块,放在 Agent 管理菜单下,供管理员/运营人员手动增删改查记忆数据,并扩展 Agent 工具能力。

2. 数据库 & 存储设计

2.1 新增 netaclaw_memory_typeMySQL

管理可用的记忆类型,两种存储后端共享此配置。

字段 类型 说明
id int BaseEntity 自增主键
key varchar(50) 类型标识,如 environment
name varchar(100) 显示名称,如"环境信息"
description varchar(500) 类型说明
icon varchar(50) 图标nullable
isSystem tinyint(1) 1=系统内置不可删除0=自定义

预置系统类型:

key name description
user 用户画像 用户偏好、角色、习惯
project 项目知识 项目进展、决策、约束
feedback 行为反馈 用户对 Agent 行为的纠正或确认
reference 引用资源 外部资源链接、文档地址

2.2 SQLite 迁移策略

SQLite 不支持 ALTER TABLE DROP CONSTRAINT,已有 .db 文件带旧 CHECK 约束。需要在 SqliteMemoryProvider 构造函数中加自动迁移:

// 迁移逻辑(在构造函数中执行)
// 1. 检测当前表是否有旧 CHECK 约束(查 sqlite_master 的 sql 列)
// 2. 如果有:
//    a. CREATE TABLE memory_new (... type TEXT NOT NULL ...);  -- 无 CHECK
//    b. INSERT INTO memory_new SELECT * FROM memory;
//    c. DROP TABLE memory;
//    d. ALTER TABLE memory_new RENAME TO memory;
//    e. 重建索引和 FTS 触发器
// 3. 如果没有:跳过迁移

新建表的 DDL 去掉 CHECK 约束:

CREATE TABLE IF NOT EXISTS memory (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  agent_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  type TEXT NOT NULL,  -- 不再有 CHECK 约束
  name TEXT NOT NULL,
  content TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  metadata TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

2.3 netaclaw_memoryMySQL

不改动。type 字段为 varchar(20),本身无约束,已支持任意值。

3. 后端架构

3.1 MemoryProviderRegistry — Provider 实例缓存池

解决问题:避免管理页面每次请求都创建新 Provider 实例,防止 SQLite 文件句柄泄漏和 WAL 锁冲突。

@Provide()
@Scope(ScopeEnum.Singleton)
export class MemoryProviderRegistry {
  private cache = new Map<string, { provider: MemoryProvider; backend: string }>();

  // 注入 Agent Entity 和 Memory Entity
  @InjectEntityModel(NetaClawAgentEntity)
  agentEntity: Repository<NetaClawAgentEntity>;
  @InjectEntityModel(NetaClawMemoryEntity)
  memoryEntity: Repository<NetaClawMemoryEntity>;

  async getProvider(agentName: string): Promise<{ provider: MemoryProvider; backend: string }> {
    if (this.cache.has(agentName)) return this.cache.get(agentName)!;
    // 查 Agent 配置 → 创建 provider → 缓存
    const agent = await this.agentEntity.findOneBy({ name: agentName });
    const config = agent?.config?.memory ?? { enabled: true, backend: 'mysql' };
    const provider = createMemoryProvider(config, this.memoryEntity);
    const entry = { provider, backend: config.backend };
    this.cache.set(agentName, entry);
    return entry;
  }

  // Agent 配置变更时清除缓存
  invalidate(agentName: string) {
    const cached = this.cache.get(agentName);
    if (cached) { cached.provider.close?.(); this.cache.delete(agentName); }
  }
}

SQLite provider 额外配置 busy_timeout

this.db.pragma('busy_timeout = 5000');

3.2 文件结构

packages/backend/src/modules/netaclaw/
├── entity/
│   ├── memory.ts              # 已有,不改
│   └── memory_type.ts         # 新增
├── memory/
│   ├── provider.ts            # 扩展:新增 page/countuserId 可选
│   ├── mysql_provider.ts      # 扩展:实现 page/count
│   ├── sqlite_provider.ts     # 扩展:实现 page/count + 迁移 + busy_timeout
│   ├── factory.ts             # 已有,不改
│   └── registry.ts            # 新增Provider 实例缓存池
├── service/
│   ├── memory_admin.ts        # 新增
│   └── memory_type.ts         # 新增
├── controller/admin/
│   ├── memory.ts              # 新增
│   └── memory_type.ts         # 新增

3.3 MemoryProvider 接口扩展

// provider.ts 修改

export interface MemorySearchOpts {
  agentName: string;
  userId?: string;   // 改为可选 — 管理页面跨用户查询
  type?: MemoryType;
  limit?: number;
}

export interface MemoryPageOpts {
  agentName?: string; // 可选 — 不传则查所有(仅 MySQL 后端有效)
  userId?: string;    // 可选 — 管理页面不限定用户
  type?: string;
  keyWord?: string;
  page: number;
  size: number;
}

export interface MemoryPageResult {
  list: MemoryEntry[];
  pagination: { page: number; size: number; total: number };
}

export interface MemoryProvider {
  // 已有方法
  save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
  update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
  delete(id: number): Promise<void>;
  search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  getById(id: number): Promise<MemoryEntry | null>;
  close?(): Promise<void>;

  // 新增方法
  page(opts: MemoryPageOpts): Promise<MemoryPageResult>;
  count(opts: Omit<MemoryPageOpts, 'page' | 'size'>): Promise<number>;
}

3.4 MemoryAdminService — 查询优化策略

@Provide()
export class MemoryAdminService extends BaseService {
  @Inject()
  registry: MemoryProviderRegistry;

  @InjectEntityModel(NetaClawMemoryEntity)
  memoryEntity: Repository<NetaClawMemoryEntity>;

  @InjectEntityModel(NetaClawAgentEntity)
  agentEntity: Repository<NetaClawAgentEntity>;

  async page(params: { agentName?: string; type?: string; keyWord?: string; page: number; size: number }) {
    if (params.agentName) {
      // 指定 Agent → 走对应 provider
      const { provider, backend } = await this.registry.getProvider(params.agentName);
      const result = await provider.page(params);
      // 给每条记录附加 backend 标识
      result.list.forEach(e => (e as any)._backend = backend);
      return result;
    }

    // 未指定 Agent → 分两路查询
    // 1. MySQL 后端:直接在 netaclaw_memory 表上查一次(不按 agentName 过滤)
    // 2. SQLite 后端:只遍历使用 SQLite 的 Agent
    // 合并结果,按 updatedAt DESC 排序,做应用层分页
  }

  async stats() {
    // MySQL 后端:一条 SQL GROUP BY agentName
    const mysqlStats = await this.memoryEntity
      .createQueryBuilder('m')
      .select('m.agentName', 'agentName')
      .addSelect('COUNT(*)', 'count')
      .groupBy('m.agentName')
      .getRawMany();

    // SQLite 后端:只遍历使用 SQLite 的 Agent
    const sqliteAgents = await this.agentEntity.find(/* memory.backend = 'sqlite' */);
    const sqliteStats = [];
    for (const agent of sqliteAgents) {
      const { provider } = await this.registry.getProvider(agent.name);
      const count = await provider.count({ agentName: agent.name });
      sqliteStats.push({ agentName: agent.name, backend: 'sqlite', count });
    }

    return [
      ...mysqlStats.map(s => ({ ...s, backend: 'mysql' })),
      ...sqliteStats,
    ];
  }
}

3.5 API 接口

记忆管理(自定义 Controller

所有接口严格遵循 Cool Admin 的请求/响应格式,确保前端 cl-crud 组件可直接对接。

方法 路径 说明
POST /admin/netaclaw/memory/page 分页查询,支持 agentName/type/keyWord 筛选
GET /admin/netaclaw/memory/info 详情query: agentName, id
POST /admin/netaclaw/memory/add 新增body 含 agentName
POST /admin/netaclaw/memory/update 更新body 含 agentName + id + updatedAt 乐观锁)
POST /admin/netaclaw/memory/delete 删除body: { agentName, ids }
GET /admin/netaclaw/memory/stats 各 Agent 各后端记忆统计

复合标识:所有操作使用 {agentName, id} 作为记忆的唯一标识。前端表格行数据必须携带 agentName。分页响应中每条记录附加 _backend 字段标识存储来源。

乐观锁update 接口要求传入 updatedAt,后端比对后决定是否允许更新,防止管理员和 Agent 同时修改同一条记忆。

类型管理(标准 @CoolController CRUD

方法 路径 说明
POST /admin/netaclaw/memory_type/page 分页
POST /admin/netaclaw/memory_type/add 新增类型
POST /admin/netaclaw/memory_type/update 更新类型
POST /admin/netaclaw/memory_type/delete 删除isSystem=1 的拒绝删除)
POST /admin/netaclaw/memory_type/list 列表(供下拉选择)

4. 前端页面

4.1 路由与菜单

  • 路由:/agent/memory
  • 前端文件:modules/agent/views/memory.vue
  • 菜单:挂在 Agent 管理(id=112) 下orderNum=8

4.2 页面布局

┌──────────────────────────────────────────────────┐
│  记忆管理                                         │
├───────────┬──────────────────────────────────────┤
│ 左侧统计   │  筛选栏:[Agent ▼] [类型 ▼] [关键词]  │
│           │  [+ 新增记忆] [类型管理]               │
│ Agent A   │──────────────────────────────────────│
│ MySQL     │  记忆列表(表格)                      │
│ 15条      │  ID | 标题 | 类型(tag) | Agent        │
│           │  存储后端(tag) | 描述 | 更新时间 | 操作  │
│ Agent B   │──────────────────────────────────────│
│ SQLite    │  分页                                 │
│ 8条       │                                      │
└───────────┴──────────────────────────────────────┘

4.3 交互说明

  • 左侧统计卡片:调 /stats 接口,按 Agent 分组展示记忆数量和存储后端标签
  • 点击卡片:自动筛选该 Agent 的记忆
  • 表格:类型列用彩色 Tag 展示,存储后端用 MySQL/SQLite 标签区分
  • 新增/编辑弹窗:
    • 选择 Agent下拉
    • 选择类型(下拉,数据来自 memory_type/list
    • 填写标题、内容textarea、描述
    • metadataJSON 编辑器,可选)
  • 类型管理:点击"类型管理"按钮弹出 Dialog表格展示所有类型系统内置类型带锁图标不可删除

5. Agent 工具扩展

5.1 类型校验 Fallback 机制

Agent 工具的类型校验不强依赖 MySQL 的 memory_type 表:

async function getAvailableTypes(memoryTypeRepo?: Repository<MemoryTypeEntity>): Promise<string[]> {
  const BUILTIN_TYPES = ['user', 'project', 'feedback', 'reference'];
  if (!memoryTypeRepo) return BUILTIN_TYPES;
  try {
    const types = await memoryTypeRepo.find();
    return types.map(t => t.key);
  } catch {
    return BUILTIN_TYPES; // MySQL 不可用时回退内置类型
  }
}

这确保纯 SQLite 部署场景下 Agent 工具仍可正常工作。

5.2 新增 memory_list_types 工具

让 Agent 感知可用的记忆类型,存储时选择正确类型。

// 参数:无
// 返回:可用类型列表 [{ key, name, description }]
// 实现:优先查 memory_type 表fallback 返回内置 4 种

5.3 扩展 memory_save

  • type 参数:从固定枚举改为 Type.String(),运行时通过 fallback 机制校验
  • 新增 metadata 参数:Type.Optional(Type.Record(Type.String(), Type.Unknown()))

5.4 扩展 memory_recall

  • 新增 id 参数:支持按 ID 精确查询
  • 返回格式增加 metadata 信息

5.5 新增 memory_stats 工具(可选)

让 Agent 了解自己已存储的记忆分布,辅助决策是更新还是新建。

// 参数:无
// 返回:{ total: number, byType: { user: 3, project: 5, ... } }

6. 架构决策记录

# 决策 原因
1 Provider 实例缓存池MemoryProviderRegistry 单例) 避免每次请求创建新 SQLite 连接,防止文件句柄泄漏和 WAL 锁冲突
2 SQLite 自动迁移去 CHECK 约束 SQLite 不支持 ALTER TABLE DROP CONSTRAINT需建新表迁移数据
3 类型校验 Fallback 机制 保持 SQLite 后端独立性,纯 SQLite 部署不依赖 MySQL
4 MySQL 后端查询不按 agentName 扇出 共享同一张表,一次 SQL 即可,避免 O(N) 查询
5 MemorySearchOpts.userId 改为可选 管理页面需要跨用户查询Agent 运行时仍传 userId
6 复合标识 {agentName, id} 跨后端 ID 可能冲突,必须用复合标识定位记录
7 update 接口乐观锁(基于 updatedAt 防止管理员和 Agent 同时修改同一条记忆导致数据丢失
8 SQLite busy_timeout = 5000ms 缓解管理员和 Agent 并发写入时的 SQLITE_BUSY 错误

7. 改动清单

文件 操作
Entity entity/memory_type.ts 新增
Provider memory/provider.ts 扩展接口page/countuserId 可选
Provider memory/mysql_provider.ts 实现 page/count
Provider memory/sqlite_provider.ts 实现 page/count + CHECK 迁移 + busy_timeout
Registry memory/registry.ts 新增Provider 实例缓存池
Service service/memory_admin.ts 新增:管理页面 Service
Service service/memory_type.ts 新增:类型管理 Service
Controller controller/admin/memory.ts 新增:记忆管理 Controller
Controller controller/admin/memory_type.ts 新增:类型管理 Controller
Tool tools/builtin/memory.ts 扩展 save/recall + fallback 类型校验
Tool tools/builtin/memory_types.ts 新增memory_list_types + memory_stats
前端 views/memory.vue 新增:记忆管理页面
前端 config.ts 注册路由
数据库 base_sys_menu 新增菜单记录
注册 entities.ts 注册新 Entity