
1. 从hindsight说起为什么Agent的记忆问题值得单独拎出来做hindsight这个词本身很有意思字面意思是事后的洞察力也就是我们常说的后见之明。把这个词用在Agent Memory智能体记忆这个方向上其实点出了一个很核心的痛点大部分基于LLM的Agent在任务执行完之后是没有回头看的能力的。它做完一件事对话结束上下文清空下一次遇到类似场景又是从零开始。我最早接触Agent记忆这块是因为一个很实际的问题我搭了一个基于LLM的自动化助手用来处理一些重复性的信息整理工作。跑单次任务的时候效果很好但只要涉及上次我们讨论到哪了之前那个方案为什么被否掉了这类需要跨会话回忆的场景它就完全抓瞎。后来我意识到这不是模型能力的问题而是记忆架构的缺失——LLM本身是无状态的它的记忆完全依赖于你喂给它的上下文窗口而上下文窗口是有限的、易失的。hindsight这个项目标题我理解它的定位是给LLM-based Agent补上一套可回溯、可检索、可复用的记忆层。它要解决的不是模型聪不聪明而是模型记不记得住、能不能从过去的交互里提取经验。这个方向最近热度很高从热搜词里能看到agent memory、MCP、Docker、a-memguard这些关键词频繁出现说明整个社区都在往让Agent有长期记忆这个方向使劲。这篇文章适合谁看如果你正在做LLM应用开发尤其是涉及多轮对话、任务型Agent、知识库问答这类场景那Agent记忆是你绕不开的一环。如果你只是刚接触LLM想搞清楚记忆到底是怎么一回事这篇也会从最基础的概念讲起。我会尽量把架构思路、实操步骤、踩坑经验都摊开讲让你看完能自己动手搭一套最小可用的记忆系统。2. Agent Memory的核心设计思路拆解2.1 为什么LLM需要外挂记忆而不是靠上下文硬撑先说一个很多人容易混淆的点LLM的上下文窗口context window和记忆是两回事。上下文窗口是模型单次推理能看到的token上限比如128K、200K它确实能塞很多内容进去但它是临时的、线性的、无结构的。你把一堆历史对话塞进去模型能读到但它不会自动区分哪些是重要的、哪些是过期的、哪些是互相矛盾的。真正的记忆系统需要具备几个特征持久化关掉进程还在、可检索能按相关性捞出来、可更新新信息能覆盖旧信息、有结构不是一坨文本堆在那。这就是为什么大家开始做Agent Memory——把记忆从上下文里的一段文本变成一个独立管理的存储层。hindsight这个方向本质上就是在做这层存储和检索的抽象。它要回答的问题是Agent在什么时候该写入记忆、写入什么格式、下次怎么找到它、找到之后怎么用。2.2 记忆的三种类型working memory、episodic memory、semantic memory在动手之前得先把记忆的分类搞清楚不然架构会乱。参考认知科学和目前主流的Agent记忆实践一般分三类Working Memory工作记忆当前任务正在用的信息生命周期最短任务结束就丢。比如你现在让Agent订机票它需要记住出发地北京、目的地上海、日期下周三这些信息在任务完成后就没用了。工作记忆通常直接放在上下文里或者放在一个临时的session存储里。Episodic Memory情景记忆记录什么时候发生了什么。比如上周三用户让我查了A项目的进度我返回了三个风险点。这类记忆带时间戳用于回溯和审计。Semantic Memory语义记忆从多次交互中提炼出的稳定知识。比如用户偏好用表格形式看数据这个项目的负责人是张三。这类记忆是去时间化的是Agent对世界的认知。hindsight如果要做完整这三层都得覆盖。但实际落地时我建议先从working memory和semantic memory入手因为episodic memory的写入频率高、检索需求相对低容易变成存储垃圾场。2.3 为什么选MCP作为记忆的接入协议热搜词里MCP出现频率极高这里得解释一下。MCPModel Context Protocol是一个让LLM应用和外部工具/数据源对接的协议标准。它的核心价值是解耦记忆系统作为一个独立的MCP Server任何支持MCP的客户端比如各种IDE、Agent框架都能接进来用不用为每个框架单独写适配。这就像USB接口——以前每个设备一个专用口现在统一成USB-C谁都能插。MCP让记忆层变成了一个可插拔的组件这是它比直接在代码里写个数据库调用更优雅的地方。用MCP做记忆接入大致流程是记忆服务暴露几个工具比如write_memory、search_memory、update_memoryAgent在需要的时候调用这些工具。好处是记忆逻辑和Agent逻辑完全分离你可以单独升级记忆系统而不动Agent代码。2.4 Docker在这套架构里扮演什么角色热搜词里Docker、Docker Desktop、docker安装这些词反复出现说明很多人卡在环境这一步。记忆系统通常需要跑几个组件向量数据库存embedding、关系数据库存结构化记忆、可能还有一个缓存层。这些组件用Docker跑是最省事的因为依赖隔离、版本可控、迁移方便。我自己的习惯是所有记忆相关的服务都用docker-compose编排一个文件拉起向量库关系库记忆服务本身。这样换机器的时候docker compose up就完事了不用重新配环境。后面实操部分我会给一个具体的compose配置。3. 核心细节解析与实操要点3.1 记忆的写入策略什么时候该记记什么这是最容易做错的地方。很多人一上来就把所有对话都往记忆库里塞结果检索的时候全是噪音。我的经验是写入要有触发条件不能无脑写。常见的写入触发条件有这么几种显式指令用户说记住这个以后都按这个来直接写。任务完成节点一个任务结束时把关键结论、决策、产出写进去。信息密度阈值当一轮对话里出现了新的实体、新的偏好、新的约束条件时写。定期摘要每隔N轮对话让LLM做一次摘要把摘要写入semantic memory。写入的内容格式也很关键。我推荐用结构化的三元组自然语言描述的混合格式。热搜词里有个说法很形象key是我谁、query我在找什么、value我能提供什么。这其实就是把记忆拆成主体-关系-客体的结构。比如{ key: user_preference_format, query: 用户喜欢什么格式的数据展示, value: 表格形式带对比列, timestamp: 2025-01-15T10:30:00Z, source: session_20250115_001, confidence: 0.85 }这种结构的好处是检索时可以用key做精确匹配也可以用value做语义匹配两条路都通。3.2 记忆的检索向量检索关键词检索的混合方案检索是记忆系统的核心。纯向量检索的问题是它对精确匹配不敏感。比如你搜张三的电话向量检索可能返回一堆联系人相关的记忆但不一定精确命中张三那条。纯关键词检索的问题是它无法处理语义相似但用词不同的情况。我的做法是混合检索先用关键词/元数据过滤缩小范围再用向量相似度排序。具体流程用户query进来先做一次实体抽取提取出关键实体人名、项目名、时间等。用实体做元数据过滤从记忆库里捞出候选集。对候选集做向量相似度计算排序取Top-K。如果候选集为空退化为纯向量检索。这个方案实测下来召回率和准确率都比单一方案好很多。代价是需要维护两套索引但用现成的向量库比如Milvus、Qdrant都支持payload过滤实现起来不复杂。3.3 记忆的更新与遗忘别让记忆库变成垃圾场记忆系统做久了最大的问题不是记不住而是记太多。过期的、矛盾的、低价值的记忆如果不清理检索质量会断崖式下降。我的更新策略是同key覆盖如果新记忆的key和旧记忆相同且新记忆的confidence更高直接覆盖。矛盾检测如果新记忆和旧记忆在语义上矛盾比如用户喜欢简洁vs用户喜欢详细标记为冲突让LLM做一次裁决或者保留时间更新的那条。TTL机制给每类记忆设一个生存时间。working memory可能几小时就过期episodic memory保留30天semantic memory长期保留但定期做摘要压缩。访问频率加权被频繁检索到的记忆权重更高长期不被访问的记忆降权甚至归档。这里有个坑不要用硬删除。记忆删了就找不回来了万一后面发现删错了很麻烦。我一般用软删除加一个deleted_at字段检索时过滤掉但数据还在需要的时候能恢复。3.4 用MCP封装记忆服务的具体做法把记忆系统封装成MCP Server需要定义几个工具。我一般会暴露这几个工具名功能输入参数输出memory_write写入一条记忆content, key, tags, ttlmemory_idmemory_search检索记忆query, top_k, filters记忆列表memory_update更新记忆memory_id, new_content状态memory_forget软删除记忆memory_id状态memory_summarize对一段记忆做摘要memory_ids摘要文本MCP Server的实现可以用Python的mcp库也可以用TypeScript的SDK。核心是把上面这些操作包装成MCP的tool调用。Agent侧只需要在system prompt里告诉模型你有这些记忆工具可用模型就会在合适的时候调用。注意MCP工具的description要写清楚因为模型是靠description来决定什么时候调用的。description写得太模糊模型要么不调用要么乱调用。4. 实操过程与核心环节实现4.1 环境准备用Docker Compose拉起记忆系统全家桶先把环境搭起来。我假设你已经装了Docker DesktopWindows/Mac或者Docker EngineLinux。如果没装去官网下对应平台的安装包一路下一步就行。Windows上如果提示Virtualization support not detected去BIOS里把虚拟化打开。下面是一个docker-compose.yml包含向量库Qdrant、关系库PostgreSQL、缓存Redis和记忆服务本身version: 3.8 services: qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./data/qdrant:/qdrant/storage postgres: image: postgres:16 environment: POSTGRES_USER: memory POSTGRES_PASSWORD: memory_pass POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - ./data/postgres:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - 6379:6379 memory-service: build: ./memory-service ports: - 8080:8080 environment: QDRANT_URL: http://qdrant:6333 POSTGRES_URL: postgresql://memory:memory_passpostgres:5432/agent_memory REDIS_URL: redis://redis:6379 depends_on: - qdrant - postgres - redis启动命令就一句docker compose up -d等几十秒四个服务都起来之后docker compose ps能看到状态都是running。提示第一次拉镜像会比较慢尤其是Qdrant和Postgres的镜像。如果网络环境不好可以配置国内镜像源加速。另外数据卷挂载到本地目录方便备份和迁移。4.2 记忆服务的核心代码实现记忆服务我用Python写核心是三个模块写入、检索、更新。先看写入import uuid from datetime import datetime, timedelta from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance class MemoryStore: def __init__(self, qdrant_url, collection_nameagent_memory): self.client QdrantClient(urlqdrant_url) self.collection collection_name self._ensure_collection() def _ensure_collection(self): collections self.client.get_collections().collections if self.collection not in [c.name for c in collections]: self.client.create_collection( collection_nameself.collection, vectors_configVectorParams(size768, distanceDistance.COSINE) ) def write(self, content, keyNone, tagsNone, ttl_hoursNone): memory_id str(uuid.uuid4()) embedding self._embed(content) payload { content: content, key: key, tags: tags or [], created_at: datetime.utcnow().isoformat(), expires_at: (datetime.utcnow() timedelta(hoursttl_hours)).isoformat() if ttl_hours else None, access_count: 0, deleted: False } self.client.upsert( collection_nameself.collection, points[PointStruct(idmemory_id, vectorembedding, payloadpayload)] ) return memory_id_embed方法负责把文本转成向量可以用OpenAI的embedding API也可以用本地的sentence-transformers模型。本地模型的好处是不依赖外部服务坏处是占内存。我一般用bge-base-zh这类中文效果好的模型。检索部分def search(self, query, top_k5, filtersNone): query_vector self._embed(query) results self.client.search( collection_nameself.collection, query_vectorquery_vector, limittop_k, query_filterself._build_filter(filters) ) # 过滤掉已删除和已过期的 valid [] now datetime.utcnow().isoformat() for r in results: p r.payload if p.get(deleted): continue if p.get(expires_at) and p[expires_at] now: continue valid.append({id: r.id, score: r.score, **p}) return valid_build_filter负责把tags、key这些元数据条件转成Qdrant的filter对象。这样就能实现前面说的先过滤再排序的混合检索。4.3 把记忆服务接成MCP ServerMCP Server的代码结构大概是这样的from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(agent-memory) store MemoryStore(qdrant_urlhttp://localhost:6333) app.list_tools() async def list_tools(): return [ Tool( namememory_write, description写入一条长期记忆。当用户表达了偏好、约束、重要事实时调用。, inputSchema{ type: object, properties: { content: {type: string, description: 记忆内容}, key: {type: string, description: 记忆的唯一标识用于覆盖更新}, tags: {type: array, items: {type: string}}, ttl_hours: {type: integer, description: 过期时间不填则永久} }, required: [content] } ), Tool( namememory_search, description检索相关记忆。在回答用户问题前先检索是否有相关历史记忆。, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } ) ] app.call_tool() async def call_tool(name, arguments): if name memory_write: mid store.write(**arguments) return [TextContent(typetext, textf已写入记忆 {mid})] elif name memory_search: results store.search(**arguments) return [TextContent(typetext, textformat_results(results))]启动方式python -m memory_service.server然后在支持MCP的客户端里配置这个Server的启动命令就能用了。实测下来模型在收到记住我喜欢用表格这类指令时会主动调用memory_write在回答新问题前会先调memory_search看看有没有相关记忆。4.4 参数选择与性能调优几个关键参数的选择经验向量维度取决于你用的embedding模型。bge-base-zh是768维text-embedding-3-small是1536维。维度越高精度越好但存储和计算成本越高。一般768维够用。Top-K检索返回几条。太小可能漏掉相关记忆太大引入噪音。我一般设5-10然后让LLM做二次筛选。相似度阈值低于某个分数的直接丢弃。我一般设0.6-0.7具体看模型。太低会召回不相关的太高会漏掉。TTLworking memory设1-24小时episodic设7-30天semantic不设或设很长。批量写入如果一次要写多条记忆用Qdrant的批量upsert比逐条写快很多。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路这是最高频的问题。检索不准通常有三个原因embedding模型不适合你的语言/领域。如果你做的是中文场景用英文为主的embedding模型效果会很差。换成bge系列或者m3e这类中文模型。记忆内容太短或太碎。一条记忆只有好的两个字embedding出来没有区分度。写入时要做内容规范化把上下文补全。没有做元数据过滤。纯向量检索在记忆量大时噪音很多。加上tags、时间范围这些过滤条件。排查方法把检索结果打出来人工看Top-10里有多少是真正相关的。如果低于50%说明检索策略有问题。5.2 Docker环境常见报错报错信息原因解决方法Virtualization support not detectedBIOS虚拟化未开启进BIOS开启VT-x/AMD-Vport is already allocated端口被占用改compose里的端口映射或杀掉占用进程no space left on device磁盘满清理docker system prune或迁移数据目录network not foundcompose网络问题docker compose down后重新uppermission denied挂载目录权限Linux下chown对应目录或加user配置5.3 记忆冲突与幻觉的处理Agent有时候会记错把不存在的事写进记忆。这种情况要靠写入前的校验让LLM在写入前先确认这条信息是用户明确说的还是我推断的。推断的内容标记低confidence检索时降权。记忆冲突的话我一般保留时间更新的那条但把旧的标记为superseded不删除。这样万一新的是错的还能回溯。5.4 性能瓶颈与扩展单机跑几千条记忆没问题上万条之后检索延迟会上升。扩展方向Qdrant开分片和副本加Redis缓存热点记忆把embedding计算异步化写入时先落库再算向量定期做记忆压缩把多条相关记忆合并成一条摘要提示不要过早优化。大部分场景几千条记忆足够了先把功能跑通性能问题等真遇到了再解决。6. 关于hindsight方向的一些个人判断我做Agent记忆这块有一段时间了最大的体会是记忆系统的价值不在于记得多而在于记得准。一个只记100条但每条都精准的系统比记10000条但一半是噪音的系统有用得多。hindsight这个方向我觉得接下来会往几个方向走一是记忆的自动摘要和压缩会越来越重要因为原始记忆的增长速度远超检索能力的提升二是记忆的权限和隔离会成为一个刚需多用户场景下不能互相看到对方的记忆三是记忆的可解释性用户得能知道Agent为什么记得这个。如果你现在要动手做我的建议是先用最简单的方案跑起来——一个向量库加一个MCP Server能写能查就行。别一上来就搞三层记忆、冲突检测、自动摘要那些都是后面根据实际需求加的。我见过太多人卡在架构设计上最后一行代码没写。最后分享一个小技巧在system prompt里明确告诉模型你有记忆工具在回答前先检索比让它自己判断要不要检索要可靠得多。模型的自驱性没你想的那么强该给的指令要给足。