# 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; 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): Promise; update(id: number | string, partial: Partial>): Promise; delete(id: number | string): Promise; search(query: string, opts: MemorySearchOpts): Promise; list(opts: MemorySearchOpts): Promise; getById(id: number | string): Promise; close?(): Promise; // SQLite 需要关闭连接 } ``` ### 4.2 工厂函数 ```typescript // memory/factory.ts export function createMemoryProvider(config: AgentMemoryConfig, mysqlRepo?: Repository): MemoryProvider { if (config.backend === 'sqlite') { return new SqliteMemoryProvider(config.sqlitePath); } return new MysqlMemoryProvider(mysqlRepo); } ``` ### 4.3 MysqlMemoryProvider - 注入 TypeORM `Repository` - 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\n${memoryContext}\n` : agentConfig.systemPrompt; const messages: LLMMessage[] = [ { role: 'system', content: systemContent }, ...history, { role: 'user', content: userMessage }, ]; ``` ### 6.3 memoryContext 格式 ```xml 以下是与当前对话可能相关的长期记忆: [user] 用户偏好-简洁风格 用户偏好简洁直接的回答风格,不喜欢冗长的解释。 [project] 当前冲刺目标 本周冲刺目标是完成支付模块重构,截止日期 2026-04-15。 [feedback] 不要自动格式化代码 用户明确要求不要自动格式化代码,保持原有风格。 ``` ### 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 连接。