Weaviate Blog

Agent Memory with Engram: A Practical Guide

8.5内容质量
Agent Memory with Engram: A Practical Guide

TL;DR · AI 摘要

Engram通过异步处理和主题描述机制,实现代理记忆的自动化管理,影响检索模式和提示位置的四个关键决策。

核心要点

  • Engram使用异步管道处理原始数据,提取并转换记忆
  • 主题描述决定记忆提取范围和内容粒度
  • 检索模式和提示位置需人工配置记忆边界

结构提纲

按章节快速跳转。

  1. 介绍Engram作为全托管记忆服务的核心价值和使用流程。

  2. 描述Engram异步处理原始数据的三阶段流程:提取、转换、提交。

  3. 解析用户自定义主题描述如何决定记忆提取的范围和粒度。

  4. 说明四个关键配置点对记忆存储和检索的影响机制。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Agent Memory with Engram
    • 核心机制
      • 异步数据处理管道
      • 主题描述规则
    • 配置决策
      • 记忆边界定义
      • 检索模式选择
      • 提示位置配置

金句 / Highlights

值得收藏与分享的关键句。

#Agent Memory#Engram#Weaviate#AI
打开原文

使用 Engram 的代理记忆:实用指南 | Weaviate

使用 Engram 的代理记忆:实用指南

2026 年 9 月 22 日

·

18 分钟阅读

Prajjwal Yadav

开发者倡导者

你设置过的每个工具都有一个你直接点击跳过的引导界面。通常默认值都是由比你有更多时间思考的人选择的。

Engram 的设置也有这样一个界面,需要花几分钟才能完成:

命名项目 → 选择模板 → 跳过已为你填写好的主题 → 生成 API 密钥。

注意

Engram 是我们的全托管记忆和上下文服务,专为帮助代理记住、学习和改进而设计。

然后你只需调用 memories.add 传入原始对话数据,在模型调用前使用 memories.search,它就能正常工作。

它还会持续工作。这才是有趣的地方,因为没有什么会把你带回到那个界面。但就在“它正常工作”和“它按我预期的方式工作”之间,有四个决策实际上需要你来做,而不是 Engram:

  • 哪些内容会被记住(主题描述)
  • 记忆的数量和归属(受限主题和范围)
  • 返回的内容(检索模式)
  • 存放的位置(在提示中的位置)

在本文中,我们将逐步讲解这四个决策,并展示一个主要使用标准个性化模板项目的实际输出示例。

Engram 如何将原始数据转换为记忆

Engram 会异步处理你发送给它的任何原始数据。默认情况下,它会:

  • 提取关键记忆,
  • 将其与已存储的内容进行转换,
  • 提交结果。

Victoria 的演示视频涵盖了从端到端的完整流程,包括控制台设置和 SDK 使用:

memories.add 返回的是一个运行状态,而不是直接返回记忆,直到该运行完成,你发送的内容才变得可搜索。运行状态指南涵盖了各种状态及其原因,通常你不应该等待某个运行完成。

关于流水线本身的运作方式,Weaviate 的 Engram:记忆博客文章逐步讲解了整个过程。不过,从你发送的数据中提取哪些记忆,是由你的主题描述决定的,因此我们从这里开始。

主题描述如何决定哪些内容会被记住

当你在控制台中创建一个新的 Engram 项目时,用户个性化模板会为你设置 UserProfile 和 UserKnowledge,并提供 ConversationSummary 作为可选的第三个主题。我选择了这三个主题,然后发送了一段关于我的信息:

“我是 Prajjwal,Weaviate 的开发者倡导者。我用 Python 编写所有演示,并有一条硬性规则:它们必须保持在 100 行以内。”

跨三个主题返回了五条记忆:

code
[UserProfile]         用户的名字是 Prajjwal。
[UserKnowledge]       Prajjwal 在 Weaviate 担任开发者倡导者。
[UserKnowledge]       Prajjwal 用 Python 编写所有演示。
[UserKnowledge]       Prajjwal 有一条硬性规则:他的演示必须保持在 100 行以内。
[ConversationSummary] Prajjwal 介绍了自己是 Weaviate 的开发者倡导者,
提到他用 Python 编写演示,并遵循一条规则:
保持演示在 100 行以内。

还有其他现成的项目模板可供使用,例如 Coding Assistant 和 Personal Claw Agent。你也可以从空白模板开始,完全自定义适合你领域的主题。

UserKnowledge 将那句话拆分为三个独立的事实,每个事实都可以单独检索。ConversationSummary 则将其保持为连贯的叙述。相同的输入、相同的流程、相同的时刻,却产生了两种完全不同的结构,因为这两个主题以不同的方式描述自身。

我们没有编写任何路由逻辑或格式化器。描述本身完成了这两项任务,因为主题描述就是记忆提取的提示,它控制着三个方面:

要排除的内容

UserKnowledge 是模板的兜底主题,用户可能随时间变化的任何个人信息都应归入其中。这使得它在需要广泛覆盖时非常有用。但当你需要一个聚焦且细分的记忆时,描述不仅要说明包含什么,还需要定义排除什么。

例如一条临时消息:

"两小时浪费在 Docker 重建上,我的耳机在通话中途坏了,我出门时天突然下起雨来。不管怎样,我终于把演示切换到了 qwen3-embedding-8b。"

系统返回了四条记忆。其中三条涉及天气、硬件和其他临时事件。现在代理知道周四下雨了,除非执行删除操作,否则这个事实会无限期保留。

为了解决这个问题,只需指定不想被记住的内容,就可以用一个聚焦主题替代宽泛主题。我创建了一个名为 UserFacts 的新主题来替代 UserKnowledge,其描述新增了一条规则:

"不要记录事件、事故或临时状况。"

这一行文字产生了关键影响,因为它为模型提供了忽略内容的通用标准。我用三条新消息进行测试:一班晚点的火车、键盘上的猫、被偷的午餐,每条消息都配有一个持久决策。在 24 次运行中,UserFacts 创建的记忆没有收到任何噪音事件,而所有决策都被成功捕获。

因此,对于聚焦主题,定义排除规则比列出包含内容更有效。指出你希望排除的事物类别,而不是列举所有可能的例子,规则可以泛化到你从未预料到的情况。此外,UserFacts 仅用于本次实验,从现在起我重新切换回标准的 UserKnowledge 主题。

输出格式的定义

你几乎肯定会将记忆读入提示中,因此应描述你希望的格式,而不仅仅是主题。例如,“两到三句平实的散文,第二人称,无标题或项目符号”会给你一个可以直接插入上下文而无需修改的内容。或者要求获取原子事实,你就可以逐条检索独立的记录。

判断记忆价值的时机

描述应用于单个输入,同时结合流程为该输入提取的相关记忆。这使它擅长即时判断,但无法处理依赖先前内容的事项。

累积是独立的步骤。默认情况下,流程会串联提取、转换和提交步骤,第四个步骤“缓冲”可以插入其中任何位置。它会保留记忆或原始输入,直到触发条件(如计数器或定时器)被激活。关于随时间累积的规则应放在缓冲步骤中,而不是描述的措辞里。

默认项目运行提取、转换和提交。添加缓冲或重新排列步骤顺序的配置是按项目进行的,目前仅限企业计划提供。

此外,主题可以在控制台随时进行编辑,因此无需重新创建项目即可添加、删除或修改主题。这使得可以轻松迭代直到达到预期效果。

其他配置字段 ​

描述是其中一个字段。其余字段属于配置项而非文字描述,它们各自具有特定效果。这是在控制台添加自定义主题时看到的表单:

字段 | 决定内容 | 如果配置错误 ---|---|--- Name | 在代码中引用主题的方式<br>``topics=["UserProfile"]` | - Description | 提取提示:提取什么内容以及如何书写 | 主题会保留所有内容或无法使用任何内容 User scoped | 记忆属于某个用户<br>`user_id` | 取消勾选后所有用户共享一个池 Property scopes | 类似`conversation_id`或`repo``的额外分区键 | 每次写入必须携带这些键,否则无法到达主题 Bounded | 每个作用域最多一个记忆 | 不启用时无法保证读取到单一记忆

主题文档会更深入地讲解这些概念。

有界主题和作用域的保证 ​

有界主题限制了主题可以保存的记忆数量。作用域决定了特定读写请求可以访问哪些记忆。这两者都涉及当新消息到达已包含记忆的主题时会发生什么。

继续之前100行示例的演示,假设四天后我发送:

"更新:演示现在有400行了。100行规则正式终止。"

零个新记忆创建,两个记忆更新。之后的UserKnowledge记忆状态为:

code
[UserKnowledge] Prajjwal 在 Weaviate 担任开发者倡导者。
[UserKnowledge] Prajjwal 用 Python 编写所有演示。
[UserKnowledge] Prajjwal 不再强制执行之前的硬性规则(保持演示在100行以内);自2026年9月5日起,他的演示长度为400行。

关于100行规则的记忆被原地重写并消失。其他两个记忆保持不变,因为新消息中没有与它们矛盾的内容。这是Engram管道的转换步骤在发挥作用,所有主题都会获得这种处理,无论是否启用有界限制。请注意删除数量为零,因为协调机制通常会覆盖而非彻底删除。如果需要明确删除某个记忆,请使用memories.delete()方法。

有界限制 ​

有界限制增加了关于数量的承诺。有界主题每个唯一作用域最多保存一个记忆。Engram根据主题名称和作用域推导出记忆的ID,因此后续所有写入操作都会更新该ID对应的记忆,而不是新增另一个。

上述更新操作只运行一次。将介绍信息和更新消息发送给五个全新用户(每个用户都从空存储开始),统计最终生成的记忆数量:

code
运行次数 0   1   2   3   4
UserKnowledge(无界)   2   4   4   3   3
UserProfile(有界)     1   1   1   1   1

无界主题生成的记忆数量在2到4之间波动:有时撤回操作会生成一个记忆,有时生成两个,依此类推。而UserProfile始终只保留一个记忆,五次运行中每次都如此,因为它启用了有界限制。因此如果代码需要读取某个主题的单一稳定记忆,应始终启用主题的有界限制而非依赖数量统计。ConversationSummary模板也启用了有界限制,出于相同原因:对话应有一个持续更新的摘要,而非每条消息都生成新摘要。

作用域 ​

作用域对主题进行分区,存在两种类型。用户作用域是一种严格隔离,因为每次写入和读取都必须携带 user_id。属性作用域如 conversation_id 或 repo 在写入时是必需的,但在读取时是可选的,因此你可以一次性读取一个分区或全部分区。

这种可选性正是使用属性的原因。模板通过 conversation_id 对 ConversationSummary 进行作用域划分,因此每个线程都能保持自己的摘要,同时你仍然可以获取用户的所有摘要。如果两个分区从未需要同时读取,请不要使用属性,而是为其分配不同的 user_id。如果某个事实需要始终跟随用户,就将其保留在用户作用域中,无需添加其他内容。

写入操作必须携带主题声明的每个键,否则会像这样被拒绝:

code
insufficient scope: missing required scope properties [conversation_id] to write memories
insufficient scope: missing required user_id to write memories
invalid scope property: [repo] not configured on any topic (configured properties: [conversation_id])

空字符串会被视为缺失,而任何主题未声明的属性会返回项目已配置的属性名称错误。读取操作仅强制要求 user_id。空的 conversation_id 会被视为不存在,会搜索所有对话,尽管写入时会拒绝此操作,而从未写入过的 conversation_id 会从对话作用域主题中直接返回空结果。

如何读取记忆

共有四种检索模式。vector、bm25 和 hybrid 都用于搜索:你提供查询,它们会对每条记忆进行评分,然后按排名顺序返回最佳结果。如果没有指定模式,hybrid 会作为默认模式运行,而 fetch 则专为直接的非排名记忆检索而设计。

搜索

search 会根据查询的相关性进行排序。在聊天应用中,该查询通常是用户的当前消息:

code
from
engram
import
HybridRetrieval
hits
=
client
.
memories
.
search
(
query
=
user_message
,
retrieval_config
=
HybridRetrieval
(
limit
=
3
)
,
user_id
=
uid
,
properties
=
props
,
)
context
=
"\n"
.
join
(
f"-
{
m
.
content
}
"
for
m
in
hits
)

将 limit 设置为实际需要放入提示中的记忆数量。默认值为 10,无论第十条是否与问题相关,你都会得到 10 条结果。如果请求过多,每次交互都需要支付获取无关记忆的费用;如果请求过少,代理可能会错过关键事实。

获取(fetch)

fetch 更接近于列表操作而非搜索:指定主题,获取其所有记忆,无需排序。

code
from
engram
import
FetchRetrieval
profile
=
client
.
memories
.
search
(
query
=
"unused"
,
# fetch 忽略此参数,但 API 要求提供
retrieval_config
=
FetchRetrieval
(
limit
=
1
)
,
topics
=
[
"UserProfile"
]
,
user_id
=
uid
,
properties
=
props
,
)

由于结果未排序,因此不会返回评分。这种模式适合边界明确的主题,因为“哪条记忆”在此类主题中通常只有一个答案。

获取(get)

如果你已经持有某条记忆的 ID,使用 get 可直接读取它:

code
memory
=
client
.
memories
.
get
(
memory_id
,
user_id
=
uid
,
)

因此,从 Engram 读取记忆时:当需要查询相关结果时使用 search,当需要获取主题所有内容时使用 fetch,当需要通过 ID 查看特定记忆时使用 get。

由于 API 具体实现可能变化,因此请始终参考当前定义的搜索记忆和管理记忆文档。后者还涵盖删除操作,删除是永久性的。

检索到的记忆在提示中的位置

最直观的做法是将搜索结果粘贴到系统提示中——每次交互都检索记忆、重建系统提示、发送请求。这种方法运行良好,在简短对话中你永远无法察觉任何问题。

但长期对话中账单会显著增加,因为大多数LLM提供商都会从提示开头缓存内容。如果请求以先前请求的相同文本开头,共享的开头部分会以极低价格从缓存中读取。缓存内容可延续到断点位置。若在断点前修改任何内容,从修改位置之后的所有内容都将重新计费。

由于记忆检索结果每次交互都可能变化,而将它们粘贴到系统提示中时,这些内容会出现在所有其他内容之前。因此它们背后的历史几乎从不被缓存,每次交互都需要为整个提示全额付费。

这意味着提示中可以包含记忆的两个位置:

  • 断点之前:用于整个会话中保持不变的文本
  • 断点之后:用于每次交互都会变化的文本

Engram的两种读取方式与此对应:fetch会返回完整主题而无需查询,search会返回与当前消息(或查询)匹配的内容。

搜索结果应置于最后

更优方案是将搜索结果移动到提示末尾,作为用户消息之后的独立消息。下一次交互时,用新的搜索结果替换该消息而非保留旧内容。将断点设置在用户消息之前。这样系统提示和完整历史都能从缓存读取,仅对新消息和记忆块部分计费。

断点设置在此处至关重要,因为如果不设置,提供商会在最后一条消息末尾自动设置断点,而在此布局中最后一条消息正是记忆块。如果该块下次交互发生变化,前缀将不再匹配。在这种情况下,GPT-5.6模型只会缓存系统提示,Claude模型则完全无法命中缓存。

不同提供商设置断点的字段也存在差异:

  • GPT-5.6及后续版本:在用户消息的内容块中使用prompt_cache_breakpoint
  • Claude模型:在相同块中使用cache_control

持续性主题应置于最前

有些记忆需要每次交互都使用,无论用户提问什么。例如在我构建的某个内部代理中,这些信息包括用户的个人资料和写作偏好。如果某人说明自己使用英式英语或自我介绍,会话中所有回复都应知晓这些信息,而不仅限于搜索恰好返回这些信息的回复。

每次交互都搜索这些信息是浪费资源,且会将永不变化的文本放入频繁变化的块中。更优方案是会话开始时一次性获取这些信息,并将其结果作为用户消息放在系统提示之后。该内容在整个会话中保持不变,因此从第二次交互起都能从缓存读取。

哪些主题应置于最前取决于它们被重写频率,而非是否受限制。在我们的示例中,UserProfile很少变化,因此可以置于最前。ConversationSummary虽然受限制,但每次消息都会重写,因此保留在每次交互的搜索中。

此外,两个调用中都应显式命名主题,否则相同记忆可能重复出现。普通搜索可能将用户资料与其他内容一起返回,因此在我们的示例中拆分方式应为:

  • 会话开始时执行fetch with topics=["UserProfile"]
  • 每次回合都对“其余主题”进行搜索

这里的权衡在于新鲜度。无论用户在当前会话中说了什么,都会保留在历史记录中,但如果另一个会话修改了已获取的记忆,当前会话需要等到下一次会话开启才能看到更改。

如果整个存储空间足够小,可以放入提示中,也可以直接跳过搜索,一开始就获取所有内容。缓存的令牌在每次回合中仍然会产生费用,因此只有当存储空间保持相对较小的时候才值得这样做。

这些是本文测试运行中的四种布局 - 每种布局在 gpt-5.6-luna 上进行 25 次回合,以及在首次回合之后每次回合需要支付的费用:

布局

每次回合全部计费

将记忆粘贴到系统提示中

整个提示

记忆最后发送,未设置断点

系统提示之后的所有内容

记忆最后发送,在用户消息处设置断点

新消息和记忆(包括始终启用的记忆)

在开始时获取始终启用的主题,最后发送搜索结果

仅新消息和搜索结果

当始终启用的主题在开始时获取,搜索结果最后发送时,最终请求大约有 3,500 个令牌,其中只有大约 100 个未从缓存中读取。

因此,您能节省多少取决于记忆块前面的内容有多少以及会话持续时间有多长。在调整提示布局时,务必在自己的系统上测量效率并检查缓存令牌数量,因为缓存失效时不会报错,账单只会增加。

总结

修复代理记忆最快的方法不是添加更多应用程序代码。而是打开设置过程中您跳过的主题,写下您真正希望保留的内容 - 要排除什么、要以什么形式书写、以及哪些内容根本不值得记录(除非您的设置恰好可以使用我们的模板之一)。

然后限定必须单一的主题,划定需要分区的主题范围,设置实际希望在提示中包含的记忆数量上限,并始终将已获取的内容放在提示开头,将搜索结果放在提示末尾。通常只需在该配置屏幕上花费几分钟!否则您将得到一个只会记住您名字而不会记住其他内容的代理。

立即在 Weaviate Cloud 上使用 Engram 开始使用,并创建您的免费项目。

如有任何问题、想法或只是想聊天,请随时加入我们的社区论坛讨论。

愉快地构建!