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

334 lines
11 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.

# Agent 长期记忆系统设计
> 日期: 2026-04-12
> 模块: netaclaw/memory
> 状态: 设计阶段
## 1. 目标
为 NetaClaw Agent 添加长期记忆能力,使 Agent 能够跨会话记住用户偏好、项目上下文、行为反馈和外部引用。记忆按 Agent + 用户组合隔离,支持 MySQL 和 SQLite 两种存储后端,由用户在 Agent 管理界面自由选择。
## 2. 核心决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 记忆归属粒度 | Agent + 用户组合 | 最细粒度隔离,互不干扰 |
| 存储后端 | MySQL FULLTEXT + SQLite FTS5 双后端 | 用户可按 Agent 选择持久化方式 |
| 记忆分类 | user / project / feedback / reference | 覆盖四种典型记忆场景 |
| 写入方式 | Agent 工具主动写入 | 简单可控Agent 自主决策存什么 |
| 检索注入 | 每轮对话前 prefetch不持久化 | 避免污染消息历史 |
## 3. 数据模型
### 3.1 MemoryEntry 接口
```typescript
interface MemoryEntry {
id: number;
agentName: string; // 归属 Agent
userId: string; // 归属用户
type: 'user' | 'project' | 'feedback' | 'reference';
name: string; // 记忆标题
content: string; // 记忆正文
description: string; // 一行描述,用于检索时快速判断相关性
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
```
### 3.2 MySQL 表结构 (netaclaw_memory)
```sql
CREATE TABLE netaclaw_memory (
id INT AUTO_INCREMENT PRIMARY KEY,
agent_name VARCHAR(100) NOT NULL,
user_id VARCHAR(100) NOT NULL,
type ENUM('user', 'project', 'feedback', 'reference') NOT NULL,
name VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
description VARCHAR(500) NOT NULL DEFAULT '',
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_agent_user (agent_name, user_id),
INDEX idx_type (type),
FULLTEXT INDEX ft_content (name, content, description) WITH PARSER ngram
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.3 SQLite 表结构
```sql
-- 主表
CREATE TABLE memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_name TEXT NOT NULL,
user_id TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),
name TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
metadata TEXT, -- JSON string
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_agent_user ON memory(agent_name, user_id);
-- FTS5 虚拟表
CREATE VIRTUAL TABLE memory_fts USING fts5(
name, content, description,
content='memory', content_rowid='id',
tokenize='trigram'
);
-- 同步触发器
CREATE TRIGGER memory_ai AFTER INSERT ON memory BEGIN
INSERT INTO memory_fts(rowid, name, content, description)
VALUES (new.id, new.name, new.content, new.description);
END;
CREATE TRIGGER memory_ad AFTER DELETE ON memory BEGIN
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
VALUES ('delete', old.id, old.name, old.content, old.description);
END;
CREATE TRIGGER memory_au AFTER UPDATE ON memory BEGIN
INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
VALUES ('delete', old.id, old.name, old.content, old.description);
INSERT INTO memory_fts(rowid, name, content, description)
VALUES (new.id, new.name, new.content, new.description);
END;
```
## 4. Provider 抽象层
### 4.1 MemoryProvider 接口
```typescript
// memory/provider.ts
export interface MemorySearchOpts {
agentName: string;
userId: string;
type?: MemoryEntry['type'];
limit?: number; // 默认 10
}
export interface MemoryProvider {
save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
update(id: number | string, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
delete(id: number | string): Promise<void>;
search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
getById(id: number | string): Promise<MemoryEntry | null>;
close?(): Promise<void>; // SQLite 需要关闭连接
}
```
### 4.2 工厂函数
```typescript
// memory/factory.ts
export function createMemoryProvider(config: AgentMemoryConfig, mysqlRepo?: Repository<NetaClawMemoryEntity>): MemoryProvider {
if (config.backend === 'sqlite') {
return new SqliteMemoryProvider(config.sqlitePath);
}
return new MysqlMemoryProvider(mysqlRepo);
}
```
### 4.3 MysqlMemoryProvider
- 注入 TypeORM `Repository<NetaClawMemoryEntity>`
- search 实现: `SELECT * FROM netaclaw_memory WHERE MATCH(name, content, description) AGAINST(? IN BOOLEAN MODE) AND agent_name = ? AND user_id = ? LIMIT ?`
- 复用现有数据库连接,无额外依赖
### 4.4 SqliteMemoryProvider
- 使用 `better-sqlite3` 管理独立 .db 文件
- 默认路径: `{dataDir}/memory/memory.db`(所有 Agent 共用一个 db按字段过滤
- search 实现: `SELECT m.* FROM memory m JOIN memory_fts f ON m.id = f.rowid WHERE memory_fts MATCH ? AND m.agent_name = ? AND m.user_id = ? LIMIT ?`
- 初始化时自动建表(如不存在)
## 5. Agent 工具定义
### 5.1 memory_save 工具
```typescript
// tools/builtin/memory.ts
const memorySaveTool: AgentTool = {
name: 'memory_save',
label: '保存记忆',
description: '存储、更新或删除长期记忆。记忆会在未来对话中自动召回。',
parameters: Type.Object({
action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
name: Type.String({ description: '记忆标题' }),
type: Type.Union([
Type.Literal('user'), Type.Literal('project'),
Type.Literal('feedback'), Type.Literal('reference')
]),
content: Type.Optional(Type.String({ description: '记忆正文' })),
description: Type.Optional(Type.String({ description: '一行描述' })),
id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
}),
async execute(id, params) {
// 由运行时注入 agentName + userId + provider
}
};
```
### 5.2 memory_recall 工具
```typescript
const memoryRecallTool: AgentTool = {
name: 'memory_recall',
label: '检索记忆',
description: '搜索长期记忆中的相关信息。',
parameters: Type.Object({
query: Type.String({ description: '搜索关键词' }),
type: Type.Optional(Type.Union([
Type.Literal('user'), Type.Literal('project'),
Type.Literal('feedback'), Type.Literal('reference')
])),
limit: Type.Optional(Type.Number({ description: '返回条数,默认 5', default: 5 })),
}),
async execute(id, params) {
// 由运行时注入 agentName + userId + provider
}
};
```
## 6. Prefetch 注入流程
### 6.1 触发时机
`controller/chat.ts` 的 chat 方法中,调用 `runAgent()` 之前:
```
1. 检查 Agent 配置 memory.enabled
2. 如果启用 → createMemoryProvider(agentConfig.memory)
3. 用用户消息作为 query → provider.search(userMessage, { agentName, userId, limit: 5 })
4. 格式化为 memoryContext 字符串
5. 传入 runAgent() 的参数中
```
### 6.2 注入位置
`runtime/agent.ts` 消息组装处,将 memoryContext 拼接到 systemPrompt 中(而非添加第二个 system 消息,因为 Anthropic provider 只读取第一个 system 消息):
```typescript
const systemContent = memoryContext
? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
: agentConfig.systemPrompt;
const messages: LLMMessage[] = [
{ role: 'system', content: systemContent },
...history,
{ role: 'user', content: userMessage },
];
```
### 6.3 memoryContext 格式
```xml
<memory-context>
以下是与当前对话可能相关的长期记忆:
[user] 用户偏好-简洁风格
用户偏好简洁直接的回答风格,不喜欢冗长的解释。
[project] 当前冲刺目标
本周冲刺目标是完成支付模块重构,截止日期 2026-04-15。
[feedback] 不要自动格式化代码
用户明确要求不要自动格式化代码,保持原有风格。
</memory-context>
```
### 6.4 关键约束
- memoryContext 消息不写入 `netaclaw_message`
- 每次请求重新 prefetch保证记忆是最新的
- 如果 search 返回空,不注入任何内容
## 7. 系统提示词扩展
当 Agent 启用记忆时,在 systemPrompt 末尾追加:
```
## 记忆系统
你拥有长期记忆能力。使用 memory_save 工具存储重要信息,使用 memory_recall 工具检索过往记忆。
记忆类型:
- user: 用户画像(偏好、角色、习惯)
- project: 项目知识(进展、决策、约束)
- feedback: 行为反馈(用户对你行为的纠正或确认)
- reference: 引用(外部资源链接、文档地址)
存储原则:
- 当用户透露个人偏好、角色、习惯时,存为 user 类型
- 当了解到项目进展、决策、约束时,存为 project 类型
- 当用户纠正或确认你的行为时,存为 feedback 类型
- 当提到外部资源链接时,存为 reference 类型
- 更新已有记忆而非创建重复条目
- 只存储对未来对话有价值的信息
```
## 8. Agent 配置扩展
### 8.1 AgentEntity.config 扩展
```typescript
// entity/agent.ts config 字段新增
interface AgentConfig {
// ...现有字段
memory?: {
enabled: boolean;
backend: 'mysql' | 'sqlite';
sqlitePath?: string; // 仅 sqlite 后端,默认 dataDir/memory/memory.db
prefetchLimit?: number; // prefetch 返回条数,默认 5
};
}
```
### 8.2 Agent 管理界面
在 Agent 编辑页面新增"记忆配置"区域:
- 开关:启用/禁用记忆
- 下拉存储后端MySQL / 本地 SQLite
- 数字输入prefetch 条数(高级选项)
## 9. 新增文件清单
```
src/modules/netaclaw/
├── memory/
│ ├── provider.ts # MemoryProvider 接口 + MemoryEntry 类型
│ ├── factory.ts # createMemoryProvider 工厂
│ ├── mysql_provider.ts # MysqlMemoryProvider 实现
│ ├── sqlite_provider.ts # SqliteMemoryProvider 实现
│ └── prefetch.ts # prefetchMemory() 函数,格式化注入内容
├── entity/
│ └── memory.ts # NetaClawMemoryEntity (MySQL)
├── tools/builtin/
│ └── memory.ts # memory_save + memory_recall 工具定义
```
## 10. 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `runtime/agent.ts` | AgentRunParams 新增 memoryContext 参数,拼接到 systemPrompt 中 |
| `controller/chat.ts` | 注入 AgentService + MemoryRepo加载 agent 配置读取 memory config执行 prefetch注入 memory 工具body 新增 userId 字段 |
| `src/entities.ts` | 注册 NetaClawMemoryEntity |
## 11. 依赖
| 依赖 | 用途 | 条件 |
|------|------|------|
| `better-sqlite3` | SQLite FTS5 全文检索 | 仅 sqlite 后端需要 |
| `@types/better-sqlite3` | TypeScript 类型 | 开发依赖 |
MySQL FULLTEXT 无额外依赖,复用现有 TypeORM 连接。