大模型很聪明,但它没有记忆。每次对话都是一张白纸。这篇文章记录了我如何用 Elasticsearch 的原生能力,给 Agent 装上一套真正能用的长期记忆系统。
跟大模型聊天的人都有过这种体验:你昨天刚告诉它你喜欢用 Python,今天它又问你用什么语言。
这不是模型笨,而是 LLM 天生无状态。每次请求都是一次全新的对话,之前说过什么,它一个字都不记得。所谓的"上下文",不过是把聊天记录塞进 prompt 里假装记忆罢了。
这在闲聊场景无所谓,但如果你想让 Agent 真正"认识"一个用户——记住他的偏好、了解他的项目背景、知道上次任务做到哪了——你就必须给它一个外部记忆层。
市面上的方案五花八门:有人用 Redis 存 JSON,有人用 Pinecone 做向量检索,有人干脆把所有历史对话丢进一个 txt 文件,或者全部装到大模型里。这些方案要么太粗糙,要么太割裂——你需要一个东西同时搞定存储、检索、安全、过期这四件事。
我的答案是 Elasticsearch。不是因为它时髦,而是因为它恰好把这四件事都做得很好。
先说我的核心观点:Agent 的记忆本质上是一个搜索问题,不是一个存储问题。
你想想,人类的记忆是怎么工作的?你不会把过去三十年的经历按时间顺序从头回放一遍来回答"你最喜欢吃什么"。你的大脑做的是检索——根据当前的问题,瞬间从海量经历中捞出最相关的那几条。
这恰恰是 Elasticsearch 最擅长的事。而且它不只是一个搜索引擎,它还提供了几个关键能力,让它成为 Agent 记忆层的理想选择:
需求 | Elasticsearch 的回答 |
|---|---|
语义理解 |
|
精确匹配 | 原生 BM25 全文检索 |
两者融合 |
|
权限隔离 | Document Level Security (DLS),查询级别的数据隔离 |
自动过期 | Ingest Pipeline + Painless 脚本,写入时自动计算过期时间 |
工具编排 | Agent Builder 定义 Agent,Workflows 定义可复用的工具 |
最后一行是重点。Elastic 近期推出的 Agent Builder 和 Workflows 这两个功能,让你不用写一行应用代码,就能把上面所有能力串成一个完整的记忆系统。Agent 自己决定什么时候存、什么时候取,Elasticsearch 负责执行。
在动手之前,有一个设计决策至关重要:
把每条记忆当作一条独立的日志(Append-Only),而不是一个不断被覆盖的文档。
很多人的第一反应是维护一个"用户画像"文档,每次有新信息就 Update 进去。这条路我走过,问题很多:并发冲突、信息丢失、无法追溯。
更好的做法是:每次对话结束时,AI 主动提取高价值信息,作为一条新文档写入 Elasticsearch。查询时用搜索来找最相关的记忆,而不是读取一个越来越臃肿的 JSON。
这套方案包含四个核心机制:
user_id,记忆互不可见下面一步步落地。
第一步,我们需要一个 Ingest Pipeline,让每条记忆在写入时自动计算过期时间。
思路很简单:你告诉我每条记忆要保留多少天(ttl_days),我用 Painless 脚本算出 expires_at,后续查询时只要过滤掉过期的就行。
PUT _ingest/pipeline/agent-memory-pipeline
{
"description": "Calculate expires_at based on created_at and ttl_days",
"processors": [
{
"set": {
"field": "created_at",
"value": "{{_ingest.timestamp}}"
}
},
{
"script": {
"lang": "painless",
"source": """
if (ctx.ttl_days != null && ctx.created_at != null) {
def created = ZonedDateTime.parse(ctx.created_at);
ctx.expires_at = created.plusDays(ctx.ttl_days).toString();
}
"""
}
}
]
}这比用 ILM 删除整个索引要精细得多——你可以让同一个索引里的不同记忆有不同的生命周期。用户偏好("喜欢用 YAML 格式")存 365 天,任务进展("上次写到第三章")存 7 天,各管各的。
索引的 mapping 是整个方案的骨架。关键设计点是 memory_text 的多字段结构:
PUT ai-agent-memory-v2
{
"settings": {
"default_pipeline": "agent-memory-pipeline"
},
"mappings": {
"properties": {
"user_id": { "type": "keyword" },
"memory_text": {
"type": "text",
"fields": {
"semantic": {
"type": "semantic_text",
"inference_id": ".jina-embeddings-v5-text-small"
}
}
},
"memory_category": { "type": "keyword" },
"importance_score": { "type": "integer" },
"ttl_days": { "type": "integer" },
"created_at": { "type": "date" },
"expires_at": { "type": "date" }
}
}
}这里有一个值得展开说的细节:semantic_text 字段类型。
这是 Elasticsearch 近期引入的一个非常优雅的设计——你只需要声明一个字段是 semantic_text 并指定一个 inference_id(比如 Jina Embeddings),Elasticsearch 就会在写入时自动调用推理 API 生成向量嵌入,查询时也自动处理。你不需要自己管理 embedding pipeline,不需要维护向量维度,不需要手动调用任何 API。
而 memory_text 本身还是一个普通的 text 字段,支持 BM25 全文检索。一个字段,两种检索能力,这就是混合搜索的基础。
另外,注意一下 default_pipeline 的配置。
Elasticsearch 底层准备好了,接下来要让 Agent 能自主调用这些能力。这就是 Elastic Agent Builder 和 Workflows 发挥作用的地方。
简单解释一下这两个东西的关系:
Workflows 的精髓在于:它是声明式的,不是命令式的。 你不需要写 Python 代码来调用 ES 客户端,你只需要用 YAML 描述"我要做什么",Elasticsearch 自己执行。这意味着整个记忆系统的工具层,零应用代码。
这个工具让 AI 在对话过程中,把识别到的高价值信息写入 Elasticsearch。
name: Save_Memory
enabled: true
description: "提取对话中的高价值信息作为一条记忆持久化。"
inputs:
- name: user_id
type: string
- name: memory_text
type: string
description: "精炼的记忆文本,例如:'用户喜欢用 Python 写后端'"
- name: memory_category
type: string
default: "preference"
- name: ttl_days
type: number
description: "记忆有效期天数"
default: 30
steps:
- name: insert_memory
type: elasticsearch.request
with:
method: POST
path: ai-agent-memory-v2/_doc
body:
user_id: '{{inputs.user_id}}'
memory_text: '{{inputs.memory_text}}'
memory_category: '{{inputs.memory_category}}'
ttl_days: ${{inputs.ttl_days}}Workflows 可以在定义之后,直接测试:

通过 tools 暴露于大模型的可调用列表:


注意 AI 不是把原始对话存进去,而是提炼后再存。比如用户说"我之前用 Go 写过一个微服务,但现在转 Rust 了",AI 存的是:"用户目前使用 Rust 进行后端开发,此前有 Go 经验"。这个提炼过程由 Agent 的 system prompt 控制——你在 prompt 里告诉它提取规则,它就会按规则执行。

经过 ingest pipeline处理之后的记忆:

这是整套方案的精髓。一次查询,同时完成四件事:语义理解、关键字匹配、权限隔离、过期过滤。
name: Recall_Memory_Hybrid
enabled: true
description: "通过混合搜索(语义+关键字)和 RRF 算法,精准检索历史记忆。"
inputs:
- name: user_id
type: string
- name: query
type: string
steps:
- name: search_memory
type: elasticsearch.request
with:
method: POST
path: ai-agent-memory-v2/_search
body:
retriever:
rrf:
retrievers:
# 策略一:语义搜索
- standard:
query:
bool:
must:
- semantic:
field: memory_text.semantic
query: '{{inputs.query}}'
filter:
- term:
user_id: '{{inputs.user_id}}'
- bool:
should:
- range:
expires_at:
gte: "now"
- bool:
must_not:
exists:
field: expires_at
minimum_should_match: 1
# 策略二:关键字匹配 (BM25)
- standard:
query:
bool:
must:
- multi_match:
query: '{{inputs.query}}'
fields: ["memory_text"]
filter:
- term:
user_id: '{{inputs.user_id}}'
- bool:
should:
- range:
expires_at:
gte: "now"
- bool:
must_not:
exists:
field: expires_at
minimum_should_match: 1
rank_window_size: 50
rank_constant: 20图片:Agent Builder 中 Recall_Memory_Hybrid 工具的配置界面
我来拆解一下这个查询为什么值得细看。
retriever + rrf:这是 Elasticsearch 的 Retriever API,允许你在一次请求中组合多个检索策略。RRF(Reciprocal Rank Fusion)算法不需要你手动调权重——它根据每个结果在不同检索策略中的排名自动融合打分。语义搜索找到的结果和关键字匹配找到的结果,谁在两边都排名靠前,谁就胜出。
为什么需要两种检索? 举个例子:用户问"我之前提到的那个 K8s 项目"。语义搜索能理解"K8s"和"Kubernetes"是一回事,但如果记忆里写的是"用户正在做 K8s 集群迁移",BM25 的精确匹配反而更快更准。两者互补,RRF 融合,效果远好于单一策略。
filter 部分:每个 retriever 内部都带了两层过滤——user_id 确保只看自己的记忆,expires_at 过滤掉已过期的记忆。注意那个 must_not exists 的分支:如果一条记忆没有设置过期时间(比如某些永久性事实),它也不会被误杀。
这整个查询在 Workflow 里就是一段 YAML。没有 Python,没有 Node.js,没有任何胶水代码。Elasticsearch 既是数据库,也是执行引擎。

配置好工具后,在 Agent Builder 中把这两个工具分配给你的 Agent,再在 system prompt 里加上记忆管理的指令。


整个流程是这样的:
用户:"帮我写一段 Rust 的错误处理代码"
Agent 内部流程:
1. 调用 Recall_Memory_Hybrid(query="用户编程语言偏好")
2. 召回记忆:"用户目前使用 Rust,偏好简洁风格"
3. 结合记忆生成代码(知道用户喜欢简洁,就不会写一堆冗余的 match 嵌套)
4. 对话结束前,发现新信息:"用户在做 CLI 工具开发"
5. 调用 Save_Memory(memory_text="用户正在用 Rust 开发 CLI 工具", ttl_days=30)关键在于:Agent 自己决定什么时候存、什么时候取。 你不需要在应用层写 if-else 来判断"这条信息值不值得记住"。LLM 本身就擅长做这种判断,你只需要在 prompt 里给它规则。
如果你只是给自己做一个私人助手,user_id 过滤就够了。但在企业内部,记忆隔离是硬性要求。
Elasticsearch 的 Document Level Security(DLS)在这里提供了一个非常干净的方案:你可以在角色定义中直接写查询级别的过滤条件,让不同用户只能看到属于自己的文档。应用代码完全不需要关心权限逻辑——Elasticsearch 在查询执行层就把不该看的数据过滤掉了。
Elastic 官方博客中有一个很有意思的类比:美剧《人生切割术》(Severance)。主角 Mark 的大脑被植入芯片,在公司内外拥有完全隔离的两套记忆。用 Elasticsearch 的 DLS,你可以给 Agent 实现完全一样的效果——同一个 Agent,面对不同用户(或不同角色),看到的记忆完全不同,而且这个隔离是在数据层强制执行的,不是靠应用层的 if 语句。

做完这套系统之后,我回头想了想它解决了什么问题:
1. 精准投喂,而不是暴力灌入
不再把几万字的历史对话塞进 prompt。每次只召回与当前问题最相关的几条记忆。这不仅省 token,更重要的是避免了 Elastic 官方博客中提到的"上下文污染"(Context Poisoning)——无关信息太多,模型反而会被带偏。
2. 越用越聪明,但也会遗忘
importance_score 和 ttl_days 的组合让 Agent 像人一样工作:核心偏好记得牢,短期细节自然淡忘。你上周让它帮你调的那个 bug,一个月后它不会再提起;但"你喜欢简洁的代码风格"这件事,它会记一整年。
3. 零胶水代码
从 Ingest Pipeline 到索引 Mapping,从 Workflow 工具到 Agent 配置,全部在 Elasticsearch 平台内完成。没有外部服务,没有中间件,没有需要维护的 Python 脚本。Elasticsearch 既是记忆的存储层,也是记忆的计算层,还是 Agent 的执行层。
这第三点是我最想强调的。Agent Builder + Workflows 的组合,本质上是把 Elasticsearch 从一个"被调用的数据库"变成了一个"主动参与的智能体基础设施"。你不是在 Elasticsearch 旁边搭一个应用来操作它,你是在 Elasticsearch 内部定义 Agent 的行为。
这套方案已经在跑了,但我知道它还不够完美。几个我在思考的方向:
importance_score。但这些都是锦上添花。核心架构已经成立:Elasticsearch 作为 Agent 的记忆层,Agent Builder 定义行为,Workflows 定义工具,混合搜索保证召回质量,Ingest Pipeline 管理生命周期。
这套东西不需要你是 Elastic 专家才能搭建。如果你有一个 Elastic Cloud 账号,从创建 Pipeline 到 Agent 跑起来,大概一个下午的时间。
我一直觉得,AI Agent 领域最被低估的问题不是"推理能力",而是"上下文工程"。模型越来越聪明,但如果你喂给它的上下文是垃圾,输出就是垃圾。
记忆系统就是上下文工程的核心基础设施。而 Elasticsearch 恰好在这个位置上,提供了一套从存储到检索到执行的完整能力栈。
不是每个问题都需要一个新框架。有时候,答案就在你已经熟悉的工具里。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。