@smyslenny/agent-memory 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +22 -0
- package/CHANGELOG.md +20 -0
- package/README.md +46 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/agent-memory.js +1118 -301
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/db-DsY3zz8f.d.ts +16 -0
- package/dist/index.d.ts +148 -18
- package/dist/index.js +968 -130
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +940 -181
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/0004-agent-memory-integration.md +316 -0
- package/docs/design/0005-reranker-api-integration.md +276 -0
- package/docs/design/0006-multi-provider-embedding.md +196 -0
- package/docs/roadmap/integration-plan-v1.md +139 -0
- package/docs/roadmap/memory-architecture.md +168 -0
- package/docs/roadmap/warm-boot.md +135 -0
- package/package.json +3 -1
- package/dist/db-CMsKtBt0.d.ts +0 -9
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# DD-0004: Markdown × agent-memory 生命周期融合
|
|
2
|
+
|
|
3
|
+
**Status:** Draft
|
|
4
|
+
**Author:** Noah (Claude Opus sub-agent)
|
|
5
|
+
**Date:** 2026-02-21
|
|
6
|
+
**Repo:** agent-memory
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Background / 背景
|
|
11
|
+
|
|
12
|
+
OpenClaw 当前有两套记忆系统并行运行:
|
|
13
|
+
|
|
14
|
+
- **Markdown 记忆**:`memory/YYYY-MM-DD.md` 日记 + `MEMORY.md` 长期记忆 + `RECENT.md` 短期摘要。由 `memory-sync`(14:00 & 22:00)和 `memory-tidy`(03:00)两个 cron 维护。自动注入 session 上下文,人类可读可编辑。
|
|
15
|
+
- **agent-memory**(v2.1.0):SQLite-backed 结构化记忆,提供 Ebbinghaus 衰减、BM25+向量混合搜索、URI 路径树、知识图谱链接。通过 mcporter MCP bridge 暴露 9 个工具。
|
|
16
|
+
|
|
17
|
+
**两处集成 Gap(来自 `integration-plan-v1.md`):**
|
|
18
|
+
|
|
19
|
+
1. **Decay 引擎不会自动触发**:agent-memory 的衰减是被动的,只在显式调用 `reflect` 时执行。当前虽已有独立 cron 每天凌晨 4 点触发,但与 memory-tidy 的"深度睡眠"周期脱节,且无法利用 tidy 阶段的上下文信息。
|
|
20
|
+
2. **双存储状态分裂**:memory-sync 只写 Markdown,agent-memory 只在主 session 手动 `remember` 时写入。两边数据源独立增长,日渐分裂。`RECENT.md` 虽已由独立 `memory-surface` cron 生成,但其生成逻辑尚未标准化到 integration-plan 的设计规范。
|
|
21
|
+
|
|
22
|
+
**PR #2(`memory-janitor-phase5.md`)** 已提出"Phase 5"概念——在 janitor 收尾时触发 decay + consistency check,但只是示例 prompt 片段,尚未落地到 OpenClaw cron 中。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Goals / 目标
|
|
27
|
+
|
|
28
|
+
1. **Capture 同步**:`memory-sync` 写日记时,同步通过 `mcporter call agent-memory.remember` 将每条新增 bullet 写入 agent-memory,实现 1:1 增量同步。
|
|
29
|
+
2. **Consolidate 联动**:`memory-tidy` 收尾时触发 `mcporter call agent-memory.reflect phase=all`(含 decay + tidy + govern),替代独立的凌晨 4 点 cron。
|
|
30
|
+
3. **Surface 标准化**:`memory-surface` cron 从 agent-memory 提取高 vitality / 近期记忆,按标准模板生成 `RECENT.md`,供主 session 上下文自动加载。
|
|
31
|
+
4. **Best-effort 容错**:所有 mcporter 调用失败时仅 warn,不影响 Markdown 写入主流程。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. Non-Goals / 非目标
|
|
36
|
+
|
|
37
|
+
- **不改 agent-memory 代码**:v1 纯 prompt 改造 + cron 配置,零代码变更。
|
|
38
|
+
- **不引入新依赖/新模型**:分类用关键词规则,surface 用模板拼接。
|
|
39
|
+
- **不做双向合并**:Markdown 为 source of truth,agent-memory 为派生索引层,不反向回写日记。
|
|
40
|
+
- **不做统一检索入口**:memory_search + agent-memory recall 的 RRF 合并留给 v2。
|
|
41
|
+
- **不扩大上下文窗口**:`RECENT.md` 控制在 ≤80 行。
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 4. Proposal / 方案
|
|
46
|
+
|
|
47
|
+
### 4.1 架构概述
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Session (主对话)
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌─────────────┐ 14:00 & 22:00 ┌──────────────────┐
|
|
54
|
+
│ memory-sync │ ──────────────────▶│ memory/YYYY-MM-DD│ (Markdown)
|
|
55
|
+
│ (cron) │ ──── NEW ────────▶│ agent-memory DB │ (remember)
|
|
56
|
+
└──────┬──────┘ └──────────────────┘
|
|
57
|
+
│ +5min
|
|
58
|
+
▼
|
|
59
|
+
┌──────────────┐ recall top-N ┌──────────────────┐
|
|
60
|
+
│memory-surface│ ◀────────────────│ agent-memory DB │
|
|
61
|
+
│ (cron) │ ──────────────▶ │ RECENT.md │
|
|
62
|
+
└──────────────┘ └──────────────────┘
|
|
63
|
+
|
|
64
|
+
┌─────────────┐ 03:00 ┌──────────────────┐
|
|
65
|
+
│ memory-tidy │ ── compress ────▶ │ weekly/ archive/ │
|
|
66
|
+
│ (cron) │ ── distill ─────▶ │ MEMORY.md │
|
|
67
|
+
│ │ ── NEW ─────────▶ │ reflect phase=all │
|
|
68
|
+
└─────────────┘ └──────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**数据流方向**:Session → memory-sync → (Markdown ∥ agent-memory) → memory-surface → RECENT.md → Session。闭环。
|
|
72
|
+
|
|
73
|
+
### 4.2 方案对比
|
|
74
|
+
|
|
75
|
+
| 维度 | A: 纯 Prompt 改造(本方案) | B: agent-memory 新增 surface CLI |
|
|
76
|
+
|------|---------------------------|--------------------------------|
|
|
77
|
+
| 复杂度 | 低(只改 3 个 cron prompt) | 中(需写 `agent-memory surface` 子命令) |
|
|
78
|
+
| 灵活性 | 高(LLM 可自适应格式) | 中(固定模板输出) |
|
|
79
|
+
| 可维护性 | 中(prompt 变更需人工同步到示例) | 高(代码版本化) |
|
|
80
|
+
| 一致性 | 中(LLM 可能偏离格式) | 高(模板确定性输出) |
|
|
81
|
+
| 落地速度 | 快(今天就能改) | 慢(需开发+测试+发版) |
|
|
82
|
+
|
|
83
|
+
**选择方案 A**:v1 优先落地速度,用 prompt 约束格式。v1.1 视需要再补 CLI。
|
|
84
|
+
|
|
85
|
+
### 4.3 详细设计
|
|
86
|
+
|
|
87
|
+
#### 4.3.1 memory-sync Prompt 修改
|
|
88
|
+
|
|
89
|
+
在现有 prompt 的 **Step 5(Append to journal)之后**,新增 Step 5.5:
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
### 5.5 Sync to agent-memory (best-effort)
|
|
93
|
+
|
|
94
|
+
For each NEW bullet you just appended to the journal, also write it to agent-memory.
|
|
95
|
+
|
|
96
|
+
**Classification rules (keyword-based, no LLM needed):**
|
|
97
|
+
- Contains 喜欢/讨厌/禁止/偏好/必须/记住/prefer/must/rule → type=knowledge
|
|
98
|
+
- Contains 开心/安心/难过/害羞/生气/担心/爱/想/感动/温柔/happy/sad/angry → type=emotion
|
|
99
|
+
- Otherwise → type=event
|
|
100
|
+
|
|
101
|
+
**For each bullet, run:**
|
|
102
|
+
```
|
|
103
|
+
exec: mcporter call agent-memory.remember \
|
|
104
|
+
content="<bullet text>" \
|
|
105
|
+
type=<knowledge|emotion|event> \
|
|
106
|
+
uri="<type>://journal/YYYY-MM-DD#HHMM-N" \
|
|
107
|
+
source="memory-sync:YYYY-MM-DD"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Where:
|
|
111
|
+
- YYYY-MM-DD = today's date
|
|
112
|
+
- HHMM = current time (from the section header)
|
|
113
|
+
- N = sequential number within section (1, 2, 3...)
|
|
114
|
+
|
|
115
|
+
**Error handling:** If mcporter call fails, log a warning and continue.
|
|
116
|
+
Do NOT let agent-memory failures block journal writing.
|
|
117
|
+
|
|
118
|
+
**Dedup:** agent-memory has built-in URI dedup. If a URI already exists, the call
|
|
119
|
+
is a no-op. Safe to retry.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**关键设计决策:**
|
|
123
|
+
- **1:1 映射**(一条 bullet = 一条 memory):便于独立衰减和精确检索。
|
|
124
|
+
- **URI 作为去重键**:`event://journal/2026-02-21#2200-1` 天然幂等。
|
|
125
|
+
- **source 字段**:标记来源为 `memory-sync`,与手动 `remember` 区分。
|
|
126
|
+
|
|
127
|
+
#### 4.3.2 memory-tidy Prompt 修改
|
|
128
|
+
|
|
129
|
+
在现有 prompt 的 **Phase 3(Distill to MEMORY.md)的 Step 17(Wrap up)之前**,新增 Phase 4:
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
[Phase 4: agent-memory Reflect]
|
|
133
|
+
16.5. Trigger agent-memory sleep cycle (decay + tidy + govern):
|
|
134
|
+
exec: mcporter call agent-memory.reflect phase=all
|
|
135
|
+
Record result in summary. If the call fails, log warning and continue.
|
|
136
|
+
|
|
137
|
+
16.6. Quick consistency spot-check (optional, skip if reflect failed):
|
|
138
|
+
exec: mcporter call agent-memory.recall query="当前最重要的事" limit=3
|
|
139
|
+
Compare top results with MEMORY.md content.
|
|
140
|
+
If clear conflict found → mark ⚠️ CONFLICT in summary, prefer MEMORY.md as truth.
|
|
141
|
+
If no conflict → record "consistency check: OK"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**与独立 decay cron 的关系**:本方案上线后,应移除原有的独立凌晨 4 点 reflect cron(如果存在),让 reflect 统一由 memory-tidy 在 03:00 触发,时序一致。
|
|
145
|
+
|
|
146
|
+
#### 4.3.3 memory-surface Prompt(新 Cron / 改造现有)
|
|
147
|
+
|
|
148
|
+
当前已有 `memory-surface` cron(14:05 & 22:05,在 memory-sync 后 5 分钟执行)。标准化其 prompt:
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
MEMORY SURFACE — You are a memory surfacing agent. Generate RECENT.md from agent-memory.
|
|
152
|
+
|
|
153
|
+
## Steps
|
|
154
|
+
|
|
155
|
+
### 1. Fetch high-vitality memories (recent 7 days)
|
|
156
|
+
exec: mcporter call agent-memory.recall query="最近重要的事 情感 决策" limit=30
|
|
157
|
+
|
|
158
|
+
### 2. Fetch identity + knowledge memories
|
|
159
|
+
exec: mcporter call agent-memory.recall_path path="knowledge://" limit=20
|
|
160
|
+
exec: mcporter call agent-memory.recall_path path="emotion://" limit=10
|
|
161
|
+
|
|
162
|
+
### 3. Deduplicate and rank
|
|
163
|
+
From the combined results:
|
|
164
|
+
- Remove duplicates (same URI or >90% content overlap)
|
|
165
|
+
- Sort by: vitality DESC, then created_at DESC
|
|
166
|
+
- Keep top 40 entries max
|
|
167
|
+
|
|
168
|
+
### 4. Generate RECENT.md
|
|
169
|
+
Write to ~/.openclaw/workspace/RECENT.md with this exact structure:
|
|
170
|
+
|
|
171
|
+
```markdown
|
|
172
|
+
# RECENT.md
|
|
173
|
+
|
|
174
|
+
_auto-updated: YYYY-MM-DD HH:MM_
|
|
175
|
+
|
|
176
|
+
## 最近情感
|
|
177
|
+
- <emotion entries, ≤8 lines>
|
|
178
|
+
|
|
179
|
+
## 最近决策/知识
|
|
180
|
+
- <knowledge entries, ≤15 lines>
|
|
181
|
+
|
|
182
|
+
## 最近事件
|
|
183
|
+
- <event entries, ≤15 lines>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Hard limits:**
|
|
187
|
+
- Total ≤ 80 lines (including headers and blank lines)
|
|
188
|
+
- Each entry: 1 line, ≤ 200 chars. Truncate if needed.
|
|
189
|
+
- If agent-memory returns nothing (empty DB or mcporter failure):
|
|
190
|
+
fall back to reading memory/YYYY-MM-DD.md for recent 3 days and summarize.
|
|
191
|
+
|
|
192
|
+
### 5. Done
|
|
193
|
+
Reply ANNOUNCE_SKIP
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**RECENT.md 生成策略要点:**
|
|
197
|
+
|
|
198
|
+
| 维度 | 规范 |
|
|
199
|
+
|------|------|
|
|
200
|
+
| 总行数上限 | 80 行(含空行和标题) |
|
|
201
|
+
| 分区 | 情感(≤8行)、决策/知识(≤15行)、事件(≤15行) |
|
|
202
|
+
| 时间窗口 | 最近 7 天 |
|
|
203
|
+
| 排序依据 | vitality DESC → created_at DESC |
|
|
204
|
+
| 降级策略 | agent-memory 不可用时,fallback 读近 3 天日记手动摘要 |
|
|
205
|
+
| 更新频率 | 每天 14:05 & 22:05(memory-sync 后 5 分钟) |
|
|
206
|
+
| 幂等性 | 每次全量覆盖 RECENT.md,不做增量 |
|
|
207
|
+
|
|
208
|
+
#### 4.3.4 Cron 时序编排
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
14:00 memory-sync — 扫描 session → 写日记 → 同步 agent-memory
|
|
212
|
+
14:05 memory-surface — 读 agent-memory → 生成 RECENT.md
|
|
213
|
+
22:00 memory-sync — 同上
|
|
214
|
+
22:05 memory-surface — 同上
|
|
215
|
+
03:00 memory-tidy — 压缩/归档 → distill MEMORY.md → reflect(all) → consistency check
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
依赖关系:`memory-surface` 依赖 `memory-sync` 先完成(5 分钟间隔足够,sync 通常 1-2 分钟完成)。`memory-tidy` 独立运行,内含 reflect。
|
|
219
|
+
|
|
220
|
+
#### 4.3.5 URI 命名约定
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
event://journal/YYYY-MM-DD#HHMM-N # 日记事件
|
|
224
|
+
emotion://journal/YYYY-MM-DD#HHMM-N # 日记情感
|
|
225
|
+
knowledge://journal/YYYY-MM-DD#HHMM-N # 日记知识/偏好
|
|
226
|
+
identity://core/<topic> # 身份认知(手动写入,P0 不衰减)
|
|
227
|
+
knowledge://preferences/<topic> # 用户偏好
|
|
228
|
+
knowledge://lessons/<topic> # 经验教训
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### 4.3.6 关键词分类规则
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
KNOWLEDGE_KEYWORDS = [
|
|
235
|
+
'喜欢', '讨厌', '禁止', '偏好', '必须', '记住', '规则', '习惯',
|
|
236
|
+
'prefer', 'must', 'rule', 'always', 'never', 'remember'
|
|
237
|
+
]
|
|
238
|
+
EMOTION_KEYWORDS = [
|
|
239
|
+
'开心', '安心', '难过', '害羞', '生气', '担心', '爱', '想你',
|
|
240
|
+
'感动', '温柔', '幸福', '寂寞', '心疼', '甜',
|
|
241
|
+
'happy', 'sad', 'angry', 'love', 'miss', 'worried'
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
def classify(text):
|
|
245
|
+
if any(kw in text for kw in KNOWLEDGE_KEYWORDS): return 'knowledge'
|
|
246
|
+
if any(kw in text for kw in EMOTION_KEYWORDS): return 'emotion'
|
|
247
|
+
return 'event'
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
实际在 prompt 中以自然语言指令实现,LLM 按规则判断即可。
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 5. Risks / 风险
|
|
255
|
+
|
|
256
|
+
| 风险 | 影响 | 缓解措施 |
|
|
257
|
+
|------|------|----------|
|
|
258
|
+
| mcporter 调用超时/失败 | agent-memory 不写入,日记正常 | Best-effort:失败只 warn,不阻塞 Markdown 写入 |
|
|
259
|
+
| LLM 分类不准(event 误判为 emotion) | 衰减曲线不匹配(emotion 365d vs event 14d) | 可接受:错分只影响衰减速度,不丢数据;v1.1 可加修正 |
|
|
260
|
+
| memory-sync 运行时间变长(逐条 remember) | 超出 5 分钟窗口,surface 读到旧数据 | 实测单条 mcporter call <500ms,30 条 <15s;如仍超时可改为批量 |
|
|
261
|
+
| RECENT.md 格式漂移 | 主 session 上下文解析异常 | 硬编码模板 + 行数上限;surface prompt 严格约束格式 |
|
|
262
|
+
| reflect phase=all 耗时长 | memory-tidy 整体运行时间增加 | reflect 通常 <5s(纯 SQLite 操作);放在 tidy 最后一步不影响其他 phase |
|
|
263
|
+
| 双重 reflect 触发(旧 cron 未移除) | 多次 decay 不会损坏数据(幂等),但浪费资源 | 文档明确要求移除旧独立 cron |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 6. Test Plan / 测试方案
|
|
268
|
+
|
|
269
|
+
- [ ] **Capture 验证**:手动触发 memory-sync → 检查日记新增 N 条 bullet → `mcporter call agent-memory.status` 确认新增 N 条 memory → URI 格式正确
|
|
270
|
+
- [ ] **Dedup 验证**:再次触发 memory-sync(无新对话)→ agent-memory 记忆数不变(URI 去重生效)
|
|
271
|
+
- [ ] **Consolidate 验证**:手动触发 memory-tidy → 日志中出现 `reflect phase=all` 调用 → `agent-memory.status` 显示 decay 已执行
|
|
272
|
+
- [ ] **Surface 验证**:手动触发 memory-surface → `RECENT.md` 更新 → 行数 ≤ 80 → 包含三个分区 → 时间戳正确
|
|
273
|
+
- [ ] **Fallback 验证**:停止 agent-memory MCP → 触发 memory-sync → 日记正常写入 → 日志有 warn → 触发 memory-surface → fallback 到读日记生成
|
|
274
|
+
- [ ] **端到端**:在主 session 对话 → 等 14:00 sync → 等 14:05 surface → 新 session 启动 → 确认 RECENT.md 已含最新内容
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. Rollback Plan / 回滚方案
|
|
279
|
+
|
|
280
|
+
1. **Revert prompt 修改**:将三个 cron 的 prompt 恢复到修改前版本(prompt 变更应 git commit 到 `agent-memory/examples/` 目录)。
|
|
281
|
+
2. **RECENT.md 安全**:即使 surface 异常,RECENT.md 只是被覆盖,不影响 MEMORY.md 和日记。手动删除 RECENT.md 即可回退到无 surface 状态。
|
|
282
|
+
3. **agent-memory 数据**:sync 写入的数据不会影响已有记忆。如需清理,可按 `source="memory-sync:*"` 批量 forget。
|
|
283
|
+
4. **恢复独立 decay cron**:如果移除了旧的独立 reflect cron,回滚时需重新创建。
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 8. Decision Log / 决策变更记录
|
|
288
|
+
|
|
289
|
+
_实现过程中如果偏离本文档,在此记录变更原因_
|
|
290
|
+
|
|
291
|
+
| 日期 | 变更 | 原因 |
|
|
292
|
+
|------|------|------|
|
|
293
|
+
| 2026-02-21 | memory-surface 未直接在 cron prompt 内串联多次 `mcporter call`,改为调用 `~/.openclaw/workspace/scripts/memory_surface.py`,由脚本统一执行 recall/recall_path、去重、排序、fallback、写 RECENT.md | 降低 prompt 漂移风险,保证 `RECENT.md` 结构和 80 行上限稳定;便于后续维护与调试 |
|
|
294
|
+
| 2026-02-21 | `recall_path` 参数由设计稿中的 `path=` 调整为 `uri=` | 实际 MCP 工具 schema 要求 `uri` 字段;`path` 会触发参数校验错误 |
|
|
295
|
+
| 2026-02-21 | memory-tidy 保留现网 `MEMORY.md` 200 行上限,仅补充 Phase 4 reflect+consistency,不回退到 80 行 | 避免对现有长期记忆容量策略造成行为回退;本 DD 目标聚焦于 Markdown × agent-memory 融合链路 |
|
|
296
|
+
| 2026-02-21 | 额外补充 `~/.openclaw/openclaw.json` 的 `cron` 配置块(enabled/store/maxConcurrentRuns/sessionRetention) | 使 cron 持久化位置与并发参数显式化,便于运维核对与后续迁移 |
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Appendix A: 现有 Cron Prompt 完整修改 Diff
|
|
301
|
+
|
|
302
|
+
### memory-sync:新增 Step 5.5
|
|
303
|
+
|
|
304
|
+
位置:在 `### 5. Append to journal` 之后、`### 6. Done` 之前插入。
|
|
305
|
+
|
|
306
|
+
### memory-tidy:新增 Phase 4
|
|
307
|
+
|
|
308
|
+
位置:在 `[Phase 3: Distill to MEMORY.md]` 的 Step 16 之后、`[Wrap up]` Step 17 之前插入。
|
|
309
|
+
|
|
310
|
+
### memory-surface:完整新 prompt
|
|
311
|
+
|
|
312
|
+
见 §4.3.3,替换现有 memory-surface cron 的 prompt。
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
_Generated by DD workflow · Claude Opus sub-agent_
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# DD-0005: External Reranker API Integration (Qwen3-Reranker-8B)
|
|
2
|
+
|
|
3
|
+
**Status:** Draft
|
|
4
|
+
**Author:** Noah (Claude Opus)
|
|
5
|
+
**Date:** 2026-02-22
|
|
6
|
+
**Repo:** agent-memory
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Background / 背景
|
|
11
|
+
|
|
12
|
+
agent-memory v2.1.0 的搜索流水线目前是:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
BM25 (全文) ──┐
|
|
16
|
+
├── RRF 融合 ──→ 本地 rerank(priority/recency/vitality 加权)──→ 最终结果
|
|
17
|
+
Embedding 向量 ─┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
其中 `rerank.ts` 是一个**纯本地的数学加权函数**(优先级乘数 + 时间衰减 + 活力因子),它不理解语义。
|
|
21
|
+
|
|
22
|
+
我们刚刚成功接入了 Qwen3-Embedding-8B(通过 momo API),embedding 搜索已上线。但 momo 上同时提供了 **Qwen3-Reranker-8B**——一个 80 亿参数的交叉编码重排模型,能以 query-document pair 粒度做精读打分。
|
|
23
|
+
|
|
24
|
+
当前问题:RRF 融合后的候选列表质量已经不错,但最终排序仅靠本地数学公式,缺乏对 query↔document 的深层语义理解。接入外部 Reranker 可以在最终输出前再做一轮"精读",大幅提升 Top-K 精度。
|
|
25
|
+
|
|
26
|
+
**已验证 API 可用性:**
|
|
27
|
+
```bash
|
|
28
|
+
POST https://momo.woshizhu.mom/v1/rerank
|
|
29
|
+
{
|
|
30
|
+
"model": "Qwen/Qwen3-Reranker-8B",
|
|
31
|
+
"query": "...",
|
|
32
|
+
"documents": ["...", "..."]
|
|
33
|
+
}
|
|
34
|
+
# 返回: { "results": [{ "index": 0, "relevance_score": 0.xxx }, ...] }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Goals / 目标
|
|
40
|
+
|
|
41
|
+
- 在 `rerank.ts` 中增加可选的外部 Reranker API 调用,不破坏现有纯本地 rerank 逻辑
|
|
42
|
+
- 新增 `RerankProvider` 接口和 `getRerankerProviderFromEnv()` 工厂函数(类比现有的 `EmbeddingProvider`)
|
|
43
|
+
- 支持 OpenAI 兼容的 `/v1/rerank` 端点(Jina/Cohere/vLLM 风格),使其对 momo/自建 API 通用
|
|
44
|
+
- 在 `searchHybrid()` 或 MCP `recall` 工具中自动触发外部 rerank(当 provider 可用时)
|
|
45
|
+
- 通过环境变量配置(`AGENT_MEMORY_RERANK_PROVIDER`, `AGENT_MEMORY_RERANK_MODEL` 等),零代码可切换
|
|
46
|
+
- 保持 best-effort 原则:外部 reranker 不可用时,静默降级到本地 rerank,不中断搜索
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3. Non-Goals / 非目标
|
|
51
|
+
|
|
52
|
+
- 不改变 BM25 / Embedding / RRF 融合逻辑
|
|
53
|
+
- 不实现批量 rerank(当前记忆条目少,逐次够用)
|
|
54
|
+
- 不支持流式 rerank
|
|
55
|
+
- 不改变 MCP 工具的对外 schema(`recall` 工具参数不变)
|
|
56
|
+
- 不引入新的数据库表或存储
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 4. Proposal / 方案
|
|
61
|
+
|
|
62
|
+
### 4.1 方案概述
|
|
63
|
+
|
|
64
|
+
在现有搜索流水线的最后一步(`rerank`),插入一个可选的外部 API 调用层:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
BM25 ──┐ ┌── 外部 Reranker API ──┐
|
|
68
|
+
├── RRF 融合 ──→ 候选列表 (limit*2) ──→ │ ├── 本地 rerank ──→ 最终结果
|
|
69
|
+
Embed ─┘ └── (不可用时跳过) ───┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**核心思路:** 外部 reranker 替换候选列表的 score 值(用 `relevance_score`),然后本地 rerank 在新 score 基础上继续叠加 priority/vitality/recency 加权。这样既利用了 8B 参数的语义精读能力,又保留了我们独有的记忆优先级系统。
|
|
73
|
+
|
|
74
|
+
### 4.2 方案对比
|
|
75
|
+
|
|
76
|
+
| 维度 | 方案 A: 外部 rerank 替换 score | 方案 B: 外部 rerank 作为独立排序 |
|
|
77
|
+
|------|------|------|
|
|
78
|
+
| 复杂度 | 低——插入一步 score 替换 | 中——需要决定最终以谁的排序为准 |
|
|
79
|
+
| 与现有系统兼容 | ✅ 本地 rerank 逻辑完全保留 | ⚠️ 可能忽略 priority/vitality |
|
|
80
|
+
| 语义精度 | ✅ API score + 本地加权双重保障 | ✅ 纯 API score 精度最高但丢失 priority |
|
|
81
|
+
|
|
82
|
+
**选择方案 A**:外部 reranker 的 `relevance_score` 替换 RRF 融合后的 score,然后本地 rerank 继续叠加。
|
|
83
|
+
|
|
84
|
+
### 4.3 详细设计
|
|
85
|
+
|
|
86
|
+
#### 4.3.1 新增文件:`src/search/rerank-provider.ts`
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export interface RerankProvider {
|
|
90
|
+
id: string;
|
|
91
|
+
model: string;
|
|
92
|
+
rerank(query: string, documents: string[]): Promise<RerankResult[]>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RerankResult {
|
|
96
|
+
index: number;
|
|
97
|
+
relevance_score: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getRerankerProviderFromEnv(): RerankProvider | null {
|
|
101
|
+
const provider = (process.env.AGENT_MEMORY_RERANK_PROVIDER ?? "none").toLowerCase();
|
|
102
|
+
if (provider === "none" || provider === "off") return null;
|
|
103
|
+
|
|
104
|
+
if (provider === "openai" || provider === "jina" || provider === "cohere") {
|
|
105
|
+
const apiKey = process.env.AGENT_MEMORY_RERANK_API_KEY
|
|
106
|
+
?? process.env.OPENAI_API_KEY; // 复用 embedding 的 key
|
|
107
|
+
const model = process.env.AGENT_MEMORY_RERANK_MODEL ?? "Qwen/Qwen3-Reranker-8B";
|
|
108
|
+
const baseUrl = process.env.AGENT_MEMORY_RERANK_BASE_URL
|
|
109
|
+
?? process.env.OPENAI_BASE_URL
|
|
110
|
+
?? "https://api.openai.com/v1";
|
|
111
|
+
if (!apiKey) return null;
|
|
112
|
+
return createOpenAIRerankProvider({ apiKey, model, baseUrl });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createOpenAIRerankProvider(opts: {
|
|
119
|
+
apiKey: string;
|
|
120
|
+
model: string;
|
|
121
|
+
baseUrl?: string;
|
|
122
|
+
}): RerankProvider {
|
|
123
|
+
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
124
|
+
return {
|
|
125
|
+
id: "openai-rerank",
|
|
126
|
+
model: opts.model,
|
|
127
|
+
async rerank(query: string, documents: string[]) {
|
|
128
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/rerank`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
authorization: opts.apiKey.startsWith("Bearer ")
|
|
133
|
+
? opts.apiKey
|
|
134
|
+
: `Bearer ${opts.apiKey}`,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ model: opts.model, query, documents }),
|
|
137
|
+
});
|
|
138
|
+
if (!resp.ok) {
|
|
139
|
+
const body = await resp.text().catch(() => "");
|
|
140
|
+
throw new Error(`Rerank API failed: ${resp.status} ${body}`.trim());
|
|
141
|
+
}
|
|
142
|
+
const data = await resp.json() as {
|
|
143
|
+
results?: Array<{ index: number; relevance_score: number }>
|
|
144
|
+
};
|
|
145
|
+
return (data.results ?? []).map(r => ({
|
|
146
|
+
index: r.index,
|
|
147
|
+
relevance_score: r.relevance_score,
|
|
148
|
+
}));
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 4.3.2 修改 `src/search/rerank.ts`
|
|
155
|
+
|
|
156
|
+
在现有 `rerank()` 函数之前新增一个异步的 `rerankWithProvider()` 函数:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import type { RerankProvider } from "./rerank-provider.js";
|
|
160
|
+
|
|
161
|
+
export async function rerankWithProvider(
|
|
162
|
+
results: SearchResult[],
|
|
163
|
+
query: string,
|
|
164
|
+
provider: RerankProvider,
|
|
165
|
+
): Promise<SearchResult[]> {
|
|
166
|
+
if (results.length === 0) return results;
|
|
167
|
+
|
|
168
|
+
const documents = results.map(r => r.memory.content);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const apiResults = await provider.rerank(query, documents);
|
|
172
|
+
// 用 API 的 relevance_score 替换原有 score
|
|
173
|
+
const scoreMap = new Map(apiResults.map(r => [r.index, r.relevance_score]));
|
|
174
|
+
return results.map((r, i) => ({
|
|
175
|
+
...r,
|
|
176
|
+
score: scoreMap.get(i) ?? r.score, // fallback 到原 score
|
|
177
|
+
matchReason: scoreMap.has(i)
|
|
178
|
+
? `${r.matchReason}+rerank`
|
|
179
|
+
: r.matchReason,
|
|
180
|
+
}));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// best-effort: 失败时静默降级
|
|
183
|
+
console.warn("[agent-memory] External rerank failed, falling back:", err);
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
原有的同步 `rerank()` 函数**不做任何修改**。
|
|
190
|
+
|
|
191
|
+
#### 4.3.3 修改 `src/mcp/server.ts`
|
|
192
|
+
|
|
193
|
+
在 MCP server 初始化时加载 reranker provider:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { getRerankerProviderFromEnv } from "../search/rerank-provider.js";
|
|
197
|
+
import { rerankWithProvider } from "../search/rerank.js";
|
|
198
|
+
|
|
199
|
+
// 初始化
|
|
200
|
+
const rerankerProvider = getRerankerProviderFromEnv();
|
|
201
|
+
|
|
202
|
+
// recall 工具中,在本地 rerank 之前插入外部 rerank
|
|
203
|
+
async ({ query, limit }) => {
|
|
204
|
+
const { intent, confidence } = classifyIntent(query);
|
|
205
|
+
const strategy = getStrategy(intent);
|
|
206
|
+
let raw = await searchHybrid(db, query, { ... });
|
|
207
|
+
|
|
208
|
+
// 外部 reranker(可选)
|
|
209
|
+
if (rerankerProvider) {
|
|
210
|
+
raw = await rerankWithProvider(raw, query, rerankerProvider);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const results = rerank(raw, { ...strategy, limit });
|
|
214
|
+
// ...
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### 4.3.4 环境变量
|
|
219
|
+
|
|
220
|
+
| 变量 | 必填 | 默认值 | 说明 |
|
|
221
|
+
|------|------|--------|------|
|
|
222
|
+
| `AGENT_MEMORY_RERANK_PROVIDER` | 否 | `"none"` | `"openai"` / `"jina"` / `"cohere"` / `"none"` |
|
|
223
|
+
| `AGENT_MEMORY_RERANK_MODEL` | 否 | `"Qwen/Qwen3-Reranker-8B"` | 模型名 |
|
|
224
|
+
| `AGENT_MEMORY_RERANK_API_KEY` | 否 | 继承 `OPENAI_API_KEY` | API 密钥 |
|
|
225
|
+
| `AGENT_MEMORY_RERANK_BASE_URL` | 否 | 继承 `OPENAI_BASE_URL` | API 端点 |
|
|
226
|
+
|
|
227
|
+
#### 4.3.5 导出
|
|
228
|
+
|
|
229
|
+
在 `src/index.ts` 中新增:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
export { getRerankerProviderFromEnv, createOpenAIRerankProvider, type RerankProvider, type RerankResult } from "./search/rerank-provider.js";
|
|
233
|
+
export { rerankWithProvider } from "./search/rerank.js";
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 5. Risks / 风险
|
|
239
|
+
|
|
240
|
+
| 风险 | 影响 | 缓解措施 |
|
|
241
|
+
|------|------|----------|
|
|
242
|
+
| API 超时/不可用 | 搜索变慢或失败 | best-effort + try/catch 降级到本地 rerank |
|
|
243
|
+
| API 费用 | 每次 recall 多一次 API 调用 | 仅在 provider 配置时启用;候选数量上限 20 |
|
|
244
|
+
| score 尺度不一致 | API relevance_score 和 BM25 score 量纲不同 | API score 直接替换,不做混合;本地 rerank 仅做乘法加权 |
|
|
245
|
+
| 增加延迟 | recall 多一个网络 round-trip | 记忆条目少(<100),payload 小,延迟可控 |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 6. Test Plan / 测试方案
|
|
250
|
+
|
|
251
|
+
- [ ] Unit test: `rerank-provider.ts` — mock fetch 验证请求格式和响应解析
|
|
252
|
+
- [ ] Unit test: `rerankWithProvider()` — 正常路径 score 替换 + matchReason 追加
|
|
253
|
+
- [ ] Unit test: `rerankWithProvider()` — API 失败时静默降级,返回原始 results
|
|
254
|
+
- [ ] Unit test: `getRerankerProviderFromEnv()` — 各种环境变量组合
|
|
255
|
+
- [ ] Integration test: MCP recall 在有/无 reranker 时都能正常返回
|
|
256
|
+
- [ ] Manual verification: 对比有/无 reranker 时 Top-5 结果质量
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 7. Rollback Plan / 回滚方案
|
|
261
|
+
|
|
262
|
+
- 删除环境变量 `AGENT_MEMORY_RERANK_PROVIDER`(或设为 `none`)即可完全禁用,零影响
|
|
263
|
+
- 代码层面:`rerankWithProvider` 是独立函数,`rerank` 本身未被修改,删除新代码即可回滚
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 8. Decision Log / 决策变更记录
|
|
268
|
+
|
|
269
|
+
| 日期 | 变更 | 原因 |
|
|
270
|
+
|------|------|------|
|
|
271
|
+
| 2026-02-22 | 选择方案 A(API score 替换 + 本地加权叠加) | 保留 priority/vitality 系统的同时获得语义精读 |
|
|
272
|
+
| 2026-02-22 | Rerank API key 默认继承 OPENAI_API_KEY | 减少配置负担,momo 的 embedding 和 rerank 共用一个 key |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
_Generated by DD workflow · Noah (Claude Opus)_
|