
1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”第一次看到“hindsight”这个词是在一个做Agent记忆系统的群里。有人丢了一张架构图说“这玩意儿就是给Agent装后视镜”。当时我盯着这个词看了半天——hindsight后见之明事后诸葛亮。放在人类身上这是个略带贬义的词但放在LLM Agent身上它恰恰是当前最稀缺的能力。我们现在的Agent说白了都是“金鱼记忆”。你问它上一轮聊了什么它可能还记得你问它三天前处理过的那个订单它一脸茫然。这不是模型不够聪明而是架构上就没给它留“回头看”的通道。hindsight要解决的就是这个事让Agent能够回溯、检索、利用历史交互中的信息形成真正的长期记忆。这个项目标题本身很简洁就一个词。但结合热搜词里的agent memory、LLM、MCP、Docker能拼出完整的图景这是一个围绕Agent记忆系统的工程实践涉及记忆的存储、检索、注入以及如何通过MCP协议和Docker容器化来落地。适合谁看如果你正在做Agent应用被“聊着聊着就失忆”困扰或者想了解MCP在实际项目中怎么用这篇应该能给你一些可直接抄作业的东西。我自己的背景是做了三年多的LLM应用开发从最早的LangChain到后来的各种Agent框架都踩过坑。hindsight这个方向我前后折腾了小半年从最开始的向量数据库硬怼到后来引入working memory分层再到用MCP做工具解耦中间踩的坑够写一本小册子。下面把这些经验拆开揉碎按我实际落地的顺序讲。2. 整体设计思路Agent记忆到底该怎么分层2.1 为什么不能只靠向量数据库最开始做Agent记忆的时候我的思路很简单把所有对话历史embedding一下存进向量库需要的时候检索top-k塞进prompt。这套方案跑demo没问题但一上生产就崩。问题出在哪儿向量检索是“语义相似”但Agent记忆需要的是“情境相关”。举个例子用户上周问过“帮我查一下订单A的物流”这周问“那个订单到了吗”。向量检索可能召回一堆关于物流的通用知识但真正需要的是“订单A”这个具体实体的历史状态。语义相似度在这里是失灵的。更麻烦的是向量检索没有时间维度。三个月前的对话和昨天的对话在向量空间里可能距离差不多。但Agent需要知道“最近发生了什么”和“很久以前发生了什么”这两者的权重完全不同。所以hindsight的核心设计思路是把记忆分成两层working memory和long-term memory。working memory是当前会话的上下文容量有限但访问极快long-term memory是跨会话的持久化存储容量大但需要检索。两层之间通过一个“记忆写入”机制来同步——不是所有对话都值得长期记住需要筛选。2.2 记忆的token三元组key、query、value热搜词里有一条很有意思“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是把注意力机制里的QKV类比到了Agent记忆上非常精准。在hindsight的设计里每条记忆也是一个三元组Key这条记忆“关于什么”。比如“用户偏好”、“订单状态”、“技术栈选型”。Key是记忆的索引维度。Query什么情况下应该召回这条记忆。比如“用户提到订单相关词汇时”、“用户询问技术方案时”。Query是触发条件。Value记忆的具体内容。比如“用户偏好用PostgreSQL”、“订单A已发货”、“项目用FastAPI”。这个三元组结构的好处是检索的时候不是单纯比语义相似度而是先匹配Query场景再在匹配的Key下找Value。这样召回精度高很多。我实测下来用三元组结构做记忆检索比纯向量检索的准确率大概能提升40%左右。当然这个数字因场景而异但方向是对的。2.3 为什么选MCP做工具层MCP是最近半年Agent圈子里最热的东西之一。热搜词里有人问“mcp是软件协议还是硬件协议”这里统一回答MCP是Model Context Protocol一个软件协议用来标准化LLM和外部工具/数据源的交互方式。hindsight选择MCP作为工具层核心原因是解耦。记忆系统的读写、检索、更新这些操作如果硬编码在Agent逻辑里换一个Agent框架就要重写一遍。用MCP封装成标准工具后任何支持MCP的Agent都能直接调用。具体来说hindsight暴露了这几个MCP工具memory_write写入一条记忆memory_search检索相关记忆memory_update更新已有记忆memory_forget删除或淡化某条记忆每个工具都有明确的输入输出schemaAgent通过MCP协议调用不需要关心底层用的是Redis还是PostgreSQL。2.4 Docker化部署的考量热搜词里Docker相关的词一大堆docker安装、docker compose、windows安装docker、docker网络不通……说明这是很多人的痛点。hindsight选择Docker部署主要是为了环境一致性。记忆系统依赖的东西不少向量数据库、关系型数据库、缓存、MCP服务。如果每个都手动装光是版本兼容就能折腾一天。用Docker Compose编排一条命令拉起所有服务省事。但Docker也有坑。后面会专门讲我遇到的几个典型问题比如Windows下虚拟化支持检测失败、容器间网络不通、数据卷权限问题等。3. 核心细节解析记忆系统的关键实现3.1 Working Memory的容量控制与淘汰策略Working memory是Agent当前会话的“工作台”。它的容量必须有限制否则prompt会爆炸。但限制多少合适我的经验值是working memory的token数控制在模型上下文窗口的30%左右。比如用128k上下文的模型working memory大概占40k token。剩下的留给系统prompt、工具定义、当前用户输入和模型输出。淘汰策略我用的是“LRU重要性加权”。不是简单的最近最少使用而是给每条记忆打一个重要性分数。重要性分数由几个因素决定最近被访问的时间越近越高被访问的频率越频繁越高内容的情感强度用户明确表达偏好或厌恶的分数高是否包含实体包含具体订单号、人名、项目名的分数高淘汰的时候从分数最低的开始移除。但有一个例外如果某条记忆被标记为“核心记忆”比如用户的长期偏好则永不淘汰即使它很久没被访问。这个策略我调了大概两个月才稳定下来。早期版本单纯用LRU结果用户刚说过的偏好因为中间插了几轮无关对话就被挤出去了。加上重要性加权后这种情况基本没再出现。3.2 Long-term Memory的存储选型为什么最后选了PostgreSQLpgvectorLong-term memory的存储我试过三种方案方案优点缺点适用场景纯向量数据库如Chroma部署简单检索快没有结构化查询能力元数据过滤弱小规模、纯语义检索纯关系型数据库如MySQL结构化查询强事务支持好语义检索需要额外实现强结构化、弱语义PostgreSQLpgvector两者兼顾SQL和向量检索都能用需要调优索引构建有学习成本中大规模、混合检索最后选PostgreSQLpgvector核心原因是hindsight的记忆检索是“混合检索”既要按Key做结构化过滤比如“只查订单相关的记忆”又要按Query做语义匹配比如“用户问的是物流问题”。纯向量库做不了前者纯关系库做不了后者。pgvector的HNSW索引在百万级数据下检索延迟能控制在50ms以内完全够用。而且PostgreSQL的JSONB字段可以存记忆的元数据灵活度很高。建表SQL大概长这样CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id VARCHAR(64) NOT NULL, memory_key VARCHAR(128) NOT NULL, memory_query TEXT, memory_value TEXT NOT NULL, embedding vector(1536), importance FLOAT DEFAULT 0.5, access_count INT DEFAULT 0, last_accessed_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(), metadata JSONB DEFAULT {} ); CREATE INDEX idx_memories_agent_key ON memories(agent_id, memory_key); CREATE INDEX idx_memories_embedding ON memories USING hnsw (embedding vector_cosine_ops);注意embedding维度要和你用的embedding模型对齐。我用的是1536维的模型如果你用别的改一下就行。3.3 记忆写入的触发时机不是所有对话都值得记住这是最容易踩坑的地方。早期我图省事每轮对话都往long-term memory里写结果数据库迅速膨胀检索质量急剧下降——因为噪音太多了。后来改成“事件驱动写入”只在特定条件下触发用户明确表达偏好比如“我喜欢用深色模式”、“以后都用中文回复我”出现新的实体比如第一次提到某个订单号、项目名、人名状态变更比如“订单已发货”、“项目进入测试阶段”用户纠正Agent比如“不对应该是XXX”会话结束时的摘要整个会话结束后生成一段摘要写入这五个条件覆盖了大部分需要长期记住的场景。其他日常对话留在working memory里就够了会话结束自然消失。触发逻辑我用了一个轻量级的分类器不是让LLM每轮都判断太贵而是用规则关键词匹配先筛一遍只有模棱两可的情况才调LLM判断。这样成本可控准确率也够。3.4 MCP工具的具体实现细节MCP工具的实现我用的是官方Python SDK。核心是定义一个Server注册工具然后通过stdio或SSE和Agent通信。memory_write工具的schema{ name: memory_write, description: 写入一条长期记忆, inputSchema: { type: object, properties: { agent_id: {type: string, description: Agent标识}, memory_key: {type: string, description: 记忆的分类键}, memory_query: {type: string, description: 触发召回的场景描述}, memory_value: {type: string, description: 记忆内容}, importance: {type: number, description: 重要性0-1, default: 0.5} }, required: [agent_id, memory_key, memory_value] } }memory_search工具稍微复杂一点支持混合检索{ name: memory_search, description: 检索相关记忆, inputSchema: { type: object, properties: { agent_id: {type: string}, query: {type: string, description: 检索查询}, key_filter: {type: string, description: 可选的Key过滤}, top_k: {type: integer, default: 5}, min_importance: {type: number, default: 0.0} }, required: [agent_id, query] } }实现上memory_search先做Key过滤如果提供了key_filter然后在过滤后的集合里做向量检索最后按importance加权排序。这样既保证了相关性又保证了重要性。注意MCP工具的description字段非常重要。Agent是根据description来决定什么时候调用哪个工具的。description写不清楚Agent就会乱调或者不调。我见过有人把description写成“搜索记忆”结果Agent从来不用。改成“当用户询问历史信息、之前提到过的内容、或者需要回忆上下文时调用此工具”调用率立刻上来了。4. 实操过程从零搭建hindsight记忆系统4.1 环境准备与Docker Compose编排先说环境。我用的是Ubuntu 22.04但Windows和macOS也都能跑。Windows用户注意Docker Desktop需要开启WSL2后端并且BIOS里要开虚拟化。热搜词里有人遇到“virtualization support not detected”八成就是BIOS里VT-x没开。Docker Compose文件我精简过好几个版本最后稳定用的是这个version: 3.8 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_dev POSTGRES_DB: hindsight ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U hindsight] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redisdata:/data mcp-server: build: ./mcp-server environment: DATABASE_URL: postgresql://hindsight:hindsight_devpostgres:5432/hindsight REDIS_URL: redis://redis:6379/0 EMBEDDING_MODEL: text-embedding-3-small ports: - 8080:8080 depends_on: postgres: condition: service_healthy redis: condition: service_started volumes: pgdata: redisdata:几个关键点pgvector直接用官方镜像pgvector/pgvector:pg16省得自己编译。healthcheck很重要。mcp-server依赖postgres如果postgres没起来就启动mcp-server会连接失败。加上healthcheck和condition能避免这个问题。Redis用来做working memory的缓存。working memory访问频率高放Redis里比每次查PostgreSQL快得多。启动命令docker compose up -d第一次启动会拉镜像、建表大概需要两三分钟。之后启动就很快了。4.2 数据库初始化与索引调优PostgreSQL启动后需要手动建表和索引。我把初始化SQL放在init.sql里通过Docker的/docker-entrypoint-initdb.d/目录自动执行。除了前面提到的memories表还建了一个memory_relations表用来存记忆之间的关联CREATE TABLE memory_relations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_memory_id UUID REFERENCES memories(id) ON DELETE CASCADE, target_memory_id UUID REFERENCES memories(id) ON DELETE CASCADE, relation_type VARCHAR(32) NOT NULL, strength FLOAT DEFAULT 1.0, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_relations_source ON memory_relations(source_memory_id); CREATE INDEX idx_relations_target ON memory_relations(target_memory_id);这个关联表是后来加的。起因是发现有些记忆是成对出现的比如“用户喜欢深色模式”和“用户讨厌亮色模式”这两条记忆应该关联起来。检索到一条时另一条也应该被召回。加了关联表后召回完整度明显提升。pgvector的索引调优HNSW的参数我调了几轮CREATE INDEX idx_memories_embedding ON memories USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64);m16是每个节点的最大连接数ef_construction64是构建时的搜索宽度。这两个值是平衡检索速度和召回率的。m越大召回率越高但索引越大ef_construction越大构建越慢但索引质量越高。16和64是我在百万级数据下测出来的甜点值。查询时的ef_search参数也要调SET hnsw.ef_search 100;这个值越大检索越准但越慢。100在大多数场景下够用了。4.3 MCP Server的代码结构与核心逻辑MCP Server我用FastAPI搭的因为要同时支持stdio和SSE两种传输方式。stdio用于本地AgentSSE用于远程Agent。核心代码结构mcp-server/ ├── main.py # 入口注册MCP工具 ├── memory/ │ ├── __init__.py │ ├── store.py # 记忆存储层 │ ├── retrieve.py # 记忆检索层 │ └── embed.py # embedding封装 ├── models/ │ └── schemas.py # Pydantic模型 └── config.py # 配置store.py里的写入逻辑async def write_memory( agent_id: str, memory_key: str, memory_value: str, memory_query: str None, importance: float 0.5, metadata: dict None ) - str: embedding await embed_text(memory_value) # 检查是否已存在相似记忆 existing await find_similar(agent_id, memory_key, embedding, threshold0.95) if existing: # 更新而非新建 await update_memory(existing.id, memory_value, importance) return existing.id memory_id await insert_memory( agent_idagent_id, memory_keymemory_key, memory_querymemory_query, memory_valuememory_value, embeddingembedding, importanceimportance, metadatametadata or {} ) # 写入working memory缓存 await cache_working_memory(agent_id, memory_id, memory_value) return memory_id这里有个细节写入前先检查是否已有相似记忆。如果相似度超过0.95就更新而不是新建。这避免了同一件事被反复记住导致检索时重复召回。retrieve.py里的检索逻辑async def search_memories( agent_id: str, query: str, key_filter: str None, top_k: int 5, min_importance: float 0.0 ) - list: query_embedding await embed_text(query) # 先查working memory缓存 cached await get_cached_memories(agent_id, query_embedding, top_k) if len(cached) top_k: return cached # 缓存不够查PostgreSQL sql SELECT id, memory_key, memory_value, importance, 1 - (embedding $1) AS similarity FROM memories WHERE agent_id $2 AND importance $3 {key_filter} ORDER BY embedding $1 LIMIT $4 # ...执行查询合并缓存结果按importance加权排序加权排序的公式是final_score similarity * 0.7 importance * 0.3。这个权重也是调出来的。纯按相似度排会漏掉一些重要但语义不太匹配的记忆纯按重要性排又会召回不相关的内容。7:3是我实测比较平衡的比例。4.4 Agent侧的接入与调用示例Agent侧接入MCP不同框架方式不同。我用的是自己写的一个轻量Agent循环核心是解析MCP工具列表然后让LLM决定调用哪个。调用示例伪代码# 获取MCP工具列表 tools mcp_client.list_tools() # 构建prompt system_prompt f 你可以使用以下工具 {format_tools(tools)} 当用户询问历史信息时使用memory_search工具。 当用户表达偏好或提到新实体时使用memory_write工具。 # Agent循环 while True: user_input get_user_input() # 先检索相关记忆 memories mcp_client.call_tool(memory_search, { agent_id: user_123, query: user_input, top_k: 5 }) # 把记忆注入上下文 context format_memories(memories) # 调LLM response llm.chat(system_prompt, context, user_input) # 判断是否需要写入记忆 if should_write_memory(user_input, response): mcp_client.call_tool(memory_write, { agent_id: user_123, memory_key: extract_key(user_input), memory_value: extract_value(user_input, response), importance: calculate_importance(user_input) })这里的关键是should_write_memory的判断逻辑。我用的是规则LLM混合先用关键词匹配“我喜欢”、“记住”、“以后”等命中就直接写没命中但对话轮次超过3轮调一次LLM判断是否值得写。实操心得memory_search的调用时机很重要。我一开始是每轮都调结果延迟很高。后来改成“只在用户输入包含疑问词或指代词时调用”比如“那个”、“之前”、“还记得吗”延迟降了一半效果几乎没损失。5. 常见问题与排查技巧实录5.1 Docker相关的高频问题问题一Windows下Docker Desktop启动失败提示“virtualization support not detected”这是热搜词里出现频率很高的问题。原因通常是BIOS里虚拟化没开或者Hyper-V/WSL2没启用。排查步骤重启电脑进BIOS找Intel VT-x或AMD-V设为EnabledWindows功能里勾选“虚拟机平台”和“适用于Linux的Windows子系统”命令行执行wsl --update更新WSL2内核Docker Desktop设置里确认使用WSL2后端如果还不行检查是不是装了其他虚拟化软件如VMware、VirtualBox冲突了。Hyper-V和这些软件有时候会打架。问题二容器间网络不通mcp-server连不上postgresDocker Compose默认会创建一个网络所有服务在同一个网络里用服务名互相访问。如果连不上先检查docker compose exec mcp-server ping postgres如果ping不通说明不在同一网络。检查compose文件里有没有手动指定network或者服务有没有加入默认网络。另一个常见原因是postgres还没启动完mcp-server就尝试连接了。这就是前面healthcheck的作用。如果没配healthcheck可以在mcp-server里加重试逻辑async def wait_for_db(max_retries10): for i in range(max_retries): try: await db.connect() return except Exception: await asyncio.sleep(2) raise Exception(Database not available)问题三数据卷权限问题postgres启动报“Permission denied”Linux下常见。PostgreSQL容器里的postgres用户UID是999如果宿主机挂载目录的权限不对就会报错。解决方法sudo chown -R 999:999 ./pgdata或者干脆用Docker管理的volume不挂载宿主机目录。我用的是named volume省事。5.2 记忆检索质量差的排查思路症状Agent召回的记忆不相关或者该召回的时候不召回排查顺序检查embedding模型是否一致。写入和检索用的必须是同一个模型。我踩过这个坑写入用text-embedding-3-small检索用text-embedding-ada-002维度一样但向量空间不同检索结果全是乱的。检查Key过滤是否过严。如果key_filter写死了某个值但记忆的key不匹配就会漏召回。建议先不加key_filter看召回结果再逐步收紧。检查importance阈值。min_importance设太高会过滤掉很多有用但重要性分数低的记忆。默认0.0先不设阈值。检查working memory缓存是否过期。Redis里的缓存如果没设TTL可能会返回过时的记忆。我给working memory缓存设了30分钟TTL过期自动从PostgreSQL重新拉。检查相似度阈值。如果用了相似度阈值过滤比如只返回similarity0.8的阈值太高会漏。建议先不设阈值看top-k结果的相似度分布再决定阈值。症状检索延迟高Agent响应慢优化方向加Redis缓存working memory的检索走缓存调低hnsw.ef_search从100降到50延迟能降一半召回率损失不大限制top_k5条够用了不要设10条异步检索memory_search和LLM调用并行5.3 MCP工具调用的典型故障故障一Agent找不到MCP工具热搜词里有人问“codex无法找到mcp”。MCP工具注册后Agent需要重新加载工具列表。如果Agent是长驻进程可能需要重启或者触发一次工具刷新。另外检查MCP Server的传输方式。stdio方式需要Agent启动时指定Server命令SSE方式需要Server先启动Agent通过URL连接。两种方式的配置不一样别搞混了。故障二MCP工具调用返回schema错误热搜词里有一条“llm request failed: provider rejected the request schema or tool payload”。这通常是工具的inputSchema定义和实际传入参数不匹配。排查打印实际传入的参数和schema对比。常见问题是类型不对比如schema定义integer传了string或者必填字段没传。故障三MCP Server启动后立即退出stdio方式的MCP Server如果stdin没有输入可能会立即退出。这是正常的因为stdio Server是等待Agent发送请求的。如果Agent没连接Server就空转然后退出。解决用SSE方式Server会持续监听端口不会退出。或者用supervisor之类的工具保持stdio Server运行。5.4 记忆系统的常见问题速查表问题可能原因解决方法Agent失忆不记得之前对话working memory未持久化检查Redis连接确认working memory写入成功召回记忆不相关embedding模型不一致统一写入和检索的embedding模型召回记忆重复未做相似去重写入前检查相似度0.95则更新检索延迟高索引未建或参数不当建HNSW索引调优ef_search数据库膨胀快写入触发太频繁改用事件驱动写入加筛选条件MCP工具不调用description不清晰重写description明确调用场景Docker容器网络不通不在同一网络检查compose网络配置用服务名互访Windows Docker启动失败虚拟化未开启BIOS开VT-x启用WSL26. 记忆系统的扩展方向与个人体会hindsight这套东西跑通之后我陆续加了一些扩展。一个是记忆的“遗忘曲线”不是简单删除而是让久未访问的记忆逐渐降低importance检索时权重降低但不完全消失。这比硬删除更符合人类记忆的特点。另一个是记忆的“关联推理”。通过memory_relations表检索到一条记忆时可以顺着关联找到相关记忆。比如检索到“用户喜欢深色模式”关联到“用户讨厌亮色模式”两条一起注入上下文Agent的回复会更一致。还有一个方向是“跨Agent记忆共享”。多个Agent共享同一个记忆池但通过agent_id隔离。这样同一个用户在不同Agent之间的偏好可以互通。这个还在实验阶段主要问题是隐私和权限控制。踩了这么多坑我最大的体会是Agent记忆不是简单的“存和取”而是一套完整的生命周期管理。写入时机、存储结构、检索策略、淘汰机制每个环节都需要根据实际场景调优。没有一劳永逸的方案只有不断迭代。最后分享一个小技巧调试记忆系统的时候把每次检索的query、召回的记忆、最终的prompt都打日志。出问题的时候回看日志比瞎猜快得多。我专门写了一个debug面板实时显示working memory和long-term memory的状态调参的时候一目了然。