在本文中,我们将学习如何使用记忆技术,以 Elasticsearch 作为记忆和知识的数据库,让 Agent 变得更智能。
有一个概念常常让人困惑:与 LLM 的对话是完全无状态的。每次发送消息时,你都需要包含整个对话历史,以“提醒”模型之前发生了什么。在单个对话会话中,能够追踪提问和回答的能力,我们称之为短期记忆。
但有趣的地方在于:我们完全可以操控这段对话历史,而不仅仅是简单地存储它。例如,当我们想要跨不同对话持久保存用户偏好等记忆时,我们可以在需要时将这些记忆注入到新的对话中,这被称为长期记忆。
有三个令人信服的理由,让我们不仅仅是简单地将每条新消息和响应追加到一个不断增长的列表中,然后每次请求都发送给 LLM:
这带来了一些科幻般的可能性。想象一下,一个 Agent 能够根据其环境或对话对象,有选择性地记住事物,就像电视剧 《人生切割术》(Severance) 一样。剧中主角马克(Mark)的大脑中被植入了一颗芯片,根据他是在办公室内(“内我”)还是办公室外(“外我”),芯片会创造出两个拥有不同记忆的独立身份,并根据地点进行切换。

并非所有记忆都服务于相同的目的,如果将它们视为可互换的对话历史,将会限制 Agent 的扩展能力。现代 Agent 架构,包括像 语言 Agent 的认知架构(CoALA) 这样的框架,区分了程序性记忆、情景性记忆和语义性记忆。这些架构并非将所有上下文视为一个不断增长的单一缓冲区,而是认识到每种记忆类型都需要不同的存储、检索和整合策略。
程序性记忆 定义了 Agent 的行为方式,而非它知道或记住什么。
在实践中,这包括:
在我们的系统中,程序性记忆主要存在于应用程序代码和提示词(Prompt)中,并不存储在 Elasticsearch 中。相反,Elasticsearch 是由程序性记忆所使用的。
程序性记忆决定了记忆如何使用,而非存储了什么内容。
情景性记忆 捕捉与某个实体和上下文相关的特定经历。
例如:
这是最动态、最个人化的记忆形式,也是最容易因处理不当而导致上下文污染的一种。
在我们的架构中:
这就是 Innie/Outie 模型作为情景性记忆隔离示例的体现。
语义性记忆 代表关于世界的抽象化、通用化知识,独立于任何单一交互或个人背景。与情景性记忆(与谁说了什么、何时说的相关)不同,语义性记忆捕捉的是普遍为真的事实。
在我们的类比中,关于卢蒙公司(Lumon) 的知识——即剧中马克工作的公司——是 Innie 和 Outie 共享的客观事实。
像公司手册和规则这类内容,就是作为语义性记忆的知识的一部分。
虽然情景性记忆检索优先考虑精确性和强大的上下文过滤器(如身份、角色和时间),但语义性记忆则偏向于高召回率、概念层面的检索。它的设计目的是呈现普遍为真的信息,这些信息可以为推理提供基础,而不是与特定情境相关的个人经历。
让我们转向架构部分,看看这些想法如何转化为我们 Agent 的记忆系统。
此应用程序的完整 Python 笔记本可在此处找到:点击这里。
Elasticsearch 是存储知识和记忆的理想解决方案,因为它是一个原生向量数据库,并且已准备好进行扩展。它为我们提供了管理选择性记忆所需的一切:
选择性记忆不仅关乎正确性和隔离性,它还对延迟和模型性能有直接影响。通过在运行语义检索之前,使用结构化过滤器(如记忆类型、用户或时间)来缩小搜索空间,Elasticsearch 减少了需要评分的向量数量以及需要注入到 LLM 的上下文数量。这带来了更快的检索速度、更小的提示词,以及模型更集中的注意力。在实践中,这转化为更低的延迟、更少的 Token 消耗和更准确的响应。
情景性记忆本质上是时间性的:最近的经历通常比旧的经历更相关,并且并非所有记忆都应永远以相同的详细程度保留。在人类认知中,经历会逐渐被遗忘、总结或整合成更抽象的知识。
记忆压缩是一个完全不同的话题,但你可以实施一些策略来总结和存储旧的记忆,同时完整地检索新的记忆。
遵循《人生切割术》 的概念,我们创建一个名为马克(Mark)的 Agent,他拥有两套独立的记忆集:
当马克与 Innie 交谈时,他不应该记得与 Outie 的对话,反之亦然。

首先,我们定义记忆模式(Schema):
mappings = {
"properties": {
"user_id": {"type": "keyword"},
"memory_type": {"type": "keyword"},
"created_at": {"type": "date"},
"memory_text": {
"type": "text",
"fields": {
"semantic": {
"type": "semantic_text"
}
}
}
}
}请注意,我们对 memory_text 使用了多字段,这样我们就可以对同一字段内容进行全文搜索和语义搜索(使用默认的 Elastic Learned Sparse EncodeR (ELSER) 模型)。
这为我们提供了语义搜索能力,同时保留了用于过滤的结构化元数据。
这是实现选择性记忆的关键部分。我们创建两个独立的角色:一个用于 Innie,一个用于 Outie,每个角色都内置了查询级别的过滤器。当具有 Innie 角色的用户查询记忆索引时,Elasticsearch 会自动应用一个过滤器,只返回 memory_type 等于 "innie" 的记忆。
你可以在此处找到更多关于访问控制的说明性示例,以及关于角色管理的文档。
以下是 Innie 角色的配置:
innie_role_descriptor = {
"indices": [
{
"names": ["memories"],
"privileges": ["read", "write"],
"query": {
"bool": {
"filter": [
{"term": {"memory_type": "innie"}}
]
}
}
}
]
}我们为 Outie 创建一个类似的角色,只需将过滤器改为 "memory_type": "outie" 即可。

然后,我们创建用户并将其分配给这些角色。例如:
当马克(我们的 Agent)收到查询时,他使用提问者的凭据。如果彼得提问,马克就使用彼得的凭据,这意味着 Elasticsearch 会自动过滤,只显示 Outie 的记忆。如果珍妮丝提问,则只显示 Innie 的记忆。
应用程序代码不需要处理用户管理过滤,它与应用程序逻辑完全解耦。Elasticsearch 自动处理所有安全事务。
我们为 Agent 定义了三个关键函数:
GetKnowledge:在知识库中搜索相关上下文(传统的检索增强生成 RAG)。GetMemories:使用混合搜索(语义 + 关键词)检索记忆:def get_memory(query: str):
es_query = {
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"semantic": {
"field": "semantic_field",
"query": query
}
}
}
},
{
"standard": {
"query": {
"multi_match": {
"query": query,
"fields": ["memory_text"]
}
}
}
}
],
"rank_window_size": 50,
"rank_constant": 20
}
}
}
response = user_es_client.search(index="memories", body=es_query)
return response请注意,我们没有在查询中应用安全过滤器;Elasticsearch 会根据用户的凭据自动处理。
SetMemory:存储新的记忆(其实现使用 LLM 将对话转换为结构化的记忆记录)。当用户向马克提问时,流程如下:
GetMemories,查询参数为“最喜欢的家庭度假目的地”。get_memory("最喜欢的家庭度假目的地")。以下是处理此循环的实际代码:
# 初始调用,提供可用工具
response = client.responses.create(
model="gpt-4.1-mini",
input=messages,
tools=tools,
parallel_tool_calls=True
)
# 执行 LLM 请求的任何工具调用
for tool_call in response.output:
if tool_call.name == "GetMemories":
result = get_memory(tool_call.arguments["query"])
# 将结果添加到消息中
# 再次调用 LLM,传入工具结果以生成最终答案
final_response = client.responses.create(
model="gpt-4.1-mini",
input=messages # 现在包含工具结果
)关键点在于:应用程序不决定检索哪些记忆或何时检索。LLM 根据用户的问题来决定,而 Elasticsearch 则确保只有正确的记忆可以被访问。
让我们看看实际效果:
Outie 对话(彼得):
彼得:嘿,马克,明天是我的生日!我晚餐想吃牛排。
马克:太好了!(记忆已存储)马克将此存储为与彼得关联的 Outie 记忆。以下是该记忆在 Elasticsearch 中的样子:
{
"user_id": "peter125",
"memory_type": "outie",
"created_at": "2025-10-11T18:02:52.182780",
"memory_text": "彼得的生日是明天。他晚餐想吃牛排。"
}Innie 对话(珍妮丝):
珍妮丝:嘿,马克,记得我们明天早上 9 点必须完成年终报告。
马克:谢谢你提醒我!(记忆已存储)这会创建一个独立的 Innie 记忆:
{
"user_id": "janice456",
"memory_type": "innie",
"created_at": "2025-10-11T19:15:33.445821",
"memory_text": "明天早上 9 点与珍妮丝一起提交年终报告的截止日期。"
}假设彼得也在卢蒙公司工作。一位同事存储了一条与他相关的工作记忆:
{
"user_id": "innie-peter",
"memory_type": "innie",
"created_at": "2025-10-11T20:30:00.000000",
"memory_text": "彼得需要在周五之前审查第四季度的预算电子表格。"
}这条记忆存在于 Elasticsearch 中,但彼得当前的凭据只授予他 Outie 角色。当他向马克询问工作任务时,这条记忆对他来说是不可见的;Elasticsearch 的文档级安全确保它永远不会被返回。
注意:为了允许与这些记忆进行交互,你需要为彼得创建一个单独的用户(或分配一个额外的角色),赋予其“Innie”访问权限。这留作练习,但它展示了同一个人可以拥有隔离的记忆上下文,而访问权限完全由安全层控制。
现在彼得开始一个新的对话:
彼得:嘿,马克,你记得我生日想要什么吗?
马克:记得!你想要牛排。
彼得:你什么时候需要完成年终报告?
马克:你在说什么?完美!马克在与彼得交谈时,只访问 Outie 记忆。Agent 的“大脑”真正实现了分裂,就像剧中一样。
完整的可工作实现可在此笔记本中找到,你可以:
记忆不仅仅是存储过去对话的地方。它是 Agent 架构的一部分。通过超越原始的对话历史,分离程序性、情景性和语义性记忆,我们可以构建出推理更清晰、扩展性更好、能在长时间交互中保持专注的 Agent。
选择性检索减少了上下文污染,降低了延迟,并提高了发送给 LLM 的信息质量。情景性记忆可以按用户和时间进行过滤,语义性记忆可用于基于共享知识来支撑答案,而程序性记忆则控制着如何以及何时使用所有这些。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。