
1. 从“hindsight”说起为什么我们需要给 Agent 装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在 LLM Agent 的语境里它指向一个非常具体且要命的问题Agent 的记忆到底该怎么存、怎么取、怎么用。你肯定遇到过这种情况——跟一个 AI 助手聊了半小时前面说过的偏好、约束、上下文到后面它全忘了或者记串了。这不是模型不够聪明而是记忆架构没设计好。我最近在折腾一套基于 MCP 协议的 Agent 记忆系统核心目标就是让 Agent 具备真正的“hindsight”能力不是简单地存聊天记录而是能在后续任务中主动回溯、关联、修正之前的记忆。这套东西涉及几个关键技术点——Agent Memory 的分层设计、LLM 的 token 预算管理、MCP 协议作为工具调用层、以及用 Docker 做环境隔离和快速部署。如果你正在做 Agent 开发或者对 LLM 应用架构感兴趣这篇内容应该能帮你少踩不少坑。先说清楚这套方案解决什么问题。传统的 Agent 记忆方案基本是两种一种是全量塞进 context window简单粗暴但 token 烧得飞快而且长上下文里模型注意力会稀释另一种是只存摘要省 token 但丢细节回溯的时候经常找不到关键信息。我想要的“hindsight”能力是让 Agent 在需要的时候能精准调取历史记忆同时不把 context 撑爆。这需要一套分层存储 按需检索 主动修正的机制而不是一个简单的向量数据库能搞定的。适合谁来参考如果你已经在用 LLM 做 Agent 开发对 MCP 协议有基本了解或者至少用过 Docker 部署过服务那这篇内容可以直接抄作业。如果你刚入门也没关系我会把每个设计决策背后的“为什么”讲清楚你至少能理解一套生产级 Agent 记忆系统该长什么样。2. 整体架构设计分层记忆 MCP 工具层 Docker 隔离2.1 为什么不用“一个向量库走天下”很多人做 Agent 记忆的第一反应是搞个向量数据库把对话历史 embedding 进去需要的时候 similarity search 一下不就完了我一开始也这么想但实际跑下来问题很多。向量检索擅长语义相似但不擅长精确回溯。比如 Agent 之前记录了一个 API key 的过期时间是“下周三”你用语义搜索“什么时候过期”可能能找到但如果你问“那个 key 的具体过期时间戳是多少”向量检索就抓瞎了。所以我采用的是分层记忆架构参考了认知科学里 working memory 和 long-term memory 的划分记忆层存储内容存储介质生命周期检索方式Working Memory当前会话的最近 N 轮对话内存/Redis会话级直接拼接Episodic Memory具体事件、操作记录、时间戳SQLite/Postgres持久化结构化查询 时间过滤Semantic Memory抽象知识、用户偏好、规则向量库持久化语义检索Procedural Memory工具调用模板、成功路径文件/DB持久化模式匹配这个划分的核心逻辑是不同性质的记忆用不同的检索方式。时间相关的走结构化查询语义相关的走向量检索操作流程走模板匹配。这样既省 token又提高召回精度。2.2 MCP 协议在记忆系统里的角色MCPModel Context Protocol在这里的作用是把记忆系统封装成 Agent 可调用的工具。Agent 不需要知道记忆底层是怎么存的它只需要通过 MCP 暴露的几个工具来读写记忆。我定义了这几个核心工具memory_store写入一条记忆需要指定类型episodic/semantic/procedural、内容、元数据memory_recall根据查询条件检索记忆支持时间范围、类型过滤、语义相似度阈值memory_update修正已有记忆比如发现之前存的信息有误memory_forget主动删除过期或错误的记忆这样做的好处是解耦。Agent 的 prompt 里只需要描述“你可以使用 memory_recall 来查找历史信息”而不需要把记忆管理的逻辑硬编码进去。换记忆后端的时候Agent 侧完全不用改。2.3 Docker 隔离为什么不用裸机部署这套系统涉及多个组件记忆存储服务、MCP server、向量数据库、可能还有 Redis 做缓存。裸机部署的话依赖冲突、端口占用、版本不一致这些问题能把你折腾疯。我用 Docker Compose 把整个栈编排起来每个组件一个容器网络隔离数据卷持久化。注意Windows 上装 Docker Desktop 经常遇到 “Virtualization support not detected” 的报错这不是 Docker 的问题是 BIOS 里虚拟化没开。进 BIOS 把 Intel VT-x 或 AMD-V 打开就行。如果开了还报错检查一下 Hyper-V 和 WSL2 是不是冲突了。3. 核心细节解析Token 预算、记忆写入策略与召回排序3.1 LLM 的 token 三个点Key、Query、Value 的记忆映射热词里有个很有意思的说法“LLM 的 token 三个点key 我是谁、query 我在找什么、value 我能提供什么”。这其实是在用注意力机制的视角理解记忆检索。在 Transformer 里attention 的计算就是 Q 和 K 做点积得到权重然后对 V 加权求和。放到 Agent 记忆系统里Key我是谁这条记忆的“身份标识”包括类型、时间、来源、标签Query我在找什么当前任务的检索需求可能是语义查询、时间范围查询、或者精确匹配Value我能提供什么记忆的实际内容以及它的置信度、时效性我在设计memory_recall工具的时候就是按照这个框架来定义参数的。Agent 调用的时候需要提供 query 的语义描述、时间范围可选、记忆类型可选然后系统返回按相关性排序的 value 列表。3.2 记忆写入策略什么时候存、存什么、存多细这是最容易踩坑的地方。我一开始的做法是每轮对话都存结果记忆库爆炸检索噪声极大。后来改成事件驱动写入显式指令写入用户说“记住这个”或者 Agent 判断信息重要时主动调用memory_store任务完成写入一个任务结束后把关键决策、结果、异常写入 episodic memory定期摘要写入每 N 轮对话做一次摘要压缩后存入 semantic memory存多细也有讲究。太细了检索噪声大太粗了回溯没细节。我的经验是episodic memory 存原始事件 关键元数据semantic memory 存抽象后的规则和偏好。比如用户说“我下周三要去北京出差”episodic 里存完整句子 时间戳 实体标注semantic 里存“用户有出差需求偏好提前一周通知”。3.3 召回排序不只是余弦相似度向量检索默认按余弦相似度排序但实际场景里这远远不够。我加了几层重排序def rerank_memories(query, candidates): scored [] for mem in candidates: semantic_score cosine_sim(query.embedding, mem.embedding) recency_score 1.0 / (1 days_since(mem.timestamp)) confidence_score mem.confidence # 0-1 type_match 1.0 if mem.type in query.preferred_types else 0.5 final_score ( 0.4 * semantic_score 0.3 * recency_score 0.2 * confidence_score 0.1 * type_match ) scored.append((final_score, mem)) return sorted(scored, reverseTrue)权重可以根据场景调。做事实性问答的时候semantic_score 权重高一些做流程回溯的时候recency_score 和 type_match 更重要。实操心得recency 的衰减函数不要用简单的线性衰减用指数衰减更符合实际。我试过exp(-days/30)一个月前的记忆权重降到 0.37三个月前的降到 0.05比较合理。4. 实操过程从零搭建一套可运行的 Agent 记忆系统4.1 环境准备与 Docker Compose 编排先确保 Docker 和 Docker Compose 装好。Windows 用户如果遇到启动失败先检查 BIOS 虚拟化再确认 WSL2 后端正常。Linux 用户直接装 docker-ce 和 docker-compose-plugin 就行。目录结构这样组织agent-memory/ ├── docker-compose.yml ├── mcp-server/ │ ├── Dockerfile │ ├── requirements.txt │ └── src/ │ ├── main.py │ ├── memory_store.py │ └── tools.py ├── vector-db/ │ └── data/ └── config/ └── settings.yamldocker-compose.yml核心内容version: 3.8 services: mcp-server: build: ./mcp-server ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://vector-db:8000 - REDIS_URLredis://redis:6379 depends_on: - vector-db - redis volumes: - ./config:/app/config vector-db: image: qdrant/qdrant:latest ports: - 8000:8000 volumes: - ./vector-db/data:/qdrant/storage redis: image: redis:7-alpine ports: - 6379:6379 command: redis-server --appendonly yes这里选 Qdrant 做向量库是因为它支持 payload 过滤可以在向量检索的同时做结构化条件筛选省得我再搭一个 Postgres。Redis 用来做 working memory 的缓存会话级的最近对话直接放 Redis读写快过期自动清理。4.2 MCP Server 的核心实现MCP Server 我用 Python 写基于mcp官方 SDK。核心是定义工具和资源。先看工具定义from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types server Server(agent-memory) server.list_tools() async def handle_list_tools() - list[types.Tool]: return [ types.Tool( namememory_store, description存储一条记忆到长期记忆库, inputSchema{ type: object, properties: { content: {type: string, description: 记忆内容}, memory_type: { type: string, enum: [episodic, semantic, procedural], description: 记忆类型 }, metadata: { type: object, description: 元数据如时间戳、来源、标签 }, confidence: { type: number, minimum: 0, maximum: 1, default: 1.0 } }, required: [content, memory_type] } ), types.Tool( namememory_recall, description从长期记忆库检索相关记忆, inputSchema{ type: object, properties: { query: {type: string, description: 检索查询}, memory_types: { type: array, items: {type: string}, description: 限定记忆类型 }, time_range: { type: object, properties: { start: {type: string, format: date-time}, end: {type: string, format: date-time} } }, top_k: {type: integer, default: 5} }, required: [query] } ) ]memory_store的实现逻辑async def handle_store(content, memory_type, metadataNone, confidence1.0): # 1. 生成 embedding embedding await embed_text(content) # 2. 构建 payload payload { content: content, memory_type: memory_type, timestamp: metadata.get(timestamp, datetime.utcnow().isoformat()), confidence: confidence, access_count: 0, **(metadata or {}) } # 3. 写入 Qdrant point_id str(uuid.uuid4()) await qdrant_client.upsert( collection_nameagent_memory, points[{ id: point_id, vector: embedding, payload: payload }] ) # 4. 如果是 working memory 类型同时写 Redis if memory_type working: await redis_client.lpush( fsession:{metadata[session_id]}:working, json.dumps(payload) ) await redis_client.ltrim( fsession:{metadata[session_id]}:working, 0, 19 ) # 只保留最近 20 条 return {status: ok, id: point_id}memory_recall的实现要复杂一些涉及多路召回和重排序async def handle_recall(query, memory_typesNone, time_rangeNone, top_k5): # 1. 语义检索 query_embedding await embed_text(query) semantic_results await qdrant_client.search( collection_nameagent_memory, query_vectorquery_embedding, limittop_k * 3, # 多召回一些用于重排序 query_filterbuild_filter(memory_types, time_range) ) # 2. 时间范围精确检索如果有 time_results [] if time_range: time_results await qdrant_client.scroll( collection_nameagent_memory, scroll_filterbuild_time_filter(time_range), limittop_k * 2 ) # 3. 合并去重 all_candidates deduplicate(semantic_results time_results) # 4. 重排序 reranked rerank_memories(query, all_candidates) # 5. 更新访问计数 for _, mem in reranked[:top_k]: await qdrant_client.set_payload( collection_nameagent_memory, payload{access_count: mem.payload[access_count] 1}, points[mem.id] ) return [format_memory(m) for _, m in reranked[:top_k]]4.3 与 Agent 的集成让 LLM 学会用记忆工具MCP Server 跑起来之后Agent 侧需要配置 MCP 连接。以常见的 Agent 框架为例配置大概长这样{ mcpServers: { agent-memory: { command: docker, args: [exec, -i, agent-memory-mcp-server-1, python, -m, src.main], env: { VECTOR_DB_URL: http://localhost:8000, REDIS_URL: redis://localhost:6379 } } } }Agent 的 system prompt 里要明确告诉它什么时候用记忆工具你拥有长期记忆能力。在以下情况下调用 memory_recall用户提到之前讨论过的话题、需要回溯历史决策、需要确认用户偏好。在以下情况下调用 memory_store用户明确要求记住某事、你做出了重要决策、发现了新的用户偏好或约束。这里有个关键技巧不要指望 LLM 自己判断什么时候该存记忆。我试过纯靠 prompt 引导结果要么存太多要么存太少。后来加了一个轻量的规则引擎做预判比如检测到“记住”“下次”“以后”这些关键词时强制触发memory_store。4.4 记忆的修正与遗忘机制“hindsight”的核心不只是记住还包括发现记错了能改。我实现了memory_update和memory_forgetasync def handle_update(memory_id, new_contentNone, new_confidenceNone): if new_content: new_embedding await embed_text(new_content) await qdrant_client.upsert( collection_nameagent_memory, points[{ id: memory_id, vector: new_embedding, payload: {content: new_content, updated_at: now()} }] ) if new_confidence is not None: await qdrant_client.set_payload( collection_nameagent_memory, payload{confidence: new_confidence}, points[memory_id] )遗忘机制我用了软删除 定期清理。软删除就是标记deletedTrue检索时过滤掉。定期清理任务每周跑一次把confidence 0.3且access_count 0且超过 90 天的记忆物理删除。踩过的坑不要用硬删除。有一次我误删了一条关键记忆结果 Agent 的行为完全跑偏。后来改成软删除至少有个后悔药。5. 常见问题与排查技巧实录5.1 Docker 网络不通导致 MCP Server 连不上向量库这是最高频的问题。表现是 MCP Server 日志里报Connection refused或Timeout。排查步骤先确认容器都在同一网络里docker network inspect agent-memory_default在 MCP Server 容器里 ping 一下向量库容器名docker exec -it mcp-server ping vector-db如果 ping 不通检查 compose 文件里有没有定义networks或者服务名有没有写错我遇到过一次是因为向量库容器启动比 MCP Server 慢MCP Server 启动时连接失败就退出了。解决办法是在 compose 里加depends_on的condition: service_healthy并给向量库配 healthcheck。5.2 记忆检索召回率低排查思路如果 Agent 经常说“我不记得之前讨论过这个”按这个顺序排查排查项检查方法常见原因记忆是否写入成功查 Qdrant collection 的 point 数量写入逻辑异常或 embedding 失败embedding 模型是否一致对比写入和检索用的模型写入用 A 模型检索用 B 模型过滤条件是否过严去掉 memory_types 和 time_range 再试类型标注错误或时间范围写错相似度阈值是否过高降低 threshold 看结果默认阈值 0.7 可能太高重排序权重是否合理调整 recency 和 semantic 权重新记忆被旧记忆压制我实测下来最常见的原因是写入时 embedding 模型和检索时不一致。比如写入用了text-embedding-ada-002检索时换了bge-large向量空间不兼容相似度计算完全没意义。5.3 Token 超限记忆召回太多把 context 撑爆memory_recall返回 top_k5 条记忆每条平均 200 token就是 1000 token。加上 system prompt、对话历史、工具定义很容易超。我的做法是给memory_recall加一个max_tokens参数返回前做截断在 Agent 侧做 token 预算管理记忆召回最多占 context 的 20%对长记忆做摘要后再返回而不是返回原文def truncate_memories(memories, max_tokens): total 0 result [] for mem in memories: mem_tokens count_tokens(mem[content]) if total mem_tokens max_tokens: # 尝试摘要 summary summarize(mem[content], max_tokens - total) if summary: result.append({**mem, content: summary}) break result.append(mem) total mem_tokens return result5.4 MCP 连接失败Chrome 扩展和 IDE 的配置差异热词里提到“谷歌浏览器扩展设置中启用 MCP 连接”和“Trae IDE 搭载 Burp Suite MCP Server”说明 MCP 的接入方式因客户端而异。常见问题Chrome 扩展需要在扩展设置里手动启用 MCP 连接并指定 MCP Server 的地址。如果 Server 跑在 Docker 里地址要用host.docker.internal而不是localhostIDE 插件配置文件路径各不一样Trae 是在设置里找 MCP 配置项VS Code 是.vscode/mcp.json认证问题如果 MCP Server 配了 token 认证客户端配置里要带上Authorizationheader实操心得先在命令行用curl测试 MCP Server 的 HTTP 端点通不通再排查客户端配置。这样能快速定位是网络问题还是配置问题。5.5 记忆冲突新旧信息不一致怎么处理用户之前说“我喜欢用 Python”后来又说“我现在主要用 Go 了”。两条记忆都存着检索的时候都返回Agent 就懵了。我的处理策略写入时检测冲突新记忆写入前先检索语义相似的旧记忆如果相似度 0.85 且内容矛盾标记旧记忆为superseded检索时优先新记忆重排序时给新记忆更高的 recency 权重显式冲突标记在 payload 里加supersedes字段指向被替代的记忆 IDasync def store_with_conflict_check(content, memory_type, metadata): # 检索相似记忆 similar await recall(content, top_k3) for mem in similar: if cosine_sim(content, mem[content]) 0.85: if is_contradictory(content, mem[content]): # 标记旧记忆被替代 await update_memory(mem[id], {superseded_by: new_id}) # 写入新记忆 return await store(content, memory_type, metadata)这套机制跑下来Agent 的记忆一致性明显提升。但要注意矛盾检测不要用 LLM 做太慢且不稳定。我用的是规则 关键词匹配比如检测到“不再”“改成”“现在用”这些词就触发冲突检查。6. 记忆系统的扩展方向与个人经验这套系统目前跑在我自己的几个 Agent 项目上稳定运行了几个月。有几个扩展方向我觉得值得尝试一是跨 Agent 记忆共享多个 Agent 共用一个记忆库通过命名空间隔离二是记忆的自动摘要与压缩用 LLM 定期把 episodic memory 压缩成 semantic memory三是基于记忆的主动学习Agent 发现记忆中的模式后主动调整行为策略。最后分享一个我踩过的最大的坑不要一开始就追求完美的记忆架构。我最初花了大量时间设计复杂的记忆分类和检索算法结果发现 Agent 根本用不起来因为工具调用太复杂了。后来简化成三个核心工具store、recall、update反而效果好很多。记忆系统的价值不在于架构多精妙而在于 Agent 能不能在合适的时机用上它。先把最小可用版本跑起来再根据实际使用中的痛点迭代比纸上谈兵强得多。