☰
Agent记忆架构实战:基于MCP与Docker构建hindsight经验记忆层
2026/9/30 15:54:08 网站建设 项目流程

1. 从“hindsight”说起:为什么记忆是 Agent 落地的最后一公里

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。把这个词放到 LLM Agent 的语境里,它指向的东西非常具体:Agent 能不能在任务执行完之后,把发生过的事情沉淀下来,在下次遇到类似场景时调用这些经验。这就是 agent memory 要解决的核心问题。

我接触过不少做 Agent 的团队,模型选型、工具调用、Prompt 工程都做得不错,但一上生产环境就露馅。用户上周问过的问题,这周再问,Agent 表现得像第一次见面;同一个任务失败了三次,第四次还是用同样的方式撞墙。这不是模型能力的问题,是记忆架构缺失的问题。hindsight 这个项目标题,本质上就是在追问一件事:Agent 的记忆该怎么设计,才能让它真正“吃一堑长一智”。

这篇文章适合三类人看。第一类是正在做 Agent 产品、被“上下文窗口不够用”和“状态管理混乱”折磨的开发者;第二类是对 MCP 协议、Docker 部署、LLM 记忆机制感兴趣,想找一个完整案例上手的技术爱好者;第三类是做 RAG 或知识库方向,想搞清楚“记忆”和“检索”到底差在哪里的工程师。我会从架构设计讲到 Docker 实操,从 MCP 协议讲到记忆分层,尽量把每个决策背后的“为什么”说清楚。

需要提前说明的是,hindsight 作为一个项目标题,本身没有给出完整的技术方案。下面的内容是我基于当前 Agent 记忆领域的主流实践,结合 MCP、Docker 这些热词所指向的技术栈,做的一次完整推演和落地拆解。你可以把它当成一个“如果我来做 hindsight 这个项目,我会怎么设计”的参考方案。

2. 记忆架构的整体设计:三层记忆与 hindsight 的定位

2.1 为什么 Agent 需要分层记忆

人类大脑的记忆不是铁板一块。你记得昨天午饭吃了什么,这是短期记忆;你记得怎么骑自行车,这是程序性记忆;你记得某个同事的性格特点,这是长期语义记忆。Agent 的记忆设计如果只用一个向量数据库全部塞进去,结果就是检索时噪声极大,该记住的没记住,不该翻出来的全翻出来了。

我在实际项目里踩过这个坑。早期做一个客服 Agent,把所有对话历史都 embed 之后存进向量库,结果用户问“我的订单到哪了”,检索出来的却是三个月前另一个用户抱怨物流慢的对话。原因很简单:没有区分“事实记忆”和“经验记忆”。事实记忆是“用户 A 的订单号是 12345”,经验记忆是“查询订单状态时,先调订单接口再调物流接口的成功率更高”。这两类东西的存储结构、检索方式、更新策略完全不同。

hindsight 这个项目,我倾向于把它定位成经验记忆层的实现。它不负责存原始对话,也不负责存用户画像,它负责的是:从 Agent 的执行轨迹中提取可复用的经验,并在未来任务中主动提供这些经验。这个定位决定了它的技术选型。

2.2 三层记忆的职责划分

我把 Agent 记忆分成三层,hindsight 落在最上面一层:

记忆层级存储内容典型实现生命周期
工作记忆当前会话的上下文上下文窗口 + 滑动窗口摘要单次会话
事实记忆用户偏好、实体属性、历史事实结构化数据库 + 向量检索长期,需更新
经验记忆任务执行策略、成功/失败模式图结构 + 语义检索长期,需抽象

工作记忆就是 LLM 的上下文窗口,这个没什么好说的,token 用完就丢。事实记忆是 RAG 的主场,用户问什么就检索什么。经验记忆是 hindsight 的核心,它要回答的问题是:“上次遇到类似任务时,我是怎么做的,结果如何,这次要不要换个做法。”

这个分层的好处是,每一层的检索策略可以独立优化。工作记忆用滑动窗口加摘要,事实记忆用混合检索(关键词 + 向量),经验记忆用图遍历加语义相似度。如果混在一起,调参就是灾难。

2.3 hindsight 的核心数据结构

经验记忆的存储结构,我推荐用有向图而不是纯向量。原因很简单:经验是有因果关系的。“因为接口 A 超时了,所以改用了接口 B,结果成功了”——这是一个因果链,向量数据库表达不了这种关系。

具体来说,每个经验节点包含这几个字段:

  • trigger:触发条件,用自然语言描述,比如“用户查询订单状态且订单号存在”
  • action:执行的动作序列,比如“先调 order_api,再调 logistics_api”
  • outcome:结果,成功/失败/部分成功
  • context:环境上下文,比如“订单状态为已发货”
  • embedding:trigger 和 action 的向量表示,用于语义检索

节点之间的边表示因果关系或时序关系。这样当新任务进来时,先用 trigger 的 embedding 做语义检索找到候选节点,再沿着边遍历找到相关的经验链。

注意:经验节点不要存原始对话文本,要存抽象后的策略描述。原始文本噪声太大,而且容易泄露用户隐私。抽象的过程可以用 LLM 来做,prompt 大概是“从以下执行轨迹中提取可复用的操作策略,忽略具体实体名称”。

3. MCP 协议在 hindsight 中的角色:让记忆可插拔

3.1 MCP 到底是什么

MCP 最近热度很高,但很多人对它的理解还停留在“又一个协议”的层面。我用一句话解释:MCP 是让 LLM 应用和外部工具/数据源之间用统一接口通信的协议。你可以把它类比成 USB-C——以前每个设备有自己的充电口,现在统一了,插上就能用。

在 hindsight 的架构里,MCP 的价值在于把记忆层做成一个独立的服务,而不是嵌在 Agent 代码里。这样做的好处是:Agent 可以用任何语言写,记忆服务可以用任何语言写,两者通过 MCP 协议通信。换 Agent 框架不用重写记忆逻辑,换记忆实现也不用改 Agent 代码。

MCP 的核心概念有三个:Resources(数据源)、Tools(可调用的函数)、Prompts(预定义的提示模板)。hindsight 作为记忆服务,应该暴露这几个 MCP 接口:

  • memory.store:存入一条经验
  • memory.retrieve:根据当前任务检索相关经验
  • memory.feedback:任务执行后反馈结果,用于更新经验权重
  • memory.forget:删除或降权过时经验

3.2 为什么用 MCP 而不是直接写 SDK

我试过两种方式。早期直接在 Agent 代码里 import 记忆模块,简单直接,但问题很快暴露:Agent 跑在 Python 里,记忆服务想用 Go 重写性能瓶颈部分,就得搞跨语言调用;多个 Agent 共享记忆时,每个 Agent 都要连数据库,连接池管理很麻烦。

换成 MCP 之后,记忆服务变成一个独立进程,Agent 通过标准协议调用。好处很明显:

  • 语言无关:Agent 用 Python,记忆服务用 Rust,互不影响
  • 部署解耦:记忆服务可以单独扩容,Agent 无状态
  • 权限隔离:记忆服务可以统一做鉴权和审计
  • 复用性:同一个记忆服务可以给多个 Agent 用

代价是多了一层网络调用,延迟会增加。实测下来,本地 Docker 网络内调用延迟在 2-5ms,对于记忆检索这种非高频操作完全可以接受。如果是高频调用,可以在 Agent 侧加一层本地缓存。

3.3 MCP 连接的实际配置

MCP 服务通常通过 stdio 或 SSE 两种方式通信。Docker 部署场景下,我推荐用 SSE,因为容器间通过 stdio 通信很别扭。配置大概长这样:

{ "mcpServers": { "hindsight-memory": { "url": "http://hindsight:8080/mcp/sse", "transport": "sse", "timeout": 30000 } } }

如果你在本地开发,Agent 和记忆服务都在宿主机上,用 stdio 更简单:

{ "mcpServers": { "hindsight-memory": { "command": "python", "args": ["-m", "hindsight.server", "--stdio"], "env": { "HINDSIGHT_DB": "postgresql://localhost:5432/hindsight" } } } }

提示:MCP 的 SSE 连接在某些客户端上需要手动启用。如果你用的是支持 MCP 的浏览器扩展或 IDE 插件,记得在设置里打开“MCP 连接”开关,否则会一直连不上。

4. Docker 化部署:从零搭建 hindsight 服务

4.1 环境准备与 Docker 安装

hindsight 服务依赖 PostgreSQL(存图结构)和 Redis(做缓存和会话状态)。用 Docker Compose 编排是最省事的方案。先确认你的机器支持虚拟化,Windows 上需要开启 Hyper-V 或 WSL2,Mac 上需要确认 Docker Desktop 的虚拟化后端正常。

Windows 用户如果遇到 “Virtualization support not detected” 报错,通常是 BIOS 里的虚拟化选项没开,或者 Hyper-V 和 WSL2 冲突。解决办法是进 BIOS 开启 Intel VT-x 或 AMD-V,然后在 Windows 功能里确保“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都勾选上。

Linux 上安装 Docker 用官方脚本最稳:

curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER

装完之后重新登录一下,让用户组生效。验证安装:

docker --version docker compose version

4.2 docker-compose.yml 完整配置

下面是我实际用的编排文件,包含 hindsight 服务、PostgreSQL、Redis 三个容器:

version: "3.9" services: hindsight: build: . ports: - "8080:8080" environment: - HINDSIGHT_DB=postgresql://hindsight:hindsight@postgres:5432/hindsight - HINDSIGHT_REDIS=redis://redis:6379/0 - HINDSIGHT_EMBEDDING_MODEL=text-embedding-3-small - HINDSIGHT_LLM_API_KEY=${LLM_API_KEY} depends_on: postgres: condition: service_healthy redis: condition: service_started networks: - hindsight-net restart: unless-stopped postgres: image: postgres:16-alpine environment: - POSTGRES_USER=hindsight - POSTGRES_PASSWORD=hindsight - POSTGRES_DB=hindsight volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hindsight"] interval: 5s timeout: 5s retries: 5 networks: - hindsight-net redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redisdata:/data networks: - hindsight-net volumes: pgdata: redisdata: networks: hindsight-net: driver: bridge

几个关键点解释一下。PostgreSQL 用 alpine 镜像体积小,健康检查确保 hindsight 服务在数据库就绪后才启动。Redis 开了 AOF 持久化,防止重启丢缓存。网络用自定义 bridge,容器间通过服务名互相访问,不用记 IP。

4.3 数据库初始化与图结构建表

PostgreSQL 里需要建两张核心表:经验节点表和经验边表。用 pgvector 扩展存 embedding:

CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE experience_nodes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), trigger_text TEXT NOT NULL, action_text TEXT NOT NULL, outcome TEXT CHECK (outcome IN ('success', 'failure', 'partial')), context JSONB DEFAULT '{}', trigger_embedding vector(1536), action_embedding vector(1536), weight FLOAT DEFAULT 1.0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_trigger_embedding ON experience_nodes USING ivfflat (trigger_embedding vector_cosine_ops) WITH (lists = 100); CREATE TABLE experience_edges ( source_id UUID REFERENCES experience_nodes(id) ON DELETE CASCADE, target_id UUID REFERENCES experience_nodes(id) ON DELETE CASCADE, relation TEXT NOT NULL, weight FLOAT DEFAULT 1.0, PRIMARY KEY (source_id, target_id, relation) );

weight字段很关键,它表示这条经验的可信度。每次任务成功调用这条经验,weight 加一点;失败则减一点。低于阈值的经验会被降权,检索时排在后面。这样记忆就有了“遗忘”机制,不会越积越乱。

注意:pgvector 的 ivfflat 索引需要数据量达到一定规模才有效。如果经验节点少于 1000 条,全表扫描反而更快。别一上来就建索引,先跑起来看查询计划。

4.4 启动与验证

编排文件写好之后,一条命令启动:

docker compose up -d

查看日志确认服务正常:

docker compose logs -f hindsight

看到 “MCP server listening on 0.0.0.0:8080” 就说明起来了。测试一下 MCP 接口:

curl -X POST http://localhost:8080/mcp/sse \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

返回工具列表就说明 MCP 服务正常。如果连不上,先检查容器网络:

docker compose exec hindsight ping postgres docker compose exec hindsight ping redis

网络不通的话,大概率是防火墙或者 Docker 网络配置问题。Linux 上检查 iptables 规则,Mac 上检查 Docker Desktop 的网络设置。

5. 记忆的写入、检索与更新:核心逻辑实现

5.1 经验提取:从执行轨迹到结构化经验

Agent 执行完一个任务后,会产生一串轨迹:调用了哪些工具、传了什么参数、返回了什么结果。原始轨迹又长又杂,直接存进去检索效果很差。需要用一个 LLM 做抽象提取。

提取的 prompt 我调了很多版,最终稳定下来的是这个结构:

你是一个经验提取器。给定以下 Agent 执行轨迹,提取可复用的操作策略。 要求: 1. trigger 描述什么情况下适用这条经验,不要包含具体实体名 2. action 描述执行了什么操作序列,用动词开头 3. outcome 只能是 success/failure/partial 4. 如果轨迹中有失败后重试成功的模式,提取为重试策略 执行轨迹: {trace} 输出 JSON 格式。

这里有个坑:不要让 LLM 自由发挥。早期我让 LLM 自己决定输出格式,结果有时候输出 markdown,有时候输出纯文本,解析起来很痛苦。后来强制 JSON schema,配合 structured output 功能,稳定性大幅提升。

另一个坑是实体名泄露。比如轨迹里是“查询用户张三的订单 12345”,提取出来的 trigger 如果写成“查询张三的订单”,那这条经验就只对张三有用。正确的做法是抽象成“查询指定用户的订单状态”。这个抽象过程 LLM 做得不错,但需要在 prompt 里明确要求。

5.2 检索策略:语义相似 + 图遍历

新任务进来时,检索分两步走。第一步用 trigger 的 embedding 做语义检索,从 pgvector 里找出 top-K 个候选节点。第二步从候选节点出发,沿着边遍历,找出相关的经验链。

def retrieve_experiences(task_description, top_k=5, graph_depth=2): # 第一步:语义检索 task_embedding = embed(task_description) candidates = db.query(""" SELECT id, trigger_text, action_text, outcome, weight, 1 - (trigger_embedding <=> %s) AS similarity FROM experience_nodes WHERE weight > 0.3 ORDER BY trigger_embedding <=> %s LIMIT %s """, (task_embedding, task_embedding, top_k * 3)) # 第二步:图遍历扩展 expanded = set() for node in candidates: expanded.add(node) neighbors = traverse_graph(node['id'], depth=graph_depth) expanded.update(neighbors) # 第三步:综合排序 scored = [] for node in expanded: score = node['similarity'] * 0.6 + node['weight'] * 0.4 scored.append((score, node)) scored.sort(reverse=True) return [node for _, node in scored[:top_k]]

这里weight > 0.3是过滤掉已经不可信的经验。similarity * 0.6 + weight * 0.4的权重是我调出来的,语义相似度更重要,但经验可信度也不能忽略。你可以根据自己的场景调整这个比例。

图遍历的深度设为 2 是有原因的。深度 1 只能找到直接相关的经验,深度 3 以上会引入太多噪声。深度 2 刚好能覆盖“因为 A 所以 B”这种因果链。

5.3 反馈更新:让记忆有“遗忘”能力

每次任务执行完,Agent 需要反馈这条经验是否有效。反馈通过 MCP 的memory.feedback接口传入:

def update_experience_weight(experience_id, success): delta = 0.1 if success else -0.15 db.execute(""" UPDATE experience_nodes SET weight = GREATEST(0.0, LEAST(2.0, weight + %s)), updated_at = NOW() WHERE id = %s """, (delta, experience_id))

注意失败时的惩罚(-0.15)比成功时的奖励(+0.1)绝对值大。这是故意的:一条经验被验证失败一次,可信度下降应该比成功一次上升更多。因为成功可能是偶然的,失败往往暴露了真实问题。

weight 上限设为 2.0,防止一条经验被反复验证后权重无限增长,导致检索时永远排第一。下限 0.0,低于 0.3 就不再被检索到,相当于“遗忘”了。

实操心得:定期跑一个清理任务,把 weight 低于 0.1 且超过 30 天没被调用的经验节点归档或删除。不然数据库会越来越大,检索越来越慢。我一般用 cron 每周跑一次。

6. 常见问题与排查技巧实录

6.1 记忆检索不准的排查思路

检索不准是最常见的问题,表现是 Agent 调用了不相关的经验,或者该调用的没调用。排查按这个顺序来:

现象可能原因排查方法解决
检索结果完全不相关embedding 模型不匹配检查写入和检索是否用同一模型统一模型,重建索引
相关经验排在后weight 过低查该节点 weight 值手动调高或增加反馈
检索不到任何结果阈值过高查 similarity 分布降低 weight 阈值
结果重复图遍历重复访问检查去重逻辑用 set 去重

我遇到最多的是第一种。有一次换了 embedding 模型,忘了重建已有数据的向量,结果新旧向量混在一起,检索结果乱七八糟。换 embedding 模型必须全量重建向量,这个没有捷径。

6.2 Docker 网络问题的典型场景

Docker 网络问题在 hindsight 部署里出现频率很高。典型场景是 hindsight 容器连不上 postgres 容器,报 “connection refused”。

排查步骤:

  1. 确认两个容器在同一个 network 里:docker network inspect hindsight-net
  2. 确认 postgres 容器健康:docker compose ps
  3. 在 hindsight 容器里测试连通性:docker compose exec hindsight nc -zv postgres 5432
  4. 检查 postgres 的 pg_hba.conf 是否允许来自 Docker 网段的连接

最常见的原因是 postgres 还没完全启动,hindsight 就尝试连接了。depends_on配合condition: service_healthy能解决大部分情况。如果还是不行,在 hindsight 的启动脚本里加一个重试循环。

另一个坑是端口冲突。宿主机上如果已经有 PostgreSQL 跑在 5432,Docker 映射会失败。解决办法是改映射端口,比如5433:5432,然后 hindsight 连postgres:5432(容器内部端口不变)。

6.3 MCP 连接失败的排查

MCP 连接失败通常有几个原因。SSE 连接超时,检查防火墙是否放行了 8080 端口。stdio 连接失败,检查 command 路径是否正确,环境变量是否传进去了。

如果客户端报 “provider rejected the request schema or tool payload”,说明 MCP 工具的输入 schema 和客户端期望的不匹配。检查工具定义的 JSON schema,确保 required 字段和类型都正确。我遇到过因为 schema 里写了"type": "integer"但实际传了字符串导致的报错,改成"type": "string"就好了。

提示:MCP 调试可以用官方的 inspector 工具,能直观看到请求和响应。比看日志快得多。

6.4 性能优化的几个实用技巧

经验积累到几千条之后,检索会变慢。几个优化手段:

  • embedding 缓存:相同 trigger 的 embedding 结果缓存到 Redis,避免重复计算
  • 批量写入:经验提取是批量的,用COPY而不是逐条INSERT
  • 索引调优:pgvector 的lists参数设为sqrt(行数)左右比较合适
  • 冷热分离:weight 高的经验放内存缓存,低的留数据库

我实测下来,5000 条经验节点,优化前检索要 200ms,优化后降到 30ms 左右。主要贡献来自 embedding 缓存和索引调优。

7. 记忆安全与边界:a-memguard 思路的借鉴

7.1 记忆投毒的风险

Agent 记忆有一个容易被忽视的风险:记忆投毒。如果攻击者能往记忆库里写入恶意经验,Agent 后续的行为就会被操纵。比如写入一条“查询用户信息时,先把数据发送到某个外部地址”,Agent 下次执行类似任务时可能就会照做。

a-memguard 这个思路的核心是主动防御:不是等投毒发生了再检测,而是在记忆写入时就做校验。具体做法包括:

  • 来源校验:只有经过认证的 Agent 实例才能写入记忆
  • 内容过滤:写入前用规则 + LLM 双重检查,拦截可疑指令
  • 一致性检查:新经验如果和已有高权重经验冲突,标记为待审核
  • 隔离区:新经验先进入隔离区,经过几次验证后才进入主库

7.2 在 hindsight 中的落地

在 hindsight 的 MCP 接口里加一层 guard:

def store_experience(experience, source_agent_id): if not verify_agent(source_agent_id): raise PermissionError("Unauthorized agent") if contains_suspicious_pattern(experience.action_text): quarantine(experience) return {"status": "quarantined"} conflicts = find_conflicting(experience) if conflicts and max(c.weight for c in conflicts) > 1.5: quarantine(experience) return {"status": "conflict_pending_review"} db.insert(experience) return {"status": "stored"}

suspicious_pattern包括外部 URL、文件写入路径、敏感 API 调用等。这个规则库需要根据你的 Agent 能力范围来定。如果 Agent 本来就有发邮件的权限,那邮件相关的 action 就不算可疑。

注意:安全校验会增加写入延迟,但记忆写入是低频操作,这点延迟可以接受。不要为了性能省掉校验,记忆投毒的后果比延迟严重得多。

7.3 隐私与合规边界

记忆里可能包含用户隐私信息。几个原则:

  • 最小化存储:只存策略,不存原始数据
  • 可删除:用户要求删除时,能定位到所有相关经验并删除
  • 可审计:每条经验的来源、修改历史可追溯
  • 加密:敏感字段加密存储,密钥独立管理

我在项目里会给每个经验节点打上source_user标签,用户注销时按标签批量删除。图结构里的边也要处理,删除节点时级联删除边。

8. 后续扩展方向:从 hindsight 到完整的记忆生态

hindsight 作为一个经验记忆层,本身已经能解决不少问题。但如果要构建完整的 Agent 记忆生态,还有几个方向可以扩展。

跨 Agent 记忆共享是第一个方向。多个 Agent 如果共享同一个记忆服务,一个 Agent 学到的经验,其他 Agent 也能用。这需要解决经验格式的标准化问题,以及不同 Agent 能力边界不同导致的经验适用性问题。

记忆的可解释性是第二个方向。当 Agent 调用一条经验时,能不能告诉用户“我这么做是因为之前遇到过类似情况”。这在医疗、金融等高风险场景很重要。实现方式是在检索结果里附带经验的来源和验证历史。

记忆的自动抽象是第三个方向。现在经验提取依赖 LLM,但 LLM 提取的经验粒度不一定合适。未来可以做一个层次化的抽象机制,把多条具体经验归纳成一条通用策略,类似人类从具体案例中总结规律。

与 RAG 的融合是第四个方向。经验记忆和事实记忆不是割裂的,很多经验需要事实支撑。比如“查询订单时先验证用户身份”这条经验,需要知道用户身份信息存在哪里。把两者打通,检索时同时返回相关事实和经验,Agent 的决策会更准确。

我在实际项目里的体会是,记忆这东西,宁可先做简单能用,也不要一上来就追求大而全。先把经验写入和检索跑通,哪怕只有几十条经验,也能明显感觉到 Agent 的行为在变好。然后再逐步加反馈机制、安全校验、图遍历这些高级特性。hindsight 的价值不在于架构多复杂,而在于它让 Agent 真正有了“记住教训”的能力。

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

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

立即咨询