OpenHuman 记忆树检索:memory_tree 多模式原语、确定性 walk 路由与记忆子智能体
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
OpenHuman 的记忆系统分为“写路径”与“读路径”:Memory Tree 把一天的信息流折叠成磁盘上的分块、评分与层级摘要树,而Retrieval(检索)则负责从树中读出正确答案——找到合适的节点、水合出原始 chunk、并把 “Alice” 这样的表面名字解析成稳定 id。本文基于 gitbooks/features/obsidian-wiki/retrieval.md 展开,结合当前仓库源码,讲清memory_tree工具的 8 种 mode、统一的RetrievalHit返回结构、实体规范化与 co-occurrence 图、无 LLM 的确定性walk算法,以及专职记忆子智能体的配置与性能基准方式。
设计哲学:检索层没有分类器、闸门和编排器
retrieval.md 明确了一点:检索层刻意不设置 classifier、gate 或 composer。各个检索原语是确定性的、作用域单一的;至于“该调哪个原语”“结果如何组合”,完全交给调用方的 agent 决策(对于确定性的walk,则由一个纯路由算法决策)。这意味着模型面对的是一个“工具箱”而非黑盒:每个原语行为可预测,便于测试和组合。
memory_tree工具:单一入口、多模式分发
agent 面向的入口是一个名为memory_tree的多模式工具,定义在 src/openhuman/memory/query/mod.rs。其mode字段路由到底层实现,所有 mode 返回同一种RetrievalHit结构,因此模型看到的 schema 与具体 mode 无关。
源码中MemoryTreeTool的parameters_schema给出了全部合法 mode 与参数(mod.rs 的parameters_schema方法):
| Mode | 用途 | 典型场景 |
|---|---|---|
search_entities | 对规范化实体索引做模糊LIKE查询,把表面名字解析为 canonical id | 用户提到人名时先调用它(“Alice 说过什么?”) |
query_source | 按 source 类型 + 时间窗过滤的 per-source 摘要检索,可选语义重排 | “总结一下我上周 Slack #eng 的内容” |
drill_down | 对摘要节点的child_ids做 BFS 下钻,一层或多层,可选重排 | 把粗粒度摘要展开成更细的子节点 |
cover_window | 求覆盖[since_ms, until_ms]时间窗的最小节点集合 | “过去 24 小时”类时间限定回顾 |
fetch_leaves | 按 id 批量水合原始叶子 chunk(上限 20 个) | 摘要命中后拉取原文用于引用 |
ingest_document | 把文档写入树以备日后检索(唯一的写模式) | 持久化抓取的网页/GitHub 文件;重复source_id会替换旧 chunk |
walk/smart_walk | 确定性 E2GraphRAG 检索——抽取查询实体、在实体图与稠密摘要间路由,全程无 LLM,返回排序后的证据 | 一条自然语言问题一次问完,无需 agent 循环 |
各 mode 的具体参数(来自源码 schema)包括:
query:search_entities的匹配子串;query_source的可选语义重排查询;walk的自然语言问题;kinds:search_entities的实体类型过滤(email、url、handle、person等);source_kind:query_source的 source 类型过滤(chat、email、document等);time_window_days:query_source/walk/smart_walk的回看窗口(天数),作用于 walk 的稠密分支;max_hops:walk/smart_walk的实体图关联跳数阈值(默认 2,上限 4);node_id/max_depth:drill_down要展开的摘要节点及其深度(默认 1,最大 3);chunk_ids:fetch_leaves要拉取的 chunk id 列表;title/body/source_id/provider/source_ref:ingest_document的写入参数,provider缺省为agent,重复source_id的摄取会替换旧 chunk;limit:结果数上限(默认值随 mode 变化),仅mode为必填字段。
一个值得注意的历史演进:早期的query_global与query_topic两个 mode已被移除——source 树已经承载了全部内容,走 source 层级加实体索引即可重建时间与主题两个投影(retrieval.md 指出 dispatcher 测试断言了它们不存在;src/openhuman/memory/query/mod.rs 中的match分支也确认当前合法 mode 只有上述 8 种,未知 mode 会返回明确的错误提示)。
RetrievalHit:所有原语的统一返回结构
每个原语都输出RetrievalHit,其 JSON schema 在 src/openhuman/memory/tree/retrieval/schemas.rs 中声明(多处TypeSchema::Ref("RetrievalHit")数组即各查询的命中列表)。关键字段:
node_id/node_kind——leaf(一条原始mem_tree_chunks行)或summary(一条已封存的mem_tree_summaries行)。消费方据此分支,例如“只对 summary 做drill_down”;tree_id/tree_kind/tree_scope/level—— 溯源信息,UI 可以显示“来自 Slack #eng”;content—— 片段(摘要文本或原始 chunk 正文);entities/topics—— 节点携带的 canonical id 与标签;time_range_start/time_range_end—— RFC3339 格式,让不同工具返回的命中可以按同一时间轴排序;score—— 相关性分数;child_ids—— 下一层的 id(叶子为空),是drill_down的游标;source_ref—— 回指原始 source 的指针(叶子上填充)。
查询类 mode 还会把命中包进QueryResponse { hits, total, truncated },其中total是截断前的匹配总数——agent 由此判断“加大 limit 再查一轮是否会多出结果”,避免误判为“没有更多数据”。
实体解析与 canonical id
名字是脏的,id 不是。回答“关于某人的问题”之前,agent 需要把表面形式解析为 canonical id,例如person:alice或email:alice@example.com:
search_entities在树 summariser 维护的实体索引上做模糊查询,完成解析;- 规范化注册表位于 Obsidian vault 中:每个实体一个 Markdown 文件,路径为
<content_root>/entities/<kind>/<canonical_id>.md,带 YAML frontmatter(id、kind、display_name、aliases、emails、handles),外加用户可在 Obsidian 里直接编辑的自由 notes 正文;lookup_alias按 alias / email / handle / display name 做不区分大小写的匹配; kind与memory_tree::score::extract::EntityKind对齐,保证评分器产出的 id 能原样往返于注册表;- vault 是唯一事实来源——Obsidian、grep 与向量搜索看到的是同一份数据,不需要额外数据库。
实体图:只读、派生、纯 SELF-JOIN
OpenHuman 通过实体图暴露实体间关系,但没有平行的三元组表。其前提是:图就是树映射出来的——两个实体共同出现在同一个树节点上就构成一条边,权重为共同节点的个数。
co_occurring_entities(config, subject, limit)—— 返回按权重排序的GraphEdge { subject, object, weight };neighbors(config, subject, limit)—— 仅返回邻居 id。
从源码结构看,这是一次对mem_tree_entity_index的只读 SELF-JOIN:不建新表、不改 schema。这个图正是下文确定性walk路由的直接依赖。
确定性walk/smart_walk:无 LLM 的 E2GraphRAG
walk与smart_walk都经由fast_retrieve实现(dispatcher 中二者都路由到 src/openhuman/memory/query/fast_walk.rs 的run_fast_walk),这是一个E2GraphRAG 风格算法,用于替代旧的逐轮 agentic 循环,从不调用 LLM。路由完全由查询实体与共现图的跳数距离决定:
- 抽取查询实体
Eq(spaCy NLP,regex 兜底); Eq为空 →global模式:在摘要树上做稠密重排;- 否则计算
h跳内的相关实体对:- 无相关对 →带出现度排序的 global:稠密 top-2k,再按每个摘要提及了多少
Eq实体重排; - 找到相关对 →local模式:对各实体对的实体索引节点集求交,当候选超过
k时收紧h,最后按实体覆盖率与新鲜度排序幸存者。
- 无相关对 →带出现度排序的 global:稠密 top-2k,再按每个摘要提及了多少
可调参数(FastRetrieveOptions,字段在 src/openhuman/memory/query/backend.rs 中定义为limit、max_hops、time_window_days等):limit(k,默认 10、上限 100)、max_hops(h,默认 2、上限 4)、可选的time_window_days稠密分支回看窗口。输出是结构化的QueryResponse命中列表——不生成任何合成叙述文本——留给上层 context agent 消化。
时间窗检索:cover_window
对“过去 24 小时发生了什么”这类问题,cover_window计算覆盖[since_ms, until_ms](epoch 毫秒)的最小节点集。由于摘要节点自带time_range_start/time_range_end,一个高层摘要节点就可能覆盖整个时间窗,而不必扇出到每个叶子——agent 只有在需要细节或引用时才进一步drill_down或fetch_leaves。实现位于 src/openhuman/memory/query/cover_window.rs。
memory_recall:旧版命名空间键值检索
与树并列、独立存在的是memory_recall(src/openhuman/memory/tools/recall.rs),它检索更早的命名空间键值记忆:memory_recall { namespace, query, limit },命名空间如global、background、autocomplete或skill-{id}。源码中resolve_namespace在未指定时回落到默认 scope(global),并防止模型丢失本已想好的命名空间。它返回按分排序的结果,最适合树出现之前的精确偏好/事实查询(“用户喜欢深色模式吗?”)。
记忆子智能体:专职检索的 specialist
src/openhuman/memory/agent/ 下是一个专职检索子智能体,通过call_memory_agent工具被调用。它组合各原语暴露的策略来回答关于记忆树的问题:向量搜索、原始文件关键词搜索、实体搜索与关系追踪、层级树浏览、直接读内容、source 列表。
其工具白名单与行为参数定义在 src/openhuman/memory/agent/agent/agent.toml 中,从源码可以看到几个关键配置:
named工具列表:memory_recall、memory_tree(含全部 mode,含确定性walk/smart_walk)、query_memory、memory_doctor、memory_flavour(只读的人格/风格剖面)、ask_user_clarification;max_iterations = 6—— 注释解释了动机:合法答案只需要几步(walk→ 可选drill_down/fetch_leaves→ 作答),过大的预算反而会让子智能体空转约 80 秒后以失败结束,小预算确保在记忆树退化/为空时快速失败;sandbox_mode = "read_only"、temperature = 0.2、agent_tier = "worker",以及omit_identity = true、omit_safety_preamble = true等上下文裁剪,让子智能体保持轻量;- prompt 与迭代上限同目录维护(
agent/prompt.md+agent/prompt.rs),性能由基准脚本 scripts/bench-memory-walk.sh 追踪。
该脚本调用 core CLI 对一组测试查询做记忆树遍历基准,支持--query、--content-root、--max-turns、--model、--verbose等参数,输出每查询延迟与汇总统计,可用于回归对比检索改动前后的表现。
小结与延伸阅读
OpenHuman 的检索路径可以概括为:统一 schema 的多模式原语(memory_tree)+ 实体规范化(Obsidian vault 为唯一事实来源)+ 派生共现图 + 无 LLM 的确定性路由(walk)+ 专职受限子智能体。每一层都可单独测试(如 src/openhuman/memory/query/ 下每个 mode 都有独立的*_tests.rs),也可通过bench-memory-walk.sh做端到端基准。
相关文档(均以仓库根目录为起点):
- gitbooks/features/obsidian-wiki/memory-tree.md —— 构建出检索所读之树的写路径;
- gitbooks/features/obsidian-wiki/memory-diff.md —— 记忆变更如何被追踪;
- gitbooks/features/obsidian-wiki/README.md —— Obsidian 支持 wiki 的功能索引;
- gitbooks/features/obsidian-wiki/scoring.md —— 树评分(含实体抽取)的细节;
- src/openhuman/memory/agent/README.md —— 记忆子智能体模块说明。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考