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

388 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 记忆管理模块设计
> 日期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 |