# OPC Agent 永久记忆系统 — 技术方案与踩坑记录

> 最后更新：2026-06-25
> 适用对象：OPC（OpenClaw 多 agent 平台）上的所有 AI 助手（xiaofan/xiaoqi/xiaozhen/xiaomiao/...）
> 本文记录了"把 agent 的对话变成可永久检索的记忆"这套系统的**架构、分层、实现细节，以及实战中真实踩过的坑**。后续维护/重建请先读本文。
>
> 🟢 **当前架构（2026-06-25 起）：单一向量集合 `opc_memories`**。早期的"个人记忆 + 个人知识库"双集合、以及 mem0 提炼层，**均已废弃**——记忆只有一种，统一存进 `opc_memories`，靠 `payload.type`（conversation / knowledge / daily / file_backfill）做来源标记，靠 `agent_id` 做隔离。本文正文已按单集合改写；演进过程见 §10、合并细节见 §11。

---

## 1. 目标与核心结论

**目标**：让每个 agent 的对话不随 session 重置而丢失，而是沉淀为可语义检索的"永久记忆"，并能跨 agent 共享 Bo 的关键信息。

**一句话架构**：记忆系统是一套**独立于 OpenClaw 的服务**（slim 版 Python API + Qdrant 向量库，已去 mem0/去 LLM 提炼层），OpenClaw/agent 只通过 HTTP 调用它。两者**松耦合、无代码级绑定**。全部记忆存于**单一集合 `opc_memories`**。

**关键设计决策**：自动入库用**系统 crontab**（而非 OpenClaw 内部 hook），因为 OpenClaw 升级/重装会冲掉其内部配置，而系统 cron 不受影响。

---

## 2. 系统架构

```
┌──────────────────────────┐          ┌─────────────────────────────────────┐
│  OpenClaw 平台            │          │  记忆服务栈（独立, systemd 管理）       │
│  (gateway / agents)       │          │  systemd unit: opc-memory.service     │
│                           │  HTTP    │                                       │
│  ① agent 对话(插件自动注入) │ ──curl→  │  slim API  (127.0.0.1:17760)          │
│     + 主动调 API            │          │      │  api_server.py (多线程 HTTP)    │
│                           │ ←──────  │      ▼                                │
│  workspace-<a>/           │  结果     │  Qdrant 向量库 (127.0.0.1:6333)       │
│    AGENTS.md / MEMORY*.md  │          │      - collection: opc_memories(唯一) │
│    MEMORY_SYSTEM.md        │          │        (type 区分 来源, agent_id 隔离)│
└──────────────────────────┘          │  embedder: fastembed (本地 gte-large) │
                                        │  无 LLM 提炼层(已去 mem0)             │
        ② 文件层回填 backfill_files.py ──▶                                      │
                                        └─────────────────────────────────────┘
```
> 注：原图里的第二个集合 `opc_knowledge_base` 与"每日 cron / mem0 抽取 LLM"已废弃；现在只有一个集合、无 LLM 提炼。

### 组件与端口

| 组件 | 地址 | 说明 |
|---|---|---|
| Qdrant 向量库 | `127.0.0.1:6333` | 实际存向量+payload，磁盘持久化 `/root/opc-memory/data/qdrant` |
| slim API | `127.0.0.1:17760` | `api_server.py`，直连 Qdrant（无 mem0/无 LLM），提供 `/memory/*` `/kb/*`（均作用于单一集合 `opc_memories`） |
| Claude 本地代理 | `127.0.0.1:18765` | `claude-proxy.mjs`，桥接 Max 订阅的 claude CLI。记忆服务**已不再用它**（去 mem0 后无 LLM 提炼）；仅历史/其他用途保留 |
| systemd 单元 | `opc-memory.service` | ExecStart=`/root/opc-memory/start_all.sh`（拉起 Qdrant + api_server） |

### 关键文件

| 路径 | 作用 |
|---|---|
| `/root/opc-memory/api_server.py` | HTTP API（`/memory/add` `/memory/search` `/memory/context` `/kb/add` `/kb/search` ...）+ 防空转 `_spin_guard` |
| `/root/opc-memory/memory_service.py` | `OPCMemory` 类（slim：单集合检索 + 混合重排 + 关键词直召；无 mem0） |
| `/root/opc-memory/opc_agent_memory.py` | `AgentMemory` 包装层（api_server 实际调用它） |
| `/root/opc-memory/backfill_files.py` | **文件层→向量**唯一同步脚本（写 `opc_memories`，uuid5 去重可重复跑） |
| `/root/opc-memory/daily_memory.py` | ⚠️ 旧每日入库脚本，**已停用**（曾往旧 KB 双写，是重复来源；cron 已禁用，勿再启用） |
| `/root/opc-memory/import_session_memories.py` | **历史 session 回填**脚本（一次性） |
| `/etc/cron.d/opc-daily-memory` | 旧每日 cron（**已禁用**） |
| `/root/.openclaw/workspace-<agent>/MEMORY_SYSTEM.md` | 给 agent 看的"如何用记忆 API"说明书 |
| `/root/.openclaw/workspace-<agent>/MEMORY.md` `memory/<date>.md` | 文件层热记忆 |

---

## 3. 分层记忆模型（两层）

记忆分两层，**互补、用途不同**：

### 第 1 层：文件层（热记忆 / Hot Layer）
- **位置**：`workspace-<agent>/MEMORY.md`（精炼长期记忆）+ `memory/YYYY-MM-DD.md`（每日原始记录）
- **特点**：每次 session 开场由 `AGENTS.md` 指示直接读入 → 0 延迟、0 检索成本、给模型当即时上下文
- **容量**：有限（不能无限增长，否则吃满上下文窗口）
- **谁写**：agent 自己（对话中）

### 第 2 层：向量层（永久记忆 / Permanent Layer）
- **位置**：Qdrant **单一 collection `opc_memories`**（不再有第二个集合）。
  - 所有记忆都进这一个库，靠 `payload.type` 标记来源：`conversation`（对话）/ `knowledge`（用户明确"记住"的重点，经 `/kb/add` 写入）/ `daily`（历史每日提炼）/ `file_backfill`（文件层回填）。
  - 归属字段统一为 `agent_id`（早期 KB 用的 `added_by` 仍保留作冗余，检索两者都认）。
- **特点**：容量无限、按语义检索（embedding 相似度）、跨 session/跨 agent 永久存在
- **怎么用**：对话前 `POST /memory/context` 查相关记忆塞进上下文（通常由 `opc-memory-inject` 插件自动调）；对话后 `POST /memory/add` 写入；用户说"记住这个/加入知识库"时 `POST /kb/add`（仍写同一个库，只是把该条标成 `type=knowledge` 优先召回）。
- **谁写**：agent 主动调用 + 插件注入读取；`backfill_files.py` 把文件层同步进向量库。

> 类比：文件层 = 人的"工作记忆/便签"，向量层 = 人的"长期记忆/可回忆的经历"。
> ⚠️ 不要再用"个人记忆 vs 个人知识库"这种二分——它们是同一个库里的不同 `type` 标记，不是两套存储。

---

## 4. 写入路径

| 路径 | 触发方式 | 脚本/机制 | 可靠性 |
|---|---|---|---|
| **① 插件注入 + agent 主动写** | 每回合 `opc-memory-inject` 自动注入记忆；agent 按 `MEMORY_SYSTEM.md` 提示主动 `/memory/add` | hook 注入读取（强制）+ 模型自觉写 | 读取✅；写入⚠️靠模型自觉 |
| **② 历史回填** | 人工一次性运行 | `import_session_memories.py` | ✅ 可靠（一次性补全历史） |
| **③ 文件层回填** | 人工/按需运行 | `backfill_files.py`（写 `opc_memories`，uuid5 去重幂等） | ✅ 可靠，**唯一的批量同步入口** |

> ⚠️ 旧的"每日 cron `daily_memory.py` 自动入库"**已停用**：它往旧 KB 集合写，和 `backfill_files.py` 对同一份文件层重复双写；统一单集合后只保留 `backfill_files.py` 一条入口。需要把新的文件层记忆同步进向量库时，手动跑 `backfill_files.py [agent]` 即可。

**关于路径①**：写入是"软集成"，模型不一定每次都调 `/memory/add`；但**读取已由插件强制**（每回合自动 `/memory/context` 注入），所以"记不起来"的问题已根治，剩下的只是"写得勤不勤"。

### 历史回填脚本用法
```bash
cd /root/opc-memory
python3 import_session_memories.py xiaofan --dry-run   # 先看提炼效果
python3 import_session_memories.py xiaofan             # 正式入库
python3 import_session_memories.py xiaozhen            # 老 agent 会自动含 .reset 历史文件
```
流程：读 session JSONL → 提炼成逐条事实 → 写入单集合 `opc_memories`（`source=session-import`）。

### 文件层回填（backfill_files.py）— 当前唯一的批量同步入口
```bash
cd /root/opc-memory
python3 backfill_files.py            # 所有 agent
python3 backfill_files.py xiaozhen   # 单个 agent
```
- 把 `workspace-*/memory/*.md` + `MEMORY.md` 的要点写进 `opc_memories`（`/memory/add`，`agent_id` 归属，`source=file_backfill`）。
- uuid5 去重、幂等，可随时重复跑。
- ⚠️ 旧 `daily_memory.py` 已停用（见上）；不要再用它把记忆写进已废弃的 KB 集合。

---

## 5. Qdrant 关键机制（slim 版）

### 无 mem0、无 LLM、无 infer
> 历史：早期用 mem0 的 `add(infer=True/False)`，`infer=True` 会调 LLM 抽取事实+去重，单条要 2 分钟、批量超时。**slim 版已彻底去掉 mem0**：写入就是"原文 → fastembed 向量 → 直接 upsert"，无任何 LLM 往返；去重靠确定性 uuid5（见 §10.2-C）。`/kb/add` 的 `infer` 入参现已无意义（保留仅为兼容旧调用）。

### Collection / payload schema（单集合）
- **唯一集合 `opc_memories`**；向量：dense 1024 维（fastembed `thenlper/gte-large`, cosine）+ sparse `bm25`。
- payload 字段：`agent_id`（归属，隔离用）、`user_id`(=`bo_huang`)、`type`（来源标记：conversation / knowledge / daily / file_backfill）、`data`（正文）、`timestamp`；知识类附 `topic` / `source`；从旧 KB 合并来的点保留 `added_by`（冗余，等于 `agent_id`）。
- ✅ 归属字段统一看 `agent_id`。检索时 `should:[agent_id, added_by]` 两者都认，向后兼容旧 KB 点。
- ⚠️ 旧集合 `opc_knowledge_base` 仍在磁盘上但**已弃用、代码不读写**，留作冷备，可日后 `DELETE /collections/opc_knowledge_base` 回收空间。

### 跨 agent 共享与隔离
所有写入都挂在 `user_id="bo_huang"` 下；各 agent 的记忆靠 `agent_id` 隔离，检索严格只返回本 agent 的记忆（实测正反向均不串）。

---

## 6. ⚠️ 容易踩的坑（实战血泪，重点章节）

### 坑 1：孤儿进程 — 改了代码不生效
- **现象**：改了 `memory_service.py`/`api_server.py` 并 `systemctl restart`，但行为完全没变。
- **根因**：`start_all.sh` 用 `nohup python3 api_server.py &` 启动并写 `api.pid`；该进程**脱离了 systemd 的 cgroup**。restart 时新进程因端口被占 `OSError: Address already in use` 而崩溃，旧进程（旧代码）继续服务。`start_all.sh` 还会因 `api.pid` 里的旧 PID 存活而跳过重启。
- **排查**：`ss -tlnp | grep 17760` 看真正监听的 PID，`ps -o lstart -p <PID>` 看它的启动时间是否早于你的改动。
- **正解**：
  ```bash
  systemctl stop opc-memory.service
  kill -9 <监听17760的PID>          # 杀掉孤儿
  rm -f /root/opc-memory/api.pid    # 清理过期 pidfile
  systemctl start opc-memory.service
  # 确认新 PID 的 lstart 晚于改动时间
  ```

### 坑 2：claude 本地代理每次调用都很慢（50–75 秒）
- **现象**：mem0 的任何 LLM 步骤都极慢甚至超时。
- **根因**：`:18765` 代理每次请求都要冷启动 claude CLI，单次往返 ~50s（sonnet）/ ~73s（haiku）。**与模型无关，是代理固有开销。**
- **影响**：`infer=True` 的 add 要多次 LLM 调用 → 累计 2 分钟+ → 客户端超时、写不进去。
- **正解**：① 提炼用一次性 LLM 调用（摘要）后改用 `infer=False` 入库，把 LLM 调用次数降到最低；② 别用 haiku（经此代理比 sonnet 还慢），统一 `claude-sonnet-4-6`。

### 坑 3：mem0 的 LLM 默认是 haiku，且比 sonnet 还慢
- `memory_service.py` 里 `MEM0_CONFIG`/`KNOWLEDGE_CONFIG` 的 `llm.model` 原为 `claude-haiku-4-5`，经代理 ~73s/次。已改为 `claude-sonnet-4-6`。

### 坑 4：infer=False 首次调用仍慢（~30s）
- **现象**：第一条 `infer=False` 要 30s，之后才 ~2s。
- **根因**：fastembed（gte-large ONNX，~640MB）是**懒加载**，首次 add 才载入模型；本机内存紧张、有 swap，加载更慢。
- **结论**：这是一次性热身，正常。批量任务第一条慢、后续快即正确。

### 坑 5：Qdrant 重启丢未落盘数据
- **现象**：短时间内多次重启服务后，最近写入的数据消失（曾丢失 89 条）。
- **根因**：Qdrant 段未及时 flush 时被重启打断；早先写入（已 flush）的数据安然无恙。
- **结论**：① 批量入库后**不要立刻反复重启服务**，给 Qdrant 时间落盘；② 验证持久化：写入后等待，再 `count` 确认；③ 重要变更后用 `points/count` 做前后对账。

### 坑 6：单线程 API server 串行阻塞
- **现象**：一个慢请求会把其它请求全堵住、连带超时。
- **根因**：`api_server.py` 基于 `BaseHTTPRequestHandler`，请求串行处理；并发的入库任务（如另一个 agent 的回填）会互相排队。
- **结论**：批量任务别并发跑多个；或后续改成 `ThreadingHTTPServer` + mem0 实例加锁。

### 坑 7：agent_id 默认值导致误归属
- **现象**：一批两性话题的记忆被写成了 `added_by=main`。
- **根因**：调用方没传 `agent_id`，api_server 默认 `"main"`。
- **结论**：入库脚本**务必显式传 `agent_id`**；事后用 payload 过滤核对归属。

### 坑 8：cron.d 时区误解
- **现象**：注释写"03:00 CST"，实际 `0 19 * * *` 在晚上 7 点跑。
- **根因**：误以为 cron 走 UTC（19:00 UTC=03:00 CST+1），但本机 `/etc/cron.d` 走**系统本地时区 Asia/Shanghai**，`0 19` 即 19:00 CST。
- **结论**：本机 cron 直接按本地时间写；要 03:30 就写 `30 3 * * *`。

### 坑 9：session JSONL 格式细节
- 每行一个 JSON 对象；只有 `type=="message"` 是对话，其余是 `session`/`model_change`/`custom` 等元数据行。
- user 消息正文前常有 `Conversation info (untrusted metadata):` + ```json 块 + `Sender (...)` + `[message_id: ...]` 头，**入库前必须剥掉**，否则污染记忆。
- 老 agent 的历史在 `*.jsonl.reset.<timestamp>` 文件里（session 重置时重命名而来）；`cleanup-sessions.sh` 把 >100KB 的活动 session 重命名为 `.reset.*`（**只改名不删数据**）。回填要 glob `*.jsonl` + `*.jsonl.reset.*`，排除 `.deleted`/`.checkpoint`。

### 坑 10：把记忆系统当成 OpenClaw 的一部分
- 它**不是**。`openclaw.json`/extensions 里搜不到任何 `qdrant/mem0/17760/6333`。能引用记忆 API 的只有 agent 的 markdown 文件。**自动化要放在 OpenClaw 之外**（系统 cron），这样平台升级不影响记忆管线。

---

## 7. 运维 / 验证速查

```bash
# 健康检查
curl -s http://127.0.0.1:17760/health
curl -s http://127.0.0.1:6333/collections

# 某 agent 的记忆条数（单集合，统一用 agent_id）
curl -s -X POST http://127.0.0.1:6333/collections/opc_memories/points/count \
  -H "Content-Type: application/json" \
  -d '{"exact":true,"filter":{"must":[{"key":"agent_id","match":{"value":"xiaofan"}}]}}'

# 按 agent_id / type / source 统计分布（单集合）
curl -s -X POST http://127.0.0.1:6333/collections/opc_memories/points/scroll \
  -H "Content-Type: application/json" -d '{"limit":3000,"with_payload":true,"with_vector":false}' \
| python3 -c "import sys,json,collections;d=json.load(sys.stdin);print(collections.Counter(p['payload'].get('agent_id','?') for p in d['result']['points']))"

# 语义检索测试（agent 真实路径）
curl -s -X POST http://127.0.0.1:17760/memory/search \
  -H "Content-Type: application/json" \
  -d '{"agent_id":"xiaofan","query":"签名香水","limit":5}'

# 把某 agent 的文件层同步进向量库
cd /root/opc-memory && python3 backfill_files.py xiaofan

# 固定检索用例自检（小金/慧慧 + 防空转，秒级不超时）
bash /root/bin/opc-memory-cases.sh

# 看谁在监听 17760、何时启动（排查孤儿进程）
ss -tlnp | grep 17760 ; ps -o pid,lstart,cmd -p <PID>
```

---

## 8. 历史状态快照（2026-05-26，已被 §10/§11 取代）

> ⚠️ 本节是 mem0+双集合时代的旧快照，仅作追溯。现状以 §11 为准（单集合、去 mem0、每日 cron 停用）。

- 历史回填完成：**xiaofan 22 / xiaoqi 14 / xiaozhen 166** 条（写入当时的 `opc_knowledge_base`，现已并入 `opc_memories`）
- xiaofan 文件层补齐：`MEMORY.md` / `MEMORY_SYSTEM.md` / `memory/2026-05-24.md`
- 每日自动入库已修复并验证通过（cron `30 3 * * *`，`daily_memory.py` 用 sonnet + infer=False）
- mem0 抽取模型已从 haiku 切到 sonnet；`/kb/add` 支持 `infer` 入参

## 9. 已知局限与未来改进
1. **代理慢**是根本瓶颈（50s+/次）。可考虑给 mem0 配一个更快的本地/直连 LLM 端点，或彻底跳过 LLM 提炼。
2. **单线程 API**。高并发场景应改 `ThreadingHTTPServer` 并对 mem0 实例加锁。
3. **路径①（模型自觉写）**仍不可靠。已用 cron 兜底；若要"实时"入库，可在 OpenClaw 之外加一个 session 文件 watcher（inotify）增量入库。
4. **去重**：`infer=False` 不去重，回填+每日可能产生少量重复事实；检索 top-k 影响不大，必要时可定期做语义去重。
5. **Qdrant 落盘**：建议确认 `qdrant.yaml` 的 flush/WAL 配置，避免重启丢数据。

---

# 10. 2026-06-25 重大重构：去 mem0 + 防绕过 + 自动化监控（迁移必读）

> 本章是对 §9「未来改进」的全面落地。若要把本记忆系统迁到新机器，**以本章为准**，前面章节的 mem0 部分已被取代。

## 10.1 这次解决的四个根本问题（根源）

| 问题 | 现象 | 根源 |
|---|---|---|
| **OOM 打爆内存 / SSH 登不上** | 机器(3.6G)频繁 swap 抖死，SSH 超时 | `api_server.py` 的 `_memory_cache` 按 agent **无上限**缓存 `AgentMemory`，每个内部 `Memory.from_config`(mem0) 各自把 **gte-large 嵌入模型载入 RAM(~600MB-1.2G，mem+kb 两份≈1.3G/agent)**。多 agent 上线 → 内存累加 → 爆。 |
| **调取慢(46s)** | `/memory/context` 实测 46s，`/memory/search` 24s | mem0 在 add/search 路径调 **Sonnet LLM** 做事实抽取；context 同时查记忆+知识库=2 次 LLM 往返。慢的不是向量检索(Qdrant 仅 31MB，毫秒级)，是 mem0 套的 LLM 层。 |
| **Agent 绕过记忆** | agent 不调取也不存储，凭空回答 | 记忆系统是**被动 skill**（要 agent 自己想起来 curl），而 claude-mem 等是 hook 驱动自动注入，主动的赢被动的。 |
| **重复写入 / 库膨胀** | 同一条事实存了 9 份 | slim 去 mem0 后**无去重**（mem0 当年靠 LLM 去重）。agent 一回合多调几次 `/memory/add` 就堆重复，长期污染检索。 |

**为什么没迁去 claude-mem（thedotmack/claude-mem，即"Cloud Memo"）**：① 它按 cwd/项目隔离，**无 agent_id 概念**，迁过去丢掉按 agent 隔离这一头号需求；② 它 `CLAUDE_MEM_MODEL=claude-sonnet-4-6`，**一样靠 LLM**，治不好慢；③ 无 import 命令，迁 2696 条要烧 2696 次 LLM。结论：迁移是帮倒忙。openclaw 自带的 `active-memory`/`memory-core` 也未采用（用它自己的库不是 Qdrant，且 active-memory 跑 LLM 子代理太重）。

## 10.2 修复方案（已上线）

### A. slim 版 memory_service.py（去 mem0，根治 OOM + 慢）
- 重写 `/root/opc-memory/memory_service.py` **内部**，`OPCMemory` 方法签名全不变 → `api_server.py`/`opc_agent_memory.py` 一行没改。
- 核心：**全进程只载一份** fastembed `thenlper/gte-large`（模块级单例 `_get_embedder()`+`threading.Lock`），裸 HTTP 调 Qdrant（不引 qdrant_client，更省内存），个人记忆检索按 `payload.agent_id` 过滤实现隔离，复用现有集合与历史向量 → **零数据迁移**。彻底不 import mem0、不调 LLM。
- 效果：内存「每 agent +1.3G 无限涨」→「全局恒定 ~1.24G，多 agent 轮调纹丝不动」；延迟 46s → 热查 ~2.5s（冷启动载模型 ~15-20s 一次性）；agent 隔离正反向实测 OK。

### B. 止血补丁：LRU 缓存上限（防 OOM 复发）
- `api_server.py` 的 `_memory_cache` 改 `OrderedDict` + `_MAX_CACHED_AGENTS`(默认 1，env `OPC_MEM_MAX_AGENTS` 可调)，淘汰时断引用+gc。slim 版下模型已全局共享，此补丁作为双保险。
- systemd 仍有 `MemoryMax=2048M` 兜底（`/etc/systemd/system/opc-memory.service.d/memory-limit.conf`）。

### C. 去重：确定性 ID（治重复/膨胀）
- `add` / `add_to_knowledge_base` 的 point id 由随机 uuid4 改为 **uuid5**：
  - 个人记忆 `uuid5(OPC_UUID_NS, "mem|{agent_id}|{content}")`
  - 知识库 `uuid5(OPC_UUID_NS, "kb|{topic}|{content}")`
- 同 (agent/topic, 内容) 重复写入 → 同一点 → upsert 覆盖，**永不产生完全重复**。向后兼容（旧点保留各自随机 id）。

### D. 防绕过 hook = openclaw 插件 `opc-memory-inject`
- 位置 `/root/.openclaw/extensions/opc-memory-inject/`（`index.js`+`openclaw.plugin.json`+`package.json`），模板抄自 `opc-persona`。
- 机制：openclaw typed hook **`before_prompt_build`**（异步）。每回合从 `event.messages` 取最后一条用户消息 → 调 `/memory/context` → 返回 `{ prependSystemContext }` 把相关记忆+存储纪律注入系统提示。**代码级执行，模型绕不过**。8s 超时 + 任何错误 fail-open（返回 `{}`），绝不阻断对话。
- 仅对**有记忆 agent**生效：判据 = `workspace-<id>/MEMORY_SYSTEM.md` 是否存在 → **自动覆盖全部 15 个，零硬编码**（新增有记忆 agent 自动纳入）。
- 启用三步（迁移关键）：
  1. 插件目录放进 `/root/.openclaw/extensions/`（会被发现但默认 disabled）；
  2. `openclaw config patch`：`plugins.allow` 加 `"opc-memory-inject"`，`plugins.entries."opc-memory-inject" = {enabled:true, hooks:{allowPromptInjection:true}}`；
  3. `systemctl restart openclaw-gateway`。
- 验证已触发：`/root/opc-memory/logs/api.log` 出现 `POST /memory/context`，检索词=完整用户消息（用户不会手动这样搜）。

## 10.3 自动化测试与监控（新增）

### E. 端到端自检脚本 `/root/bin/opc-memory-e2e.sh [agent]`（默认 xiaozhen）
- 三步：A 发指令让 agent 写记忆 → B **用全新 session-key** 让其读回（验证持久向量记忆而非对话上下文）→ C 直接查 Qdrant 核对。跑完**自清理**本次测试码，不污染真实记忆。
- 末行输出 `RESULT=PASS|FAIL` + `SUMMARY={...json...}`，退出码 0/1。
- 调 agent：`openclaw agent --agent <id> --session-key agent:<id>:<key> --message "..." --json`。**PATH 必须含** `/root/.local/share/pnpm`(openclaw 本体) 和 `/root/.nvm/versions/node/<ver>/bin`(node)，否则 cron/timeout 下报 "openclaw: No such file or directory"。

### F. 每日健康报告 `/root/bin/opc-memory-health-report.sh`
- cron `/etc/cron.d/opc-memory-health`：`30 9 * * * root`（每天 09:30 CST，避开 watchdog 的 09:00）。
- 跑 e2e + 查基础设施(服务状态/health/内存 RSS/记忆条数/系统可用内存) → 组装"有没有记忆"报告 → **Telegram 发 Bo**（`BOT_TOKEN`/`CHAT_ID=8694870488`，同 opc-watchdog），署名"小维(运维监控)"。日志 `/var/log/opc-memory-health.log`。

## 10.4 文件与备份清单（迁移时一并带走）

```
/root/opc-memory/memory_service.py            # slim 版（核心：单集合+混合检索+关键词直召，去 mem0）
/root/opc-memory/api_server.py                # 含 LRU 止血补丁 + 防空转
/root/opc-memory/backfill_files.py            # 文件层→向量 唯一同步脚本
/root/opc-memory/*.bak.before-merge           # 合并为单集合前的备份
/root/opc-memory/*.bak.before-kwrecall        # 关键词直召修复前备份
/root/opc-memory/backup_opc_memories.json     # 合并后单集合全量冷备
/root/opc-memory/backup_opc_knowledge_base.json # 旧 KB 全量冷备(合并源)
/root/opc-memory/snapshots/                    # Qdrant 原生快照(两集合)
/root/.openclaw/extensions/opc-memory-inject/ # 防绕过插件
/root/bin/opc-memory-e2e.sh                   # 端到端自检
/root/bin/opc-memory-cases.sh                 # 固定检索用例(小金/慧慧+防空转)
/root/bin/opc-memory-health-report.sh         # 每日健康报告
/etc/cron.d/opc-memory-health                 # 调度
/etc/systemd/system/opc-memory.service.d/memory-limit.conf  # 内存兜底
```

## 10.5 迁移到新机器的步骤（技术方案）
1. 装 Qdrant + python3(fastembed)；拷 `/root/opc-memory/` 整目录（含 `data/qdrant` 与 `fastembed_cache`，后者避免重新下模型）。
2. 起 `opc-memory` systemd 服务（`start_all.sh`：先 qdrant 后 `api_server.py`，带 `memory-limit.conf`）。
3. 健康检查 `curl 127.0.0.1:17760/health`；语义检索冒烟测试。
4. 装 openclaw 插件 `opc-memory-inject`（见 §10.2-D 三步），重启 gateway。
5. 拷 `/root/bin/opc-memory-e2e.sh`、`opc-memory-health-report.sh`、`/etc/cron.d/opc-memory-health`；按新机器改 `BOT_TOKEN/CHAT_ID` 与 openclaw/node 的 PATH。
6. 跑一次 `opc-memory-e2e.sh xiaozhen` 验收（期望 `RESULT=PASS`）。

## 10.6 仍存的小问题（非阻塞）
- agent 一回合可能多次调 `/memory/add`（LLM 过度重试），数据层已被去重兜住；若要根治需调 agent 提示词。
- 单进程载一份 gte-large 仍 ~1.2G；若新机器内存更紧，可换小嵌入模型(bge-small 类 ~300MB)，但需重嵌全部向量（一次性）。
- 健康报告走 Telegram 脚本署名"小维"；如需真人 agent 亲发可改。

## 10.7 统一检索 + 相关性阈值（2026-06-25 晚补）

> 📌 §10.7 / §10.8 是"双集合时代"的演进记录（当时是两个集合 + 统一**读取**）。**最终在 §11 把两个集合物理合并成一个 `opc_memories`**，此后不再有 KB 集合。读现状以 §11 为准；这里保留是为追溯空转问题的根因与修法。

**问题**：agent 搜"和某人相关的记忆"会空转 20+ 分钟最后才靠读文件层日记答对。
**根因（重要）**：写入与读取走了不同集合——`daily_memory.py` 把每日提炼记忆写进**共享知识库** `opc_knowledge_base`(`/kb/add`)，但 agent 的 `/memory/search` 只查**个人库** `opc_memories`(按 agent_id)。于是 agent 的近期记忆对它自己"隐身"，搜不到 → 反复换词空转 → 直到 openclaw 回合超时(1800s)。
**修复**（`memory_service.py`）：
1. `OPCMemory.search()` 改为**统一检索 = 个人库(agent_id 过滤) + 知识库(added_by=本agent 过滤)**，合并按分排序取 top-k。两路都限本 agent，**跨 agent 不串**（隔离不变）。`smart_query()`(=/memory/context) 同步改用统一 search，避免与 KB 重复。
2. 新增相关性阈值 `SCORE_THRESHOLD`(env `OPC_MEM_SCORE_THRESHOLD`，默认 0.88)：低于阈值的命中丢弃，无相关时返回空，避免 agent 拿到噪声继续空转。
**实测**：小珍搜"小金"从 个人0→空转，变为 第一次即 个人0+KB5→5条 命中，2 次搜索答完、不超时。
**已知局限**：gte-large 对中文余弦基线偏高(~0.88-0.91)，绝对阈值只能挡"明显无关"，挡不住语义漂移到主簇的噪声。要更干净需换嵌入模型或加 reranker。备份 `memory_service.py.bak.before-unified-*`。

## 10.8 空转根治：混合检索 + 回填 + 防空转机制（2026-06-25 深夜）

**两个真实 case**（小金、慧慧）暴露空转的三层根因，全部修复：

**根因1 — 写读集合错配**（已在 §10.7 修）：每日记忆进共享 KB，agent 只搜个人库。→ 统一检索(个人库 agent_id + 本agent KB added_by)。

**根因2 — 向量库残缺**：部分文件层每日记忆(如慧慧 6-21)从没进向量库。
→ `/root/opc-memory/backfill_files.py`：把所有 `workspace-*/memory/*.md`+`MEMORY.md` 的要点回填进**向量个人库**(/memory/add, agent_id)，uuid5 去重可重复跑。一次性全量约 2600+ 条。

**根因3（最隐蔽）— gte-large 中文排序差**：即便数据在库里，查"慧慧咖啡邀约"，真句子只排#12(0.857)，垃圾"## 情绪/状态"反排#1(0.877)。绝对阈值救不了(噪声地板就 0.877)。
→ **混合检索**(`memory_service.py` `search()`)：向量召回 limit*5 候选(不卡阈值) + **关键词重排**：`_salient_terms()` 抽查询的空格词+中文2/3-gram，命中关键词的记忆 +0.05/词 顶到最前；命中关键词的豁免阈值(必相关)，无命中的才需过阈值。效果：慧慧/小金 从 #12/不可见 → #1。隔离不变(仍按 agent_id/added_by)。

**防空转硬机制**(`api_server.py` `_spin_guard`)：同一 agent 180s 内 /memory/search ≥3次→结果附加"停止搜索"提示，≥6次→只返回 system_stop 强制叫停。**任何残留空转最多6次搜索封死**，不再有 17 分钟空转。

**每日测试**(`/root/bin/opc-memory-cases.sh`，已接入健康报告)：固定用例断言 小金/慧慧 能从向量库搜到 + 防空转机制在线；快(秒级)、不经 agent LLM、绝不超时。健康报告总结果 = e2e PASS && cases PASS。

**新增/改动文件**：`memory_service.py`(混合检索+阈值)、`api_server.py`(防空转)、`backfill_files.py`(回填)、`opc-memory-cases.sh`(用例)、`opc-memory-health-report.sh`(接入用例)。备份 `*.bak.before-hybrid-*` / `*.bak.before-spin-*`。
**后续可选**：gte-large 中文质量差是根上短板，彻底解可换 bge-m3 等中文强模型(需重嵌)；当前靠"混合检索+防空转"已把空转问题工程性解决。

---

## 11. 双集合合并为单集合（2026-06-25 收尾，重要）

**动机**：原"个人记忆 `opc_memories` + 个人知识库 `opc_knowledge_base`"的区分名存实亡——KB 里 85% 是每日 cron 自动写的 `daily-batch`，手动"记住这个"仅几十条；且两集合字段名不一致(`agent_id` vs `added_by`)、写读易错配。决定**物理合并为单一集合 `opc_memories`**。

**变更**：
1. **数据迁移**：`opc_knowledge_base`(2628) → `opc_memories`，`added_by`→`agent_id`、复用现有 dense+sparse 向量(不重嵌)、`mem|agent|内容` uuid5 去重。合并后 ~2916 条。原 KB 集合**保留为冷备**(未删，可日后 drop)。
2. **代码** `memory_service.py`：`KB_COLLECTION = MEM_COLLECTION = "opc_memories"`；"知识库"退化为 `type=knowledge` 逻辑视图；`/kb/add` 同时写 `agent_id`；`search()` 单集合 `should[agent_id,added_by]` 隔离。`/kb/*` 接口保留(薄封装,不破坏旧调用)。
3. **停重复回填**：`daily_memory.py`(→KB) 与 `backfill_files.py`(→memories) 对同一文件层双写=重复。停掉正在跑的 backfill；`daily_memory` cron 保持禁用；**`backfill_files.py` 成为唯一文件层→向量同步工具**(写 opc_memories)。⚠️ 勿再启用 daily_memory 往 KB 写。

**检索修复(根治长查询漏召)**：注入插件 `/memory/context` 传整句用户消息，长句稀释向量→含关键词的目标排不进向量 top-N→漏召(e2e read=FAIL)。修：
- `_keyword_recall()`：scroll 出本 agent `data` 含查询关键词的记忆，兜底进候选池(不靠向量分)。
- 直召条目**基线分=`SCORE_THRESHOLD`(0.80)**，`_rank=0.80+0.05×kw`，使含独特短语者顶到最前、弱匹配仍排在真向量相关项之下，不淹没结果。
- `_hybrid_rerank()` 抽成 `search()`/`search_knowledge_base()` 共用。

**skill 一致性**：`opc-memory-inject` 以 `workspace-<id>/MEMORY_SYSTEM.md` 存在为开关。6 个有数据却无该文件的 agent(`xiaokang/xiaotan/xiaomi/huangmao1/huangmuwen/zhengwenfang`)被插件跳过→已补建；另刷新 16 个为统一单库措辞。

**备份**：`/root/opc-memory/snapshots/`(Qdrant 快照) + `backup_opc_memories.json`/`backup_opc_knowledge_base.json`；代码 `.bak.before-merge` / `.bak.before-kwrecall`。
**验收**：`opc-memory-cases.sh`(小金/慧慧+防空转) PASS；`/memory/context` 带标点长查询命中；跨 agent 隔离保持；`opc-memory-e2e.sh xiaozhen` write/read/db 三段 PASS。
