agent-memory-router
README.md
# Agent Memory Router v2
> 项目级自治 + 用户级索引:每个项目一套独立记忆库,本机所有项目由一份清单统一查看。
> Agent 先检索摘要,再按需拉取原文,长任务不撑爆上下文。
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](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
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues