334 lines
11 KiB
Markdown
334 lines
11 KiB
Markdown
|
|
# 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 连接。
|