Hindsight 记忆数据增删改实操:70 个 API 端点的 CRUD 地图

预计阅读时间:25 分钟

一、先看全景:70 个端点的 CRUD 地图

Hindsight 的 API 按资源组织:memories(记忆/事实)、documents(原始文档)、entities(实体)、operations(异步任务)、banks(记忆库)、config(配置)、directives/mental-models/webhooks(扩展能力)。

操作 端点 说明 实测耗时
POST /v1/default/banks/{bank_id}/memories retain:写入记忆(核心) sync 17.9s / async 54ms
POST /v1/default/banks/{bank_id}/files/retain 文件写入
PUT /v1/default/banks/{bank_id} 创建/更新 bank
GET /v1/default/banks/{bank_id}/memories/list 列出记忆 56ms
GET /v1/default/banks/{bank_id}/memories/{memory_id} 单条详情 17ms
POST /v1/default/banks/{bank_id}/memories/recall 语义召回 150-270ms
GET /v1/default/banks/{bank_id}/documents 列出文档 58ms
GET /v1/default/banks/{bank_id}/operations/{operation_id} 异步任务状态 秒级
PATCH /v1/default/banks/{bank_id}/documents/{document_id} 更新文档 tags(传播) 75ms
POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocess 用原参数重新提取
PATCH /v1/default/banks/{bank_id}/config 更新 bank 配置
DELETE /v1/default/banks/{bank_id}/documents/{document_id} 删文档 + 级联删事实 68ms
DELETE /v1/default/banks/{bank_id}/memories?type=world 按事实类型删 30ms
DELETE /v1/default/banks/{bank_id}/memories 清空整个 bank 的记忆 53ms
DELETE /v1/default/banks/{bank_id}/memories/{memory_id}/observations 清某事实派生的 observation 11ms
DELETE /v1/default/banks/{bank_id} 删除整个 bank
🚫 DELETE /v1/default/banks/{bank_id}/memories/{memory_id} 不存在!405 Method Not Allowed 6ms

核心认知:Hindsight 没有「单条 memory 删除」memories 是事实的集合,删除粒度是 document(级联)、fact_type(类型)、bank(整体)。这跟关系数据库的 DELETE FROM table WHERE id=1 完全不同——设计上「事实不可单删」是为了保护图谱一致性(删除一条事实会牵连 entities、links、observations)。

1.1 数据库 CRUD 与 Hindsight CRUD 的本质差异

维度 关系数据库 Hindsight 0.8.0
写入成本 毫秒级 INSERT 秒级(LLM 抽取 + embedding)
单条删除 DELETE WHERE id=1 不支持(405)
删除粒度 document / fact_type / bank
修改内容 UPDATE SET col=... 只能改 tags,内容走 reprocess
一致性 外键约束 级联 + 异步图谱维护
写入并发 天然支持 建议 async 批量 + operation 跟踪

为什么设计成这样? 因为 Hindsight 的「一条记录」不是数据库的一行——一次 retain 的 content 会被 LLM 抽取成多条 fact(experience/world/observation),还要挂实体、建图谱边。删除单条 fact 意味着要处理它牵连的所有实体关系,API 层干脆不暴露这个粒度,把复杂度收敛到 document 级。理解了这个设计哲学,你操作 Hindsight 数据时就不会用「数据库思维」去套。


二、增:retain 的三种模式

2.1 同步模式(sync):17.9s 拿到完整结果

import urllib.request, json

def api(method, path, body=None, timeout=180):
    url = "http://<your-server>:8888" + path
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode())

# 同步写入一条记忆
resp = api("POST", "/v1/default/banks/test_bank/memories", {
    "items": [{
        "content": "Hindsight CRUD 实验:单条写入测试,验证 retain 同步模式",
        "context": "test/crud",
        "tags": ["topic:code", "stage:process"],
        "metadata": {"experiment": "crud-20260821", "step": "1-create"},
    }],
    "async": False,   # 同步模式
})

print(resp)
# {'success': True, 'items_count': 1, 'async': False, 'operation_id': None,
#  'usage': {'input_tokens': 3029, 'output_tokens': 2628, 'total_tokens': 5657}}

实测 17921ms。为什么这么慢?因为同步模式要等完整管线跑完:分块 → LLM 抽取事实(DeepSeek)→ 实体解析 → embedding → 写入。返回里带 usage(token 消耗),这是同步模式独有的——你能精确知道一次写入花了多少 token。

2.2 异步模式(async):54ms 返回,后台 25s 完成

resp = api("POST", "/v1/default/banks/test_bank/memories", {
    "items": [{"content": f"批量写入第 {i} 条", "context": "test/crud-batch",
               "tags": ["topic:code", "stage:process"],
               "metadata": {"idx": str(i)}} for i in range(10)],
    "async": True,    # 异步模式
})
print(resp)
# {'success': True, 'items_count': 10, 'async': True,
#  'operation_id': '24259077-8873-4e2e-be35-c568c62eafd5', 'usage': None}

实测 54ms 返回 HTTP,后台 25 秒完成。异步模式返回 operation_id,用轮询查状态:

import time
op_id = resp["operation_id"]
for i in range(12):
    time.sleep(5)
    st = api("GET", f"/v1/default/banks/test_bank/operations/{op_id}")
    print(f"t+{(i+1)*5}s status={st['status']}")
    if st["status"] == "completed":
        break
# t+5s  status=pending
# t+10s status=pending
# t+20s status=pending
# t+25s status=completed

关键认知:async 不等于快,等于「不阻塞」。10 条异步总耗时 25s,跟同步单条 18s 相比,10 条才花了 1.4 倍时间——这才是批量写入的正确姿势。

2.3 三种模式的决策表

模式 HTTP 返回 总耗时 适用场景
同步单条 17.9s 17.9s 需要立即知道结果/耗 token 精确值
异步单条 54ms ~5-10s 不阻塞主流程
异步批量 10 条 54ms 25s 批量写入(推荐)

2.4 踩坑 1:metadata 的值必须是字符串

HTTP 422: Input should be a valid string
{"type": "string_type", "loc": ["body", "items", 0, "metadata", "idx"], "msg": "..."}

metadatadict[str, str]——传 int 直接 422。必须 str(i)

2.5 踩坑 2:同步 vs 异步的事实类型分布不同

同样内容,同步写入生成 observation + world(2 条),异步批量生成 experience + world。原因:同步模式跑完了完整 extraction + consolidation,异步模式只完成 extraction,observation 等 consolidation 阶段才生成。如果你依赖 observation 做召回,同步模式更可预期

2.6 踩坑 3:一条 content 会生成多条 fact(双语展开)

10 条 content 实测生成 12 条 memory(1.2x)。Hindsight 0.8.0 会自动把中文事实翻译成英文(fact_type=world),实现双语召回。不要试图去重——这是设计。


三、查:list / get / recall

3.1 list:56ms 列出记忆

items = api("GET", "/v1/default/banks/test_bank/memories/list?limit=50")
for m in items["items"][:5]:
    print(f"{m['id'][:8]} type={m.get('fact_type')} tags={m.get('tags')}")
# 999cf0be type=observation tags=['stage:process', 'topic:code']
# c34f1d81 type=world       tags=['stage:process', 'topic:code']

3.2 recall:150-270ms 语义召回

r = api("POST", "/v1/default/banks/test_bank/memories/recall",
        {"query": "Hindsight CRUD 批量写入实验", "limit": 5})
# hits=13, 端到端 281ms

recall 返回的字段:id / text / type / entities / context / occurred_start / occurred_end / mentioned_at / document_id / metadata / chunk_id / tags / source_fact_ids。注意没有分数字段——召回结果在服务端已经 rerank 完,返回的是最终排序。

3.3 性能对比表

操作 实测延迟
GET single memory 17ms
GET memories/list 56ms
GET documents 58ms
POST recall 150-270ms
GET operations 秒级

四、改:PATCH document 的传播语义

4.1 改 tags:75ms,一处改、全链传播

# 找到 document
docs = api("GET", "/v1/default/banks/test_bank/documents")
doc_id = docs["documents"][0]["id"]

# 改 document tags
r = api("PATCH", f"/v1/default/banks/test_bank/documents/{doc_id}",
        {"tags": ["topic:code", "stage:decision", "tag:patched"]})
# {'success': True} 75ms

# 验证:document 的 tags 已更新
doc = api("GET", f"/v1/default/banks/test_bank/documents/{doc_id}")
# tags=['topic:code', 'stage:decision', 'tag:patched']

# 验证:关联的所有 memory units 的 tags 也被传播
items = api("GET", "/v1/default/banks/test_bank/memories/list?limit=50")
patched = [m for m in items["items"] if "tag:patched" in m.get("tags", [])]
# 10 条全部带上了新 tag!

源码语义(memory_engine.py update_document):改 document tags 会: 1. UPDATE documents SET tags=$1(75ms) 2. UPDATE memory_units SET tags=$1 WHERE document_id=$2——级联传播到全部关联事实 3. 找到引用这些事实的 observation,删除它们(invalidation) 4. 重置 consolidated_at 触发重新 consolidation

注意update_document 只支持改 tags,不支持改 content——改内容要走 reprocess。

4.2 reprocess:用原参数重新提取

r = api("POST", f"/v1/default/banks/test_bank/documents/{doc_id}/reprocess")
# 返回 {'success': True, 'operation_id': ..., 'items_count': ...}

源码逻辑:get_document() 取回 original_text + retain_params,用 update_mode=replace 重新跑 retain。适合「提取规则升级后重灌」或「当时提取有误想重跑」。


五、删:没有单条 DELETE,只有三种粒度

5.1 边界确认:单条 DELETE 是 405

r = api("DELETE", f"/v1/default/banks/test_bank/memories/{some_memory_id}")
# HTTP 405: {"detail": "Method Not Allowed"}  6ms

Hindsight 0.8.0 没有 DELETE /memories/{id} 端点。想删单条事实,只有两条路: 1. 删它所属的 document(级联,但会连同文档其他事实一起删) 2. 用 DELETE /memories/{id}/observations 只清它派生的 observation(保留事实本身)

5.2 删 document:68ms 级联删 10 条

r = api("DELETE", f"/v1/default/banks/test_bank/documents/{doc_id}")
# {'success': True,
#  'message': "Document '90bd...' and 10 associated memory units deleted successfully",
#  'memory_units_deleted': 10}

源码里 delete_document 的完整清理链: 1. 捕获要删的 memory unit IDs(先于级联,否则 join 找不到) 2. enqueue_relink_victims——把「引用这些实体的其他记忆」加入 relink 队列(图谱维护) 3. DELETE FROM documents WHERE id=$1——CASCADE 删 memory_units + 全部 links 4. _delete_stale_observations_for_memories——删除引用这些事实的 observation(在删除之后跑,能捕获并发插入的孤儿 observation) 5. 触发异步 consolidation + graph maintenance

删不存在的 document 返回 404Document not found),幂等性靠 404 而非「删除 0 行」。

5.3 按类型删 / 清空 bank

# 只删 world 类型(英文翻译版)
r = api("DELETE", "/v1/default/banks/test_bank/memories?type=world")
# {'success': True}  30ms

# 清空整个 bank 的所有记忆
r = api("DELETE", "/v1/default/banks/test_bank/memories")
# {'success': True}  53ms

# 删除整个 bank(含文档、实体、图谱)
r = api("DELETE", "/v1/default/banks/test_bank")
# {'success': True, 'message': "Bank 'test_bank' and all associated data deleted successfully"}

5.4 删除粒度决策表

想删什么 用什么 连带影响
单条事实 ❌ 无 API 需删 document 或 PG 直操作
单条事实的 observation DELETE /memories/{id}/observations 保留事实,重置 consolidated_at 重新合并
整个文档的所有事实 DELETE /documents/{id} 级联删 memory_units + links + observations
某种类型的事实(world) DELETE /memories?type=world 只删该类型,实体保留
整个 bank DELETE /banks/{id} 全删(文档/实体/图谱/配置)

六、高效操作实战:一次完整的 CRUD 流程

把上面所有操作串成一个真实可跑的脚本(2026-08-21 实测通过,测试 bank 已清理):

import urllib.request, json, time

BASE = "http://<your-server>:8888"
BANK = "test_crud_demo"

def api(method, path, body=None, timeout=180):
    url = BASE + path
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={"Content-Type": "application/json"})
    t0 = time.time()
    with urllib.request.urlopen(req, timeout=timeout) as r:
        raw = r.read().decode()
        return json.loads(raw), (time.time() - t0) * 1000

# 1. 增:异步批量(推荐)
resp, dt = api("POST", f"/v1/default/banks/{BANK}/memories", {
    "items": [{"content": f"CRUD 演示第 {i} 条:Hindsight 批量写入测试",
               "tags": ["topic:code", "stage:process"],
               "metadata": {"demo": "true", "idx": str(i)}} for i in range(10)],
    "async": True,
})
print(f"增: {dt:.0f}ms, op={resp['operation_id'][:8]}")
# 增: 54ms

# 2. 等异步完成(轮询 operation)
for _ in range(10):
    time.sleep(5)
    st, _ = api("GET", f"/v1/default/banks/{BANK}/operations/{resp['operation_id']}")
    if st["status"] == "completed":
        break

# 3. 查:list + recall
items, dt = api("GET", f"/v1/default/banks/{BANK}/memories/list?limit=50")
print(f"查 list: {dt:.0f}ms, {len(items['items'])} 条")
rec, dt = api("POST", f"/v1/default/banks/{BANK}/memories/recall",
              {"query": "Hindsight 批量写入测试", "limit": 5})
print(f"查 recall: {dt:.0f}ms, {len(rec['results'])} hits")

# 4. 改:PATCH document tags
docs, _ = api("GET", f"/v1/default/banks/{BANK}/documents")
doc_id = docs["documents"][0]["id"]
_, dt = api("PATCH", f"/v1/default/banks/{BANK}/documents/{doc_id}",
            {"tags": ["topic:code", "stage:decision", "demo:patched"]})
print(f"改: {dt:.0f}ms")

# 5. 删:DELETE document(级联)
del_resp, dt = api("DELETE", f"/v1/default/banks/{BANK}/documents/{doc_id}")
print(f"删: {dt:.0f}ms, {del_resp.get('memory_units_deleted')} 条级联删除")

# 6. 清理:删整个 bank
api("DELETE", f"/v1/default/banks/{BANK}")
print("清理完成")

输出(真实实测):

增: 54ms
查 list: 56ms, 12 条
查 recall: 281ms, 13 hits
改: 75ms
删: 68ms, 10 条级联删除
清理完成

七、给 Agent 开发者的操作建议

  1. 默认异步批量:单条同步 18s,10 条异步也才 25s。写入场景一律 async=True + operation 轮询,除非你需要同步拿 token 消耗。
  2. 删前先查:没有单条删除,DELETE /documents/{id} 是级联——删之前 GET /documents 确认这个 document 下有多少事实,避免误删。
  3. 测试用独立 bankDELETE /banks/{id} 可以整体清理,测试数据永远用独立 bank(test_crud_xxx),用完删 bank,零污染生产数据。这是本文实验的隔离方式。
  4. metadata 值全部字符串化dict[str, str] 是硬约束,传 int/bool 会 422。
  5. 改 tags 会触发 observation 重建:PATCH document 不是「轻量改元数据」——它会删旧 observation + 触发重新 consolidation。高频改标签要考虑这个成本。

八、版本迁移注意:0.7.1 → 0.8.0 路径变了

如果你是从旧版 Hindsight 升级上来的,CRUD 端点路径在 0.8.0 有破坏性变更,照着旧文档调会全部 404

操作 0.7.1 路径 0.8.0 路径
召回 POST /banks/{id}/recall POST /banks/{id}/memories/recall
列记忆 GET /banks/{id}/memories GET /banks/{id}/memories/list
配置 PATCH 平铺 body 必须 {"updates": {...}} 包装
写入 POST /banks/{id}/retain(部分版本) POST /banks/{id}/memories
统计 GET /banks/{id}/stats 不变(仍可用,但异步刷新有 lag)

stats 端点有 30-60 秒的刷新 lag——异步 retain 后立即查 stats 会看到数字没动,不要误判写入失败。可靠的验真方式是 GET /memories/list 直接搜刚写入的内容。

九、常见错误场景速查(全部实测)

场景 返回 含义
DELETE /memories/{id} 405 Method Not Allowed 端点不存在,删除粒度不支持单条
metadata 含 int 422 string_type 值必须 str
DELETE /documents/{不存在} 404 Document not found 幂等靠 404
async retain 后立即查 stats 数字没变 stats lag,30-60s 后刷新
PATCH /documents/{id} body 无 tags 422 At least one field 目前只支持改 tags
async=False 且服务端开了 batch API 400 大批次必须异步

十、什么时候该绕过 API 直操 PostgreSQL

Hindsight 的 API 刻意不暴露单条删除——但在运维场景(清重复数据、修脏标签、紧急回滚)你可能真的需要。建议路径

  1. 首选 API:document 级联删除(DELETE /documents/{id})覆盖 95% 的「删一批」需求
  2. 次选 APIDELETE /memories?type=xxx 按类型清理(如清空全部英文翻译版 world 事实)
  3. 最后手段 PG 直操作:Hindsight 数据在容器的 PostgreSQL 里(memory_units 表),直接 DELETE FROM memory_units WHERE ... 可以做到单条级精度——但必须同时清理 memory_linksunit_entitiesobservation_sources 的关联行,否则图谱会残留孤儿引用导致召回异常。这也是 API 层不提供单条删除的原因——级联逻辑太容易漏。

操作规范:PG 直操作前先备份(pg_dump 该 bank 相关表),操作后手动触发 POST /banks/{id}/consolidation/recover 修复维护状态。

十一、70 端点速查表(完整)

记忆与检索(核心)

方法 路径 用途
POST /banks/{id}/memories 写入(retain)
POST /banks/{id}/memories/recall 语义召回
GET /banks/{id}/memories/list 列出
GET /banks/{id}/memories/{memory_id} 单条详情
GET /banks/{id}/memories/{memory_id}/history observation 变更历史
DELETE /banks/{id}/memories 清空(可按 type)
DELETE /banks/{id}/memories/{id}/observations 清某条派生 observation
GET /banks/{id}/graph 记忆图谱

文档(写入的原始载体)

方法 路径 用途
GET /banks/{id}/documents 列文档
GET /banks/{id}/documents/{doc_id} 文档详情
GET /banks/{id}/documents/{doc_id}/chunks 分块列表
PATCH /banks/{id}/documents/{doc_id} 改 tags(传播)
DELETE /banks/{id}/documents/{doc_id} 删文档(级联)
POST /banks/{id}/documents/{doc_id}/reprocess 重新提取
GET /banks/{id}/tags 标签列表

实体与图谱

方法 路径 用途
GET /banks/{id}/entities 列实体
GET /banks/{id}/entities/graph 实体图
GET /banks/{id}/entities/{entity_id} 实体详情
POST /banks/{id}/entities/{entity_id}/regenerate 重生成实体 observation

异步任务

方法 路径 用途
GET /banks/{id}/operations 任务列表
GET /banks/{id}/operations/{op_id} 任务状态
DELETE /banks/{id}/operations/{op_id} 取消任务
POST /banks/{id}/operations/{op_id}/retry 重试失败任务

Bank 与配置

方法 路径 用途
GET /banks bank 列表
PUT /banks/{id} 创建/更新 bank
PATCH /banks/{id} 更新 bank
DELETE /banks/{id} 删除 bank
GET /banks/{id}/stats 统计(有 lag)
GET /banks/{id}/config 读配置
PATCH /banks/{id}/config 改配置
DELETE /banks/{id}/config 重置配置
POST /banks/{id}/consolidate 手动触发合并
POST /banks/{id}/consolidation/recover 修复合并状态
GET /banks/{id}/profile 人格档案
POST /banks/{id}/background 添加背景
POST /banks/{id}/import 导入模板
GET /banks/{id}/export 导出模板
POST /banks/{id}/document-transfer 批量导文档
POST /banks/{id}/reflect 反思
GET /banks/{id}/mental-models 心智模型
GET /banks/{id}/directives 指令列表
GET /banks/{id}/audit-logs 审计日志
GET /banks/{id}/llm-requests LLM 调用追踪

完整 70 端点含 webhooks 管理(增删改查 5 个)、audit-logs/stats、llm-requests/stats 等,均为同构 REST。核心链路就是上表的记忆/文档/任务三块。

十二、高效操作五连(来自实测的最终建议)

  1. 写入一律 async:同步 18s vs 异步 54ms 返回——HTTP 层不阻塞是硬收益,后台完成时间两种模式几乎一样。唯一例外是你要精确统计 token 消耗(async 返回 usage: null)。
  2. 批量上限经验值:一次 10 条 async 后台 25s 完成,没有限流报错。大批量导入(数百条)建议分片(每片 10-20 条)+ 每片一个 operation_id 跟踪,避免单任务过长。
  3. 验真用 list 不用 statsGET /memories/list 是即时的,GET /stats 有 30-60s 异步 lag。判断「写入成功没」永远查 list,别查 stats 然后以为失败了。
  4. 测试永远用独立 bankPOST /banks/{test_id}/memories 自动创建 bank,最后 DELETE /banks/{test_id} 一键清理。生产 bank 里做实验 = 数据污染事故。
  5. 删除前先想级联DELETE /documents/{id} 会连带删掉该文档提取的所有事实(实测 10 条)。如果只想删一条,考虑「reprocess 换内容」或「PG 直操作 + 手动清理关联表」,不要盲目删 document。

十三、写入参数详解(retain 的 items 结构)

retain 请求体的每个 item 支持这些字段,用途和坑一次说清:

字段 类型 用途
content str 原始文本(必填) 会被 LLM 抽取成多条 fact
context str 上下文标签 帮助检索
timestamp str 事件时间 省略 = 无时间约束
metadata dict[str,str] 业务元数据 值必须 str,int 会 422
document_id str 归组标识 相同 id 会被当作同一文档处理
entities list 显式实体 省略 = LLM 自动提取
tags list[str] 标签 前缀格式 topic:xxx / stage:xxx
observation_scopes str 合并范围 per_tag 按标签分组合并
update_mode str 更新模式 append / replace
strategy str 写入策略 分组按策略处理

最后提醒:本文所有实验都跑在独立测试 bank 上,用完即删——这也是你上手 Hindsight 数据操作时最值得抄的第一条习惯:永远先开测试 bank 练手,再上生产


十四、边界与声明

  • 所有端点路径来自 Hindsight 0.8.0-slim 容器内 api/http.py 实际路由(grep 提取 70 个端点)
  • 所有耗时数据为 2026-08-21 真实 API 实测(内网部署,地址脱敏),测试 bank 使用后已删除
  • DELETE /memories/{id} 返回 405 为实测行为,属设计约束而非 bug
  • 源码语义(update_document / delete_document / reprocess_document)来自 memory_engine.py 实际代码
  • 注:官方最新文档提到的「Curate memory unit」(编辑单条 memory)端点,在当前 0.8.0-slim 实例中不存在——版本差异以实际部署为准

本文由 admin 原创,转载请注明出处。

相关推荐

评论

0
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多