☰
Agent记忆系统实战:从hindsight到MCP的Docker部署与检索优化
2026/10/3 3:58:34 网站建设 项目流程

1. 从“hindsight”这个词说起:为什么记忆是 Agent 最被低估的能力

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在 Agent 和 LLM 的语境里,它指向一个非常具体、也非常要命的问题:一个 Agent 在完成一轮任务之后,能不能把这一轮里发生的事、踩过的坑、验证过的结论,变成下一轮可以直接调用的经验。

大多数人做 Agent 的时候,注意力都放在“这一轮怎么把任务跑通”上——prompt 怎么写、工具怎么调、MCP 怎么接、Docker 怎么起。这些当然重要,但真正决定一个 Agent 能不能从“玩具”变成“生产力”的,是它在时间维度上的连续性。一个没有记忆的 Agent,每次对话都是第一次见面,你昨天教它的东西,今天它忘得一干二净。这就是为什么agent memory、working memory、agent 存储这些词会反复出现在热词榜上。

我先把结论摆在前面:hindsight 这类项目的核心价值,不是让 Agent 记住更多,而是让 Agent 记住“对的东西”,并且在正确的时机把它取出来。记住更多是存储问题,记住对的、取对的是检索和建模问题。前者用 Docker 起个数据库就能解决,后者才是真正拉开差距的地方。

这篇文章适合三类人看:第一类是自己动手搭过 Agent、被“失忆”问题折磨过的开发者;第二类是在选型阶段,想搞清楚 agent memory 到底该怎么设计的架构同学;第三类是对 MCP、LLM 应用感兴趣,想找一个具体项目把概念落地的人。我会围绕 hindsight 这个主题,把记忆的存储、建模、检索、注入整条链路拆开讲,中间穿插 Docker 部署、MCP 集成这些实操细节,尽量做到看完能上手。

需要先说明一点:hindsight 这个标题本身给的信息很少,正文和关键词都是空的,所以我下面的内容是基于“agent memory + LLM + MCP + Docker”这组热词所指向的典型场景做的合理推演和补全。凡是涉及具体实现的地方,我都会说明这是基于常见工程实践的方案,你可以根据自己的技术栈替换。

2. Agent 记忆到底难在哪:三个绕不过去的坎

2.1 第一个坎:什么该记,什么该忘

人脑的记忆不是录像机,它是有选择性的。你今天早上吃了什么可能记不住,但第一次被烫到的痛感能记一辈子。Agent 的记忆系统如果什么都记,很快就会变成一个垃圾场——检索的时候噪声比信号还多,反而拖累效果。

我见过不少团队的做法是“全量存进向量库”,对话历史一条不落全 embed 进去。刚开始效果还行,因为数据量小,检索出来的东西都相关。等到积累了几千条记录,问题就来了:用户问一个简单问题,检索出来的十条里有八条是无关的寒暄。这时候模型要么被带偏,要么直接忽略检索结果,记忆系统形同虚设。

所以 hindsight 要解决的第一个问题,是记忆的筛选和压缩。哪些信息值得长期保留?我的经验是看三个维度:可复用性(这个结论下次还会用到吗)、不可推导性(这个信息能不能从其他信息推出来)、代价性(重新获取这个信息的成本高不高)。三个维度都高的,比如“这个 API 的鉴权方式是 X,踩过坑”,必须记;三个维度都低的,比如“用户说了句你好”,直接丢。

2.2 第二个坎:记忆怎么组织,才能被“想起来”

存进去容易,取出来难。这里有个很反直觉的点:记忆的检索质量,很大程度上取决于写入时的组织方式,而不是检索算法本身。你写入的时候如果只是把一段文本丢进向量库,检索的时候也只能靠语义相似度硬匹配。但如果你写入的时候就把这段记忆结构化——它是什么类型、涉及哪些实体、发生在什么上下文、有效期到什么时候——检索的时候就能用多维度的条件去筛。

这就是热词里llm ontology(本体)和llm 的 token 三个点 key/query/value指向的东西。本体论听起来很学术,说白了就是给记忆定一套“分类和关系”的规则。比如一条记忆可以建模成(主体, 关系, 客体)的三元组,或者更工程化一点,建模成带类型标签的结构化记录。key 是“我是谁”(这条记忆属于哪个 Agent、哪个用户、哪个会话),query 是“我在找什么”(当前任务的意图),value 是“我能提供什么”(这条记忆的实际内容)。这三个点对齐了,检索的命中率会明显不一样。

2.3 第三个坎:记忆怎么注入,才不干扰当前推理

检索出来一堆相关记忆,怎么塞给模型也是个技术活。全塞进 system prompt 里,token 消耗大不说,还容易让模型分心。我的做法是分层注入:最相关的几条作为“强提示”放在靠前的位置,次相关的作为“背景知识”放在后面,并且明确告诉模型“以下信息是历史经验,供参考,不一定适用于当前情况”。这个“不一定适用”的免责声明很重要,否则模型会把历史记忆当成铁律,遇到新情况也不知道变通。

还有一个细节是记忆的时效性标注。一条三个月前的记忆和一条昨天的记忆,可信度是不一样的。在注入的时候带上时间戳或者“新鲜度”标签,模型在做判断时会更有分寸。这个技巧在agentpoison那类“记忆投毒”的安全场景里尤其关键——如果记忆可以被污染,那么区分“可信记忆”和“待验证记忆”就是一道必要的防线。

3. 用 Docker 把记忆服务跑起来:从零到可用的完整路径

3.1 为什么记忆服务要独立部署

很多人一开始会把记忆逻辑写在 Agent 的主进程里,用一个本地文件或者内存字典存着。小规模测试没问题,但一旦要支持多 Agent、多会话、持久化,就必须把记忆抽成一个独立的服务。原因有三个:第一,记忆的读写频率和 Agent 的推理频率不一样,混在一起不好做资源隔离;第二,记忆服务需要独立的存储后端(向量库 + 关系库),跟 Agent 的运行环境解耦更灵活;第三,独立服务才能被多个 Agent 共享,这也是tencentdb agent memory这类方案出现的背景——把记忆做成一个可复用的基础设施。

用 Docker 部署的好处不用多说,环境一致、启动快、迁移方便。下面我给一套基于常见实践的部署方案,你可以照着改。

3.2 一套可复用的 docker-compose 编排

记忆服务通常需要两个存储:一个存向量(做语义检索),一个存结构化数据(做元数据过滤和关系查询)。我用 PostgreSQL 加 pgvector 做向量存储,用 Redis 做热数据的缓存和会话状态。这套组合的好处是运维成本低,一个数据库把关系型和向量都管了。

version: "3.9" services: memory-db: image: pgvector/pgvector:pg16 container_name: hindsight-db environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_pass POSTGRES_DB: memory ports: - "5433:5432" volumes: - ./data/pg:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 10s timeout: 5s retries: 5 memory-cache: image: redis:7-alpine container_name: hindsight-cache ports: - "6380:6379" command: ["redis-server", "--appendonly", "yes"] volumes: - ./data/redis:/data memory-api: build: ./memory-api container_name: hindsight-api depends_on: memory-db: condition: service_healthy environment: DB_URL: postgresql://hindsight:hindsight_pass@memory-db:5432/memory REDIS_URL: redis://memory-cache:6379/0 EMBED_MODEL: bge-m3 ports: - "8100:8000"

这里有几个我踩过坑的地方要提醒。端口映射我特意错开了默认端口(5433、6380、8100),因为开发机上很可能已经跑着别的 Postgres 和 Redis,用默认端口会直接冲突,报错信息还不明显,排查半天。healthcheck 是必须的,memory-api 依赖数据库就绪,没有健康检查的话容器起来了但连不上库,服务会一直重启。数据卷一定要挂出来,否则容器一删记忆全没,这在调试阶段是灾难。

3.3 初始化向量扩展和表结构

pgvector 装好之后,第一件事是启用扩展,然后建表。表结构的设计直接决定了后面检索的灵活性,我建议至少包含这几个字段:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, agent_id TEXT NOT NULL, session_id TEXT, mem_type TEXT NOT NULL, -- fact / preference / experience / tool_result content TEXT NOT NULL, embedding vector(1024), entities JSONB DEFAULT '{}', -- 结构化实体,用于精确过滤 importance REAL DEFAULT 0.5, -- 重要度,写入时打分 created_at TIMESTAMPTZ DEFAULT now(), expires_at TIMESTAMPTZ, -- 可空,表示永久有效 last_access TIMESTAMPTZ ); CREATE INDEX idx_mem_agent ON memories (agent_id, mem_type); CREATE INDEX idx_mem_embedding ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

mem_type这个字段是我强烈建议加的。不同类型的记忆,检索策略和注入方式都不一样。fact是客观事实,检索时优先精确匹配;preference是用户偏好,注入时要放在显眼位置;experience是踩坑经验,适合在遇到相似任务时召回;tool_result是工具调用结果,通常有有效期,过期就该清理。有了这个分类,后面做检索的时候就能按类型加权,而不是一锅乱炖。

importance字段是写入时打的分,可以用一个轻量的 LLM 调用或者规则来算。别小看这个分数,它在检索排序时非常有用——同样语义相似度的两条记忆,重要度高的应该优先返回。last_access用来做“冷热分离”,长期没被访问的记忆可以降权甚至归档。

3.4 启动顺序和常见报错处理

按上面的编排,正常启动顺序是 db → cache → api。如果你在 Windows 上跑 Docker Desktop,有几个高频报错要提前知道。“virtualization support not detected”这个报错,八成是 BIOS 里的虚拟化没开,或者跟 Hyper-V/WSL2 冲突了,需要在系统设置里确认虚拟化平台已启用。“docker network 不通”通常是容器间用了 localhost 而不是服务名,记住在 compose 网络里,容器之间要用 service name 通信,比如memory-db而不是127.0.0.1。

启动之后先验证数据库连通性:

docker exec -it hindsight-db psql -U hindsight -d memory -c "\dx"

看到vector扩展在列表里,说明 pgvector 装好了。如果没看到,手动执行一次CREATE EXTENSION vector;。这一步没做的话,后面建表会直接报type "vector" does not exist。

4. 记忆的写入与检索:把“后见之明”变成可调用的能力

4.1 写入不是简单存文本,而是一次结构化提炼

前面说了,检索质量取决于写入质量。所以写入这一步,我建议不要直接把原始对话丢进去,而是先过一遍“提炼”。提炼的逻辑可以是一个专门的 prompt,让模型输出结构化的记忆条目。比如一轮任务结束后,让模型回答几个问题:这轮任务里有哪些结论是下次还能用的?有哪些工具调用的参数是踩坑试出来的?用户表达了哪些偏好?

提炼出来的每条记忆,写入时带上mem_type、entities、importance。entities用 JSONB 存,比如{"tool": "mysql", "error": "auth_failed"},这样后面可以用entities @> '{"tool":"mysql"}'做精确过滤,比纯向量检索靠谱得多。

这里有个经验:写入时给记忆打“来源标签”。是用户明确说的,还是模型推断的,还是工具返回的?来源不同,可信度不同。用户明确说的偏好,可信度最高;模型推断的结论,需要后续验证。这个标签在检索时可以作为一个过滤维度,避免把模型的臆测当成事实反复使用。

4.2 混合检索:向量 + 关键词 + 元数据三路并行

纯向量检索的问题是“语义相似但事实不符”。比如用户问“MySQL 怎么连”,向量检索可能召回一条“PostgreSQL 连接失败”的记忆,因为语义上都是“数据库连接”。这时候元数据过滤就派上用场了——先按entities里的tool字段过滤,再在过滤后的子集里做向量检索。

我的检索策略是三路并行然后融合:

检索路径适用场景权重建议
向量语义检索模糊意图、经验类记忆0.5
关键词/全文检索精确术语、工具名、错误码0.3
元数据精确过滤指定 Agent、指定类型、时间范围0.2

三路结果用 RRF(Reciprocal Rank Fusion)融合,比简单加权求和更稳,因为它不依赖分数的绝对大小,只看排名。融合之后再按importance和last_access做一次重排,把重要且新鲜的记忆顶上来。

4.3 检索结果怎么注入才不添乱

检索出 top-k 之后,注入方式我前面提了分层。具体实现上,我会把记忆格式化成一段带标签的文本,而不是直接拼原始内容:

[历史经验 | 可信度:高 | 3天前] MySQL 8.0 默认认证插件是 caching_sha2_password, 旧客户端连接会报 auth 错误,需要改用户认证方式。 [历史经验 | 可信度:中 | 15天前] 该用户偏好用 Docker Compose 管理服务,不喜欢手动 docker run。

这种格式的好处是模型能一眼看出每条记忆的“分量”。可信度高的直接采信,可信度中的作为参考,时间久的提醒自己可能过时了。实测下来,这种带元信息的注入比裸文本注入,模型对记忆的利用率明显更高,也更少出现“被错误记忆带偏”的情况。

还有一个技巧是控制注入的记忆条数。不是越多越好,我一般控制在 3 到 5 条。超过 5 条,模型的注意力会被稀释,而且 token 成本上去了。如果检索出来很多条,宁可只取最相关的几条,也不要全塞。

5. 接上 MCP:让记忆成为 Agent 的“标准外设”

5.1 MCP 到底解决了记忆集成的什么问题

MCP(Model Context Protocol)这个词最近热度很高,很多人搞不清楚它跟硬件协议的区别。简单说,MCP 是一套软件层面的协议,规定了“模型/Agent 怎么跟外部能力(工具、数据源、服务)对话”。它跟硬件协议(比如 USB)的类比关系是:USB 规定了设备怎么插上电脑被识别,MCP 规定了工具怎么被 Agent 发现和调用。

对记忆系统来说,MCP 的价值在于标准化。在没有 MCP 之前,每个 Agent 框架接记忆服务都要写一套适配代码,LangChain 一套、自研框架一套、IDE 插件又一套。有了 MCP,记忆服务只要暴露成一个 MCP server,任何支持 MCP 的客户端都能直接调用。这就是为什么热词里会出现codex 接入 mcp、dify 浏览器 mcp、hermes 接入 mcp这些组合——大家都在往这个标准上靠。

5.2 把记忆服务包装成 MCP Server

一个记忆 MCP server 通常暴露这么几个工具:memory_write(写入记忆)、memory_search(检索记忆)、memory_forget(删除/归档记忆)。用 Python 的 MCP SDK 写起来不复杂,核心是把前面那套写入和检索逻辑包一层。

from mcp.server import Server from mcp.types import Tool, TextContent app = Server("hindsight-memory") @app.list_tools() async def list_tools(): return [ Tool( name="memory_search", description="检索历史记忆,用于获取过往经验、用户偏好、工具调用结论", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "agent_id": {"type": "string"}, "mem_type": {"type": "string"}, "top_k": {"type": "integer", "default": 5} }, "required": ["query", "agent_id"] } ), Tool( name="memory_write", description="写入一条记忆,需提供类型、内容和重要度", inputSchema={ "type": "object", "properties": { "agent_id": {"type": "string"}, "mem_type": {"type": "string"}, "content": {"type": "string"}, "importance": {"type": "number", "default": 0.5} }, "required": ["agent_id", "mem_type", "content"] } ) ]

这里有个设计上的取舍要讲清楚:工具描述(description)写得好不好,直接决定模型会不会在正确的时机调用它。我见过很多 MCP server 的 description 写得含糊,模型根本不知道什么时候该用。记忆检索的描述里,我特意强调了“获取过往经验、用户偏好、工具调用结论”,就是给模型明确的触发信号。实测下来,描述越具体,模型的调用时机越准。

5.3 客户端接入时的授权和配置坑

MCP 客户端接入的时候,最常见的坑是授权配置。比如codex 接入 figma mcp 怎么授权、codex 无法找到 mcp这类问题,本质都是配置没对齐。通用的排查思路是:先确认 MCP server 进程起来了(本地的话看端口,远程的话看连通性),再确认客户端的配置文件里 server 的启动命令或 URL 写对了,最后确认授权 token 或环境变量传进去了。

还有一个高频问题是工具名冲突。如果你同时接了多个 MCP server,不同 server 暴露了同名工具,客户端可能会混淆。解决办法是给工具名加前缀,比如hindsight_memory_search,避免跟别的 server 撞名。

6. 记忆系统的运维:清理、归档和防污染

6.1 记忆不是只增不减,定期清理是必须的

一个长期运行的 Agent,记忆库会越来越大。如果不做清理,检索延迟会上升,噪声也会变多。我的做法是设一个归档策略:超过一定时间没被访问、且重要度低于阈值的记忆,移到归档表或者直接删除。expires_at字段就是干这个的,写入时如果知道这条记忆有有效期(比如某个临时配置),直接设过期时间,到期自动清理。

清理任务可以用一个定时脚本跑,也可以做成数据库的定时任务。关键是清理要有日志,删了什么、为什么删,留个记录,方便回溯。我有一次误删了一批重要记忆,就是因为清理规则写得太激进,把“重要度中等但访问频率低”的经验类记忆也清掉了。后来把规则改成“重要度低于 0.3 且 90 天未访问”才删,就稳多了。

6.2 防污染:记忆系统也有安全边界

agentpoison这类研究提醒我们,记忆是可以被投毒的。如果攻击者能往记忆库里写入恶意内容,Agent 后续的行为就可能被操纵。防御的思路有几层:写入鉴权(不是谁都能写)、来源标记(区分可信和不可信来源)、内容审核(写入前过一遍敏感内容检测)、隔离(不同信任级别的记忆分开存储,检索时按级别过滤)。

对普通项目来说,至少要做到“写入鉴权”和“来源标记”。记忆写入接口不能裸奔,要有 token 或者会话校验。来源标记前面提过,用户明确说的和模型推断的要分开,检索时优先采信高可信来源。

6.3 监控指标:怎么知道记忆系统在正常工作

记忆系统不像业务接口那样有明确的成功失败,它的效果是“润物细无声”的。所以我建议盯几个指标:检索命中率(检索出来的记忆有多少被模型实际引用)、记忆增长率(每天新增多少条,是否异常)、检索延迟(P95 延迟,超过阈值要告警)、冷记忆占比(长期未访问的记忆比例,太高说明写入质量有问题)。

其中“检索命中率”最难测但最有价值。我的做法是在注入记忆的时候,让模型在回答里标注它用了哪几条记忆,然后统计引用率。引用率低,说明检索出来的东西不相关,要回头优化写入和检索策略。

7. 我在实际搭建中总结的几条经验

第一,先跑通最小闭环,再优化检索。很多人一上来就纠结用哪个 embedding 模型、要不要上 rerank,结果主流程都没通。我的建议是先用最简单的向量检索把“写入-检索-注入”跑通,看到效果之后再逐步加元数据过滤、混合检索、重排。每一步优化都要有对比数据,否则你不知道是变好了还是变差了。

第二,记忆的 value 不在“多”,在“准”。我见过一个项目,记忆库塞了几十万条,检索出来一堆无关内容,模型干脆忽略记忆自己瞎编。后来把写入策略收紧,只保留高价值记忆,库小了十倍,效果反而好了。这跟做搜索是一个道理,召回率重要,但准确率更重要。

第三,给记忆加“保质期”思维。不是所有记忆都该永久保存。工具调用结果、临时配置、一次性结论,这些都有时效性。写入时就判断好有效期,比事后清理省事得多。

第四,MCP 是趋势,但别为了 MCP 而 MCP。如果你的 Agent 是单体应用,记忆逻辑直接内嵌也没问题,不一定非要拆成 MCP server。MCP 的价值在多客户端复用和标准化,如果你的场景不需要这些,内嵌反而更简单。等技术栈需要扩展了再拆不迟。

第五,测试记忆系统要用“跨会话”的场景。单会话内的记忆测试不出问题,因为上下文本来就在。真正考验记忆系统的是:关掉会话,重新开一个,Agent 还能不能记得之前的关键信息。这个测试跑通了,记忆系统才算及格。

关于 hindsight 这个主题,能展开的还有很多,比如记忆的图结构建模、多 Agent 之间的记忆共享、记忆与 RAG 的边界划分。但核心思路是一致的:让 Agent 拥有跨时间的连续性,把每一次任务的“后见之明”沉淀成下一次的“先见之明”。这件事做扎实了,Agent 才真正从“一次性工具”变成“越用越顺手的伙伴”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询