1. 为什么 Agent 会话一关就“失忆”Elasticsearch 记忆系统能补上什么如果你用 Claude Code、Cline 这类开源 CLI 工具写过几天代码大概率遇到过这种场景昨天在会话里跟 Agent 反复推敲过一个架构决策今天新开一个会话它像第一次见到这个项目一样重新读文件、重新权衡甚至给出跟昨天相反的结论。这不是模型变笨了而是 Agent 本身是无状态的——会话结束工作内存清空推理过程随之蒸发。文件系统能救一部分你可以让 Agent 读历史记录、读 git log、读项目里的 markdown。但“读取文件”和“回忆相关上下文”是两回事。文件是死的检索是活的。当你有几百个 markdown、几十个会话日志、跨两台机器切换时靠 grep 和手动粘贴重建上下文摩擦成本高得离谱。我试过把 Agent 对话记忆写进 Elasticsearch用它的混合检索BM25 语义向量做召回效果比纯文件方案稳定得多。核心思路是记忆就是文档检索就是查询。Elasticsearch 天然支持semantic_text字段类型写入时自动生成嵌入向量不需要自己维护嵌入流水线ES|QL 又能把词法匹配、向量检索、时间衰减、元数据过滤组合在一个查询里。如果你技术栈里已经有 ES这只是一个新索引不是一项新服务。这篇要讲的就是怎么把这条链路跑通用开源 CLI 工具bridge把 Agent 记忆写入 Elasticsearch用 TaoToken 统一 Key 和 API 通道完成模型调用最后演示一次“写入 → 检索 → 验证”的完整动作。适合已经在用 Claude Code / Cline、手头有 Elasticsearch 实例、想让 Agent 跨会话记住上下文的开发者。全文给的是可复制的 mapping、写入命令、查询语句和配置片段跟着做就能跑起来。2. TaoToken 前置准备统一 Key 与 API 通道配置在讲 Elasticsearch 索引和 bridge CLI 之前先把模型调用这条链路理清楚。Agent 记忆系统里有两个地方需要调模型一是写入记忆时生成语义嵌入如果用semantic_text且指向外部推理端点二是bridge graph gen-handoff --synthesize这类需要模型生成叙述性文字的场景。如果每个工具各配一套 Key、各走一条通道管理起来很乱。用 TaoToken 统一 Key 和 API 通道能把这部分收敛成一份配置。TaoToken 在这里的角色是模型调用的统一入口。你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 了解整体能力API 通道地址是 https://taotoken.net/api这个地址不加 UTM 参数直接用于配置。它提供 OpenAI 兼容的接口格式所以 Claude Code、Cline、Codex 这类工具都能通过改 Base URL 接进来。先说清楚三件套Base URL、API Key、Model ID。这三样在任何 CLI 工具里都是必须写全的缺一个就连不上。Base URL 填https://taotoken.net/apiAPI Key 在控制台生成Model ID 按你实际要用的模型填。下面给一份 Claude Code 的settings.json配置片段路径是~/.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoTokenKey, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用的是 Cline配置在 VS Code 的settings.json里字段名不同但逻辑一样{ cline.apiProvider: openai, cline.openAiBaseUrl: https://taotoken.net/api, cline.openAiApiKey: sk-你的TaoTokenKey, cline.openAiModelId: claude-sonnet-4-20250514 }Codex 用户走的是~/.codex/auth.json格式如下{ OPENAI_BASE_URL: https://taotoken.net/api, OPENAI_API_KEY: sk-你的TaoTokenKey, model: gpt-4o }这里有个容易踩的坑Base URL 末尾不要多加/v1。TaoToken 的 API 通道已经处理了路径你填https://taotoken.net/api就行多写反而会 404。另外 Key 不要硬编码进会提交到 git 的文件里用环境变量或者.env文件管理。配置完之后先做一次最小验证确认通道是通的。用 curl 发一个最简单的请求curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的TaoTokenKey \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 回复 OK 两个字母}], max_tokens: 10 }如果返回里能看到choices数组和正常的 content说明 Key 和通道都没问题。这一步过了再往下配 Elasticsearch 和 bridge。如果这里就报 401先检查 Key 有没有复制错、有没有多余空格报连接失败检查网络和 Base URL 拼写。TaoToken 的 Coding Plan 适合长期跑 Agent 编码任务的场景如果你打算让 bridge 的--synthesize频繁调用模型生成交接文档可以走这个方案具体在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 看。API Key 的生成和管理在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。模型对话调试可以用 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里的对话入口先试。3. 可复制配置Elasticsearch 索引 mapping 与 bridge 环境变量这一节给的是能直接复制粘贴的配置。先建索引再配 bridge 的环境变量最后把 Hook 挂上。3.1 agent-memory 索引 mappingagent-memory是核心索引存的是 Agent 的决策、结论、上下文锚点。关键设计是title_semantic和content_semantic用semantic_text类型写入时 ES 自动算嵌入向量。下面这份 mapping 可以直接用PUT 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 } } } }几个字段的作用要理解清楚access_scope控制记忆可见范围比如shared表示所有 Agent 可见kk-only表示只有特定 Agent 可见查询时用它做作用域隔离。created_at是时间衰减的依据BRIDGE_MEMORY_DECAY_WINDOW默认 45 天越新的记忆得分越高。type和category用于按类型过滤比如只召回decision类型的记忆。inference_id指向的jina-v5-embeddings推理端点需要提前创建。自管理 ES 需要 Jina API KeyServerless 版本由 Elastic Inference Service 自动提供。创建端点的命令PUT _inference/text_embedding/jina-v5-embeddings { service: jinaai, service_settings: { api_key: 你的JinaKey, model_id: jina-embeddings-v5 } }3.2 bridge 环境变量配置bridge 通过.env文件读取配置。在项目根目录建一个.env# Elasticsearch 连接 ES_URLhttps://你的ES端点:9200 ES_API_KEY你的ES_APIKey # 记忆衰减窗口天 BRIDGE_MEMORY_DECAY_WINDOW45 # Agent 标识 BRIDGE_AGENTkk # 模型调用走 TaoToken OPENAI_BASE_URLhttps://taotoken.net/api OPENAI_API_KEYsk-你的TaoTokenKey OPENAI_MODELclaude-sonnet-4-20250514 # Kibana 仪表盘可选 KIBANA_URLhttps://你的Kibana端点BRIDGE_AGENT决定索引里agent字段的值也决定本地 fallback 目录fallback/{agent}/outbox/的路径。多台机器用同一个BRIDGE_AGENT共享同一个 ES 集群就能实现跨设备记忆。3.3 Claude Code Hook 配置Hook 是让 bridge 自动挂载到 Agent 生命周期的关键。把下面这段加到~/.claude/settings.json的hooks字段里{ hooks: { SessionStart: [ { command: bridge sync-memories bridge heartbeat, timeout: 30000 } ], PostToolUse: [ { matcher: Write|Edit|MultiEdit, command: bridge entity index-file --path $CLAUDE_FILE_PATH, timeout: 10000 } ], Stop: [ { command: bridge session record --event stop, timeout: 10000 } ] } }三个 Hook 的分工SessionStart在会话启动时同步本地记忆文件到 ES并注册 Agent 为活跃状态PostToolUse在每次写文件时自动索引该文件为知识图谱实体Stop在会话结束时记录会话日志。这样 Agent 不需要“记得”去更新记忆一切都是自动的。3.4 七个索引的创建bridge 需要七个索引agent-memory、agent-messages、agent-tasks、agent-sessions、agent-status、{agent}-entities、{agent}-entity-history。install.sh会自动创建如果你想手动建核心的agent-memory用上面的 mapping其余索引结构类似主要是字段差异。{agent}-entities和{agent}-entity-history支撑知识图谱功能实体 ID 格式是{agent}-{type}-{slug}保证重写文件时幂等更新。4. 验证请求写入一条记忆并检索召回配置完成后跑一次完整的写入和检索确认链路是通的。这一节给的是实际命令和预期结果。4.1 写入一条记忆用bridge remember写入一条决策记忆bridge remember decision 将默认分块策略改为句子级以提高短查询召回率 \ --title 分块策略调整 \ --tags retrieval,chunking \ --scope shared这条命令会往agent-memory索引写一个文档type是decisiontitle是“分块策略调整”content是那句话access_scope是shared。写入时 ES 自动对title_semantic和content_semantic生成嵌入向量。验证写入是否成功直接查 EScurl -s $ES_URL/agent-memory/_search?pretty \ -H Authorization: ApiKey $ES_API_KEY \ -H Content-Type: application/json \ -d { query: { match: { title: 分块策略 } }, size: 1 }返回里应该能看到hits.total.value至少为 1_source里有你写入的内容。4.2 混合召回查询检索用 ES|QL 的 FUSE 把 BM25 和语义两条分支融合再加时间衰减。下面这条查询来自lib/memory.sh可以直接在 Kibana 的 ES|QL 编辑器里跑FROM agent-memory METADATA _id, _score, _index | FORK ( WHERE (access_scope shared OR access_scope kk-only OR agent kk) AND (content:分块配置 OR title:分块配置 OR tags:分块配置) | SORT _score DESC | LIMIT 50 ) ( WHERE (access_scope shared OR access_scope kk-only OR agent kk) AND content_semantic:分块配置 | SORT _score DESC | LIMIT 50 ) | FUSE | EVAL final_score _score * DECAY(created_at, NOW(), 45 days) | EVAL display COALESCE(title, SUBSTRING(content, 1, 80)) | SORT final_score DESC | LIMIT 5 | KEEP memory_id, type, display, access_scope, agent这条查询的意图是用户搜“分块配置”字面上跟“分块策略调整”不完全重叠但语义分支能召回它。BM25 分支负责精确匹配比如你搜“deploy blocker”时能精确命中存了“任务 ID: kk-task-20260428-deploy-blocker”的记忆。两条分支各取前 50FUSE 融合后按时间衰减加权最后取前 5。用 bridge 的命令行封装更简单bridge recall 分块配置预期输出是 5 条记忆按final_score排序每条显示memory_id、type、display、access_scope、agent。如果返回空先确认写入是否成功再确认access_scope是否匹配查询条件。4.3 知识图谱实体检索如果你写过 markdown 文件PostToolUseHook 会自动把它索引为实体。手动触发一次全量索引bridge entity index-all然后做图谱搜索bridge graph search infrastructure blockers图谱搜索用的是FUSE LINEAR权重 BM25 占 0.3、语义占 0.7因为实体搜索更依赖语义匹配。查询语句| FUSE LINEAR WITH { weights: { fork1: 0.3, fork2: 0.7 }, normalizer: minmax }遍历关系用bridge graph relatedbridge graph related kk-initiative-platform --depth 2深度限制为 2这不是图数据库没有 Cypher价值在于给 Agent 自己的工作记录提供实体化查询。4.4 跨设备验证在两台机器上配同一个ES_URL和BRIDGE_AGENT在 A 机器写入一条记忆在 B 机器执行bridge recall应该能召回同一条。SessionStartHook 会自动跑bridge sync-memories读取~/.claude/projects/cwd/memory/*.md对每个文件算哈希只重新索引变更的部分。首次同步可能慢后续很快。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列的是实际跑这条链路时最容易撞上的报错以及对应的排查方向。401 Unauthorized。这个报错有两个来源要分开看。如果是调 TaoToken 时报 401检查ANTHROPIC_API_KEY或OPENAI_API_KEY有没有填对Key 前后有没有空格Base URL 是不是https://taotoken.net/api。如果是调 Elasticsearch 时报 401检查ES_API_KEY的权限需要agent-*索引的读写权限。有个隐蔽的坑.env文件里 Key 如果带了引号某些 shell 解析会出问题去掉引号试试。local proxy failed。这个报错通常出现在 Claude Code 启动时说明它尝试连本地代理但失败了。检查settings.json里的ANTHROPIC_BASE_URL是不是被别的配置覆盖了。Claude Code 会读多个层级的配置项目级的.claude/settings.json优先级高于用户级的~/.claude/settings.json。如果项目里有个旧配置指向了本地地址就会报这个。把项目级配置里的 Base URL 也改成https://taotoken.net/api。reading choices 报错。这个一般出现在模型返回格式不符合预期时比如返回体里没有choices字段。先确认 Model ID 填对了TaoToken 的模型名要跟实际支持的列表一致。如果 Model ID 写错返回可能是错误信息而不是标准 completion 格式解析时就会报 reading choices 失败。用第 2 节的 curl 命令先验证模型名。OAuth 相关报错。Claude Code 某些版本会尝试 OAuth 流程如果你用的是 API Key 模式需要在配置里显式禁用 OAuth。检查settings.json里有没有forceApiKey: true之类的字段或者环境变量CLAUDE_CODE_USE_API_KEY1。OAuth 报错通常伴随invalid_grant或token exchange failed看到这些就说明它在走 OAuth 而不是 API Key。ES 连接超时或 bulk 请求过大。如果长时间离线后bridge sync报 bulk 请求过大用--batch-size分批bridge sync --batch-size 100离线期间写入不会丢数据落在fallback/{agent}/outbox/目录下的 JSON 文件里连接恢复后自动刷入。DECAY 函数不支持。DECAY需要 Elasticsearch 9.3 或 Serverless。如果你的版本低会报函数不存在的错。回退方案是用等效公式| EVAL final_score _score / (1 DATE_DIFF(day, created_at, NOW()) / 45.0)把DECAY(created_at, NOW(), 45 days)替换成上面这行即可效果接近。semantic_text 写入报 inference 端点不存在。检查jina-v5-embeddings端点是否创建成功自管理 ES 需要 Jina API KeyServerless 自动提供。用GET _inference/text_embedding/jina-v5-embeddings确认端点存在。6. 把这条链路用起来从单机到跨设备的落地建议跑通之后有几个实践上的点值得注意。第一access_scope的设计要提前想清楚。如果你有多个 Agent 共享一个 ES 集群用shared让它们互相可见用{agent}-only做隔离。查询时WHERE条件里带上作用域过滤避免召回不该看的记忆。第二时间衰减窗口BRIDGE_MEMORY_DECAY_WINDOW默认 45 天这个值要按你的项目节奏调。迭代快的项目可以调到 14 天让近期记忆权重更高长期项目可以调到 90 天。调完之后旧记忆不会消失只是得分被压低。第三知识图谱层的价值取决于你的 markdown 是否有结构化前置元数据。initiative、blocked_by、depends_on这几个字段是关系边的来源如果文件里不写这些图谱就是空的。建议在项目模板里固定这几个字段。第四跨设备场景下共享 ES 索引是事实来源本地文件是每台机器的输入。SessionStart的sync-memories会把本地状态推到 ES如果另一台机器的编辑没推送新机器的本地状态会覆盖它。避免同时编辑同一个记忆文件ES 的文档版本控制能处理并发写入但语义上的冲突它管不了。第五gen-handoff生成的交接负载配合--synthesize能让新会话快速重建上下文。这个命令调模型生成叙述性文字走的就是第 2 节配的 TaoToken 通道。如果你经常在设备间切换把这个命令加到日常流程里比手动翻会话日志高效得多。最后这套方案的前提是你能从两台机器访问同一个 Elasticsearch 实例。Serverless 版本最省事自管理集群只要能连通也行。如果连接性是问题那这套方案不适用得换纯本地的记忆方案。工具本身在 https://github.com/jeffvestal/agent-memory install.sh是幂等的出问题可以重跑。