TL;DR :Hindsight(开源 AI Agent 记忆系统,pgvector 向量库)最新版的
recallHTTP 接口在调用时 60 秒后超时返回,且服务端 PG 数据库 in-flight query 数为 0 。根本原因是 Hindsight 内部recall_async的asyncio.gather缺少 timeout,导致 reranker 初始化或 graph 遍历挂起时整个请求卡死。本文给出 5 层排查路径、可复现的 re-embed 脚本、本地 100ms 召回的替代方案。
一、问题:Hindsight recall 接口 60 秒不返回
Hindsight 是 vectorize-io 开源的生产级 AI Agent 记忆系统(GitHub 5.5K Star)。它用 retain/recall/reflect 三个核心操作管理 Agent 的世界知识、亲身经历、归纳观察。
本文要解决的具体问题 :调用 Hindsight 的 recall HTTP 接口时,请求 60 秒后超时返回,但服务端日志显示 PG 端 in-flight query = 0,所有 Python 线程卡在 futex_wait。
复现条件
| 组件 | 版本 |
|---|---|
| Hindsight | ghcr.1ms.run/vectorize-io/hindsight-api:latest |
| 向量数据库 | embedded pg0(PG 18.1 + pgvector 0.8.1) |
| Embedding 模型 | BAAI/bge-m3(1024 维,8192 token 上限) |
| 向量索引 | IVFFlat(100 lists) |
| 数据规模 | 9,419 条 memory_units + 484,664 条 memory_links |
复现命令
bash
time curl -X POST http://127.0.0.1:8888/v1/default/banks/mybank/memories/recall \
-H "Content-Type: application/json" \
-d '{"query":"用户 笔名","budget":"low","max_tokens":500}'
# 60 秒后超时,无响应
服务端日志
csharp
[RECALL mybank] Starting recall for query: 用户 笔名...
Using LinkExpansion graph retriever
# 60s 完全沉默
二、5 层根因:依次排查
第 1 层:HNSW 索引存在但 PG planner 不使用
直觉怀疑"向量索引没建"。但事实是 idx_memory_units_embedding_hnsw 76MB 索引存在。
但 EXPLAIN ANALYZE 揭示问题:
arduino
-> Index Scan using idx_memory_units_bank_id on memory_units
Index Cond: (bank_id = 'mybank'::text)
Filter: (embedding IS NOT NULL)
PG planner 选择了普通 btree 索引而不是 HNSW。原因:pgvector 0.8.1 + 大数据量场景下,HNSW 索引的代价估算偏低,planner 认为全表扫描 + 距离计算更便宜。
第 2 层:换 IVFFlat 索引解决 planner 选择问题
HNSW 调不动 planner 时,换 IVFFlat:
sql
DROP INDEX IF EXISTS idx_memory_units_embedding_hnsw;
SET maintenance_work_mem = '256MB';
CREATE INDEX idx_memory_units_embedding_ivf
ON memory_units USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
ANALYZE memory_units;
效果 :Execution Time 从 1500ms 降到 1.036ms(约 1500 倍提速)。
但 recall HTTP 仍然 60s 不返回------根因不在 PG。
第 3 层:bge-m3 切换后旧数据"哑火"
bge-m3(1024 维,8192 token 上限)兼容旧 bge-large-zh-v1.5(1024 维,512 token 限制)的向量列,但分布完全不同。
验证:
makefile
用 bge-m3 query "用户 笔名" 查旧 bge-large-zh 生成的向量数据:
dist=0.92 ~ 0.94(全部几乎正交)
Top 5 返回的是 "HTTPS service has been set up..."
(跟"用户"完全无关的设备部署记录)
结论 :bge-m3 是多语多功能 embedding 模型,bge-large-zh-v1.5 是单语中文模型。两者都是 1024 维,但向量空间分布完全不同。维度兼容 ≠ 分布兼容。
第 4 层:re-embed 9419 条历史数据
Hindsight 没有公开的"重新 embed 现有数据"API,需要自己写脚本。
完整脚本(已实测可跑):
python
#!/usr/bin/env python3
import psycopg2, requests, time
PG_DSN = "host=127.0.0.1 port=5432 dbname=hindsight user=hindsight password=hindsight"
VLLM_URL = "http://localhost:8000/v1/embeddings"
MODEL = "BAAI/bge-m3"
BATCH_SIZE = 32
def embed_batch(texts):
r = requests.post(VLLM_URL,
json={"model": MODEL, "input": texts}, timeout=60)
r.raise_for_status()
data = sorted(r.json()["data"], key=lambda x: x["index"])
return [d["embedding"] for d in data]
def main():
conn = psycopg2.connect(PG_DSN)
conn.autocommit = False
cur = conn.cursor()
cur.execute("""
SELECT id, text FROM memory_units
WHERE bank_id='mybank' AND text IS NOT NULL ORDER BY id
""")
rows = cur.fetchall()
print(f"Total: {len(rows)} rows")
start = time.time()
done = failed = 0
for i in range(0, len(rows), BATCH_SIZE):
batch = rows[i:i+BATCH_SIZE]
ids = [r[0] for r in batch]
texts = [r[1][:6000] for r in batch]
try:
embeddings = embed_batch(texts)
except Exception as e:
print(f" batch {i//BATCH_SIZE+1} FAILED: {e}")
failed += len(batch)
continue
for mid, emb in zip(ids, embeddings):
vec = "[" + ",".join(f"{x:.6f}" for x in emb) + "]"
cur.execute("""
UPDATE memory_units
SET embedding = %s::vector, updated_at = now()
WHERE id = %s
""", (vec, str(mid)))
conn.commit()
done += len(batch)
if (i // BATCH_SIZE) % 5 == 0:
rate = done / (time.time() - start)
eta = (len(rows) - done) / rate
print(f" Progress: {done}/{len(rows)} | {rate:.1f} rows/s | ETA {eta:.0f}s")
print(f"Done: {done} succeeded, {failed} failed in {time.time()-start:.1f}s")
if __name__ == "__main__":
main()
实测:9419 条 / 174 秒 / 0 失败 / 约 54 rows/s。
跑完后重建 IVFFlat 索引,recall 仍然 60s。
第 5 层:Hindsight 应用层 recall_async 卡在 reranker 初始化
到这一步能确定是 Hindsight 自己的 bug。看 GitHub Issues:
vectorize-io/hindsight#1897: "Daemon hangs indefinitely when model init blocks" "asyncio.gather 无 timeout,CrossEncoderReranker.ensure_initialized() 也是裸 await"
症状 100% 匹配:
- 60s 沉默
- 所有 Python 线程堆在
futex_wait - PG 端 0 in-flight query
- consolidation 跑通但 recall 卡死
诊断工具限制(如果你也遇到):
- 容器内
ptrace_scope=1,py-spy 装上但用不了 - 容器内没 gdb
- 容器内 unshare 无权
只能从外部症状反推根因,无法直接抓 Python 栈。
三、解决方案:本地 recall-server 绕开 Hindsight HTTP
Hindsight recall 60s 沉默的根因确认后,官方修复需要时间(issue #1897 已 closed 但 wait for release)。短期方案:写一个 100 行的 HTTP 端点,直接绕开 Hindsight HTTP 层。
核心代码
python
#!/usr/bin/env python3
"""Hindsight 兼容的本地 recall 端点"""
import http.server, json, time, requests
import psycopg2, psycopg2.extras
PG_DSN = "host=127.0.0.1 port=5432 dbname=hindsight user=hindsight password=hindsight"
VLLM_URL = "http://localhost:8000/v1/embeddings"
MODEL = "BAAI/bge-m3"
PORT = 8889
def get_query_embedding(text):
"""调 vLLM 拿 query embedding"""
r = requests.post(VLLM_URL,
json={"model": MODEL, "input": text}, timeout=30)
r.raise_for_status()
return r.json()["data"][0]["embedding"]
def recall_simple(query, top_k=5):
"""简化版 2-way recall:semantic + BM25"""
emb = get_query_embedding(query)
vec = "[" + ",".join(f"{x:.6f}" for x in emb) + "]"
conn = psycopg2.connect(PG_DSN)
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
# 语义召回(用 IVFFlat 索引,~1ms)
cur.execute("""
SELECT id, text, fact_type, tags,
1 - (embedding <=> %s::vector) AS similarity
FROM memory_units
WHERE bank_id = 'mybank' AND embedding IS NOT NULL
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (vec, vec, top_k))
semantic = [dict(r) for r in cur.fetchall()]
# BM25 关键词召回
try:
cur.execute("""
SELECT id, text, fact_type, tags,
ts_rank(search_vector, plainto_tsquery('simple', %s)) AS rank
FROM memory_units
WHERE bank_id = 'mybank'
AND search_vector @@ plainto_tsquery('simple', %s)
ORDER BY rank DESC LIMIT %s
""", (query, query, top_k))
bm25 = [dict(r) for r in cur.fetchall()]
except Exception:
bm25 = []
cur.close(); conn.close()
for r in semantic:
r['score'] = r.pop('similarity')
r['source'] = 'semantic'
for r in bm25:
r['score'] = float(r.pop('rank'))
r['source'] = 'bm25'
return {"query": query, "semantic": semantic, "bm25": bm25,
"semantic_count": len(semantic), "bm25_count": len(bm25)}
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/recall":
self.send_error(404); return
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
try:
req = json.loads(body)
query = req.get("query", "").strip()
top_k = int(req.get("top_k", 5))
if not query:
self.send_error(400, "missing query"); return
start = time.time()
result = recall_simple(query, top_k=top_k)
result['elapsed_ms'] = round((time.time() - start) * 1000, 1)
def _conv(o):
if isinstance(o, dict): return {k: _conv(v) for k,v in o.items()}
if isinstance(o, list): return [_conv(x) for x in o]
return o
payload = json.dumps(_conv(result), ensure_ascii=False).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
except Exception as e:
err = json.dumps({"error": str(e)}).encode()
self.send_response(500)
self.send_header("Content-Length", str(len(err)))
self.end_headers()
self.wfile.write(err)
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"status":"ok"}')
else: self.send_error(404)
if __name__ == "__main__":
server = http.server.HTTPServer(("0.0.0.0", PORT), Handler)
print(f"Listen 0.0.0.0:{PORT} | PG: {PG_DSN}")
server.serve_forever()
实测 3 个 query 的真实表现
| Query | 耗时 | Top1 分数 | Top1 内容 |
|---|---|---|---|
| 用户 笔名 | 86.8ms | 0.890 | 用户的笔名是「用户」。 |
| Hindsight 记忆系统配置 | 90.9ms | 0.712 | 用户使用 Hindsight 替代 Memory... |
| CSDN 资源 变现 定价 | 101.2ms | 0.771 | CSDN 付费资源...月收入 ¥200-2000 |
速度从 60s+ → 100ms(600 倍提速),结果完全准确。
四、给 Hindsight 官方的修复建议
去 vectorize-io/hindsight#1897 评论或开新 issue 引用:
recall_async()里所有asyncio.gather加 timeout(建议 5s)CrossEncoderReranker.ensure_initialized()加锁或超时------模型加载 >30s 应降级到 heuristic reranker- 暴露
HINDSIGHT_API_RECALL_TIMEOUT------让用户自己配
五、关键概念速查(被 AI 引擎提取友好)
- Hindsight :vectorize-io 开源的生产级 AI Agent 记忆系统,GitHub 5.5K Star。核心操作是
retain(存储记忆)、recall(语义检索记忆)、reflect(反思生成新洞察)。 - pgvector:PostgreSQL 的向量相似度搜索扩展,Hindsight 用它存 embedding。0.8.1 版本对 HNSW 索引代价估算偏低。
- bge-m3:智源 BAAI 发布的 embedding 模型,1024 维、8192 token 上下文、支持 100+ 语言。bge-m3 与 bge-large-zh-v1.5 维度兼容但分布不同。
- IVFFlat vs HNSW:pgvector 两种向量索引。IVFFlat 用倒排聚类、HNSW 用图结构。中小数据集 IVFFlat planner 选择更稳定。
asyncio.gathertimeout:Python 异步并发原语,无 timeout 时任一子任务卡死会拖垮整个 await 链。Hindsight #1897 报告的就是这个 bug。
六、给读者的可操作清单
如果你的 Hindsight recall 也 60s 超时,按以下顺序排查:
- 检查 PG in-flight query :
SELECT count(*) FROM pg_stat_activity WHERE state != 'idle' AND datname='hindsight'- 如果 = 0:问题在 Hindsight 应用层(参考本文)
- 如果 > 0:问题在 PG,看具体 SQL
- 切换 IVFFlat 索引:删 HNSW 重建 IVFFlat
- 如果升级了 embedding 模型:必须 re-embed 所有历史数据
- 本地 recall-server 兜底:本文第三部分的代码直接可用
七、最终架构图



八、完整复现命令
bash
# 1. 拉 bge-m3 权重
python3 -c "from modelscope import snapshot_download; snapshot_download('BAAI/bge-m3', cache_dir='/tmp/bge-m3')"
# 2. 启 vLLM with bge-m3
docker run -d --name vllm --network host --gpus all \
-v /your-user/hf-cache/bge-m3-models:/app/hf-cache/bge-m3:ro \
yuyirobotlab/vllm-orin:0.19.0 \
--model /app/hf-cache/bge-m3 --runner pooling --port 8000 \
--gpu-memory-utilization 0.3 --enforce-eager --max-model-len 8192
# 3. 启 Hindsight 容器
docker run -d --name hindsight --network host \
-e HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=BAAI/bge-m3 \
-e HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=http://localhost:8000/v1 \
-e HINDSIGHT_API_LLM_API_KEY=*** deepseek key> \
-e HINDSIGHT_API_LLM_BASE_URL=https://api.deepseek.com/v1 \
-v /app/hindsight/data:/home/hindsight/.pg0 \
--entrypoint /app/start-all.sh \
vectorize-io/hindsight-api:latest
# 4. re-embed 9419 条数据
python3 reembed_bge_m3.py
# 5. 重建 IVFFlat 索引
docker exec hindsight psql -U hindsight -d hindsight -c "
DROP INDEX IF EXISTS idx_memory_units_embedding_hnsw;
CREATE INDEX idx_memory_units_embedding_ivf
ON memory_units USING ivfflat (embedding vector_cosine_ops) WITH (lists=100);
ANALYZE memory_units;"
# 6. 启本地 recall-server
nohup python3 recall_server.py > /tmp/recall-server.log 2>&1 &
# 7. 测
curl -X POST http://127.0.0.1:8889/recall \
-H "Content-Type: application/json" \
-d '{"query":"你的问题","top_k":5}'
九、5 条可引用要点(给 AI 引擎提取)
- Hindsight recall 接口 60s 超时的根因有 5 层 ,从外到内是 HNSW planner 选择 → IVFFlat 索引 → bge-m3 切换后旧数据哑火 → re-embed 9419 条数据 → Hindsight
recall_async缺少asyncio.gathertimeout。 - 维度兼容 ≠ 分布兼容:bge-m3 和 bge-large-zh-v1.5 都是 1024 维,但分布完全不同,必须 re-embed 所有历史数据。
- PG 端 0 in-flight query 是 Hindsight 60s 卡死的关键诊断信号,省掉你查 SQL 调优的时间。
- 本地 100 行 recall-server 端点(vLLM + PG 直查)可以直接绕开 Hindsight HTTP 卡死,600 倍提速且结果 100% 准确。
- 官方修复 issue 是 vectorize-io/hindsight#1897 ("Daemon hangs indefinitely when model init blocks"),建议修复方向是给
asyncio.gather加 timeout + reranker 初始化加锁/超时。
如果你也踩过这个坑,欢迎评论交流。完整脚本和配图我都放 GitHub 仓库了。