我们如何在 Elasticsearch 上构建一个持久 agent 记忆层,实现 0.89 召回率和零租户泄漏
作者来自 Elastic Noam Schwartz发现基于 Elasticsearch 构建的持久化、多租户 agent 记忆层架构三个索引、结合 RRF 和重排序器的混合检索、supersession、衰减机制以及按用户的 DLSDocument-Level Security 隔离。在 168 个问题上 R10 达到 0.89。包含完整开源实现。Agent Builder 现已正式发布。通过 Elastic Cloud 试用 开始使用并查看 Agent Builder 的文档 这里。在 Elasticsearch 上构建 agent 记忆三个索引、结合 reranker 的混合召回、替代机制supersession、衰减机制以及 DLS。这是为 agent 构建持久记忆层的架构与背后指标说明。Sarah 的智能灯泡只显示白光。她的智能家居助手建议重置 hub集线器。她在三月做过一次重置上周又做了一次两次重置都没有解决问题。agent 并不知道这些也不知道她的狗咬断了传感器电缆这件事。那些重要的历史信息 —— 什么有效、什么无效、以及 Sarah 是谁 —— 都随着每个 session 结束而消失了。常见的变通方法是把之前的上下文塞进 context window。但这在成本、延迟latency以及众所周知的 “lost in the middle中间遗失” 效应上都会失效在该效应中模型会忽略那些位于提示词两端之外的事实。1M token 的 context window 也只是一个便签板scratchpad而不是一个记忆系统。context window 是短期记忆用于单次 inference 的活跃推理空间。而缺失的是长期记忆一个持久化存储可以跨 session 存在、支持多年交互并且能够按内容、时间和用户检索事实。这篇文章讲的是一个真实 agent 记忆系统的架构构建在 Elasticsearch 之上并围绕来自认知科学cognitive science 的三类结构设计一个结合 RRF 和 cross-encoder reranker 的混合 recall query用于矛盾处理的覆盖替代以及按用户的 DLS 隔离。在一个包含 168 个问题的 QA 评测中R10 平均达到 0.89且没有跨租户泄漏。完整实现已开源在 GitHub本文重点在于解释它为什么会被设计成这样。https://www.bilibili.com/video/BV1erLD63E5g/Agent 记忆存储需要做什么当用户问“我们上次尝试了什么修复” 这是一个带时间约束的精确匹配查询。或者问“为什么我的智能灯泡只显示白色” 这需要把个人记忆与共享目录信息结合起来。记忆本身并不是统一行为用户经历过的事件、关于用户的稳定事实以及一步一步的操作流程它们各自有不同的写入频率和衰减规则因此存储系统必须识别类型并分别处理。在任何多用户部署中每个用户的记忆都必须对其他用户完全不可见。新鲜事件会快速累积如果不进行归并就会把索引变成一堆无序信息。用户一旦和已检索事实产生矛盾旧版本必须通过覆盖替代supersession而不是删除来处理以便保留审计轨迹。旧事实不应压过新事实用户频繁触达的事实也不应下沉。整个 memory 层还应该可以被任何支持 MCP 的 client 访问而不是绑定在某一个 agent runtime 上。如果把这些能力拆分到一个 vector store、一个关键词引擎、一个审计层以及一个独立认证服务中就会变成四个可能出错的系统并且每次 recall 都要额外增加往返开销。这些需求本质上描述的是一个 search engine因此这个实现直接使用一个系统完成。后文将逐一展开。Agent 记忆的三种类型情景记忆、语义记忆、程序记忆第一个设计决策是到底要存储哪些类型的记忆。如果只是把所有东西都存起来就会变成没有信号的干草堆。来自认知心理学cognitive psychology的 情景记忆episodic、语义记忆semantic、程序记忆procedural划分在 COALA 框架中已经被用于 LLM agent这一分类本身就非常合适并且可以直接映射到三个 Elasticsearch 索引。情景记忆episodic memory带时间戳的事件。每一轮用户输入在进入系统时就被记录下来尚未经过抽取或解释。其中大部分是短期的不一定需要长期保存只有少部分会成为后续稳定事实的证据。语义记忆semantic memory被提炼后的稳定用户断言。例如Sarah 拥有 Lumio Hub v2Sarah 使用 iOS 17.4Sarah 的 hub 在三月被重置。这些信息跨 session 存在是 agent 进行 grounding 的基础。程序记忆procedural memory多步骤操作流程。例如 Zigbee 断连的排查步骤。它不是事实而是过程。每条流程都会携带 success_count 和 failure_count并在 consolidation 时根据用户是否确认修复成功来更新。这些计数会作为上下文提供给 LLM用于判断是否需要优化或替换该流程。每一种 memory 都有不同的生命周期。情景记忆持续写入并衰减语义记忆会被整理、去重并在用户信息变化时被覆盖替代程序记忆则通过 success_count 和 failure_count 累积反馈用于指导 consolidation。单一结构无法表达这些差异因此采用三个索引每个 memory 类型拥有独立的写入频率、衰减规则和更新机制。在这三类之外还有第四种检索retrieval数据源已经存在于 Elasticsearch 中的世界数据目录、知识库。它在认知意义上不属于 “记忆”但 agent 通过同样的 hybrid retrieval pipeline 访问它下一节会讲因此在整体架构中被统一纳入同一视图。检索管道基于 RRF 和 reranker 的混合检索Memory 的召回通过一个两阶段的 hybrid search 实现在BM25 Jina v5 dense上做 RRF 融合然后对合并后的候选集用 cross-encoder reranker 重新排序。每一条document在一次写入中被双重索引原始文本进入 BM25 的inverted index同时通过copy_to把同样的值路由到semantic_text字段由此自动生成 Jina v5 向量。Indexing同一份内容两次并不会增加存储负担一次 source-of-truth 写入同时产生两条检索路径index mapping。每条路径解决不同问题。BM25 捕捉字面 token 匹配这些信息在 agent 改写问题时会丢失版本号、错误码、像 “Lumio Hub v2” 这样的专有名词。dense 向量捕捉语义结构即使表达不同也能匹配问题与答案。单独任何一条路径都会遗漏另一条能覆盖的情况而 RRF 在无需对 BM25 分数和 cosine 相似度做标定的情况下融合排序结果。Over-fetch。reranker 只能对已有候选集重排序因此候选集必须足够大。混合检索器每条路径取 80 个候选并用 RRF 进行融合rank_constant30比 ES 默认 60 更紧使得高排名条目权重更集中rrf_fetch。Reranker。使用 Jina v2 cross-encoder对合并后的候选与用户查询进行打分。BM25 和 bi-encoder dense 是分别独立对 query 和 document 编码而 cross-encoder 会对二者联合建模在完整 attention 下计算相关性效果更强但计算成本更高。这正是两阶段 pipeline 的原因先用便宜的 hybrid retriever 做 over-fetch再用更昂贵的 scorer 对小候选集 rerankrerank。有一个细节如上图所示。agent 的工具集包含 recall_memory定义在 tools.py模型在一次对话中会调用它。一次调用会同时横向检索所有三个 memory index 和 catalogagent 不需要选择 memory 类型因为 retriever 的 ranking 和各 index 的时间衰减已经完成路由。第二个细节是改写paraphrasing。agent 几乎总是会先改写用户消息再调用工具这会在 BM25 看到 query 之前剥离版本号、错误码和专有名词。因此每一轮都会先对用户原始消息做一次自动 pre-recall把结果注入对话中仿佛 agent 自己已经调用过该工具。写入与合并 agent memory有两个操作把 memory 从“刚发生的事件 ” 变成 “长期保留的信息”。Write。每一轮用户输入都会先写入一条 episodic eventID、原始消息、时间戳等然后 LLM 才开始响应。ID 由 Elasticsearch 写入时生成DLSDocument-Level Security 查询通过 Sarah 的 API key 在后续每次 recall 中限制数据范围时间戳用于下面的 time-decay 排序函数。agent 的回复不会被存储因为对话历史已经会在下一轮输入中携带这些信息而且回复内容通常冗长会稀释用户提供的短但高价值事实。选择在 hot-path 写入是刻意的。两种看似合理的替代方案都存在问题。把新事实留在 context window 中确实能覆盖当前 session但一旦 session 结束或崩溃in-context state 就会消失而跨 session memory 正是目标。在 session 结束时 batch 写入可以保留跨 session 状态但会破坏同一轮中的两类关键流程用户在同一条消息中提到新设备并查询设备列表时新事实必须在同一 turn 后续 recall 中可见因为 tool call 查询的是 index 而不是 conversation historysupersession 流程也会在同一 tool-call batch 中写入修正事实并立即 recall。如果延迟写入这些模式会静默出错。因此选择每条用户消息一次 Elasticsearch 写入成本是可控的单次通常低于 100ms。“哪些建议有效” 被单独记录在 procedural index 中的success_count/failure_count而不是保存完整回答文本。最近的带用户确认的事件“谢谢这有效”会触发success_count明确否定“没用”触发failure_count。对话本身作为反馈信号由 consolidation LLM 做分类不需要点赞组件。分歧还会生成refined_steps字段回写到流程中。Consolidate。episodic logs 增长很快因此需要把它们提升为 semantic facts 和 procedural playbooks使其在对话历史消失后仍然存在。该实现每轮都会运行一次用于观察 inspector 实时更新但在生产中更合理的节奏是后台任务每 24 小时一次或当用户 episodic index 超过 N 条事件时执行。逐轮执行会让 LLM 调用次数翻倍。在一次调用中promptconsolidation LLM 会接收最近事件 已有 facts 和 playbooks并生成三类输出新的 semantic facts包含supporting_episode_ids用于溯源新的 procedural playbooks当多步骤解决方案不匹配已有 trigger 时生成procedural updates基于用户反馈更新success_count/failure_count以及在用户否定时生成 refined_stepsPrompt 要求每条输出都必须包含supporting_episode_ids因此没有信息支撑的 turn 会返回空列表不会写入任何内容。Dedup 使用与 recall 相同的 hybrid retriever对每个候选 fact 先做 top-K hybrid search 缩小比较范围再交给 LLM 判断语义是否重复。还有两个额外保护条件低于 confidence 阈值的候选会被丢弃如果最相似匹配 ≥ 0.90则视为重复。在这个实现中dedup 更简单只把最近约 50 条 facts 交给 consolidation LLM并提示 “不要重复”而 post-LLM 的 confidence / similarity guard 尚未接入。hybrid-recall 和 guard 是生产架构这个版本依赖 LLM 直接比较因为corpus足够小。success_count和failure_count形成 playbook 的反馈闭环随着对话增多同一字段逐渐变成“下次优先展示哪个方案”的信号。目前这些计数已经写入但还没有影响 retrieval ranking。在少量已解决 ticket 上这个信号还只是统计噪声在正式生产中随着数据量增加它会变得有意义。Agent 如何处理矛盾与 supersession 的 memory只会不断追加、不会删除的 memory最终一定会出错。用户说 “我搬到 Edinburgh” agent 写入一个新事实。六个月后旧的 “住在 Bristol” 的事实仍然留在 index 里。两者都会在每次 recall 时被检索出来 agent 要么选错要么只能含糊处理。信任会很快崩塌。解决方法是在 system prompt 里加一条规则full prompt不引入新工具。不是删除而是让 agent 进行 supersessionIf the customer contradicts a recalled fact, call write_memory(textnew, supersedes_idold id, contradictionharsh|natural). Use contradictionharsh when the customer explicitly denies or corrects the prior fact (no, thats wrong, I never X). The new fact carries a small confidence penalty. Use contradictionnatural for routine updates (moved, upgraded, preference change). The new fact is written at full confidence. Never ask the customer to confirm before superseding. The contradiction itself is the signal. forget_memory is only for explicit forget this requests.一个完整示例Sarah 的上一次访问记录了idabc“Sarah lives in Bristol”写入 semantic index。三个月后她打开对话“we left Bristol, in Edinburgh now.”1. 召回Recall。对 Sarah 的消息进行 pre-recall返回命中结果包括 {id: abc, text: Sarah lives in Bristol, memory_type: semantic}。2. 检测Detect。agent 发现被召回的旧事实与新消息之间存在冲突。3. 分类Classify。“we left Bristol, in Edinburgh now” 被判断为自然更新而不是否认因此选择contradictionnatural。4. 写入Write。agent 调用write_memory(textSarah lives in Edinburgh, supersedes_idabc, contradictionnatural)。这一操作会同时发生两件事写入一条新文档idxyz并保持完整置信度因为是自然变化不做惩罚旧文档abc被更新为superseded_byxyz, superseded_atnow5. Recall 隐藏旧数据Recall hides the old。每次 recall 都会应用一个过滤规则filter must_not exists fieldsuperseded_by。因此abc会从 agent 视野中隐藏xyz正常返回。6. 审计保留Audit kept。文档abc仍然保留在 index 中。通过查询superseded_byxyz可以重建完整链路。注意如果 Sarah 后续问“我住过哪些地方”agent 会调用recall_memory(queryplaces sarah has lived, include_supersededTrue)。由于 DLS 作用域限制这次查询会同时返回xyzEdinburgh和abcBristol。带有superseded_at的记录表示历史状态agent 在回答时会区分它们实现位置“你现在住在 Edinburgh你之前住在 Bristol直到今年早些时候。”如果 Sarah 说的是“I never lived in Bristol, that was my sister”第 3 步会被分类为harsh。写入流程相同但新事实的置信度会被SUPERSEDE_CONFIDENCE_PENALTY降低使系统在新状态未被充分确认前保持一定谨慎。边界情况遵循同样结构一个已被 supersede 的事实可以继续被 supersedeabc → xyz → pqr低风险偏好例如 “我现在更喜欢 dark mode”同样按contradictionnatural处理。forget_memory才是硬删除机制只在用户明确要求 “忘掉 X” 时使用不用于普通冲突更新。还有一个关键细节一次 recall 可能命中多个与新陈述冲突的事实。例如 Sarah 的位置可能同时存在“Sarah lives in a Victorian flat in Bristol”semantic“Sarah has a flat in Bristol where her Hub v2 is”semantic以及某次 episodic eventagent 必须对所有冲突事实执行 supersede而不是只处理第一个命中的结果。对每个被新陈述否定的 id都要分别调用write_memory(supersedes_id…)。但那些只是 “提到 Bristol、但仍然成立” 的信息不会被覆盖例如“Bristol 的维多利亚式公寓墙体很厚会削弱 Zigbee 信号”这类知识仍然有效因此不应被 supersede因为它不依赖“是否住在 Bristol”。已 supersede 的文档默认不会出现在普通 recall 中只有在include_supersededTrue时才会返回。在生产环境中这类数据通常通过 Elasticsearch 的 ILMIndex Lifecycle Management迁移到冷/冻结层searchable snapshots。审计链仍然可查询但活跃 semantic index 保持小而高效。确保 Elasticsearch agent memory 的同一 turn 写入可见性Elasticsearch 默认的异步 refresh 间隔会在 agent 写入 memory 并在同一 conversation turn 立即进行 recall 时产生传播延迟。当用户在同一条消息中说“I have a Lumio Range Extender I never set up. Now whats my complete device list?” 时agent 会先写入 Range Extender 这一事实然后立刻执行 recall这一过程发生在同一个 turn甚至可能在同一个 tool-call batch 内。默认的 Elasticsearch refresh 间隔加上semantic_text的 inference 计算成本可能导致亚秒级传播延迟使刚写入的文档在 recall 时尚不可见。解决方案在存储层。每一次 agent 触发的write_memory都会传入 refreshTrue强制 shard 在返回前 refresh同时 inline inference processor 生成的 Jina v5embedding也会完成落盘。这样下一次 tool call 就能看到新写入的文档。Range Extender 会出现在最终回答中因为紧随写入之后的 recall 已经能检索到它。在更高写入量下refreshTrue会带来throughput吞吐成本。生产环境可能会倾向于切换为异步 indexing同时在 agent 层维护一个 “刚写入记录表”把新写入内容暂存在 LLM context 中直到 index 追上为止。目前这种更简单的方案在系统中仍然占据合理位置。Agent 记忆检索中的时间衰减与使用次数评分目前为止的检索方案对所有事实赋予相同权重而不考虑它们的创建时间或最近使用时间。这并不是一个合理的默认策略。一个在过去一周被召回两次的事实几乎肯定比一个两年前只提过一次的事实更相关。因此我们在每个结果的评分上乘以两个权重因子一个主导的时间衰减time-decay信号以及一个辅助的使用频率优化信号。时间衰减是一个高斯形状的乘子在 Painless 中基于每个 index 的日期字段计算见下文。使用频率优化是一个use-count boost1 log10(1 use_count) * weight大致效果是被召回 10 次的事实约提升 1.2 倍被召回 100 次的事实约提升 1.4 倍。这两个机制回答的是不同问题时间衰减回答 “这个事实最近是否被使用”使用次数回答 “这个事实被使用了多少次”。当多个事实共享相同的last_used_at时两者就会产生分化衰减无法区分 “只用过一次” 和 “用过四十次”但 use-count 可以。时间衰减是基础信号use-count 是在召回规模足够大后才变得有效的增强信号。每种 memory 类型的时间字段episodic 和 semantic 使用不同的时间字段。episodic 使用timestamp事件发生时间semantic 使用last_used_at写入时设置并在每次 recall 时更新。Elasticsearch 原生的gauss无法跨字段工作因为该函数要求所有 index 必须存在同一个字段名。因此时间衰减必须在 Painless 脚本 中实现根据 index 类型选择不同字段并在运行时计算高斯衰减值。procedural memory 被刻意排除在时间衰减之外。因为last_used_at会在每次 recall 时更新无论成功与否如果直接使用衰减函数会奖励“最近被尝试过”而不是“最近有效”。更合理的设计应该是引入last_success_at并结合success_count/failure_count来参与排序。在这些字段尚未完整接入之前仅靠时间衰减会过于粗糙。semantic 上的 recall-time bump 是整个机制的关键。它将“旧事实权重下降”转化为“长期未被使用的事实权重下降”。这是相关性衰减而不是事实衰减。事实是否仍然真实由 supersession 机制处理而一个五年前的事实如果仍然被频繁召回它依然会排在前列因为last_used_at是最新的。这也对应认知科学中的同一机制retrieval practice检索练习会增强记忆可用性而长期不使用会导致衰减。对last_used_at的召回时更新本质上是这一机制的工程实现。检索时的乘子retrieval-time multiplier两个因子最终都会汇入同一个function_scoreblock并包裹在每一条 RRF 分支之上{ function_score: { query: bool query: text/semantic match filters, functions: [ { filter: {terms: {_index: [atlas_memory_episodic, atlas_memory_semantic]}}, gauss: {last_used_at: {origin: now, scale: 1825d, offset: 180d, decay: 0.5}} }, { filter: {term: {_index: atlas_memory_semantic}}, script_score: {script: 1 log10(1 use_count) * 0.2} } ], score_mode: multiply, boost_mode: multiply } }在代码层面这两个函数都写在同一个 Painless 脚本里只是按 index 做分支同一套数学逻辑更少的 function_score 条目。两个_index过滤器起到双重作用它们用于限定每个函数应该影响的 memory 类型范围——时间衰减作用于 episodic 和 semantic而 use-count boost 只作用于 semantic。同时它们也把 procedural 和 catalog 排除在外如果某个 function 的 filter 不匹配就返回中性值 1.0因此即使 cross-index 查询包含这些 index也不会出现评分或解析问题。完整函数在 operations.py。两个参数控制 gauss 曲线offset180天一个平坦区间flat zone。小于180天的文档统一乘以1.0的系数不论具体新旧。否则新事实之间会因为“亚天级别”的时间噪声而互相竞争。scale1825天约5年距离 offset 之后的时间点在该位置衰减系数达到decay 0.5。可以理解为“从平坦区结束开始计算的半衰期”。衰减本身是一个刻意的权衡。当 corpus 中每个事实都是唯一且长期有效时任何衰减都会带来 recall 损失旧事实仍然正确但被人为降权。衰减真正发挥价值的场景是现实情况多个关于同一对象的事实同时存在而你希望最新或最常被使用的那个排在最前。默认 scale1825天是保守设置因为它更不容易误伤长期有效信息。在产品支持这种“事实快速过期”的场景例如产品频繁迭代的客服系统可以调小在个人助手 memory 这种“事实长期稳定”的场景可以调大。这两个参数都只是对 constants.py 的一行修改。基于 Elasticsearch DLS 的多租户隔离Document-Level SecurityDLS将隔离规则直接下沉到 cluster 层。每个用户都有一个 API key其 role descriptor 中包含一个 DLS 查询只允许访问属于该用户的文档以及共享 catalog因为它没有user_id字段。使用该 key 的 agent 无论执行什么查询都无法看到其他用户的数据 —— cluster 在返回结果前已经过滤掉了。这是生产级别的隔离保证由 server 端在每次查询时强制执行。retriever 还额外在代码层加了一层user_idfilter作为防御性兜底防止 index template 配置漂移、role descriptor 被修改但 DLS 条件丢失、或 admin key 被误用等情况。DLS 是架构级保障这一层只是低成本的 paranoia check。将共享 catalog 数据接入 agent memory 检索memory lookup 本质上是一条 Elasticsearch 查询。Sarah 的 API key 的 DLS 规则允许访问user_id sarah的文档而 catalog 和共享索引没有user_id字段因此默认对所有人可见。为了把它们纳入同一次检索DLS 查询从“必须等于 sarah”扩展为“等于 sarah 或不存在user_id”即一个bool.should条件包含user_id sarah或must_not exists: user_id。这样 catalog 与个人 memory 可以在同一次 recall 中一起返回。RRF 融合和 time decay 都不需要改变。用户初始化 key 的 bootstrap 脚本在 bootstrap script 中生成并内置了这个扩展后的 DLS 条件。因此当查询“smart bulb showing only white”时系统会同时返回 Sarah 的历史约束 catalog 中关于灯泡兼容性的条目并在同一个 ranking 里排序。user memory 和 catalog 可能在同一 recall 中出现并相互冲突。retriever 在同一个 script 内加入一个轻量 source priorCATALOG_SOURCE_PRIOR0.85作为一个额外_index分支实现不引入新机制用于在近似相关度相同时偏向 user memory。这是一个 soft bias而不是 routing rule如果 catalog 的 relevance 明显更高例如产品规格或技术查询reranker 仍然会选择 catalog。那些“必须始终信任 catalog”或“必须优先用户偏好”的硬规则则放在 agent system prompt 中而不是 retriever 层。通过 MCP 连接任意 agentmemory 层的价值在于不绑定单一 agent。Model Context Protocol 提供了这一能力。endpoint 为/api/atlas/mcp/{user_id}任何支持 MCP 的客户端Claude Desktop、Cursor 或自定义 agent都可以通过在配置中粘贴 mcp.py 提供的 JSON 片段接入。在 Claude Desktop 中配置文件位于~/Library/Application Support/Claude/claude_desktop_config.jsonmacOS或%APPDATA%\Claude\claude_desktop_config.jsonWindows。在 Cursor 中则位于 Settings → MCP。重启后三个 Atlas 工具recall_memory、write_memory、forget_memory会出现在 tool 面板中调用的是同一套 Elasticsearch indices与 FastAPI 服务共享同一 memory 层。三种 tool contract 定义在 tools.py 中。https://www.bilibili.com/video/BV1mVLD68EuB/衡量 agent-memory 的召回质量关于 “recall” 的说明在本文其他部分它指的是 memory recallagent 从存储中检索事实。而在这里它指的是信息检索指标中的RecallK正确文档是否出现在 top-K 结果中——两者含义不同。memory 架构很难验证。这里的评估采用 QA 风格的passage retrieval也就是标准的 RAG 基准测试。对于每一条采样的 documentLLM 会生成两个用户可能提出的问题这些问题的答案都指向该文档。例如“my babys sleep is fragile, anything I should remember when setting up automations?” 会指向 Sarah 关于 nursery quiet-hours 的事实。然后 retriever 必须在 top-K 中检索到对应的源文档。类似 memory 的通用 benchmark例如 LoCoMo已经存在并且可以用于跨系统对比。但这里选择 corpus-specific QA 模式有两个原因。第一它针对的是每个 persona 的真实部署语料因此 recall 数字更贴近真实对话场景。第二它专注于检索这一阶段源文档是否进入 top-K而本文中的 hybrid decay reranker pipeline 正是在优化这一部分而 LoCoMo 的 dialogue coherence 指标则衡量更下游的整体生成质量。后续文章会运行完整的 LoCoMo benchmark并进一步拆分 retrieval performance 与 LLM 选择、prompt engineering 等因素的影响。对于任何多租户记忆系统而言“泄漏数”leaks number是决定能否通过的关键指标而其余指标则反映了质量水平。在 CI 流程中评估环节设有硬性门槛由 eval_recall.py 执行要求 R10 ≥ 0.85、R5 ≥ 0.75 且泄漏数为 0。上述数值均为近似值因为重排序器reranker在服务侧存在波动在连续四次运行中R10 的结果分别为 0.85、0.88、0.89 和 0.893。Semantic facts 是更困难的情况R10 ≈ 0.81episodic 平均 0.98而 procedural 命中率 1.0。原因是 sibling collision一个关于 Sarah 的 hub disconnects 的问题在 corpus 中可能存在多个“看起来都合理”的事实retriever 有时会选错那个。值得注意的是sibling collision 通常不会降低 agent 的最终回答质量因为它仍然会拿到一个相关且真实的事实所以 R10 对 semantic 来说本质上是保守指标。Agent memory architecture关键设计决策Agent memory 是一组问题每个问题对应一个关键解法memory 不是单一事物。三个 index对应三个生命周期episodic发生了什么、semantic什么是真的、procedural什么有效。LLM 会 paraphrase削弱关键词精度。每一轮都会先对原始消息做 recall因此 retrieval 使用 hybrid再经过 rerank。append-only memory 会退化。consolidation 会把 episodes 提升为持久 factssupersession 用于淘汰用户已否认的事实。旧事实不应该与新事实同等排序。score 会随时间 decay同时 recall 会将常用事实重新抬升。多租户必须完全隔离。isolation 由 cluster 内的 DLS 实现而不是容易遗漏的应用层 filter。这些并不是独立系统catalog、isolation 和 decay 最终都组合成一条 Elasticsearch query。只要这些机制正确组合那个曾经不断让 Sarah 重置 hub 的 assistant就会记住她三月已经试过了狗会咬传感器线而家现在在 Edinburgh。原文Agent memory on Elasticsearch: hybrid retrieval and DLS - Elasticsearch Labs