164 lines
5.2 KiB
Markdown
164 lines
5.2 KiB
Markdown
# 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
|
||
```
|