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

11 KiB
Raw Permalink Blame History

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 接口

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)

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 表结构

-- 主表
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 接口

// 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 工厂函数

// 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 工具

// 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 工具

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 消息):

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 格式

<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 扩展

// 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 连接。