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

> 最后更新：2026-05-26
> 适用对象：OPC（OpenClaw 多 agent 平台）上的所有 AI 助手（xiaofan/xiaoqi/xiaozhen/xiaomiao/...）
> 本文记录了"把 agent 的对话变成可永久检索的记忆"这套系统的**架构、分层、实现细节，以及实战中真实踩过的坑**。后续维护/重建请先读本文。

---

## 1. 目标与核心结论

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

**一句话架构**：记忆系统是一套**独立于 OpenClaw 的服务**（mem0 API + Qdrant 向量库），OpenClaw/agent 只通过 HTTP 调用它。两者**松耦合、无代码级绑定**。

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

---

## 2. 系统架构

```
┌──────────────────────────┐          ┌─────────────────────────────────────┐
│  OpenClaw 平台            │          │  记忆服务栈（独立, systemd 管理）       │
│  (gateway / agents)       │          │  systemd unit: opc-memory.service     │
│                           │  HTTP    │                                       │
│  ① agent 对话时按提示词    │ ──curl→  │  mem0 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       │
│    skills/opc-memory-skill │          │      - collection: opc_knowledge_base │
└──────────────────────────┘          │  embedder: fastembed (本地 gte-large) │
                                        │  抽取LLM: claude (经本地代理 :18765)  │
        ② 历史回填脚本 ──curl────────────▶                                      │
        ③ 每日 cron  ──curl────────────▶                                       │
                                        └─────────────────────────────────────┘
```

### 组件与端口

| 组件 | 地址 | 说明 |
|---|---|---|
| Qdrant 向量库 | `127.0.0.1:6333` | 实际存向量+payload，磁盘持久化 `/root/opc-memory/data/qdrant` |
| mem0 API | `127.0.0.1:17760` | `api_server.py`，封装 mem0，提供 `/memory/*` `/kb/*` |
| Claude 本地代理 | `127.0.0.1:18765` | `claude-proxy.mjs`，桥接 Max 订阅的 claude CLI；mem0 抽取 + 摘要都走它 |
| systemd 单元 | `opc-memory.service` | ExecStart=`/root/opc-memory/start_all.sh`（拉起 Qdrant + api_server） |

### 关键文件

| 路径 | 作用 |
|---|---|
| `/root/opc-memory/api_server.py` | mem0 HTTP API（`/memory/add` `/memory/search` `/memory/context` `/kb/add` `/kb/search` ...） |
| `/root/opc-memory/memory_service.py` | `OPCMemory` 类 + mem0 配置（LLM/embedder/collection） |
| `/root/opc-memory/opc_agent_memory.py` | `AgentMemory` 包装层（api_server 实际调用它） |
| `/root/opc-memory/daily_memory.py` | **每日自动入库**脚本（cron 调用） |
| `/root/opc-memory/import_session_memories.py` | **历史 session 回填**脚本（一次性） |
| `/etc/cron.d/opc-daily-memory` | 每日 cron 定义（03:30 CST） |
| `/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 自己（对话中）、`daily_memory.py`（每日生成 `memory/<date>.md`）

### 第 2 层：向量层（永久记忆 / Permanent Layer）
- **位置**：Qdrant 两个 collection
  - `opc_memories`：对话记忆（`/memory/add`，payload 字段 `agent_id`）
  - `opc_knowledge_base`：知识库 / 提炼事实（`/kb/add`，payload 字段 `added_by`）
- **特点**：容量无限、按语义检索（embedding 相似度）、跨 session/跨 agent 永久存在
- **怎么用**：对话前 `POST /memory/context` 查相关记忆塞进上下文；对话后 `POST /memory/add` 或 `/kb/add` 写入
- **谁写**：agent 主动调用（软集成）、历史回填脚本、每日 cron

> 类比：文件层 = 人的"工作记忆/便签"，向量层 = 人的"长期记忆/可回忆的经历"。

---

## 4. 三种写入路径

| 路径 | 触发方式 | 脚本/机制 | 可靠性 |
|---|---|---|---|
| **① agent 主动写** | 模型对话中按 `MEMORY_SYSTEM.md` 提示自己发 curl | 无强制，靠模型自觉 | ⚠️ 不可靠（模型可能不调） |
| **② 历史回填** | 人工一次性运行 | `import_session_memories.py` | ✅ 可靠（一次性补全历史） |
| **③ 每日自动** | 系统 cron 每天 03:30 | `daily_memory.py` | ✅ 可靠（治本，保证"每天的对话都入库"） |

**为什么需要 ②③**：路径①是"软集成"——记忆 API 和 OpenClaw 之间没有任何强制机制，全靠大模型记得去调 API。实测下来模型经常不调，导致向量库长期接近空。**真正的"永久记忆"必须靠 ③ 这种平台外的自动管线兜底。**

### 历史回填脚本用法
```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 → 经代理 LLM 提炼成逐条事实 → 每条用 `infer=False` 快速写入 `opc_knowledge_base`（`source=session-import`）。

### 每日 cron（daily_memory.py）
- 自动发现所有装了 `opc-memory-skill` 的 agent
- 抽取"前一天(CST)"的对话 → 提炼成 5 小节摘要 → 写 `memory/<date>.md` + 逐条 `infer=False` 写向量库（`source=daily-batch`）
- cron：`30 3 * * * root flock -xn /tmp/opc-daily-memory.lock python3 daily_memory.py`
- 手动补跑：`python3 daily_memory.py 2026-05-25`

---

## 5. mem0 / Qdrant 关键机制

### infer=True vs infer=False（**最重要的一点**）
mem0 的 `add()` 默认 `infer=True`：会调 LLM 把内容**抽取成原子事实** + 做去重判断（ADD/UPDATE/NONE），**每次 add 触发多次 LLM 调用**。
- `infer=True`：适合塞入原始对话、让 mem0 自己提炼；但**慢**（多次 LLM 往返）
- `infer=False`：**跳过所有 LLM**，把内容原文 embedding 后直接入库（`main.py:_add_to_vector_store` 的 `if not infer:` 分支）

> 我们的摘要本身已经是干净的逐条事实，所以**一律用 `infer=False`**：单条热调用 ~2 秒；用 `infer=True` 则单条要 2 分钟、批量直接超时。
> `api_server.py` 的 `/kb/add` 已支持 `"infer": false` 入参（本次新增）。

### Collection / payload schema
- 向量：dense 1024 维（fastembed `thenlper/gte-large`, cosine）+ sparse `bm25`
- `opc_memories` payload：`agent_id`, `user_id`(=`bo_huang`), `type`(=conversation), `data`, `timestamp`
- `opc_knowledge_base` payload：**`added_by`**(=agent), `user_id`, `type`(=knowledge), `topic`, `source`, `data`, `timestamp`
- ⚠️ **两个 collection 的 agent 归属字段名不同**：memories 用 `agent_id`，kb 用 `added_by`。过滤计数时别搞错。

### 跨 agent 共享
所有写入都挂在 `user_id="bo_huang"` 下，对所有 agent 可见；各 agent 私有记忆靠 `agent_id`/`added_by` 隔离。

---

## 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 在知识库的记忆条数（注意 kb 用 added_by）
curl -s -X POST http://127.0.0.1:6333/collections/opc_knowledge_base/points/count \
  -H "Content-Type: application/json" \
  -d '{"exact":true,"filter":{"must":[{"key":"added_by","match":{"value":"xiaofan"}}]}}'

# 按 added_by / source 统计分布
curl -s -X POST http://127.0.0.1:6333/collections/opc_knowledge_base/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('added_by','?') for p in d['result']['points']))"

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

# 手动补跑某天的每日记忆
cd /root/opc-memory && python3 daily_memory.py 2026-05-25

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

---

## 8. 当前状态（2026-05-26）

- 历史回填完成：**xiaofan 22 / xiaoqi 14 / xiaozhen 166** 条（`opc_knowledge_base`）
- 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/*.bak.mem0-*                 # 原 mem0 版备份
/root/opc-memory/*.bak.before-lru-*           # 止血前备份
/root/opc-memory/*.bak.before-dedup-*         # 去重前备份
/root/opc-memory/full_opc_memories.json       # 全量冷备(68 条个人记忆)
/root/opc-memory/full_opc_knowledge_base.json # 全量冷备(2628 条知识库)
/root/.openclaw/extensions/opc-memory-inject/ # 防绕过插件
/root/bin/opc-memory-e2e.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 晚补）

**问题**：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。
