
1. 为什么“记忆”才是 Agent 落地的真正分水岭1.1 从“无状态调用”到“有状态协作”的认知转变如果你最近半年在折腾 LLM 应用大概率会有一种强烈的割裂感模型能力每隔几个月就上一个台阶但真正落到业务里Agent 依然像个“金鱼脑”——上一轮刚说清楚的需求下一轮就忘得一干二净。这不是模型不行而是我们长期把 LLM 当成一个无状态的函数在调用输入 prompt输出 completion中间什么都不留。hindsight这个项目标题本身就点破了这层窗户纸。Hindsight 是“事后之明”是回头看时才明白的东西。放到 Agent 语境里它指向一个非常具体的能力让 Agent 能够回看自己做过什么、说过什么、决策过什么并把这些历史转化为下一步行动的依据。这跟简单的“聊天记录拼接”完全是两码事。我见过太多团队的做法是把最近 N 轮对话直接塞进 context window美其名曰“记忆”。实测下来这种做法在超过 20 轮之后就开始崩token 成本飙升、关键信息被淹没、模型注意力被无关内容稀释。真正的 agent memory 需要解决三个层次的问题——存什么、怎么取、何时用。这三个问题不解决Agent 永远只能做 demo做不了产品。1.2 热搜词背后的技术拼图MCP、Docker 与 Agent Memory 的交汇把这次的热搜词摊开看其实能拼出一张很清晰的技术地图。agent memory、working memory、tencentdb agent memory指向的是记忆的存储与检索层MCP、MCP 协议、browser use MCP、playwright MCP指向的是 Agent 与外部工具、数据源之间的标准化连接层Docker、Docker Desktop、docker compose则是把这一切打包成可复现环境的工程底座。这三者不是孤立的热点而是一条链上的三个环节。MCP 解决“Agent 怎么跟外部世界说话”Docker 解决“这套东西怎么在任何机器上一键跑起来”而 agent memory 解决“Agent 说完话之后记住了什么”。hindsight这个项目本质上就是在这条链上补上了最容易被忽视、却最决定成败的一环。我个人的判断是2024 年大家在卷 RAG2025 年大家在卷 MCP 工具生态而接下来真正拉开差距的一定是记忆架构的设计能力。因为工具是通用的模型是通用的唯独“你这个 Agent 记得什么、怎么记”是独一无二的。1.3 这篇文章适合谁读能拿走什么如果你正在做以下任何一件事这篇内容应该能帮到你正在用 LLM 框架LangChain、LlamaIndex、Dify 等搭 Agent但被“记不住上下文”折磨想接入 MCP 协议让 Agent 调用外部工具但不确定记忆层该怎么设计需要用 Docker 把整套 Agent 环境固化下来方便团队协作和部署单纯想搞清楚 agent memory 和普通对话历史到底差在哪。我不会只讲概念。下面会从架构设计、存储选型、MCP 集成、Docker 编排一路讲到踩坑排查尽量把每个“为什么这么选”讲透。你可以直接抄作业也可以只挑其中一段用在现有项目里。2. 记忆架构的整体设计与选型逻辑2.1 三层记忆模型working memory、episodic memory、semantic memory在动手写代码之前先把记忆分层想清楚否则后面一定返工。我参考认知科学的分类结合工程实践把 agent memory 拆成三层记忆层对应概念存储周期典型实现用途Working Memory工作记忆单次会话内存 / Redis当前任务的临时上下文Episodic Memory情景记忆跨会话向量库 关系库历史交互、决策轨迹Semantic Memory语义记忆长期知识图谱 / 向量库沉淀的事实、偏好、规则Working memory 就是热搜里说的agent 存储 working memory它对应的是“当前这一轮任务我需要记住什么”。比如用户让 Agent 订机票那出发地、目的地、时间、舱位偏好就是 working memory 的内容任务结束就可以释放。Episodic memory 是“我上次帮这个用户订过什么”它需要跨会话保留并且能被检索出来。这里llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么这个说法很形象——记忆的检索本质上就是一次注意力机制的类比key 是记忆的索引特征query 是当前情境value 是记忆内容本身。Semantic memory 则是更抽象的知识沉淀比如“这个用户偏好靠窗座位”“这个项目禁止使用某个库”。它不依赖具体某次对话而是从多次交互中提炼出来的。提示不要一上来就三层全上。大多数项目从 working memory episodic memory 起步就够了semantic memory 等你有明确的沉淀需求再加否则维护成本会压垮你。2.2 为什么不用“全量塞 context”而要做检索式记忆很多人会问现在模型 context window 都到 128K 甚至 1M 了为什么还要费劲做检索直接把历史全塞进去不行吗我实测过不行原因有三个第一成本。假设每轮对话平均 500 token100 轮就是 5 万 token。如果每轮都全量塞入第 100 轮的单次调用成本是第 1 轮的 100 倍。按主流模型定价算一个日活千级的应用光 context 成本就能吃掉全部利润。第二注意力稀释。模型对长 context 的利用效率并不是线性的。有研究表明当 context 超过一定长度后中间部分的信息召回率会明显下降这就是所谓的“lost in the middle”。你塞了 5 万 token模型真正用上的可能只有头尾那几千。第三噪声干扰。历史对话里有大量寒暄、试错、废弃方案。这些内容混进去会干扰模型对当前任务的判断。检索式记忆的核心价值就是只把相关的那几条捞出来而不是全量倾倒。所以hindsight的设计思路应该是working memory 常驻episodic memory 按需检索semantic memory 定期提炼。这样既控制了 token 成本又保证了信息密度。2.3 存储选型向量库、关系库还是图数据库存储选型是另一个容易纠结的点。我的经验是不要二选一要组合用。向量库如 Milvus、Qdrant、pgvector擅长语义相似度检索适合“找意思相近的记忆”。但它不擅长精确过滤比如“找出上周三之后、关于订单 A 的所有记忆”这种带条件的查询向量库很吃力。关系库如 PostgreSQL、MySQL擅长结构化查询和事务适合存记忆的元数据——时间戳、会话 ID、用户 ID、记忆类型、重要性评分。热搜里出现的docker 安装 mysql8.0和docker 安装 redis 主从其实就是在为这套组合打基础。图数据库如 Neo4j适合表达记忆之间的关联比如“记忆 A 导致了记忆 B”“记忆 B 和记忆 C 属于同一任务”。如果你的 Agent 需要做复杂的因果推理图数据库值得考虑但初期不建议上复杂度太高。我的推荐组合是PostgreSQL带 pgvector 扩展 Redis。PostgreSQL 一张表存记忆内容和元数据pgvector 做向量检索Redis 做 working memory 和热点缓存。一套 Docker Compose 就能拉起来运维成本极低。3. 核心细节解析与实操要点3.1 记忆的写入什么时候该记记什么粒度记忆写入最忌讳的是“什么都记”。我见过一个项目把每一轮对话原封不动存进去结果检索出来的全是“好的”“明白了”这种废话。记忆写入必须做过滤和提炼。我的做法是分两步第一步规则过滤。用简单的启发式规则先筛一遍长度小于 10 个字符的丢弃、纯确认性回复丢弃、重复内容丢弃。这一步能砍掉 60% 以上的噪声。第二步LLM 提炼。对通过过滤的内容用一个小模型比如 7B 级别的做一次结构化提炼输出 JSON 格式{ memory_type: episodic, summary: 用户确认了订单 A 的收货地址为北京市朝阳区某小区, entities: [订单A, 收货地址, 北京朝阳], importance: 0.8, timestamp: 2025-01-15T10:30:00Z }这里importance字段很关键它决定了这条记忆在检索时的权重。重要性高的记忆即使语义相似度稍低也应该被优先召回。注意提炼这一步会增加延迟和成本建议异步做。用户交互走同步路径记忆提炼走后台队列不要阻塞主流程。3.2 记忆的检索混合检索策略与重排序检索是记忆系统的核心。单纯用向量相似度检索效果往往不够好因为语义相似不等于任务相关。我推荐混合检索 重排序的两阶段策略。第一阶段并行跑两路检索向量检索用当前 query 的 embedding 去 pgvector 里找 top 20 相似记忆关键词检索用 BM25 或 PostgreSQL 全文索引找 top 20 包含关键实体的记忆。第二阶段把两路结果合并去重用一个 cross-encoder 重排序模型比如 bge-reranker对候选记忆打分取 top 5 注入 context。这套流程听起来复杂但实测下来召回质量比单路向量检索高出一大截。尤其是当用户 query 里包含具体实体名订单号、人名、地名时关键词检索能捞到向量检索漏掉的精确匹配。关于llm as judge这个热词它在记忆检索里也有用武之地可以用 LLM 对召回的记忆做一次相关性判断把明显不相关的剔除。但这会增加一次模型调用建议只在召回质量要求极高的场景用。3.3 MCP 协议在记忆系统里的角色定位MCPModel Context Protocol是一个标准化的协议让 LLM 应用能以统一的方式连接外部工具和数据源。热搜里mcp 是软件协议 硬件协议那个概念叫什么来着这个问题其实问的是 MCP 的定位——它是软件层的通信协议类比的话有点像 USB-C 之于硬件它定义的是“怎么插”而不是“插什么”。在记忆系统里MCP 的价值在于把记忆的读写能力标准化成工具。你可以写一个 MCP Server暴露两个工具memory_write写入一条记忆memory_search根据 query 检索记忆。这样任何支持 MCP 的 Agent 框架Claude Desktop、Cursor、Dify 等都能直接调用你的记忆系统不需要为每个框架写适配代码。这就是标准化的威力。热搜里browser use MCP 跟 playwright MCP 有什么区别这个问题也值得说一句browser use MCP 通常是封装好的浏览器操作工具集开箱即用playwright MCP 更底层给你的是 Playwright 的原语灵活但需要自己组装。选哪个取决于你的控制粒度需求。3.4 Docker 编排把记忆系统打包成可复现环境Docker相关热搜词占了半壁江山说明环境搭建确实是大家的痛点。记忆系统涉及多个组件应用、PostgreSQL、Redis、向量库手工装一遍能折腾一整天用 Docker Compose 半小时搞定。一个典型的docker-compose.yml骨架version: 3.9 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_PASSWORD: yourpassword POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - 6379:6379 memory-api: build: . depends_on: - postgres - redis environment: DATABASE_URL: postgresql://postgres:yourpasswordpostgres:5432/agent_memory REDIS_URL: redis://redis:6379 ports: - 8000:8000 volumes: pgdata:这里用pgvector/pgvector:pg16镜像而不是官方 postgres 镜像是因为它预装了 pgvector 扩展省去手动编译的麻烦。这是很多人docker 安装 mysql 失败或装向量库踩坑的根源——镜像选错了。提示Windows 用户如果遇到virtualization support not detected或docker desktop failed to start先去 BIOS 里开虚拟化Intel VT-x 或 AMD-V再确认 WSL2 已启用。这两个是 Docker Desktop 启动失败最常见的原因。4. 实操过程与核心环节实现4.1 环境准备从零到 Docker 跑起来先把地基打好。以下步骤在 Windows 11 WSL2 和 macOS 上都验证过。第一步安装 Docker Desktop。去官网下载对应版本安装时勾选 WSL2 后端Windows。装完在终端跑docker --version和docker compose version都能输出版本号才算成功。第二步拉取镜像。国内网络环境下建议配置镜像加速器否则拉取大镜像会非常慢。配置位置在 Docker Desktop 的 Settings → Docker Engine加一段 registry-mirrors。第三步启动依赖服务。把上面的 compose 文件存成docker-compose.yml在同目录执行docker compose up -d-d是后台运行。执行完用docker compose ps看状态三个服务都是running就对了。第四步初始化数据库。进入 postgres 容器建扩展和表docker compose exec postgres psql -U postgres -d agent_memory然后在 psql 里执行CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, session_id TEXT NOT NULL, user_id TEXT, memory_type TEXT NOT NULL, summary TEXT NOT NULL, entities TEXT[], importance FLOAT DEFAULT 0.5, embedding vector(1024), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);这里vector(1024)的维度要跟你用的 embedding 模型对齐。bge-large 是 1024 维OpenAI text-embedding-3-small 是 1536 维别搞错了否则插入会报错。4.2 记忆写入接口的实现与参数选择写入接口的核心逻辑是接收原始对话 → 过滤 → 提炼 → 生成 embedding → 入库。import asyncpg from openai import AsyncOpenAI async def write_memory(pool, session_id, user_id, raw_text): if len(raw_text.strip()) 10: return None # 用 LLM 提炼结构化记忆 resp await client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 把对话提炼成一条记忆输出JSONsummary, entities, importance(0-1)}, {role: user, content: raw_text} ], response_format{type: json_object} ) data json.loads(resp.choices[0].message.content) # 生成 embedding emb await client.embeddings.create( modeltext-embedding-3-small, inputdata[summary] ) vector emb.data[0].embedding async with pool.acquire() as conn: await conn.execute( INSERT INTO memories (session_id, user_id, memory_type, summary, entities, importance, embedding) VALUES ($1, $2, $3, $4, $5, $6, $7), session_id, user_id, episodic, data[summary], data[entities], data[importance], vector )几个参数选择的理由importance 默认 0.5让 LLM 打分但给个中性默认值避免模型乱打极端分embedding 用 summary 而非原文summary 更干净检索噪声更小异步写入整个流程走后台队列不阻塞用户交互。4.3 记忆检索的完整链路与重排序实现检索链路是写入的逆过程但多了重排序这一步。async def search_memory(pool, query, session_id, top_k5): # 1. 生成 query embedding emb await client.embeddings.create( modeltext-embedding-3-small, inputquery ) qvec emb.data[0].embedding # 2. 向量检索 async with pool.acquire() as conn: rows await conn.fetch( SELECT id, summary, importance, 1 - (embedding $1) AS similarity FROM memories WHERE session_id $2 ORDER BY embedding $1 LIMIT 20, qvec, session_id ) # 3. 综合打分相似度 * 0.7 重要性 * 0.3 scored [ (r[summary], r[similarity] * 0.7 r[importance] * 0.3) for r in rows ] scored.sort(keylambda x: x[1], reverseTrue) return [s[0] for s in scored[:top_k]]是 pgvector 的余弦距离操作符1 - 距离就是相似度。综合打分里相似度权重 0.7、重要性权重 0.3这个比例是我调了几轮之后觉得比较平衡的。如果你的场景更看重“重要的事不能忘”可以把重要性权重提到 0.4 甚至 0.5。注意WHERE session_id $2这个过滤条件很重要。如果不加检索会跨会话捞记忆导致上下文串味。但如果你要做跨会话的长期记忆就要去掉这个条件改用 user_id 过滤。4.4 把记忆系统封装成 MCP Server要让 Agent 框架能调用记忆系统最优雅的方式是封装成 MCP Server。核心是定义工具描述和实现处理函数。from mcp.server import Server from mcp.types import Tool, TextContent server Server(memory-server) server.list_tools() async def list_tools(): return [ Tool( namememory_search, description检索历史记忆输入查询语句返回相关记忆列表, inputSchema{ type: object, properties: { query: {type: string}, session_id: {type: string} }, required: [query, session_id] } ) ] server.call_tool() async def call_tool(name, arguments): if name memory_search: results await search_memory( pool, arguments[query], arguments[session_id] ) return [TextContent(typetext, text\n.join(results))]工具描述description写得好不好直接决定 Agent 会不会正确调用。要写清楚“什么时候用这个工具”“输入是什么”“返回什么”别写得太抽象。5. 常见问题与排查技巧实录5.1 记忆检索召回不准的排查思路召回不准是最常见的问题排查要按链路一步步来。现象可能原因排查方法解决召回内容完全不相关embedding 模型不匹配检查写入和检索用的模型是否一致统一模型召回内容相关但不够精确只用了向量检索看是否漏了关键词检索加 BM25 混合检索重要记忆没被召回importance 权重太低打印打分明细调高 importance 权重召回结果重复写入时没去重查 memories 表重复 summary写入前做相似度去重跨会话串味session_id 过滤缺失检查 SQL 的 WHERE 条件补上过滤条件我踩过最深的一个坑是 embedding 模型不一致写入用了一个模型检索用了另一个结果相似度全是乱的。这个 bug 藏得很深因为代码不报错只是结果莫名其妙。后来我在写入和检索函数里都加了模型名断言才杜绝了这个问题。5.2 Docker 环境下的网络与依赖问题docker 网络不通是高频问题。容器之间通信要用服务名而不是 localhost。比如 memory-api 连 postgreshost 要写postgres而不是127.0.0.1因为每个容器有自己的网络命名空间。如果docker compose up之后服务起不来按这个顺序查docker compose logs service看具体报错检查端口是否被占用netstat -ano | findstr 5432检查环境变量是否传对数据库密码、URL检查依赖服务是否健康depends_on只保证启动顺序不保证就绪。docker 青龙 依赖管理这类问题本质上是容器内依赖缺失。解决办法是在 Dockerfile 里显式声明所有依赖别指望基础镜像自带。5.3 记忆膨胀与性能衰减的应对跑一段时间后memories 表会越来越大检索变慢。这是必然的要提前设计应对策略。策略一分层存储。超过 90 天的低重要性记忆归档到冷存储表主表只留热数据。策略二定期合并。把同一主题的多条记忆用 LLM 合并成一条摘要减少条目数。策略三索引优化。pgvector 的 ivfflat 索引lists参数要随数据量调整经验值是sqrt(行数)。数据量到百万级时考虑换 HNSW 索引查询更快但建索引更慢。策略四TTL 机制。working memory 设过期时间Redis 里用EXPIRE自动清理别让它无限增长。提示记忆系统的性能问题往往是渐进的不会突然爆发。建议从第一天就加监控记录每次检索的耗时和召回数量趋势不对就及时干预。5.4 几个容易忽视的实操心得最后分享几个文档里不会写、但实际很关键的点。第一记忆的时区问题。时间戳统一用 UTC 存展示时再转本地时区。我见过因为时区混乱导致“上周的记忆”检索出“上上周”的内容排查了半天。第二embedding 的批量处理。写入时如果一条条调 embedding 接口延迟很高。攒一批比如 16 条一起调吞吐能提升好几倍。第三MCP Server 的错误处理。工具调用失败时要返回清晰的错误信息给 Agent而不是抛异常。Agent 看到“数据库连接失败请稍后重试”会自己决定重试还是换策略看到堆栈信息只会懵。第四Docker 镜像体积。用python:3.11-slim而不是完整版多阶段构建把编译依赖留在构建阶段最终镜像能小一半以上。镜像小推送和拉取都快。第五别过早优化。我一开始就想上分布式向量库、图数据库、消息队列结果光环境就搭了一周。后来退回到 PostgreSQL Redis 的单机方案两天就跑通了效果完全够用。等真的遇到瓶颈再升级别为想象中的规模买单。这套记忆系统的核心思路其实就一句话把 Agent 的历史当成一等公民来管理而不是当成 context 的填充物。想清楚这一点剩下的技术选型和实现都是水到渠成的事。