Elasticsearch持久化 Agent 记忆系统(一个开源工具)

发布时间:2026/6/23 0:56:44
Elasticsearch持久化 Agent 记忆系统(一个开源工具) 概述AI 编程助手如 Claude Code本质上是无状态的。虽然你可以通过文件系统让代理读取历史记录但读取文件≠回忆相关上下文。这种“会话即忘”的模式在实际工作中会带来明显的成本重复推导结论代理无法记住之前的推理过程每次都会重新读文件、重新审视权衡甚至得出不同答案。这导致开发者在不同会话中面对同一问题时代理行为不一致你却很难定位原因。多设备摩擦如果你在多台机器之间切换每换一台设备就得从头加载代理的上下文git pull、grep搜索相关文件、手动粘贴项目状态……每一次切换都是一笔额外的“摩擦税”。跨会话上下文丢失代理产出的最有价值的东西——架构决策、阻塞任务的 ID、方案变更的理由、解释“为什么选择这条路”的上下文锚点——统统只活在当前会话的工作内存中会话结束便消失。代码提交能保存产出物却无法保留背后的推理过程。业界常见的解决方案是专为记忆打造的服务层例如单独的记忆编排服务或独立的向量数据库。这些方案确实有效但同时也意味着又一个服务、又一个 API、又一套需要构建和维护的东西。如果 Elasticsearch 已经是你技术栈的一部分那么你可能已经拥有了所需的一切。Elasticsearch你已有的搜索引擎也能成为记忆系统先别急着讨论“Agent 记忆”这个特定场景我们先看看 Elasticsearch 本身能提供哪些开箱即用的能力混合检索开箱即用Elasticsearch 天然融合了 BM25 词法匹配与稠密向量检索。通过semantic_text字段类型你只需将字段映射为该类型并指向推理端点嵌入生成就会自动完成——无需自己管理嵌入流水线。强大的查询语言 ES|QL在 Elasticsearch 中记忆就是文档而文档可以用完整的查询语言进行检索。ES|QL 支持按类型、日期范围、Agent ID、访问范围等进行过滤还支持聚合、时间函数和多路召回融合FUSE。相比大多数向量存储仅提供的轻量级元数据过滤ES|QL 的查询表达力显著更强。语义打分前的元数据过滤跨 Agent 的作用域隔离、时间窗口、类型过滤——这些都是 Elasticsearch 的标准查询能力你无需编写额外逻辑只需组合已有原语。时间衰减Temporal Decay通过BRIDGE_MEMORY_DECAY_WINDOW变量系统会对记忆按时间加权让近期的记忆比旧记忆排名更高。底层使用了 ES|QL 的DECAY函数需要 Elasticsearch 9.3 或 Serverless 版本。如果版本不支持也有等效的回退方案。现成的运维生态监控、告警、索引生命周期管理、备份——这些你已经在现有 Elasticsearch 部署中拥有了。增加新的索引并不会带来新的运维负担。本质上Elasticsearch 提供了一个可查询、可融合、可衰减的文档存储层而 Agent 的记忆数据正是这种存储层的理想负载。架构概览为了让 Elasticsearch 成为 Claude Code 的专用记忆层构建一个名为bridge的 CLI 工具。整个架构的核心关系如下Elasticsearch 索引Hook 触发读写离线写入Bulk 同步Graph 命令Graph 命令Claude Code Agentbridge CLIElasticsearchLocal Outboxagent-memoryagent-messagesagent-tasksagent-sessionsagent-status{agent}-entities{agent}-entity-history图中的关键点Claude Code 与 Elasticsearch 不直接通信所有交互都通过bridgeCLI。三个 Hook将bridge自动挂载到 Agent 的生命周期中无需代理显式调用。离线队列当 Elasticsearch 不可达时写操作不会失败而是落入本地的fallback/{agent}/outbox/目录待连接恢复后由bridge sync批量刷入。七个索引各自承载不同的数据维度其中{agent}-entities和{agent}-entity-history支撑了知识图谱功能。索引映射与存储结构下面以agent-memory索引的映射为例展示数据的存储方式{mappings:{properties:{memory_id:{type:keyword},agent:{type:keyword},type:{type:keyword},category:{type:keyword},title:{type:text,fields:{keyword:{type:keyword}}},title_semantic:{type:semantic_text,inference_id:jina-v5-embeddings},content:{type:text},content_semantic:{type:semantic_text,inference_id:jina-v5-embeddings},tags:{type:keyword},source:{type:keyword},created_at:{type:date},updated_at:{type:date},access_scope:{type:keyword}}}}关键设计点title_semantic和content_semantic使用semantic_text类型并指定 Jina v5 嵌入推理端点。写入文档时Elasticsearch 自动计算嵌入向量无需额外编码。所有字段都支持精确匹配keyword和全文检索text为混合召回提供了数据基础。access_scope用于控制记忆的可见范围如shared或特定 Agent。Elasticsearch 在这里的两个本质差异化优势查询表达力ES|QL 是完整的查询语言而不是单纯的过滤 API。你可以在一个查询中组合向量检索、精确匹配、时间衰减和聚合。运维整合如果 Elasticsearch 已在你的栈中这只是一个新索引而不是一项新服务。其他向量数据库也能定义模式但区别在于你定义完模式后能做什么以及是否增加新的运维依赖。钩子集成与离线队列自动化的 Hook 集成Claude Code 的 Hook 机制让bridge能够无缝嵌入 Agent 的日常行为中无需显式调用Hook 时机触发动作作用SessionStartbridge sync-memoriesbridge heartbeat将本地记忆文件同步到 Elasticsearch并注册 Agent 为活跃状态PostToolUseWrite/Edit/MultiEditbridge entity index-file每次写入.md文件时自动索引该文件作为知识图谱实体Stop记录会话结束事件到agent-sessions保留会话日志供后续查询这样Agent 不需要“记得”去更新记忆——一切都是自动的。第一次会话同步会哈希所有本地文件仅重新索引变更的部分因此首次同步可能较慢后续则很快。离线队列保障当 Elasticsearch 不可达时写入操作不会失败而是写入fallback/{agent}/outbox/目录下的 JSON 文件。bridge sync由SessionStart自动调用会在连接恢复后通过 bulk API 将队列刷入 Elasticsearch。离线期间数据不会丢失。读操作如bridge recall会在 ES 不可达时以非零退出码优雅失败Agent 会话仍可继续只是记忆功能降级。离线队列没有自动大小限制。长时间离线后建议使用bridge sync --batch-size 100分批发送避免 bulk 请求过大。混合召回记忆检索的核心机制许多 Agent 记忆实现仅依赖单一向量检索查询向量与存储向量做余弦相似度返回最近邻。这在语义召回上有效但在精确术语匹配和时间衰减两个场景下明显不足。两种检索分支并行融合我们的混合召回使用 ES|QL 的FUSEReciprocal Rank Fusion将两条检索路径的结果合并用户查询FORK 并行分支分支1: BM25 词法检索匹配 title/content/tags分支2: 语义检索匹配 content_semanticFUSE 融合Reciprocal Rank Fusion时间衰减 DECAY输出 Top 5 结果下面的 ES|QL 查询展示了这一过程来自lib/memory.shFROMagent-memory METADATA _id,_score,_index|FORK(WHERE(access_scopesharedORaccess_scopekk-onlyORagentkk)AND(content:vector searchORtitle:vector searchORtags:vector search)|SORT _scoreDESC|LIMIT50)(WHERE(access_scopesharedORaccess_scopekk-onlyORagentkk)ANDcontent_semantic:vector search|SORT _scoreDESC|LIMIT50)|FUSE|EVAL final_score_score*DECAY(created_at,NOW(),45days)|EVAL displayCOALESCE(title,SUBSTRING(content,1,80))|SORT final_scoreDESC|LIMIT5|KEEP memory_id,type,display,access_scope,agent混合优于纯语义精确匹配场景如果记忆存储了“任务 ID: kk-task-20260428-deploy-blocker”而查询是“deploy blocker”语义检索可能找到概念相近的内容但只有 BM25 分支能精确匹配到该 ID。没有 BM25你得到的是概念而非你需要的确切引用。语义泛化场景如果记忆标题为“将默认分块策略改为句子级以提高短查询召回率”查询“分块配置”虽然字面不重叠但语义分支能准确找到它纯关键词则找不到。时间衰减Temporal DecayBRIDGE_MEMORY_DECAY_WINDOW默认值为 45 天作为时间衰减的半衰期。同等条件下今天的记忆得分远高于 90 天前的记忆。这符合 Agent 记忆的实际规律近期的上下文几乎总是比旧的上下文更相关即使旧记忆在语义上更贴近查询。版本提示DECAY函数需要 Elasticsearch 9.3 或 Serverless。若版本不支持系统会回退到等价公式final_score _score / (1 DATE_DIFF(day, created_at, NOW()) / 45.0)。图谱搜索的权重调优在知识图谱实体搜索中lib/graph.sh我们使用了FUSE LINEAR并显式设置权重——BM25 占 0.3语义占 0.7——因为实体搜索更依赖语义匹配。你可根据需求调整权重偏向关键词精度或语义泛化。|FUSE LINEARWITH{weights: {fork1:0.3,fork2:0.7},normalizer:minmax}知识图谱层实体索引与关系提取知识图谱建立在 Elasticsearch 文档之上但需要强调这不是一个图数据库——遍历深度限制为 2没有 Cypher 查询语言也没有属性图模型。它的价值在于为 Agent 自身的工作记录提供实体化查询能力而不是构建外部世界知识的百科全书。每个 Markdown 文件通过PostToolUseHook 被索引为一个实体实体 ID 格式为{agent}-{type}-{slug}保证幂等性重写文件即更新实体。关系边从前置元数据字段initiative、blocked_by、depends_on中提取。图谱命令示例命令用途bridge graph search infrastructure blockers跨所有实体进行混合语义搜索bridge graph related kk-initiative-platform --depth 2从特定实体出发遍历关系深度1或2bridge graph check-blockers --stale-days 3找出过期的阻塞或等待实体bridge graph semantic-diff 2026-04-01 2026-04-20对比两个日期之间的实体状态变化bridge graph gen-handoff --hours 8生成跨设备交接的结构化上下文负载bridge graph reconcile移除源文件已不存在的 ES 实体语义差异与交接semantic-diff命令在周会前运行能生成结构化的变更日志新增实体、状态变化、新阻塞项、已完成项无需翻阅每个会话日志。gen-handoff命令生成包含实体更新、活跃阻塞和最近会话日志的 JSON 负载。加上--synthesize标志还可通过 Agent Builder 生成一段叙述性文字新会话加载此负载即可重建上下文而无需逐文件阅读。实践数据一个运行数月的实例跨两台笔记本累积了 533 条记忆、50,802 个会话、207 个实体以及约 1,364 条关系边从initiative、blocked_by、depends_on中解析而来。跨设备记忆实践设想如下场景你在一台工作站的台式机上工作了两天然后带着旅行笔记本出差。三天后你在笔记本上启动 Claude Code代理在一次搜索往返内就回忆起了相关的记忆条目——无需等待git pull无需文件扫描重建上下文。zhu因为两台机器都连接到同一个 Elasticsearch 集群。共享索引是事实来源而不是本地文件系统。代理调用bridge recall从共享索引获取上下文无论哪台机器存储了它。关键机制SessionStart钩子运行bridge sync-memories它会读取 Claude Code 的自动记忆文件~/.claude/projects/cwd/memory/*.md对每个文件计算哈希仅重新索引变更的文件。本地记忆文件被视为每台机器上的事实源bridge sync-memories将本地状态推送到 Elasticsearch。注意如果另一台机器上的编辑在切换前未被推送新机器的本地状态会覆盖它。同一时间两台设备并发写入时Elasticsearch 的文档版本控制能正确处理但应避免同时编辑同一记忆文件。诚实的前提这要求 Elasticsearch Serverless 或任何可从两台机器访问的 Elasticsearch 实例。自建 ES 若位于 VPN 或防火墙后也能工作但连接性是硬性要求。这不是一个纯本地方案。适用场景与权衡这种方法最适合Elasticsearch 已在你的技术栈中增加索引的开销很小运维经验可直接复用。你需要丰富的查询表达力你想用完整的查询语言来检索记忆数据而不是受限于供应商的轻量过滤 API。你的 Agent 产生结构化产物Markdown 文件包含一致的前置元数据这是知识图谱连贯性的基础。如果 Agent 不输出结构化内容图谱层的价值有限。你需要跨设备或跨 Agent 记忆共享 Elasticsearch 索引无需额外基础设施即可实现。专用的记忆服务可能更胜一筹的场景你没有现有的搜索基础设施为了记忆问题而引入 Elasticsearch 是合理的但它仍是一项有分量的基础设施投入。你只想要纯语义召回零模式管理专门的向量数据库服务抽象掉了索引映射如果你不想操心映射和字段这种 tradeoff 有价值。你预期频繁变更模式你控制的模式也是你需要维护的模式。如果记忆结构经常变化自管理索引映射的迁移开销可能超过灵活性收益。专用服务往往帮你处理模式演进。你需要全托管的 SaaS运维负担最小Elasticsearch Serverless 已大幅缩小这一差距但并非零负担。诚实的总结agent-memory不是通用答案。它是在你已运行 Elasticsearch 且希望避免给 AI 栈增加额外依赖时的正确选择。快速开始只需三条命令即可启动一个可运行的系统gitclone https://github.com/jeffvestal/agent-memorycdagent-memory ./install.sh ./bridge statusinstall.sh会交互式地引导你输入凭证或读取已有.env创建 Jina v5 语义推理端点若尚不存在创建所有七个带正确映射的索引并安装 Claude Code 钩子。脚本是幂等的——如果出问题或添加新机器可重新运行。运行前须知Jina v5 推理端点自管理 Elasticsearch 需要 Jina API KeyServerless 则由 Elastic Inference Service 自动提供无需 Key。Elasticsearch 版本Serverless 或 9.3 均支持因为混合召回使用了DECAY和FUSE语法。Kibana Dashboard若设置了KIBANA_URLinstall.sh会自动导入仪表盘位于setup/dashboards/。你需要准备Elasticsearch Serverless 端点或 9.3 自管理集群具有agent-*索引权限的 API Key本地安装jqmacOS 可用brew install jq安装后验证bridge entity index-all# 索引已有 Markdown 文件bridge remember decision安装确认成功--titlesetupbridge recallinstall# 验证混合召回然后将hooks/settings.json.template中的钩子配置添加到你的 Claude Codesettings.json中。此后每次会话启动、文件写入、会话结束都会自动更新记忆存储。完整源码含 Kibana 仪表盘位于https://github.com/jeffvestal/agent-memory