尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

OpenHuman Agent 规模性能基准实录:内存召回 O(N) 退化定位与 Recall Diet 修复

发布时间:2026/9/10 9:40:26

资讯中心
01
ARTICLE

OpenHuman Agent 规模性能基准实录:内存召回 O(N) 退化定位与 Recall Diet 修复

OpenHuman Agent 规模性能基准实录:内存召回 O(N) 退化定位与 Recall Diet 修复
OpenHuman Agent 规模性能基准实录内存召回 O(N) 退化定位与 Recall Diet 修复【免费下载链接】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 仓库中 scripts/bench/FINDINGS.md 这份性能调查报告的技术解读与源码级展开。它记录了一次典型的看似内存泄漏、实为数据规模退化的诊断全过程在持续 Agent 负载下openhuman-core 的单轮延迟线性爬升、吞吐跌至起始值三分之一、RSS 无界增长——最终被定位为每轮隐式记忆召回以 Θ(已存记忆数) 的成本扫描整个内存命名空间所致。读完本文你将掌握该基准环境的复现命令与全部参数、五条证据链的推理方法、两次被实测否决的优化教训以及最终落地并取得 2.13× 吞吐提升的 Recall Diet 修复方案。基准环境一个不依赖核心代码改动的外进程测试台调查基于 scripts/bench/run-agent-scale.sh 构建的 Agent 规模基准。它驱动一个真实构建的openhuman-core服务进程release 版本、Linux、并发 8、fresh线程模式、mock LLM从进程外部采样 CPU 与 RSS并给出泄漏判定。与 scripts/profile/ 那种把核心作为库内嵌的微基准不同本测试台包含传输层、serde、调度器与连接处理的全部成本RSS 是操作系统对该发行二进制的真实记账。两个承重事实让整个设计无需任何核心代码改动即可成立详见 README.mdBACKEND_URL重定向全部推理流量。核心从同一值派生推理基址与后端基址将其指向 mock 即可一次性接管 chat completions、embeddings 与遥测。形如a.b.local的会话令牌免去GET /auth/me往返因此无需登录、mock 也无需认证路由driver 在负载开始前预置即可。runner 还处理了三个手工复现必翻车的细节审批门必须关闭OPENHUMAN_APPROVAL_GATE0默认开启会以 10 分钟 TTL 把交互轮次挂起等待人工决策每日成本上限必须抬高核心会对 mock 上报的 token 用量计费默认 $10 预算几百轮即耗尽之后每轮瞬间失败embedding 宽度必须匹配mock 默认--embed-dims 1024。runner 全参数速查以下参数均来自 run-agent-scale.sh 的--help文本是复现本调查全部数字的前提参数默认值作用--concurrency N8并行在途轮次数--turns N300测量窗口内的总轮次--duration-ms N—改按墙钟时长运行--warmup-turns N10测量前丢弃的预热轮次--tool-depth N1mock 每轮驱动的工具调用数0表示完全无memory_search调用--latency-ms N40mock 推理延迟均值--jitter-ms N20推理延迟抖动--reply-chars N240助手回复字符数--fail-rate F0返回 500 的完成比例用于压测重试路径--thread-mode Mfreshfresh|per-worker|shared--interval-ms N250资源采样间隔--tree关同时采样子进程--keep-workspace关退出时不删除临时工作区--workspace DIR—复用已填充的工作区隐含 keep--memory-off关关闭记忆读写召回 学习--memory-writes-off关仅关闭记忆写入、保留召回读取与--memory-off互斥--out-dir DIRtarget/bench/stamp产物目录两个记忆开关互斥--memory-off已包含--memory-writes-off的写抑制合并使用会产生重复[memory]/[learning]表而被核心拒绝。--memory-off之所以要同时点名三个独立的产生者run-agent-scale.sh 注释中写得很清楚是因为核心没有单一的[memory] enabled开关需要memory.auto_save false、[learning]下六个钩子全部关闭、embedding_provider none选择空操作嵌入器外加[memory_tree] spacy_enabled false。两个必须遵守的实验卫生规则工作区禁止落在 tmpfs/ramfs 上。runner 会在启动时用findmnt检查并直接拒绝启动理由写在其注释中tmpfs 页面本身就是内存核心写盘的每个字节都会被计入机器 RAM更糟的是持续运行会写满挂载点随后核心以 Failed to write auth profile lock owner 和 SQLite disk I/O error 失败——这看起来像泄漏导致的熔毁实际是磁盘写满。必须使用 release 构建cargo build --release --bin openhuman-core --no-default-features --features $(bash scripts/ci/product-features.sh)。debug 二进制的分配行为与 CPU 成本不是产品的真实表现从中得出的泄漏结论没有意义。判定如何产生分析器 analyze.mjs 给出三类内存结论pass无增长趋势或增长在每轮预算内、plateau整体超预算但末尾三分之一停止爬升——缓存填满工作集的形状、fail超预算且末尾仍在增长。这正是它分别拟合序列尾部、而非对比早期均值与晚期均值的原因早期对晚期无法区分先涨后停与从未涨过把饱和缓存当成泄漏只会训练人们忽视这条检查。当 RSS 失败但工作区同步大幅增长时报告会标记为confounded混杂承认无法区分泄漏与对真实增长数据的索引——唯一解法是加跑--memory-off对照。另外线程数与文件描述符使用更严苛的直通阈值无合法理由无界增长且吞吐保持/liveness单独检查防止进程早已死掉但资源曲线漂亮的假绿。--thread-mode fresh是唯一支持泄漏判定的模式另外两种模式会话历史按设计累积RSS 增长属预期行为分析器只报告增长率并明确拒绝评判。核心发现不是泄漏也不是缺索引在持续 Agent 负载下核心表现出三类症状每轮延迟线性爬升、吞吐跌至起始值约三分之一、RSS 增长不见平台期。调查报告给出了明确结论这不是内存泄漏也不是缺失索引。记忆召回每次都会加载整个记忆命名空间——每个文档与每个向量分块——解码每个 embedding 并在单连接互斥锁之后于进程内打分。每轮成本为 Θ(存储记忆数)因此会话总工作量呈二次增长吞吐上限随记忆累积而下降。RSS 增长是因为工作集就是已存数据本身。证据链五步定位法证据 1成本跟随已存数据而非进程运行时长一个指向已填充工作区的全新核心进程会立即继承全部成本而非从快速状态起步运行工作区起始延迟结束延迟C1空111 ms438 msC2C1 的全新进程532 ms534 msscripts/bench/run-agent-scale.sh --duration-ms 240000 --keep-workspace --out-dir target/bench/C1 scripts/bench/run-agent-scale.sh --duration-ms 120000 --workspace target/bench/C1/workspace --out-dir target/bench/C2--workspace把测试台变成受控实验先跑一轮累积状态再让全新进程指向结果。若每轮延迟从上轮结束处起步则成本是累积数据的函数对存储状态的 O(N) 路径若重新走低则是进程运行时间的函数进程内泄漏。泄漏会随旧进程一起消失这直接排除了泄漏假说。证据 2关掉记忆子系统所有症状消失scripts/bench/run-agent-scale.sh --duration-ms 240000 --tool-depth 0 # memory on scripts/bench/run-agent-scale.sh --duration-ms 240000 --tool-depth 0 --memory-offmemory ONmemory OFF4 分钟延迟111 → 438 ms平坦 ~79 ms吞吐保持率33%102%RSS 增长109 KiB/轮−0.65 KiB/轮每轮 CPU253 ms28 ms完成轮次~8,600~24,500关键推理--tool-depth 0意味着完全没有memory_search工具调用退化依旧一模一样所以问题出自每轮隐式召回而非 Agent 的记忆工具。证据 3召回实际做了什么8,600 轮后存储持有 9,660 个文档与 10,127 个分块全部在单个global命名空间。每次召回加载两者全部chunks : 10127 rows, 39.6 MiB of embeddings materialized docs : 9660 rows ~61 ms of SQL per recall, warm cache索引存在且被使用idx_vector_chunks_ns_doc、idx_memory_docs_ns_updated——SEARCH … USING INDEX但它们帮不上忙查询没有选择性谓词它要的是命名空间里的每一行。query_namespace_hits_excluding_session确实接受limit但它在加载并打分完一切之后才应用它相关引擎引用见 direct_engine_refs_tests.rs 中对query_namespace_hits_excluding_session(ns, query, limit, exclude)签名的说明。证据 4是读路径不是写竞争读写共享同一连接而--memory-off同时关闭两者单独使用无法区分孰贵。于是对已填充工作区、写关闭但召回每轮仍全扫scripts/bench/run-agent-scale.sh --duration-ms 90000 --tool-depth 0 \ --memory-writes-off --workspace populated吞吐p50读 写14.6/s540 ms仅读15.8/s499 ms移除全部写入只换来约 8%——召回扫描才是成本写竞争只是舍入误差。证据 5单连接互斥锁设定吞吐上限UnifiedMemory持有一个MutexConnection扫描在并发轮次间被串行化吞吐随之饱和并发p50吞吐1212 ms4.7/s2239 ms8.2/s8550 ms14.5/s在 ~14.5/s 处饱和意味着串行段每轮约 65 ms与实测 ~61 ms 扫描吻合。加核心数无法提升此上限且随命名空间增长继续下降。成本是工作量不是同步两个合理优化被实现并实测均无效。二者都针对扫描的同步方式而真正花钱的是扫描触碰的数据量。决定性测量5,162 个分块、并发 8p50 326 ms, throughput 24.1/s Littles law: 24.1 × 0.326 7.85 ≈ concurrency 8 → 每个 worker 整轮繁忙 CPU: 6.85 of 14 cores (49%), 295 ms of CPU per turn每轮295 ms 的 CPU记忆关闭时仅 28 ms——这不是锁后的排队而是在以机器一半容量做真实工作且无竞争可移除。任何让同样行数被读取、解码、打分的改动至多移动几个百分点这正是两次尝试的实际结果。两次失败的优化——不要重蹈覆辙尝试 1把 embedding 解码移出连接锁。理由每次召回约 1 万次分配、约 1 千万次小端转换都在临界区内进行。交错 A/B、2 次重复、相同的已填充工作区臂重复 1重复 2均值基线14.87/s14.37/s14.62/s锁外解码14.42/s14.18/s14.30/s尝试 2为两个 O(N) 扫描引入只读连接池。理由WAL 支持并发读但单互斥连接串行化了它们且一半核心闲置。已验证池确实被使用池化臂多持有约 19 个文件描述符且单元测试断言连接真正停放而非静默回退。两个语料规模下的测量语料臂重复 1重复 2均值2,121 分块基线37.21/s35.64/s36.43/s2,121 分块读池36.04/s34.56/s35.30/s5,162 分块基线24.07/s23.91/s23.99/s5,162 分块读池23.81/s23.55/s23.68/s两组点估计在全部四对中都一致地略微为负。两处改动均被回滚vendor/tinymemory与上游f8bd9af逐字节一致git diff f8bd9af为空855 个测试通过。教训说得直白以免被重新学习召回路径既不受锁限制也不受 I/O 限制。它每轮约做 253 ms 的 CPU 工作触碰命名空间内每一行、且要触碰两遍。只有减少触碰量下文第 1、2、3、7 项才能改变它。围绕扫描做微优化已被实测两次均证明毫无价值。深入剖析成本到底在哪第一轮分析把问题框定为召回是 O(N)需要向量索引。这没错但它是最后要修的东西而不是第一个。看向调用方一侧画面完全不同大部分工作根本不需要。每轮比你想的做更多次召回每轮 4.26 次 embedding 调用三次独立运行测得一致4.26 / 4.27 / 4.26——mock-stats.json对driver.json。每次召回都要嵌入查询因此该数字是召回操作的直接代理。每轮在 LLM 调用之前、全部阻塞地执行三次全命名空间 SQL 召回加一次纯向量查询召回查询limit命名空间citations用户消息5global工作记忆working.user {msg}5global先前对话conversation_memory {msg}12conversation_memory情境偏好用户消息5user_pref_situational其中两次扫描global。子 Agent不会重新召回它们继承父级的块所以这是每用户轮次的成本而非每 Agent。这些工作买到了什么最多9 行、约 2000 字符约 500 token到达 prompt工作记忆、先前对话、跨聊天各三条均被硬上限封顶。citations 根本不进 prompt它们只填充last_turn_citations供 UI 展示。向量侧的实测浪费对真实 2,121 分块语料跑 20 多次查询99.73%被打分的分块低于 0.4 相关性下限每次查询约 6 个分块过关。而且 cosine 并非昂贵部分——给 2,121 个分块打分在 JavaScript 中只需3.5 ms在 Rust 中远低于 1 ms。成本在于每轮从 SQLite 重新读取并解码语料加上关键词侧对全文的重复规范化。最清晰的单一缺陷工作记忆召回把查询串拼成working.user {user_message}——一种意在偏置排序的文本 hack——扫描全部global、取前 5然后才过滤key.starts_with(working.user.)。这等于先扫描整个命名空间去找一个已知键前缀就能定位的条目。基准语料中global docs scanned per turn : 2025 docs with key working.user.: 0每一次这样的扫描都空手而归。这不仅慢还会静默退化随着自动保存的聊天填满globalworking.user.*条目能挤进 global top-5 的概率趋近于零功能在任何人开始 profile 之前就悄悄失效了。原文档还留下一个值得同步路径负责人核实的疑点仓库内找不到任何working.user.*键的写入方——只有查询、测试 fixture 与一条描述其为同步派生的画像事实的文档注释。若当前构建无人写这些键此块永远为空、扫描是纯成本。为什么global无界增长每条自动保存的用户消息都以空命名空间存储当时位于src/openhuman/agent/harness/session/turn/core.rs:709由sanitize_namespace映射到global——正是两条热点召回扫描的同一个命名空间。上述语料为 2,024 个user_msg:*文档加一个其他文档。命名空间分区机制早已存在并被conversation_memory、user_pref_situational使用只是没有应用到这两条昂贵的调用上。上限是锁且有闲置核心测试机有14 核记忆开启的运行只用了7.2。所以这不是 CPU 饱和。UnifiedMemory持有一个MutexConnection召回路径每次调用约获取它 8 次因此每轮约 61 ms 的串行 SQL 把吞吐封死在约 16/s 附近——与实测 14.5/s 吻合——而机器一半闲置。当前实现中这一结构依然可查证MemoryClient::profile_conn()派发的正是裸ArcMutexConnection见 profile_conn_guard_tests.rs 对该 seamy 接口的说明。修复建议从最便宜的开始前三条位于src/openhuman/而非 vendored crate减少的是扫描量而非扫描速度1. 将工作记忆召回限定到自己的命名空间。条目已由键前缀标识给它们一个命名空间并直接查询而不是事后从globaltop-5 里过滤。每轮移除一次完整的global扫描并修复静默退化 bug——工作记忆条目不再被无关聊天挤掉。复用已在别处使用的机制。即便什么都不做也要先做这条。2. 把 citations 移出轮次的临界路径。它们只服务 UI、从不进 prompt却用一次完整global扫描阻塞响应。移除延迟路径上第二次global扫描。3. 停止把原始聊天消息自动保存进global。这正是让 N 在最热命名空间里无界的根源。需要先有产品答案既然conversation_memory由 transcript 派生的持久事实与跨聊天 JSONL 扫描都已存在原始用户消息召回还值得保留吗若冗余这是一行命名空间改动永久框住问题。4. 给召回加超时。每个调用点都把召回当作 best-effort全程unwrap_or_default()失败记日志并跳过——但任何地方都没有超时而轮次会阻塞。慢召回今天可以让一轮无限期停滞。无论性能工作如何这都是值得做的健壮性修复。随后在 vendored crate 中按此顺序5. 缓存规范化后的文档文本。keyword_score_for_text每次召回都对每个文档的全文重新规范化每次调用约三次分配、约三次遍历经由normalize_search_text。结果只依赖文档、从不依赖查询所以每轮都在重复计算相同结果。语义等价。6.只读连接池。已尝试并实测——无效。见上文两次失败的优化。7. 最后才上向量索引sqlite-vec / HNSW。这是真正无界语义搜索的正解也是唯一让召回亚线性的方案——但第 1–3 项削减 N 的幅度远超索引削减常数的幅度且风险低得多。crate 内其他一切检索路径都有有界先例episodic_search、event_search_fts、segments、entities 全是ORDER BY … LIMIT只有这一条路径是例外。落地成果shipped 了什么、买到了什么第 1、3、2 项已在分支memory-recall-diet实现load_context()被移除。每轮的[User working memory]/[Prior conversations]/[Cross-chat context]块消失MemoryLoadertrait 与DefaultMemoryLoader一并删除。记忆工具原样未动模型仍可按需取记忆——这正与当前 memory_loader.rs 只剩MemoryCitation结构、collect_recall_citations()与CROSS_CHAT_HEADER常量的源码状态吻合。自动保存移出global进入conversation_rawCONVERSATION_RAW_NAMESPACE应用于 Agent 轮次与 channels 分发器。该命名空间常量在 types.rs 中定义为conversation_raw并在 core_turn.rs 的 autosave 路径使用唯一user_msg:{uuid}键规避并发轮次覆盖同一槽位的旧竞态与 processor_part_02.rs 的通道分发路径中被引用。Citations 由串行改为重叠执行。它们只服务 UI却曾是模型调用前阻塞每次回复的完整召回现在改为 spawn 后 join。源码证据清晰可见在 core_turn.rs 中citation 收集被tokio::spawn为pending_citations任务MEMORY_CITATION_LIMIT 5、MEMORY_CITATION_MIN_RELEVANCE 0.4轮次不再等待它在 runtime_impl_01_part_01.rs 的take_last_turn_citations()中调用方再来 join 仍在途的收集——到那时模型往返早已完成通常无需等待。在全新工作区、4 分钟、并发 8、tool-depth 0 下测量即增长测试双臂均从空开始基线recall dietΔ完成轮次7,80616,6012.13×吞吐32.5/s69.1/s2.13×p50 延迟232 ms102 ms降低 2.3×每轮 CPU223 ms82 ms降低 2.7×RSS 增长195 KiB/轮26 KiB/轮降低 7.5×全程延迟漂移125 → 439 ms86 → 162 ms—吞吐保持判定fail(33%)pass(55%)—命名空间变更在产物库中确认基线写入global8202diet 构建写入conversation_raw17552, global1。残余增长在写侧不在读侧diet 构建中延迟仍有漂移86 → 162 ms工作尚未完成。同一构建再禁用记忆写入即可完全隔离吞吐p504 分钟延迟基线32.5/s232 ms125 → 439 msdiet69.1/s102 ms86 → 162 msdiet、无记忆写入105.5/s68 ms75 → 76 ms平坦平坦且吞吐保持99%。因此全部残余漂移都在记忆写入路径——upsert、embedding、对增长中存储的分块插入——读侧不剩任何问题。这是更小且独立的问题深入分析时标记的候选是会话存储索引vendor/tinycortex/.../conversations/store_index.rs它几乎在每次操作时都从零折叠threads.jsonl。另外助手摘要现在跟随用户消息进入conversation_raw并使用唯一键因此并发会话既不会覆盖同一个 global 文档也不会把原始对话自动保存泄漏进默认命名空间的召回。尝试并拒绝把 embedding 解码移出连接锁——已实现、已实测、无效。只读连接池——已实现、已实测、无效同上一节。有界/近似候选集作为第一步——未尝试刻意为之它会改变哪些记忆浮出水面而第 1–3 项无需付出该代价就能取得更多。独立发现journal 写放大每次 Agent 运行都会向tinyagents_store/journal/写入约 604 KB 的 journal 文件哪怕只是一个平凡轮次。其中一行约占 424 KB模型调用事件序列化了完整 system prompt 和约 80 个工具的完整 schema。一次 4 分钟的运行留下约 5 GB关闭记忆后轮次完成更多达到约 13 GB。这是每轮 O(1) 的所以不是上述退化的原因基准也不会因此失败。标记它是因为每轮约 600 KB 的近乎静态文本对长期运行的安装是真实成本。结语这次调查给出了一套可复用的方法论用--workspace复用工作区区分数据规模成本与进程运行时长成本用--memory-off与--memory-writes-off作为控制组把记忆读写从轮次路径中剥离用 mock 侧__bench/stats与 driver 侧统计交叉核对负载真实性run-agent-scale.sh 内置的 cross-check 会拒绝driver 报 ok 但 mock 只服务了少量 completions的虚假绿色最后用 Littles law 证明吞吐饱和源于真实工作而非锁竞争。而先改调用方减少扫描量、后改存储侧加速扫描的优先级原则以及微优化围绕扫描的改动已被实测两次证明无用的教训对任何长生命周期、记忆累积型 Agent 系统的性能治理都同样适用。【免费下载链接】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),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。