研究日期:2026-07-23(Asia/Shanghai)
主仓库:tinyhumansai/openhuman
OpenHuman 源码快照:c480e6d817ad2277270c5f6d153f27c4e196eb1c
TinyCortex 子模块快照:daaaf6ba5f02635c08deae2b2b2ed7fcc8c06b6a
仓内版本 / 最新正式版本:0.63.1/v0.63.1(2026-07-22)
版本与实时校准
本文研究 2026-07-23 抓取的 main。当日 GitHub API 快照为 35,238 stars、3,455 forks、171 个 open issues + PRs;这些数字只用于说明研究时点,不作为项目质量判断。
OpenHuman 主仓快照最后一个提交为:
c480e6d817ad2277270c5f6d153f27c4e196eb1c
fix(agents): run custom registry agents with their real tools (flows + chat + tasks) (#5121)
2026-07-22T23:05:44+05:30
最新 release 是 v0.63.1,发布于 2026-07-22T16:47:35Z。官方 README 仍明确标注 Early Beta。
对原 HTML / 旧稿最重要的更新
旧材料把 Memory Tree 描述成 Source、Topic、Global 三棵树并行。这个说法已经过时:
- PR #3059 于 2026-05-31 合并,删除 Global 与 Topic 树;
- 两者只是 Source 树的派生投影,没有只存在于其中的原始内容;
- daily digest、topic routing、
query_global、query_topic与对应队列任务一并删除; - 一次性迁移会清除旧 rows、summaries、buffers、jobs、sidecars 和磁盘目录;
TreeKind::Global/Topic只为旧数据反序列化和迁移保留,不代表生产路径仍会构建它们。
当前生产设计是:
每个来源一棵 Source Tree
+
叶子 / 摘要上的实体出现索引
+
摄入时累计的实体共现边
+
确定性 graph / dense 检索
仓内文档也有漂移:gitbooks/developing/architecture/memory-tree.md 已写明移除三树设计,但用户功能文档与 src/openhuman/memory_tree/README.md 仍残留旧结构或旧调用方式。本文以当前源码、迁移和已合并 PR 为准。
研究提纲
核心论点
OpenHuman 的 Memory Tree 不是“向量库加一层摘要”,而是一台证据编译器:
- 把聊天、邮件、文档先规范成 Markdown;
- 决定哪些片段值得进入长期记忆;
- 以可重复的 bucket seal 把原文编成多层目录;
- 用实体图与 dense 分支确定性地寻找候选;
- 在需要引用时沿
child_ids回到带source_ref的叶子。
它故意进行有损压缩,但保留一条回到原始证据的无损逃生通道。真正有价值的设计不是“记住一切”,而是把遗忘、压缩、出处和失败恢复写成可检查的制度。
主要资料来源
- OpenHuman 与 vendored TinyCortex 当前默认分支源码和测试;
- OpenHuman README、Getting Started、Privacy Mode 与 Memory Tree 文档;
- PR #3059:移除 Global / Topic 树;
- PR #3947:以确定性 E2GraphRAG 替换 12–25 轮 agentic walk;
- Issue #4677 与 PR #4768:把生产
retrieve_memory从约 30–40 秒模型代理路径接到确定性快速路径; - Issue #2422:对 local-first、托管推理与 OAuth 边界的公开审计;
v0.63.1release 与 GitHub API 元数据。
关键源码入口
app/src/services/chatService.ts::chatSendapp/src/services/coreRpcClient.ts::callCoreRpcsrc/core/jsonrpc.rs::rpc_handlersrc/openhuman/web_chat/ops.rs::start_chatsrc/openhuman/web_chat/run_task.rs::run_chat_tasksrc/openhuman/agent/harness/session/turn/core.rs::Agent::turnsrc/openhuman/memory/ingest_pipeline.rsvendor/tinycortex/src/memory/ingest/pipeline.rsvendor/tinycortex/src/memory/score/mod.rssrc/openhuman/tinycortex/queue_driver.rs::HostQueueDelegatesvendor/tinycortex/src/memory/tree/bucket_seal.rsvendor/tinycortex/src/memory/retrieval/fast.rssrc/openhuman/agent/harness/subagent_runner/ops/runner.rssrc/openhuman/agent/harness/archivist/tree_ingest.rssrc/openhuman/security/egress/enforce.rs
结论先行
OpenHuman 的产品宣传是“一个记住一切的个人 AI 大脑”,但源码里更准确的表述是:它把长期记忆拆成了三套不能互相冒充的机制。
- 事实层:原始 Markdown、chunk、
source_ref与 raw refs; - 导航层:有损 summary tree、实体索引、共现图和 embedding;
- 解释层:Archivist recap、profile、最终回答与 UI citation。
事实层保存“发生过什么”;导航层回答“到哪里找”;解释层负责“这对当前问题意味着什么”。OpenHuman 最好的决策,是明确规定 Archivist 的 LLM recap 不能反过来作为 Memory Tree 证据:树吃 raw prose,不吃机器已经解释过一次的 recap。
它当前最明显的风险也来自层间接缝:
- summary 有出处链,UI 自动 citation 却主要来自另一套
Memory::recall; - Memory Tree 的 leaf
source_ref需要调用方继续下钻和渲染,不会自动变成所有回答的可见引用; - “local-first”是数据与运行姿态,不等于默认完全离线;默认 Privacy Mode 是
standard; - 代码迁移很快,公开文档、模块注释与真实调用路径存在不同步。
产品层、架构层、代码层
产品层
OpenHuman 是 React + Tauri v2 桌面应用,Rust core 在同一进程里作为 Tokio task 运行。产品不只包含记忆,还包含 agent harness、workflow、channel、集成、浏览器、研究与本地运行时。
官方 README 的“brain”由 Memory Tree、Obsidian-compatible Markdown、自动同步、Archivist / subconscious、profile memory 与 agent recall 共同组成。把 Memory Tree 单独理解为整个个人记忆系统,会错过真实产品边界。
架构层
React UI
│ JSON-RPC + per-launch bearer
▼
loopback Rust core
│
WebChat controller ── queue / cancel / progress / socket events
│
Agent::turn
├─ per-turn recall / profile / SuperContext
├─ model ↔ tools / subagents
├─ transcript persistence
└─ post-turn Archivist
│
▼
canonical Markdown → chunks → score/admit → durable jobs
│ │
└──────── Source Tree ◀────────┘
│
entity index + co-occurrence graph
│
deterministic retrieval
│
summaries → leaves → source_ref
ServiceSet 负责 background services / transports,DomainSet 负责 runtime domains。UI 是 core 的客户端,不拥有摄入队列或记忆状态。
一条真实聊天请求
chatService.chatSend()
→ callCoreRpc("openhuman.channel_web_chat")
→ loopback POST /rpc + bearer
→ jsonrpc::rpc_handler
→ controller registry validate + invoke
→ schemas::handle_chat
→ channel_web_chat
→ start_chat
→ prompt / attachment guards
→ queue mode: interrupt | steer | followup | collect | parallel
→ cancellation token + spawned task
→ run_chat_task
→ load config / profile / cached Agent
→ resume exact transcript where possible
→ Agent::run_single
→ Agent::turn
→ per-turn context + memory + SuperContext
→ tinyagents model/tool loop
→ transcript + post-turn hooks
→ chat_done / segments / usage / citations over WebChannel
Tauri 页面通常直接调用 loopback core;只有非 loopback 的明文 self-hosted runtime,才需要 shell relay 绕过 WebView mixed-content / CORS 限制。
写入:记忆先经过“编辑部”
1. 先规范成可读的 Markdown
OpenHuman host 把 chat、email、document 转成同一 CanonicalisedSource。TinyCortex 再确定性切块。完整正文写入 Markdown content store;SQLite 保存 metadata、预览、索引和队列状态。
这不是“为了 Obsidian 好看”才多写一份文件。Markdown 是可人工检查的事实载体,SQLite 是并发、查询与恢复的权威控制面。两者承担不同责任。
2. 热路径只做便宜判断
ingest_canonical() 的顺序是:
canonicalize
→ deterministic chunk ids
→ atomic Markdown staging + sha256
→ score_chunks_fast(明确禁用 LLM)
→ one SQLite transaction:
document gate
+ chunk metadata
+ cheap score
+ lifecycle=pending
+ raw refs
+ enqueue extract job
文档使用 source_id 或 source_id@version 的事务 gate;chat / email 是持续流,依赖完整内容推导的 chunk id 与队列 dedupe 保持重放幂等。
3. worker 再做深评分与准入
HostQueueDelegates::extract_chunk() 会从磁盘读回完整正文,而不是拿 SQLite 中最多约 500 字符的 preview 评分。它执行:
- token count;
- unique-word ratio;
- source / metadata / interaction prior;
- entity density;
- regex / mechanical entity extraction;
- 只有 cheap score 落在中间带时,才允许 LLM importance 参与;
- definite keep / definite drop 直接短路;
- 很短且没有实体的寒暄会被丢弃;
- priority tag 可以得到有限 boost。
被丢弃的 chunk 仍保留 score row 和 reason,便于诊断,但不会进入树。被保留的 chunk 才写 entity occurrence index,并在同一事务里累计 canonical entity pairs 的无向共现边。
这套 admission gate 是 OpenHuman 最有“个人立场”的代码:长期记忆的核心不是存储容量,而是决定什么值得被未来的自己重新看见。
4. 队列把失败变成状态
TinyCortex 拥有 durable job store 与单步 dispatch;OpenHuman host 保留 Tokio worker、Sentry、scheduler 与存储降级策略。
宿主错误分类不是统一重试:
| 错误 | backoff | 报警 / 恢复 | |---|---:|---| | SQLite busy / locked | 1 秒 | 静默 | | transient I/O | 30 秒 | 静默 | | disk full | 300 秒 | 静默,等用户处理 | | corrupt / not-a-db | 300 秒 | quarantine + rebuild,单次报告 | | host FS / readonly / ENOSPC | 300 秒 | 标记 storage degraded,单次报告 | | 未知错误 | 1 秒 | 每次报告 |
“后台记忆”若没有持久化队列与错误分级,只是把失败从用户眼前藏起来。
压缩:bucket seal 的并发语义
Source Tree 的门槛分两种:
if level == 0 {
token_sum >= input_token_budget
} else {
item_ids.len() >= summary_fanout
}
L0 按输入 token budget 封存叶子;更高层按 sibling 数量封存摘要。这样上层 fan-in 不会因某次 summarizer 写得长或短而漂移。
append_to_buffer() 在 SQLite transaction 内:
- 检查 tree 仍是 active;
- 对
(tree_id, level, item_id)幂等; - 更新 items、token sum 与 oldest timestamp;
- commit。
真正的 seal 分两段:
- 读取 exact buffer snapshot,离开数据库锁后 hydrate 正文、调用 summarizer、embedding 与 label resolver;
- 回到 transaction,只有 snapshot prefix 仍匹配时才插入 summary、写 child back-links、消费这段 buffer、把新 summary 追加到 parent,并按需要 enqueue 下一层。
在 summarizer 运行期间新到的 items 不会被误删;并发的另一个 seal 若已消费同一 snapshot,当前 seal 会干净地放弃。summarizer 报错或返回空白时使用 deterministic concatenation fallback。级联硬上限是 32 层。
摘要正文有损,但 summary 保存 child_ids;leaf 保存 source_ref。所以它提供的是:
lossy navigation
+
lossless escape hatch
读取:三分支确定性检索
PR #3947 把原来最多 12–25 次顺序 LLM 调用的 walk / smart_walk 换成 code-only E2GraphRAG。接口名为兼容保留,语义已经改变。
查询先用 spaCy sidecar 提取实体;默认开启,首次使用可能安装运行时。Python / spaCy 不可用时,退回 Rust regex / extractor。实体规范化必须和 ingest 端一致,否则图上是两种名字。
fast_retrieve() 有三条分支:
- 没有实体:
query_source做全局 dense semantic recall; - 有实体但图上没有相关 pair:先 dense 取更多候选,再按命中实体数稳定重排;
- 实体在 max hops 内相关:取 pair 两端 occurrence 的 node intersection;候选太多时逐步收紧 hops;最后按 matched entity count、最新时间、node id 稳定排序。
命中可同时是 leaf 和 summary,之后批量 hydrate,并在截断前应用 profile source-scope allowlist。
生产快速路径不是同一件事
确定性 retriever 在 2026-06 已存在,但生产 retrieve_memory 仍先启动 agent_memory,让模型多轮调用 memory tools。Issue #4677 记录的一个 turn 中,四次调用合计约 141 秒。
PR #4768 在共享 run_subagent 接缝增加了第二层 fast path:
definition.id == agent_memory
→ query 非空
→ query 必须抽到 entity / salient topic
→ fast_retrieve(limit=8)
→ 有 hits:iterations=0,直接返回 compact evidence block
→ disabled / error / no hits / ungrounded:
回退完整 model-driven memory agent
默认开启,OPENHUMAN_MEMORY_FAST_PATH=0|false|no|off 可关闭。项目 PR 报告 data-present happy path 目标从约 35 秒降到约 1–3 秒;本研究没有做真实 provider / 大语料 benchmark,因此不把该数字写成独立实测结论。
解释:Archivist 为什么不把 recap 当证据
每轮结束后,Archivist 会:
- 把 user / assistant 文本写入 FTS5 与 Markdown-backed archive;
- 按 segment 生成 recap、embedding、events 和 profile 更新;
- 把 segment 的 raw prose 另行送入 Memory Tree。
第三步有一条非常明确的注释:LLM recap 永远不入树。工具 JSON 与 base64 图片也被剥离;每个消息写入类似:
agent://session/{session_id}/segment/{segment_id}#ep{episode}
的 source_ref。Memory Tree ingest 失败是 non-fatal,不能让聊天主流程失败。
这是“证据”与“解释”的隔离。若把 recap 重新当原文摄入,下一轮 recap 会总结上轮摘要,几次循环后来源仍然看似完整,语义却只剩机器对自己的转述。
Citation 接缝尚未完全统一
Agent::turn 自动收集的 UI citations 主要来自 collect_recall_citations(Memory::recall),输出 memory id、key、namespace、score、timestamp 与 snippet。
Memory Tree 的 RetrievalHit 则有 child_ids 与 leaf source_ref。Orchestrator prompt 要求用这些字段构造脚注,但它依赖调用方真正执行 fetch_leaves 并正确渲染。
因此当前不能写成“所有 OpenHuman 回答都自动展示原始文档引用”。更准确的说法是:
- 数据结构支持从摘要下钻到 leaf provenance;
- Memory Tree tool contract 把
source_ref定义为 authoritative quote source; - UI 的通用自动 citation 仍是另一条 recall 管道;
- 两套 provenance 还没有完全统一成一个用户可见的信任界面。
Local-first 与 Privacy Mode 的真实边界
当前 README 已比 2026-05 的旧版更透明:
- Memory Tree DB、Markdown vault、workspace config 与本地 runtime state 在设备上;
- 默认设置仍使用 OpenHuman-hosted sign-in / model routing / web search proxy,以及托管 integration OAuth / tool calls;
- managed integrations 使用 Composio;
- BYOK / local provider / Composio direct mode 可以改变部分路径;
- 某些实时 trigger 仍依赖托管 backend。
2026-05 的 Issue #2422 对 OAuth、推理与 local-first 营销边界提出公开审计。当前主线已加入 Rust core 强制的 Privacy Mode:
local_only拦截外部 inference 与携带用户内容的 external egress;- local runtime 允许;
- 一些不携带用户数据的 control-plane 请求仍豁免;
- 设置变更可以热更新 live policy;
- 默认 mode 是
standard,不是local_only; sensitive当前不等于全面阻断外发。
因此“有一键本地模式”是当前事实;“安装后默认没有任何内容离开设备”不是。
代表性代码片段
1. 只有边界样本才值得调用 LLM 评分
let in_band =
cheap_total > cfg.definite_drop_threshold
&& cheap_total < cfg.definite_keep_threshold;
let llm_consulted = if in_band {
cfg.llm_extractor.as_ref()
.is_some_and(|llm| /* extraction produced importance */)
} else {
false
};
意义不在节省一次 API 调用,而在把 admission 的主导权留给可重复规则;模型只处理灰区。
2. L0 与高层使用不同封存尺度
pub fn should_seal(config: &MemoryConfig, buf: &Buffer) -> bool {
if buf.level == 0 {
buf.token_sum >= config.tree.input_token_budget as i64
} else {
(buf.item_ids.len() as u32) >= config.tree.summary_fanout
}
}
叶子看输入量,摘要看 fan-in;否则某次冗长摘要会扭曲整棵树。
3. 快速路径必须有实体 grounding
if extract_query_entities(&config, query).await.is_empty() {
return None; // defer to model walk
}
let resp = fast_retrieve(&config, query, FastRetrieveOptions {
limit: 8,
..Default::default()
}).await.ok()?;
这是关键的 relevance guard。没有它,模糊问题可能从一个很满的 profile 中随便拿 top-k,并被误报为“检索完成”。
与相邻方案的区别
| 方案 | 主索引 | 写入政策 | 导航 / 证据 | OpenHuman 的取舍 |
|---|---|---|---|---|
| 普通 vector top-k | embedding | 通常全收 | 相似 chunk | 快,但全局结构与遗忘政策弱 |
| mem0 | fact-like memory + vector / graph | LLM 推断 ADD 等操作 | memory record | 更像事实账本;第 05 篇单独拆 |
| 图优先 RAG | entity / relation graph + vector | 文档级图抽取 | local / global / hybrid query | 更像知识库编译 |
| OpenHuman | source tree + entity occurrence / co-occurrence + dense | 评分准入 + durable queue | summary 导航,leaf source_ref 取证 | 保留来源所有权,接受层级摘要的漂移风险 |
局限与未证实项
- Early Beta:主仓在数周内多次更换 memory architecture,接口和文档仍会漂移。
- 文档不一致:三树旧说法、旧 walk 描述、部分 TinyCortex “尚未 port graph”注释与当前代码冲突。
- 双图语义在迁移中:当前 fast retriever 使用持久化
mem_tree_entity_edges;TinyCortex 另有基于 occurrence self-join 的 read-only graph abstraction,模块注释称其 production adapter 仍未被热路径使用。不能把两者混写成一种实现。 - dense 分支规模:无 time window 的
query_source会扫描所选 source trees 的全部未删除 summaries,再批量 hydrate embeddings;大语料下成本不是常数。 - 实体抽取依赖:spaCy 默认开启但可能首次安装;fallback 更轻,却只能可靠识别 email、URL、handle、hashtag 等机械形式,复杂实体 grounding 质量会下降。
- summary drift:高层节点是对低层摘要的再次压缩;必须下钻验证,不能把 root 当原始事实。
- citation 未统一:数据层 provenance 与通用 UI citation 仍有接缝。
- 本地不是默认离线:默认
standard会允许配置中的托管外发;用户必须理解并主动选择 Privacy Mode / provider / integration posture。 - 迁移复杂度:Markdown + SQLite + embeddings sidecar + durable jobs 带来可检查性,也带来 orphan cleanup、rebuild 与多真相对齐成本。
- 性能数字来自项目报告:约 35 秒到 1–3 秒的改善未在本研究机器上用真实 provider 和用户语料复测。
验证记录
TinyCortex:确定性检索
../../scripts/ci-cancel-aware.sh \
cargo test memory::retrieval::fast::tests -- --nocapture
结果:7 passed,0 failed。
覆盖:
- blank query;
- limit / hop bound;
- local entity intersection;
- source-scope-before-limit;
- dense fallback;
- occurrence rerank;
- leaf / summary hydrate 与 missing-node 跳过。
TinyCortex:bucket seal
../../scripts/ci-cancel-aware.sh \
cargo test memory::tree::bucket_seal::tests -- --nocapture
结果:9 passed,0 failed。
覆盖:
- budget 与 fanout gates;
- archived tree 拒绝 append / seal;
- parent enqueue atomicity;
- summarizer 运行期间新 append 不丢失;
- document subtree;
- L1 → L2 cascade。
TinyCortex:score / admission / entity index / embedding
../../scripts/ci-cancel-aware.sh \
cargo test memory::score -- --nocapture
结果:139 passed,0 failed。
覆盖 cheap / borderline / LLM fallback、drop reasons、canonical entity、entity index、co-occurrence persistence、embedding shape 与 semantic scoring。
OpenHuman 宿主:生产 memory fast path
根级 submodules 初始化后执行:
scripts/ci-cancel-aware.sh \
cargo test --lib --no-default-features fast_path_tests -- --nocapture
结果:10 passed,0 failed,9968 filtered out;首次编译约 7 分钟。另有 6 个 unused-import warnings,以及依赖 block 0.1.6 的 future-incompatibility warning,均不影响这组测试。
覆盖:
- fast path 默认开启及 kill switch;
- empty hit 回退;
- compact hit format;
- Unicode-safe / max-result truncation;
- 小型单数字 limit。
验证边界
总计 165 项聚焦测试通过。没有运行完整 OpenHuman 近万项测试、桌面 E2E、真实 OAuth / cloud provider、真实大规模 corpus benchmark。因此本文验证的是:
- admission、seal、retrieval 核心算法;
- OpenHuman 宿主的 fast-path guard 与输出契约;
- 不是整个产品在所有 provider / integration / platform 上的发布认证。
中文文章结构
- 开场:向量袋像塞满便利贴;OpenHuman 更像一个编辑部;
- 实时纠错:三树已经收敛为 Source Tree;
- 一条 chat request 怎样触发 memory;
- 写入:Markdown、评分、准入与队列;
- 压缩:bucket seal 的事务 / 并发语义;
- 读取:三分支确定性检索 + production fast path;
- 证据与解释:Archivist raw prose、
source_ref、citation seam; - local-first 的真实边界;
- 适合谁、不适合谁、与 mem0 / 图优先 RAG / vector top-k 的区别;
- 个人思考、社会与市场变化、三个后续问题;
- 版本、测试与来源。
英文文章结构
- Thesis: memory as an evidence compiler;
- Product request path and runtime ownership;
- Canonicalization, dual storage, admission and durable queue;
- Bucket-seal algorithm with concurrency semantics;
- Deterministic E2GraphRAG branch routing;
- Why the second production fast path was still needed;
- Evidence vs interpretation in Archivist;
- Provenance and UI citation gap;
- Local-first / Privacy Mode threat boundary;
- Trade-offs, comparisons, implementation lessons, validation and sources.
三个继续追问的问题
- 当 summary 与 leaf 冲突时,系统应该自动降级到 leaf,还是把冲突本身展示给用户?
- 哪些记忆应该允许压缩,哪些应该被声明为“不可摘要、只能引用”?
- Privacy Mode 应只是一个用户开关,还是应该变成每条 memory / tool call 可审计的 egress policy?