GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-26-memory-management-design.md

388 lines
15 KiB
Markdown
Raw Normal View History

2026-05-20 21:39:12 +08:00
# 记忆管理模块设计
> 日期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_type` 表MySQL
管理可用的记忆类型,两种存储后端共享此配置。
| 字段 | 类型 | 说明 |
|------|------|------|
| 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` 构造函数中加自动迁移:
```typescript
// 迁移逻辑(在构造函数中执行)
// 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 约束:
```sql
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_memory` 表MySQL
不改动。`type` 字段为 varchar(20),本身无约束,已支持任意值。
## 3. 后端架构
### 3.1 MemoryProviderRegistry — Provider 实例缓存池
**解决问题**:避免管理页面每次请求都创建新 Provider 实例,防止 SQLite 文件句柄泄漏和 WAL 锁冲突。
```typescript
@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`
```typescript
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 接口扩展
```typescript
// 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 — 查询优化策略
```typescript
@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` 表:
```typescript
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 感知可用的记忆类型,存储时选择正确类型。
```typescript
// 参数:无
// 返回:可用类型列表 [{ 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 了解自己已存储的记忆分布,辅助决策是更新还是新建。
```typescript
// 参数:无
// 返回:{ 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 |