# Qdrant Memory Provider 接入 + 时间衰减 (2026-08-13) ## 接入状态 | # | 项 | 状态 | |---|---|---| | 1 | `config.yaml` 启用 `memory.provider: qdrant` | ✅ | | 2 | `hermes memory status` 显示 Provider: qdrant | ✅ | | 3 | 容器 `hermes-qdrant` 运行中 (7+ 天) | ✅ | | 4 | 向量库 263 points | ✅ | | 5 | 同步 cron `e9278f724fc8` 每小时运行 | ✅ | | 6 | 新 session 自动 prefetch top-3 → system prompt | ✅ | | 7 | `qdrant_recall` 工具暴露 | ✅ | | 8 | **时间衰减** (indexed_at + recency_half_life_days) | ✅ 2026-08-13 新增 | ## 架构 ``` ~/.hermes/hermes-agent/ plugins/memory/qdrant/__init__.py ← QdrantProvider (prefetch + recall) agent/agent_init.py line 1360 ← load_memory_provider() 自动加载 ~/.hermes/config.yaml ← memory.provider: qdrant ~/.hermes/qdrant/ index_memory.py ← 写入侧: chunk_text() 加 indexed_at sync_memory.sh ← cron wrapper: MEMORY.md/USER.md/git history ``` ## ⚠️ qdrant_client 1.18 API 破坏性变更 (2026-08-13 实测) **问题**: `qdrant_client 1.18` 移除了 `client.search()` 方法,改用 `client.query_points()`。 **错误信息**: ``` AttributeError: 'QdrantClient' object has no attribute 'search' ``` **修复** (`plugins/memory/qdrant/__init__.py` `_query()`): ```python # 旧 (qdrant_client < 1.18): results = self._client.search( collection_name=self._collection, query_vector=query_vector, limit=top_k, score_threshold=min_score, with_payload=True, ) # 新 (qdrant_client 1.18+): from qdrant_client.models import ExactCountQuery, ScrollQuery results = self._client.query_points( collection_name=self._collection, query=ExactCountQuery(**{"exact": False}), query_vector=query_vector, limit=top_k, score_threshold=min_score, with_payload=True, ).points ``` **验证**: `python3 -c "from qdrant_client import QdrantClient; print(hasattr(QdrantClient, 'search'), hasattr(QdrantClient, 'query_points'))"` → `False True` **Server 版本**: qdrant server 1.13, client 1.18, minor 差 5 → 有警告但功能正常: ``` Qdrant client version 1.18.0 is incompatible with server version 1.13.0. Major versions should match and minor version difference must not exceed 1. ``` ## 时间衰减机制 (2026-08-13 新增) ### 写入侧: `~/.hermes/qdrant/index_memory.py` `chunk_text()` 每个 chunk 加 `indexed_at` 字段: ```python indexed_at = datetime.now(timezone.utc).isoformat() # 每个 chunk payload: {text, source, file, indexed_at, fp} ``` ### 查询侧: `plugins/memory/qdrant/__init__.py` `prefetch()` 里 `_recency_boost()` 对结果重排: ```python def _recency_boost(self, results, half_life_days=60): """按 indexed_at 时间衰减, 新笔记优先""" for r in results: age_days = (now - indexed_at).days decay = 0.5 ** (age_days / half_life_days) # 60天前半衰 r['score'] = r['score'] * decay # 按 combined_score 降序重排 → 截 top_k ``` ### 衰减效果 (实测) | 笔记 | 原始 score | 年龄 | 衰减后 | |---|---|---|---| | 2026-08-13 新笔记 (今天, indexed_at 有) | 0.65 | 0 天 ×1.0 | **0.65** | | 2026-08-04 旧笔记 (无 indexed_at) | 0.65 | ~1 年 | **0.19** (×0.3) | 结果: 旧笔记自动沉底,新笔记冲到 top。 ### 配置项 (`config.yaml`) ```yaml memory: provider: qdrant qdrant: host: 127.0.0.1 port: 6333 collection: hermes_memory embed_model: BAAI/bge-small-zh-v1.5 top_k: 3 min_score: 0.3 enabled: true recency_half_life_days: 60 # 新增: 半衰期(天) overfetch: 4 # 新增: 多召回倍数(给重排留余地) ``` ## BOM + 截断 UTF-8 兼容 (2026-08-13) **问题**: `MEMORY.md` 有 UTF-8 BOM (0xEF 0xBB 0xBF),且末尾截断(最后字节 0xBB 不完整)。 **报错**: ``` UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 1758-1759: unexpected end of data ``` **修复** (`~/.hermes/qdrant/index_memory.py` `index_file()`): ```python try: text = file_path.read_text(encoding='utf-8-sig', errors='ignore') except Exception: text = file_path.read_text(encoding='utf-8', errors='ignore') ``` ## git 提交 | 仓库 | 提交 | 内容 | |---|---|---| | `~/.hermes/qdrant/` (新 init) | `ae090fe` | index_memory.py (indexed_at + UTF-8 容错) + sync_memory.sh + docker-compose.yml | | `~/.hermes/hermes-agent/` (fork) | `e969ec498d` | plugins/memory/qdrant/__init__.py (时间衰减 + overfetch + query_points 修复) | ## 验证命令 ```bash # 1. provider 状态 hermes memory status # 2. prefetch 召回测试 (新 session) hermes chat -q "lottery Qi Nq 期号到底用哪个" # 3. 衰减验证 (Python) cd ~/.hermes/hermes-agent && PYTHONPATH=. venv/bin/python -c " from plugins.memory.qdrant import QdrantProvider p = QdrantProvider({'host':'127.0.0.1','port':6333,'collection':'hermes_memory','top_k':3,'min_score':0.2,'recency_half_life_days':60,'overfetch':6}) p.initialize('test') print(p.prefetch('六合 cron Nq 期号', session_id='test')) " # 4. is_lock_expired 验证 (锁不过期) cd ~/.hermes/skills/trading/okx-auto-position/scripts python3 -c "from signal_tracker import is_lock_expired; print(is_lock_expired(None), is_lock_expired({}))" # 输出: False False ```