# Long memory, short context / 长期记忆与延迟 [首页](../README.md) · [基准脚本](../benchmarks/memory_latency.py) · [优化前数据](benchmarks/before.json) · [优化后数据](benchmarks/after.json) ## 实测结果 环境:macOS、Apple Silicon、Python 3.13.9;具体平台字符串和测量时间保存在 JSON 报告。每组 7 次,表格为中位数,单位毫秒。合成数据库包含等量的原始事件与提炼记忆;1 千、1 万、10 万条覆盖多年中文经历。先运行旧实现,再对同一批数据库升级索引后测新实现。 | 条数 | 特定词 recall:前 → 后 | 带问题 resume:前 → 后 | 常见词 recall:前 → 后 | 无问题 resume:前 → 后 | |---:|---:|---:|---:|---:| | 1,000 | 3.535 → 1.091 | 9.914 → 3.261 | 4.512 → 3.126 | 10.370 → 4.336 | | 10,000 | 28.363 → 1.496 | 78.763 → 7.217 | 37.952 → 9.693 | 83.688 → 7.769 | | 100,000 | 288.216 → 5.360 | 804.967 → 37.482 | 396.001 → 83.599 | 866.509 → 38.302 | 特定词是“蓝色风筝”,指向一条早期经历;三种规模均成功找回预期 ID。常见词是“朋友”,大量记录都包含它。恢复预算是 8,000,使用 UTF-8 字节保守估算;包含人格、约定、相关记录和近期上下文。新旧返回的细节等级不同,因为新实现会淡化旧经历,因此不能把恢复包大小变化全部归因于索引。 **范围和限制:** - 这是进程内 `Engine` 调用,不是端到端聊天延迟。不包括 MCP 子进程启动、传输、HTTP 网络、embedding、提炼、模型首 token 或生成时间。 - 没有清空操作系统磁盘缓存。`first_engine_resume` 只是新 Engine 对象的第一次调用,不能当成机器冷启动数据。 - 优化后最初的 1 千条首次恢复约 198 ms,包含进程内首次中文分词初始化;其余规模复用了已加载的分词器。稳态中位数不包含这个一次性成本。 - 7 个样本只适合开发阶段对比。报告保留全部样本与 p95,但 7 次的 p95 实际为最大值,不代表生产环境尾延迟分布。 - 批量生成数据库使用预分词合成数据,绕过实际提炼 API;**不能据此推断写入吞吐量**。 - 新索引使 10 万条数据库由约 169 MB 增至约 185 MB。这是空间换查询时间。 - 常见词仍受匹配量影响;没有承诺百万条、长篇原文、多用户或语义深搜的延迟上限。 ## 为什么会快 旧版先读取分区内所有记忆,再在 Python 里过滤;一次恢复会为检索、约定和情绪重复读取。新版把这些操作移到 SQL 中,用有界候选集、事件来源索引和关系索引逐步取数。FTS 用内建 rank 配合 LIMIT。已知 ID 的查找明确使用键索引,避免数据库为了时间排序而扫描整个分区。 默认最多取 200 个候选、返回 10 条记忆;相关边只扩一跳。恢复只挑有限的约定、相关记忆、情绪与近期事件,最后按调用方预算加入完整组件。预算不会限制整个宿主上下文,也不会抹去没被选中的原文。 不要在每轮对话重新总结全库。概览在提炼时产生并保存;读时只是选择呈现等级。旧数据没有概览时,使用标注清楚的少量主题词线索,避免现场调用 LLM。 ## 三层呈现与日记 | 层次 | 默认策略 | 细节如何找回 | |---|---|---| | 近期经历 | 详细内容与来源片段 | 可继续读原文 | | 180 天以上的经历 | 简短概览;时间只显示年份 | `memory_read(record_id=...)` | | 730 天以上的经历 | 概览与更粗的年代标签 | 同一原始记录 ID,精确时间仍保存在数据库 | 可通过配置调整 `gist_after_days`、`distant_after_days` 和排序用的 `memory_half_life_days`。后一个阈值必须不小于前一个。这里只是一个可解释的工程策略,不声称模拟两岁以前的真实人类记忆。 **保护稳定知识:** 核心人格、事实、偏好、约定、成长不因年龄被模糊;当前事实依然根据有效时间区间选择。旧情绪的原始表达保留,但衰减到接近基线后不再作为当前状态注入。 “模糊”意味着少带细节,不是故意写错细节,也不是不断摘要再摘要。保存的原文从未被覆盖。自动 gist 与主题词仍带有来源 ID、类型和确认程度,推测始终是推测。用户要精确细节时,先查原文再回答。 读取回忆不会自动增强它的权重或改变人格,避免模型反复读取自己的猜测,反而把它“强化”为事实。用户确认或新增经历应作为新事件提交。 ## 照片作为线索 在 `memory_record` 参数中关联一个本地照片标识,示例是合成数据: ```json { "event_id": "synthetic-kite-001", "text": "那次我们一起放了蓝色风筝。", "role": "user", "source": "synthetic-photo-diary", "occurred_at": "2010-06-01T00:00:00Z", "persona_id": "whale", "project_id": "companionship", "cues": [{"kind":"photo","key":"photo:sha256:YOUR_IMAGE_HASH","label":"蓝色风筝照片"}] } ``` 之后用 `memory_recall(query="", cue_key="photo:sha256:YOUR_IMAGE_HASH", ...)` 找到关联事件,再通过 `memory_read` 看日记。描述也可参与关键词检索。标识由宿主提供,不要求固定的哈希算法;示例字符串不是一个实际文件的哈希。 本服务不会访问任意照片路径、读取图片或执行视觉识别,也不会从同一张照片推断不存在的共同经历。后续宿主插件可以负责图片识别、得到用户确认的说明,再把它们作为有来源的数据提交。 ## 语义检索的速度与覆盖范围 | 模式 | 是否远端调用 | 检索范围 | |---|---|---| | `fast`(默认) | 否 | 全库 FTS 和线索索引中的有限候选,加一跳关系 | | `semantic` | 已配置 embedding 时会调用 | 叠加近期最多 2,000 条有效索引向量;超限提示覆盖不全 | | `deep` | 已配置 embedding 时会调用 | 叠加所有有效索引向量的线性扫描;大库可能较慢 | 语义索引需要和 embedding 模型/版本绑定。`reindex` 重建后才有完整可用向量;改模型不会混用旧向量。这里只对 `fast` 做了规模基准。需要更大规模语义检索时,后续可加近似向量索引,并以召回质量和尾延迟一起验收。 实际对话总延迟可分为:宿主调度 + 记忆检索/恢复 + 模型处理输入 + 模型生成。小恢复包通常减少输入负担,但当前没有真实模型计时,所以不报告“换模型总共只要几毫秒”。同步 `memory_record` 还可能等待提炼与生成向量的 API;原文在等待之前已提交。 ## 复现 ```sh uv sync --frozen --group dev uv run python benchmarks/memory_latency.py \ --root /tmp/memlikehuman-benchmark \ --output /tmp/memlikehuman-benchmark.json \ --sizes 1000 10000 100000 --repeats 7 ``` 目录必须是新的,避免覆盖真实档案。Windows 使用等价的新本地路径。重复测量已有合成库可加 `--reuse`。仓库保留的是旧实现的测量报告,不是另一个维护分支;当前脚本复现当前实现,不会自动回退旧算法。基准默认每条经历有概览;对已存在的 v1 合成库使用 `--reuse` 会测试迁移后的主题词回退,和此次优化后测量一致。 参考:[SQLite FTS5 的 rank 排序](https://www.sqlite.org/fts5.html#the_rank_configuration_option)。所有性能数字均来自本仓库脚本实测,不来自外部性能宣传。