Skip to main content
Glama
README.md
# Agent Memory Router v2

> 项目级自治 + 用户级索引:每个项目一套独立记忆库,本机所有项目由一份清单统一查看。
> Agent 先检索摘要,再按需拉取原文,长任务不撑爆上下文。

[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/MCP-2.0-green)](https://modelcontextprotocol.io/)
[![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)

---

## v2 改了什么

v0.1 把索引固定在本仓库的 `data/memory.db`,所有项目共用一份记忆。
v2 改成两层:

| 层级 | 位置 | 作用 |
|---|---|---|
| 项目级 | `<项目根>/memory.db`、`<项目根>/raw_data/` | 该项目**独有**的记忆,互不共享 |
| 项目级 | `<项目根>/.mcp.json` | 启动 MCP Server 时传入本项目绝对路径 |
| 用户级 | `~/.agent-workflow/projects_index.json` | 只登记本机有哪些记忆工作区,不存记忆内容 |

没有常驻服务,没有全局数据库。每个项目可以单独备份、删除、迁移。

## 架构

```mermaid
flowchart TB
    subgraph A[项目 A]
        A1[".mcp.json<br/>--project_path D:/proj-a"]
        A2["memory.db<br/>raw_data/"]
    end

    subgraph B[项目 B]
        B1[".mcp.json<br/>--project_path D:/proj-b"]
        B2["memory.db<br/>raw_data/"]
    end

    A1 --> SA["MCP Server 进程 A"]
    SA --> A2
    B1 --> SB["MCP Server 进程 B"]
    SB --> B2

    IDX["~/.agent-workflow/<br/>projects_index.json"]
    IDX -. "只登记路径" .-> A
    IDX -. "只登记路径" .-> B

    CLI["agent-projects list"] --> IDX
```

Router 代码只有一份,数据库每个项目各一份。
A 项目写记忆不会出现在 B 项目,两个 MCP 进程也不共享连接。

## 快速开始

### 1. 安装路由器

```bash
git clone https://github.com/lss486558354-ship-it/agent-memory-router-v2.git
cd agent-memory-router-v2
python -m pip install -e ".[dev]"
```

只装运行依赖也行:`python -m pip install -e .`。
不安装也可以直接用 `python <路由器目录>/projects_cli.py`。

### 2. 在新项目里一键生成骨架

```bash
cd D:/my-project
agent-projects init
```

也可以指定路径与项目名:

```bash
agent-projects init D:/my-project --name my-project
```

`init` 会依次完成:

1. 写 `.mcp.json`(保留文件里其它 MCP Server)
2. 写 `.cursorrules` 的受管标记块(不动标记块外的自定义规则)
3. 写 `.claude/skills/memory-manager/SKILL.md`(已存在则跳过)
4. 写 `.gitignore` 的受管标记块
5. 建 `<项目根>/memory.db` 与 `raw_data/`
6. 把项目登记进 `~/.agent-workflow/projects_index.json`

生成的项目结构:

```text
D:/my-project/
├── .mcp.json
├── .cursorrules
├── .claude/skills/memory-manager/SKILL.md
├── .gitignore
├── memory.db                    # 索引(gitignore)
├── raw_data/<记录ID>/raw.txt    # 原文(gitignore)
├── .agent-workflow-backups/     # 覆盖前的备份(gitignore)
└── ...你的代码
```

`.mcp.json` 的内容类似:

```json
{
  "mcpServers": {
    "agent-memory-router": {
      "command": "C:/Python313/python.exe",
      "args": [
        "D:/agent-memory-router-v2/mcp_server.py",
        "--project_path",
        "D:/my-project"
      ],
      "env": {
        "PYTHONIOENCODING": "utf-8",
        "PYTHONUTF8": "1"
      }
    }
  }
}
```

`command` 用运行 `agent-projects` 的那个解释器,`--project_path` 是项目绝对路径。
即使编辑器从别的目录启动 Server,也只会打开本项目的库。

> Cursor 读取的是 `.cursor/mcp.json`。把上面 `mcpServers` 的内容复制过去即可,
> 规则文件 `.cursorrules` 已经在项目根目录。

### 3. 查看本机所有记忆工作区

```bash
agent-projects list
```

```text
NAME                     DB   RAW  PATH
my-project               yes  yes  D:\my-project
another-app              yes  yes  D:\another-app

全局索引:C:\Users\<你>\.agent-workflow\projects_index.json
```

`agent-projects` 不带子命令时等价于 `list`。其它命令:

| 命令 | 作用 |
|---|---|
| `agent-projects init [路径]` | 生成骨架、初始化数据库、注册 |
| `agent-projects list [--json]` | 列出已注册项目及库是否存在 |
| `agent-projects register [路径]` | 只注册,不修改项目文件 |
| `agent-projects unregister <路径>` | 从全局索引移除(不删项目数据) |
| `agent-projects where` | 显示全局目录与索引文件位置 |

只想登记一个已有记忆的项目:

```bash
python register_project.py D:/my-project --name my-project
```

它只写全局索引,不生成 `.mcp.json`,也不初始化数据库。

## 数据位置与迁移

### 路径解析优先级

```text
configure(project_path)  >  MEMORY_ROUTER_HOME  >  os.getcwd()
```

- `agent-projects init` / `cli.py --project_path` / MCP `--project_path` 走第一条
- `MEMORY_ROUTER_HOME` 仅供测试或临时隔离
- 都没给时用当前工作目录

### 从 v0.1 迁移

v0.1 的数据库在 `<项目>/data/memory.db`。v2 首次初始化时:

1. 发现 `<项目>/memory.db` 不存在、`<项目>/data/memory.db` 存在
2. 用 SQLite backup API 复制(包含 WAL 中尚未落盘的事务)
3. **保留旧文件**,不做删除或移动
4. 输出 `migrated_from`

```bash
cd D:/my-project
agent-projects init
# 已迁移旧库:D:\my-project\data\memory.db
```

确认新库可用后再自行决定是否删除 `data/`。

### 全局索引格式

```json
{
  "version": 1,
  "updated_at": "2026-10-25T00:30:00+08:00",
  "projects": [
    {
      "name": "my-project",
      "path": "D:/my-project",
      "db_path": "D:/my-project/memory.db",
      "raw_dir": "D:/my-project/raw_data",
      "router_path": "D:/agent-memory-router-v2",
      "mcp_server": "D:/agent-memory-router-v2/mcp_server.py",
      "registered_at": "2026-10-25T00:20:00+08:00",
      "last_seen": "2026-10-25T00:30:00+08:00"
    }
  ]
}
```

- 路径按 `os.path.normcase(resolve)` 去重,重复运行不会产生重复条目
- 写入走临时文件 + 原子替换,中途失败不会留下半截 JSON
- 索引损坏时**明确报错**,不会静默清空你的项目列表
- 文件可读可手改;删除后重新 `register` 即可
- 这里**没有记忆内容**,删掉索引不影响任何项目的数据库

## MCP 工具

| 工具 | 作用 | 返回量 |
|---|---|---|
| `search_memory(keyword, type?, limit?)` | 在索引中检索 | 摘要 × N(每条约 200 字节) |
| `read_raw_data(record_id, max_chars?)` | 按 ID 取单条原文 | 一条原文 |
| `save_memory(type, title, content, keywords?, relates_to?)` | 写入新记忆 | 新记录 ID |
| `link_memory(source_id, target_id, relation_type)` | 建立有向关系 | 关系确认 |

所有工具返回 **JSON 字符串**而不是裸文本:模型更容易区分
「摘要」和「原文」,出错时也知道下一步该干什么。

## 手工接入(不用脚手架)

```bash
# 初始化当前项目的库
python init_db.py --project_path D:/my-project

# 手动检查 Server 会打开哪个库
python mcp_server.py --check --project_path D:/my-project

# 人用 CLI,默认当前目录
python cli.py search 报价 --type decision --project_path D:/my-project
```

MCP 配置模板与 `agent-projects init` 生成的一致,关键是
`--project_path` 必须是该项目的绝对路径。

## 数据模型

```sql
CREATE TABLE entities (
    id             TEXT PRIMARY KEY,   -- D_0001 / C_0002,前缀表示类型
    type           TEXT NOT NULL,      -- decision / code_change / issue / ...
    title          TEXT NOT NULL,      -- 一行摘要,检索时唯一可见的内容
    keywords       TEXT NOT NULL,      -- 逗号分隔
    file_path      TEXT NOT NULL,      -- 指向 raw_data/<id>/raw.txt
    created_at     TIMESTAMP NOT NULL,
    update_version INTEGER NOT NULL
);

CREATE TABLE relations (
    source_id     TEXT NOT NULL,
    target_id     TEXT NOT NULL,
    relation_type TEXT NOT NULL,       -- supersedes / caused_by / implements
    PRIMARY KEY (source_id, target_id, relation_type)
);
```

ID 前缀:`D` 决策、`C` 代码修改、`I` 问题、`N` 笔记、`R` 参考资料、`S` 会话、`M` 其他。
扫日志时一眼能看出这条是什么。

## 目录结构(路由器仓库)

```text
agent-memory-router-v2/
├── memory_core.py          # 唯一存储实现:schema、连接、CRUD、项目根解析
├── mcp_server.py           # MCP Server,支持 --project_path
├── cli.py                  # 人用 CLI,子命令支持 --project_path
├── init_db.py              # 初始化单个项目的库
├── project_registry.py     # 全局项目索引的唯一读写实现
├── register_project.py     # 只登记项目,不改项目文件
├── projects_cli.py         # agent-projects init / list / register / ...
├── templates/              # .cursorrules 与 memory-manager Skill 模板
├── tests/                  # 47 个离线测试
├── CLAUDE.md               # Claude Code 强制规则
├── .cursorrules            # Cursor 强制规则
├── .claude/skills/memory-manager/SKILL.md
└── docs/GOAL.md            # 需求原点与验收标准
```

## 设计取舍

**为什么项目自治,而不是一个全局数据库?**
全局库需要解决命名冲突、跨项目检索越界、单点损坏影响所有项目。
按项目拆开后:备份就是复制一个文件,删除项目不会留孤儿,
两个项目的 Agent 可以同时写而不抢同一把锁。
代价是跨项目检索要显式切换,这是有意的边界。

**为什么全局索引用 JSON,而不是 SQLite?**
它只存路径,条目数以「本机项目数」计,最多几百条。
JSON 能用编辑器直接看、直接改、diff 可读;SQLite 在这里只增加工具负担。
真正的记忆索引仍在各项目的 SQLite 里。

**为什么用 SQLite backup API 迁移旧库?**
直接复制文件可能丢掉 WAL 中尚未 checkpoint 的事务,也可能在写入中途得到不一致副本。
`source.backup(target)` 由 SQLite 保证一致性,并且不修改源文件 —— 旧库保留,可回退。

**为什么脚手架只改受管标记块?**
用户项目里往往已有自己的 `.cursorrules` / `.gitignore`。
标记块之外的规则一行不动,重复运行只替换块内内容,避免脚手架变成破坏性工具。

**为什么索引和原文分离?**
因为两者访问频率差了两个数量级。索引每次对话都要查,原文可能一个月读一次。
放在一起意味着每次检索都要拖着一堆正文走。

**为什么用 SQLite 而不是向量库?**
本地零依赖、单文件、可以直接 `sqlite3` 打开排查。
关键词检索对「记得大概叫什么」的场景已经够用,
而它的失败模式是**可解释的**(没匹配到就是没匹配到),
不像向量检索会返回一堆"语义相近但无关"的结果,反而浪费上下文。

**为什么 ID 用 `D_0001` 而不是 UUID?**
UUID 在日志里无法辨认。前缀 + 序号让「哦这是第 3 个决策」变成零成本判断。

**安全:record_id 直接来自模型输出,模型可能被提示词注入。**
所以有两道防线:

1. 白名单正则 `^[A-Za-z][A-Za-z0-9_]{0,63}$` —— 挡掉 `/`、`..`、空格
2. `Path.is_relative_to()` 二次校验 —— 即使将来正则被放宽,也越不出 `raw_data/`

测试里覆盖了 `../../etc/passwd`、`D_0001/../../secret`、Windows 反斜杠等 8 种变体。

**检索通配符必须转义。**
不转义的话搜 `50%` 会变成「匹配一切」,返回全库 —— 结果看起来"很全",其实全错。

**每次操作单开 SQLite 连接。**
建连接是微秒级开销,换来的是完全不用考虑线程安全 ——
MCP server 可能在任意线程里调工具。WAL 模式保证 CLI 写入时 MCP 仍可读。

**原文先写 `.tmp` 再 `rename`。**
中途崩溃不会留下半截文件;入库失败会删掉刚写的原文,不留孤儿。

## 测试

```bash
python -m pytest tests/ -q
```

47 个离线用例:

- `test_memory_core.py`:写入/读取、Unicode、LIKE 转义、8 种目录穿越、
  大小上限、版本自增、关系往返、项目隔离、旧库迁移
- `test_project_registry.py`:注册/去重/反注册、损坏索引报错、路由解析
- `test_projects_cli.py`:骨架文件、保留已有配置、skill 跳过与 `--force` 备份、
  两项目数据库隔离、CLI 与登记脚本

## 局限

- **只有关键词检索**,没有中文分词和向量召回。搜「上下文」能命中「独立浏览器上下文」,
  但搜「记忆溢出」不会命中「上下文膨胀」。缓解办法:keywords 里多写几种说法。
- **单机单用户**。多进程同时写同一个项目靠 SQLite 的锁,没有做分布式。
- **同进程不能并发切换项目**:`memory_core` 的项目根是模块级配置。
  同时操作多个项目请用多个 MCP 进程(正常使用方式),不要在同一个 Python 进程里来回 `configure`。
- **没有删除命令**(有意为之:误删记忆的代价高于留一点噪声)。需要清理时直接操作 SQLite。
- 单条原文超过 2 MB 会被拒绝加载 —— 超过这个体量就该拆成多条记录了。
- 全局索引不跨机器同步;每台机器各自维护。

## License

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues