5.2 KiB
5.2 KiB
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()):
# 旧 (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 字段:
indexed_at = datetime.now(timezone.utc).isoformat()
# 每个 chunk payload: {text, source, file, indexed_at, fp}
查询侧: plugins/memory/qdrant/__init__.py
prefetch() 里 _recency_boost() 对结果重排:
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)
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()):
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 修复) |
验证命令
# 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