Agent Builder 现已正式发布(GA)。您可以通过 Elastic Cloud 试用版立即开始使用,并在此处查阅 Agent Builder 的文档。
在 Elasticsearch 上构建智能体记忆
三个索引、结合重排序器的混合召回、替代机制、衰减机制以及 DLS。支撑智能体持久化记忆层的架构与数据。
莎拉的智能灯泡只显示白色。她的智能家居助手建议重置网关。她在三月份做过一次,上周又做了一次;两次重置都没解决任何问题。智能体对此一无所知,它也不知道那条狗咬断了她的传感器线缆。那些重要的历史记录——什么有效、什么无效、以及莎拉是谁——都随着每次会话的结束而消失了。
标准的变通方法是将先前的上下文塞入上下文窗口。但这在成本、延迟以及被充分记录的“中间迷失”效应上都会出问题——模型会忽略远离提示词边缘的事实。一个 100 万 token 的上下文窗口只是一个草稿本。它不是一套记忆系统。
上下文窗口是短期记忆:单次推理的主动推理空间。真正缺失的是长期记忆:一个持久化的存储,能在会话结束后继续存在,可扩展至多年的交互,并允许你按内容、按时间、按用户检索事实。
本文介绍的是一个真实智能体记忆系统的架构,该系统构建于 Elasticsearch 之上,并围绕认知科学中的三个类别进行组织,采用结合 RRF 和交叉编码器重排序器的混合召回查询、用于矛盾信息的替代机制,以及基于用户的 DLS 隔离。在一个包含 168 个问题的 QA 风格评估中,R@10 平均值为 0.89,且零跨租户数据泄露。
完整实现已在 GitHub 上开源;本文旨在解释其设计成这样的原因。
一个智能体记忆存储需要做到什么
有用户问“我们上次尝试了什么修复方法?”,这是一个带有精确匹配约束的时间查询。或者“为什么我的智能灯泡只显示白色?”,这需要将个人记忆与共享目录融合。记忆本身并非统一运作:用户亲身经历的事件、关于他们的稳定事实,以及逐步操作指南,都有不同的写入速率和老化规则,因此存储系统必须识别类型并分别处理。在任何多用户部署中,每个用户的记忆必须对其他用户完全不可见。新事件积累得足够快,必须整合到持久类型中,否则索引就会变成一团乱麻。当用户反驳某个已回忆的事实,旧版本必须被取代而非删除,以便保留审计轨迹。旧事实不应优先于新事实,用户经常触及的事实也不应沉没。整个记忆层应能被任何支持 MCP 的客户端访问,而不局限于某个智能体运行时。
将这些需求分散到向量存储、关键词引擎、审计层和独立的认证服务中,意味着四个可能出故障的环节,以及每次回忆时的额外往返。这些需求描述的是一个搜索引擎,因此本实现使用了一个搜索引擎。本文其余部分将逐一讲解每个环节。
三种智能体记忆类型:情景记忆、语义记忆、程序记忆
第一个设计决策是究竟要存储哪些类别的记忆。仅仅保存所有内容会构建一个毫无信号的乱麻堆。认知心理学中情景记忆、语义记忆和程序记忆的划分,在 COALA 框架中被引入用于大语言模型智能体,已经具备了正确的分类,并且它们能清晰地映射到三个 Elasticsearch 索引上。
- 情景记忆。带时间戳的事件:每次用户交互的原始记录,未经任何提取或解释。其中大部分是短暂的:并不总是值得保留。少数条目会成为后续持久事实的证据。
- 语义记忆。关于用户的提炼、稳定断言。Sarah 拥有一台 Lumio Hub v2。Sarah 使用的是 iOS 17.4。Sarah 的集线器在三月份被重置过。这些信息跨会话保留,是智能体所依据的基础。
- 程序性记忆。多步骤操作手册。如何排查 Zigbee 断连问题。是流程,而非事实。每条流程都带有 success_count 和 failure_count,当用户确认某个修复方案有效或无效时,通过 consolidation 机制递增这些计数。这些计数器会作为上下文提供给 consolidation 大语言模型,供其在考虑是否优化或替换某条操作手册时参考。
每个类别都有不同的生命周期。情景记忆被持续写入并逐渐衰减。语义记忆会随着用户变化而被整理、去重和取代。程序性记忆则累积结果反馈(success_count, failure_count),用于支撑 consolidation。单一存储桶无法建模这种差异。三种索引,每种记忆类型对应一个,让各自遵循自己的写入频率、自己的老化规则以及自己的更新规则,彼此互不耦合。
在这三种记忆之外,还有第四种检索来源:Elasticsearch 中已有的世界数据(目录、知识库)。从认知意义上说,它并非“记忆”,但智能体通过同样的混合检索管道(下一节介绍)读取它,因此它属于同一幅图景。
回忆管道:基于 RRF 和重排序器的混合检索
记忆的回忆采用两阶段混合搜索:先对 BM25 + Jina v5 稠密向量进行 RRF 融合,再对合并后的候选结果使用交叉编码器重排序器。每个文档在写入时以两种方式建立索引:原始文本进入 BM25 倒排索引,同时 copy_to 将同一值路由到自动生成 Jina v5 向量的 semantic_text 字段。对同一内容建立两次索引保持了存储占用不变:一次真实来源写入即可产生两条检索分支(索引映射)。每条分支解决不同问题。BM25 锚定智能体改写会丢失的字面 token 匹配:版本号、错误码、专有名词如“Lumio Hub v2”。稠密向量则捕捉问题语义形态,即使答案使用不同词汇。任何一条分支单独使用都会遗漏另一条能处理的场景,而 RRF 融合它们的排序结果,无需校准 BM25 分数与余弦相似度。
过度获取。重排序器只能对其所见内容进行重新排序,因此候选池需要足够宽泛。混合检索器每路获取 80 个候选结果,并使用 rank_constant=30(比 ES 默认的 60 更严格,因此排名靠前的项目占据更大优势)进行 RRF 融合。(_rrf_fetch)
重排序器。一个 Jina v2 交叉编码器将合并后的候选结果与用户查询进行评分。BM25 和双编码器稠密模型都是独立对查询和文档进行评分,而交叉编码器则对它们进行联合评分,在配对间进行完全注意力计算,这能提供更强的相关性信号,但每对的计算成本也更高。这正是采用两阶段流程的动机:先用混合检索器进行低成本的过度获取,再用成本更高的评分器对较小的候选池进行重排序。(_rerank)
如上图所示,有一个细微之处。智能体的工具包中包含 recall_memory(定义在 tools.py 中),模型会在一次交互轮次中调用它。单次调用会同时遍历所有三个记忆索引和目录:智能体不会选择记忆类型,因为检索器的排序和按索引衰减机制会代为处理路由。第二个细微之处是改写。智能体在调用该工具之前,几乎总是会先重写用户的消息,这会在 BM25 处理查询之前,从中剥离掉字面意义上的版本号、错误代码和专有名词。因此,每一轮交互都以对用户原始消息的自动预召回开始,并将结果注入对话,就像智能体自己进行了调用一样。(agent.py)
写入并整合智能体记忆。
两个操作将记忆从“刚刚发生了什么”转变为“关于这个用户,什么是持久有效的”。
写入。在 LLM 响应之前,用户的每一轮交互都会写入一个片段事件(包含 ID、确切消息、时间戳等)。ID 由 Elasticsearch 在写入时分配,Sarah 的 API 密钥上的 DLS 查询确保该文档在后续每次召回时都限定在她的范围内,而时间戳则供时间衰减函数(见下文)读取,用于将该事件与较新的事件进行排序。智能体的回复不会被存储。对话历史已经将它们带入下一次调用,而它们的长度会淹没用户所说的简短、富含事实的内容。热路径写入是一个刻意的选择。两种替代方案乍看之下似乎合理。让上下文窗口将新事实带入后续调用,在同一个开放会话的剩余时间内是可行的,但一旦会话结束或崩溃,上下文中的状态就会消失;而跨会话记忆才是整个目标。
在会话结束时批量写入可以保留跨会话状态,但它破坏了这个实现所依赖的两种同轮次模式。用户在同一消息中提及新设备并询问其设备列表时,需要新事实对同一轮次稍后运行的召回可见,因为工具调用查询的是索引,而非对话历史。而替换流程会在一个工具调用批次内写入一个修正后的事实,并针对该事实进行召回。这两种模式在延迟写入的情况下都会静默地出现错误行为。我们为此付出的代价是每条用户消息进行一次 Elasticsearch 写入,在单个对话产生的数据量下,这耗时低于 100 毫秒。
哪种建议有效,是通过过程索引上的 success_count / failure_count 分别捕获的,而不是通过存储回复的文本。最近的片段事件中包含用户确认(“谢谢,这有效”)会触发 success_count++;明确的拒绝(“这没有帮助”)会触发 failure_count++。对话本身即是反馈信号,由整合 LLM 充当分类器。无需点赞控件。当出现分歧时,还会暴露一个 refined_steps 字段,供 LLM 将内容写回行动手册。
整合。片段日志积累得很快。整合将它们提升为语义事实和程序性行动手册,这些内容在对话历史消失后依然存在。此实现在每个轮次都运行整合,因此你可以实时观察检查器更新;在生产环境中,合适的节奏是作为后台任务运行:每 24 小时一次,或者当用户的片段索引新增事件超过 N 个时。按轮次整合会使每条消息的 LLM 调用次数翻倍。
在一次调用(提示词)中,整合 LLM 被输入最近的片段以及现有事实和行动手册,并要求输出三项内容:
- 新的语义事实,附带用于溯源的支持性片段 ID。
- 新的程序性行动手册,当某个多步骤解决方案与任何现有触发条件都不匹配时生成。
- 程序性更新,根据用户是否确认修复来增加 success_count++ / failure_count++,并在用户不同意时更新 refined_steps。
提示词要求每个输出都附带支持性片段 ID,因此稀疏的轮次会返回一个空列表,不写入任何内容。
去重使用与智能体用于回忆相同的混合检索器:对于每个候选事实,针对用户的语义索引进行一次 top-K 混合搜索,以缩小比较范围,只有这些候选事实才会被送入 LLM 进行含义判断。还有两道防护措施对输出进行限制:低于置信度阈值的候选事实被丢弃,并且一个被接受的事实如果其最高相似度命中值达到 ≥ 0.90,则被视为重复项。在此实现中,去重更简单:最近的大约 50 个事实被传递给整合 LLM,并附带一条“不要重复”的指令,而 LLM 后的置信度和相似度防护尚未接入。混合检索路径和防护措施是生产架构;此快照依赖于 LLM 直接进行比较,因为语料库足够小,可以容纳。
success_count 和 failure_count 为行动手册形成了一个反馈循环:在足够多的对话中,记录“此方法有效”的同一个字段,会成为“下次优先展示此方法”的信号。目前,计数已被写入,但尚未用于影响检索排序。在少数已解决的工单上,这种提升只是统计噪声。一旦接入生产环境,当部署达到足够密度使该信号具有意义时,便会启用。
智能体记忆如何处理矛盾与替代
只增不减的记忆最终会出错。用户说"我搬到爱丁堡了",智能体写入一条新事实。六个月后,旧的"住在布里斯托尔"这条事实仍然在索引中。每次回忆时两者都会出现,智能体要么选错,要么含糊其辞。信任感很快消失。
解决方案是在系统提示词(完整提示词)中加入一条规则,无需新增工具。智能体不删除,而是进行替代:
一个实际示例。Sarah 上次访问时在语义索引中记录了 id=abc,"Sarah 住在布里斯托尔"。三个月后,她打开聊天窗口说:"我们离开布里斯托尔了,现在在爱丁堡。"
1. 回忆。对 Sarah 消息的预回忆返回匹配结果,包括 {id: "abc", text: "Sarah 住在布里斯托尔", memory_type: "semantic"}。
2. 检测。智能体发现回忆到的事实与新消息之间存在冲突。
3. 分类。"我们离开布里斯托尔了,现在在爱丁堡"是一个自然的更新,而非否认。智能体选择 contradiction="natural"。
4. 写入。智能体调用 write_memory(text="Sarah 住在爱丁堡", supersedes_id="abc", contradiction="natural")。一次操作完成两件事:
- 写入一条新文档 id=xyz,置信度设为满(无惩罚,因为矛盾是自然的)。
- 旧文档 abc 被更新,添加 superseded_by=xyz, superseded_at=<现在>。
5. 回忆时隐藏旧记录。每次回忆都会应用一个过滤器:must_not exists field=superseded_by。abc 从智能体的视野中隐藏。xyz 正常显示。
6. 审计保留。文档 abc 仍保留在索引中。查询 superseded_by=xyz 可以重建整个链条。
注意:如果 Sarah 后来问"我住过哪些地方?",智能体调用 recall_memory(query="sarah 住过的地方", include_superseded=True)。DLS 作用域内的回忆会同时返回 xyz(爱丁堡)和 abc(布里斯托尔)。带有 superseded_at 设置的命中结果是已归档状态;智能体的回复会区分它们:"你现在住在爱丁堡;你之前住在布里斯托尔(直到今年早些时候)。"
如果 Sarah 说的是“我从未在布里斯托尔住过,那是我姐姐”,那么步骤 3 会将其归类为“严厉”。同样的写入操作会发生,但新事实的置信度会因 SUPERSEDE_CONFIDENCE_PENALTY 而降低。系统会略微保持谨慎,直到新状态通过后续对话得到强化。
边界情况遵循同样的模式:一个已被取代的事实可以再次被取代(abc → xyz → pqr);一个低风险偏好(“我现在更喜欢深色模式”)会以 contradiction="natural" 的方式取代。forget_memory 是硬删除;仅当客户明确说“忘记 X”时才使用它。它不是矛盾处理工具。
有一个微妙之处。召回可能会浮现出多个被同一新陈述所矛盾的事实。Sarah 的位置信息存在于“Sarah 住在布里斯托尔一栋维多利亚式公寓里”(语义层面)、“Sarah 在布里斯托尔有一套公寓,里面放着她的 Hub v2”(语义层面),也可能存在于之前某次对话的一个情景事件中。智能体必须取代所有这些事实,而不仅仅是它看到的第一个。扫描召回结果,找出新陈述使其不成立的每一个事实,并为每个旧 ID 发起一次 write_memory(supersedes_id=…) 调用。那些仅提及布里斯托尔但依然成立的事实(“布里斯托尔的维多利亚式公寓有厚墙,会衰减 Zigbee 信号”)则不会被取代。Sarah 搬家并不会改变布里斯托尔的建筑结构。
被取代的文档会累积,但只有当智能体通过 include_superseded=True 明确请求时,才会在召回中浮现。在生产环境中,定期重建索引会将它们移入一个单独的归档索引,Elasticsearch 的索引生命周期管理(ILM)会将其依次过渡到冷层和冻结层(可搜索快照)。审计链在冷存储上仍可查询;活跃的语义索引则保持热状态且体积小巧。
确保 Elasticsearch 智能体记忆中同轮写入的可见性
Elasticsearch 的默认异步刷新间隔会导致一个问题:当智能体在同一轮对话中写入并召回记忆时,会产生传播延迟。当用户在一句话里说“我有一个 Lumio 信号扩展器,一直没设置过。现在我的完整设备列表是什么?”时,智能体会先写入该扩展器的事实,然后立即在同一轮对话中执行召回,有时甚至是在同一轮迭代的工具调用批次中。默认的 Elasticsearch 刷新间隔加上 semantic_text 的推理开销,可能导致亚秒级的传播延迟,使得刚写入的文档在召回时还不可见。
修复方案位于存储层。智能体触发的每次 write_memory 都传递了 refresh=True 参数,强制分片在调用返回前完成刷新(并使内联推理处理器生成的 Jina v5 嵌入向量落地)。下一次工具调用就能看到新文档。最终回复中出现了该扩展器,因为写入后立即执行的召回看到了它。
在写入量较高的情况下,refresh=True 会带来吞吐量开销。生产环境部署可能需要转向异步索引,再加上一个智能体层的“刚写入”注册表,将写入内容保留在大语言模型的上下文中,直到索引追上进度。目前,更简单的方案仍有其用武之地。
智能体记忆检索的时间衰减与使用次数评分
目前的检索设置对所有事实赋予相同权重,无论其创建时间或最后使用时间。这是错误的默认行为。一个在过去一周内被召回两次的事实,几乎肯定比一个两年前被提及一次、内容相同的事实更相关。
我们将每个结果的分数乘以两个乘数:一个主要的新近度信号和一个次要的频率修正。新近度信号是时间衰减:一个在 Painless 中基于每个索引的日期字段计算的高斯形乘数(详见下文)。频率修正是使用次数增益(1 + log10(1 + use_count) * weight),因此一个被召回十次的事实大约提升 1.2 倍,被召回一百次的事实大约提升 1.4 倍。
这两个指标回答的是不同的问题:时间衰减衡量的是某个事实最近被触及的时间;使用次数衡量的则是被触及的频率。当多个事实共享同一个 last_used_at 时间戳时,两者就会出现差异:衰减无法区分"被回忆过一次"和"被回忆过四十次";而使用次数可以。时间衰减承担主要负载;使用次数则是一种优化手段,只有当每个事实的回忆量足够高、能够承载有效信号时,它才能发挥价值。
每种记忆类型对应的日期字段
情景记忆和语义记忆使用不同的日期字段。情景记忆使用 timestamp(事件发生时间);语义记忆使用 last_used_at(写入时设置,回忆时更新)。ES 原生的 gauss 函数无法同时覆盖这两个字段,因为该函数只接受一个字段名,且该字段必须存在于搜索涉及的每个索引中。因此,时间衰减通过 Painless 脚本实现,该脚本根据索引选择正确的字段,并就地计算出一个高斯形状的乘数:
程序性记忆被有意排除在时间衰减之外。last_used_at 在每次回忆时都会更新,无论回忆成功与否,因此纯粹的衰减乘数会奖励"最近尝试过"而非"最近有效过"。正确的做法是将 last_success_at 字段与 success_count / failure_count 结合起来,融入排序逻辑;在这两个字段都就位之前,仅凭时效性对于程序性记忆的检索来说过于粗糙。
语义记忆中的回忆时间更新是承担主要负载的部分。它将"旧事实权重更低"转变为"智能体最近不需要的事实权重更低"。这是相关性的衰减,而非真实性的衰减。真实性的衰减由替代机制(见上文)处理。一个五年前的事实,如果智能体每周都会回忆它,那么由于 last_used_at 始终是新的,它就会保持在最顶部。
这与三个桶分类法所依据的认知科学脉络相同。检索练习(回忆某件事的行为)会增强其可访问性,而长期不用则会让它逐渐消退。对 last_used_at 的回忆时间更新,正是同一效果在工程层面的体现。
检索时的乘数
这两个因子都位于一个 function_score 块中,该块包裹着 RRF 的每个分支:
在代码中,这两个函数都位于一个 Painless 脚本中,该脚本按索引进行分支(数学计算相同,但减少了 function_score 条目数量)。
两个索引过滤器承担双重职责。它们将每个函数限定到其应影响的内存类型:时间衰减适用于情景记忆和语义记忆,使用次数提升仅适用于语义记忆。同时,它们将程序性记忆和目录排除在外:过滤器不匹配的函数返回中性值 1.0,因此包含这些索引的跨索引查询能够正确评分,无需解析器处理。完整函数位于 operations.py 中。
两个参数控制高斯曲线:
- offset(180天):一个平坦区域。距今不足 180 天的文档无论具体天数如何,均获得乘数 1.0。如果没有该参数,新事实会因亚日时间噪声而相互竞争。
- scale(1825天,约 5 年):超出平坦区域后,乘数衰减至 0.5 的距离。实际上是从平坦区域结束算起的半衰期。
衰减是一种刻意的权衡。当语料库中的每个事实都是唯一的且随时间保持正确时,应用任何衰减都会损失部分召回率:旧事实即使仍然正确也会受到惩罚。衰减发挥作用的地方在于现实场景:关于同一事物的多个相互竞争的事实共存,而你希望最新或最常用的那个排名最高。正因如此,默认 scale(1825天)是保守的。对于事实快速过时的领域(如产品迭代迅速的客户支持),可收紧该参数。对于事实多年保持相关性的个人助手记忆,可放宽该参数;两者都只需在 constants.py 中修改一行代码。
基于 Elasticsearch DLS 的多租户隔离
文档级安全(DLS)将隔离规则移至集群本身。每个用户获得一个 API 密钥,其角色描述符携带一个 DLS 查询,该查询允许访问属于该用户的文档(以及没有 user_id 字段的共享目录)。使用该密钥的智能体可以运行任何查询,但永远不会看到其他用户的文档。集群根本不会返回这些文档。这就是生产级隔离保障,在每次使用该密钥发起的查询中由服务端强制执行。
检索器还在代码中携带了一个 user_id 过滤器,作为防止配置漂移的额外安全措施:新索引模板上线时可能没有 DLS,角色描述被编辑后相关子句被静默丢弃,管理员密钥被误复用。DLS 是架构层面的设计;这个代码级别的检查在查询时几乎不消耗任何成本。
将共享目录数据集成到智能体记忆检索中
记忆查询就是一次 Elasticsearch 搜索。针对 Sarah API 密钥的 DLS 查询只允许 user_id 为 "sarah" 的文档。目录和其他共享索引根本没有 user_id 字段;它们本应对所有用户可见。为了包含这些索引,DLS 查询从"必须等于 sarah"扩展为"等于 sarah 或没有 user_id":一个 bool.should 条件,允许 user_id == "sarah" 或 must_not exists: user_id。检索器、RRF 融合和衰减函数保持不变:目录和个人记忆进入同一轮召回。
一个启动脚本会生成每个用户的 DLS 密钥,并将扩展后的查询条件内置其中。
现在,搜索"只显示白色的智能灯泡"时,会同时返回 Sarah 存储的约束条件和关于灯泡兼容性的目录条目,并按相关性排序。
用户记忆和目录可能在同一主题的同一轮召回中出现并相互矛盾。检索器在处理时间衰减的同一个脚本中(多一个 _index 分支,没有新机制)应用了一个小的来源先验权重(CATALOG_SOURCE_PRIOR,0.85),因此在相关性接近时用户记忆胜出。这是一种软性倾斜,而非路由规则:当目录具有明显更强的相关性匹配时(如产品规格、技术查询),重排序器仍会选择目录。硬性规则(如"规格查询永远信任目录"或相反地"个人偏好永远信任用户记忆")由智能体的系统提示词处理,而非检索器。
通过 MCP 连接任意智能体
当记忆层不绑定于单个智能体时,它最为有用。模型上下文协议(MCP)天然实现了这一点。端点地址是 /api/atlas/mcp/{user_id},因此任何支持 MCP 的客户端(Claude Desktop、Cursor、你自己的智能体)都可以通过将 mcp.py 中的 JSON 片段粘贴到其配置中来接入。
对于 Claude Desktop,配置文件位于 `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)或 `%APPDATA%\Claude\claude_desktop_config.json`(Windows)。对于 Cursor,将其粘贴到“设置 → MCP”下。重启客户端后,三个 Atlas 工具(`recall_memory`、`write_memory` 和 `forget_memory`)会出现在工具抽屉中,它们调用的是 FastAPI 应用所使用的同一套 Elasticsearch 索引。同一套记忆层,任意智能体,无需重写。这三个工具的接口定义在 `tools.py` 中。
衡量智能体记忆召回质量
关于“召回”的说明:本文其他部分中,它指记忆召回(智能体检索已存储的事实)。而此处,它指的是不相关的信息检索指标 Recall@K:即正确的文档是否出现在前 K 个结果中。
记忆架构很难验证。这里的评估采用问答式段落检索,即标准的 RAG 基准测试。对于每个采样的文档,大语言模型会编写两个用户可能合理提出的问题,其答案均指向该文档。例如,“我宝宝的睡眠很脆弱,设置自动化时有什么需要记住的吗?”指向的是 Sarah 的育儿室安静时段这一事实。然后,检索器必须在前 k 个结果中呈现源文档。
存在像 LoCoMo 这样的通用记忆专用基准测试,它们能使数据在系统间具有可比性。选择基于语料库的问答模式有两个原因。首先,它测试了每个角色实际部署的语料库,因此召回率能反映真实对话中的表现。其次,它隔离了混合+衰减+重排序流水线正在迭代的检索环节(源文档是否出现在前 K 个结果中?);LoCoMo 的对话连贯性指标衡量的是更下游的内容。后续文章将运行完整的 LoCoMo 基准测试,并将检索性能与 LLM 选择及提示词工程带来的干扰因素分离开来。
泄露数量是任何多租户记忆系统的准入门槛;其余部分则是质量表现。该评估在 CI(`eval_recall.py`)中设置了门控条件:R@10 ≥ 0.85,R@5 ≥ 0.75,泄露数 = 0。这些数字是近似值,因为重排序器存在服务端方差:在连续四次运行中,R@10 分别达到了 0.85、0.88、0.89 和 0.893。
语义事实是更困难的情况(R@10 ≈ 0.81);情景事实平均为 0.98,程序事实则达到 1.0。原因是兄弟冲突:关于 Sarah 的集线器断连问题,语料库中存在多个看似正确的事实,检索器有时会选错。值得注意的是:兄弟冲突通常不会降低智能体的回复质量(它仍然会得到一个相关且真实的事实),因此 R@10 对于语义事实而言是偏保守的指标。
智能体记忆架构:关键决策
智能体记忆由几个问题组成,每个问题都有唯一的解决方案:
- 记忆并非单一事物。共有三个索引,分别对应一个生命周期:情景记忆(发生了什么)、语义记忆(什么是真的)、程序记忆(什么方法有效)。
- 大语言模型会通过改写消除关键词的精确性。每一轮对话都以检索原始消息开始;检索采用混合方式,然后进行重排序。
- 仅追加的记忆会腐化。整合过程将情景提升为持久事实;取代机制则淘汰用户反驳的事实。
- 旧事实不应与新事实拥有相同的排名。分数会随时间衰减,而一次召回会将事实的排名重新提升。
- 不同租户绝不能互相可见。隔离通过 DLS 在集群层面实现,而非通过一个可能被遗忘的过滤器。
这些都不是独立的系统:目录、隔离和衰减全部整合到一次 Elasticsearch 查询中。
只要把这个做对了,那个一直告诉 Sarah 重置集线器的助手终于能记住:她在三月份已经试过了,狗会咬传感器线缆,而且家现在在爱丁堡。