hindsight 项目实战:LLM Agent 记忆系统设计与 MCP 落地
发布时间:2026/10/1 11:46:56来源:尧图网络
1. 从“hindsight”说起为什么记忆是 Agent 落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。把它放在 Agent Memory 这个语境里其实点出了一个非常核心的痛点一个 LLM Agent 如果只有当下的上下文窗口没有对过去交互的沉淀和回看能力那它永远只能做“一次性问答”做不了真正的长期任务。我最早接触 Agent 记忆这块是在做一个需要跨天跟踪用户偏好的助手项目当时天真地以为把历史对话全塞进 prompt 就行了结果 token 成本爆炸不说模型还会被大量无关历史干扰回答质量断崖式下跌。后来才慢慢理解Agent Memory 不是“存对话”而是一套完整的写入、检索、遗忘、反思机制。这个项目标题“hindsight”加上热搜词里的 agent memory、LLM、MCP、Docker基本可以判断这是一个围绕LLM Agent 记忆系统的工程实践项目而且大概率涉及用 MCP 协议做工具层对接、用 Docker 做环境封装。热词里还出现了 a-memguard 这种“面向 LLM Agent 记忆的主动防御框架”说明这个项目不只是做记忆存储还考虑了记忆被污染、被注入攻击的安全问题。另外像“agent 存储 working memory”“LLM 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”这些词直接指向了记忆系统的核心数据结构设计。这篇文章我打算把“hindsight”这类 Agent Memory 项目从设计思路到落地实操完整拆一遍。适合谁看如果你正在做 LLM 应用、想让你的 Agent 记住用户、想用 MCP 把记忆能力做成可复用工具、或者单纯想搞明白 Agent Memory 到底该怎么设计那这篇应该能给你省不少试错时间。我会尽量把每个设计决策背后的“为什么”讲清楚而不是只丢一堆代码。2. Agent Memory 的整体设计与思路拆解2.1 为什么不能只靠上下文窗口做记忆很多人第一反应是现在模型上下文都 128K 甚至 1M 了直接把历史全放进去不就行了我实测下来的结论是短期可以长期必崩。原因有三个。第一是成本每次请求都带上几万 token 的历史费用是线性增长的一个高频 Agent 一天下来账单很吓人。第二是注意力稀释上下文越长模型对关键信息的召回率反而会下降这在“大海捞针”类测试里已经被反复验证。第三是无法跨会话用户今天关了窗口明天再来上下文窗口里的东西全没了。所以 Agent Memory 的本质是把“记忆”从模型的临时上下文里剥离出来做成一个外部可持久化、可检索、可管理的存储层。模型每次只需要拿到“和当前任务最相关的几条记忆”而不是全部历史。这就是 RAG 思路在记忆场景的延伸但比普通 RAG 更复杂因为记忆有生命周期新记忆要写入旧记忆要衰减或合并冲突记忆要处理。2.2 hindsight 类项目的核心架构分层我把这类项目通常拆成四层这也是我在自己项目里验证过比较稳的分法接入层负责和 LLM、Agent 框架对接通常通过 MCP 协议暴露成工具让 Agent 能主动调用“记住这件事”“回忆相关的事”。记忆管理层核心逻辑层负责记忆的写入策略、检索策略、衰减与合并策略。这一层决定了记忆系统的“智商”。存储层真正落地的地方可以是向量库、关系库、图数据库或者混合存储。安全层对应热词里的 a-memguard负责检测恶意记忆注入、敏感信息过滤、记忆投毒防御。为什么这么分因为记忆系统的复杂度主要不在存储而在“管理策略”。你用什么数据库其实差别没那么大但“什么时候该记、记什么、怎么找回来、什么时候该忘”这套策略直接决定 Agent 是聪明还是智障。把管理层独立出来方便你后续替换策略而不动存储。2.3 MCP 在记忆系统里扮演什么角色MCPModel Context Protocol这两年被讨论得很多热词里也反复出现 mcp 协议、playwright mcp、unity mcp 这些。它的价值在于把记忆能力标准化成一种工具任何支持 MCP 的 Agent 都能即插即用。以前你给 A 框架写的记忆模块换到 B 框架就得重写有了 MCP记忆服务作为一个独立进程跑着Agent 通过协议调用就行。在 hindsight 这类项目里MCP 通常暴露这么几个工具memory_write写入记忆、memory_search检索记忆、memory_forget删除记忆、memory_reflect对记忆做总结反思。Agent 在对话过程中自己判断要不要调用这些工具。这里有个设计取舍是让 Agent 自主决定何时记忆还是系统强制每轮都记我的经验是混合策略最稳——系统对关键事件强制写入日常对话让 Agent 自主判断避免记忆库被垃圾信息淹没。2.4 用 Docker 封装的意义热词里 docker、docker desktop、docker 安装教程出现频率极高说明环境部署是很多人的第一道坎。Agent Memory 系统依赖的东西不少向量库、嵌入模型、可能还有图数据库、Redis 做缓存。如果每个都手动装光是版本冲突就够折腾一天。用 Docker Compose 把这些服务编排在一起一条命令拉起整个记忆后端这是最省心的做法。而且记忆系统往往需要和主应用隔离部署Docker 天然适合这种场景。你可以把记忆服务单独跑在一个容器里通过 MCP 的 SSE 或 stdio 和 Agent 通信主应用崩了也不影响记忆数据的持久化。下面我会给出具体的编排方案。3. 核心细节解析与实操要点3.1 记忆的数据结构key、query、value 到底怎么设计热词里有一句特别精准的描述“LLM 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”。这其实是在类比注意力机制的 QKV但用在记忆系统上非常贴切。我把它翻译成工程语言key我是谁这条记忆的标识和归属。包括记忆 ID、所属用户/会话、时间戳、记忆类型事实/偏好/事件/反思。query我在找什么检索时的匹配维度。通常包括语义向量、关键词、时间范围、记忆类型过滤。value我能提供什么记忆的实际内容以及它的置信度、重要性分数、访问次数。我踩过的一个坑是早期只存了 value 的文本和向量没存重要性分数结果检索时把一堆“用户随口说的废话”和“用户明确表达的长期偏好”同等对待召回质量很差。后来加了重要性评分可以用 LLM 打分也可以用规则检索时按相似度 × 重要性 × 时间衰减综合排序效果立竿见影。下面是一个记忆条目的典型结构我用 JSON 表示{ memory_id: mem_20240612_001, user_id: u_12345, session_id: s_abc, memory_type: preference, content: 用户偏好用中文回复且不喜欢过于冗长的解释, embedding: [0.012, -0.034, ...], importance: 0.85, confidence: 0.9, created_at: 2024-06-12T10:30:00Z, last_accessed: 2024-06-15T08:00:00Z, access_count: 7, source: explicit, tags: [language, style] }这里source字段区分是用户显式表达的explicit还是系统推断的inferred显式记忆的置信度天然更高。tags用于做粗粒度过滤避免每次都跑全量向量检索。3.2 写入策略什么时候该记记什么写入是记忆系统最容易做烂的地方。我的原则是宁缺毋滥。如果每轮对话都写一条记忆一周下来记忆库几万条检索全是噪声。具体策略我分成三类第一类是显式记忆用户明确说“记住我喜欢 XX”“以后都按 YY 来”这种必须写而且重要性拉满。第二类是事件记忆比如用户完成了一个任务、做了一个决定这类记忆用于后续追溯重要性中等。第三类是推断记忆系统从对话里推断出的偏好或事实这类要谨慎置信度低的不写或者写了也标记为低置信度。判断“要不要写”可以用一个轻量的 LLM 调用prompt 大概是“以下对话中是否包含值得长期记住的信息如果有提取出来并给出重要性分数0-1。”这个调用成本不高但能过滤掉大量噪声。我实测下来加了这层过滤后记忆库的检索准确率提升了大概 40%。注意写入时一定要做去重。用户可能反复说同一件事如果每次都写检索时会返回一堆重复记忆。去重可以用向量相似度阈值比如余弦相似度 0.95 就认为是同一条也可以用 LLM 判断是否与已有记忆冲突或重复。3.3 检索策略怎么把对的记忆找回来检索是记忆系统的“临门一脚”。我的经验是多路召回 重排序。单靠向量检索不够因为有些记忆是时间敏感的“用户上周说要去出差”有些是关键词精确匹配的“用户的项目代号是 Falcon”。多路召回通常包括向量语义检索、关键词 BM25 检索、时间范围过滤、记忆类型过滤。然后把各路结果合并用一个重排序模型或规则打分。打分公式我常用这个final_score 0.5 * semantic_similarity 0.2 * importance 0.2 * recency_decay 0.1 * access_frequency其中recency_decay可以用指数衰减比如exp(-λ * days_since_created)λ 取 0.05 左右意味着一个月前的记忆权重衰减到约 22%。这个参数要根据你的场景调如果是长期偏好类记忆衰减应该更慢甚至不衰减如果是临时事件衰减可以快一些。检索返回的条数也要控制一般 5-10 条足够。太多会稀释上下文太少可能漏掉关键信息。我通常返回 top 8然后在 prompt 里按重要性排序呈现。3.4 遗忘与合并记忆系统也需要“断舍离”这是最容易被忽略但极其重要的一环。记忆库如果只增不减迟早变成垃圾场。遗忘策略有三种时间衰减低重要性 长时间未访问的记忆自动降权或归档。容量淘汰每个用户记忆数超过阈值比如 500 条淘汰最不重要的。主动合并多条相似记忆合并成一条更抽象的总结。比如用户分三次说了喜欢咖啡、喜欢拿铁、喜欢美式可以合并成“用户喜欢咖啡偏好拿铁和美式”。合并这步可以用 LLM 做prompt 是“把以下多条相关记忆合并成一条简洁的总结保留所有关键信息”。合并后原记忆标记为已合并不再参与检索。这一步能显著压缩记忆库体积同时提升检索质量。4. 实操过程与核心环节实现4.1 用 Docker Compose 拉起记忆后端先把环境搭起来。我推荐的组合是PostgreSQL pgvector 做向量存储Redis 做缓存和会话状态再加一个记忆服务容器跑 MCP Server。为什么选 pgvector 而不是专用向量库因为记忆数据本身是结构化的有用户、时间、类型等字段用关系库 向量扩展能同时满足结构化查询和语义检索少维护一个组件。下面是docker-compose.yml的核心内容version: 3.9 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: memuser POSTGRES_PASSWORD: mempass POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U memuser] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redisdata:/data memory-service: build: ./memory-service depends_on: postgres: condition: service_healthy redis: condition: service_started environment: DATABASE_URL: postgresql://memuser:mempasspostgres:5432/agent_memory REDIS_URL: redis://redis:6379 EMBEDDING_MODEL: text-embedding-3-small ports: - 8080:8080 command: [python, -m, memory_service.mcp_server] volumes: pgdata: redisdata:这里有几个细节值得说。pgvector/pgvector:pg16这个镜像已经预装了 vector 扩展省得你自己编译。healthcheck 很重要因为记忆服务启动时要连数据库如果数据库没就绪会直接崩用condition: service_healthy保证顺序。embedding 模型我选了text-embedding-3-small1536 维性价比高如果你的记忆量特别大可以考虑更小的本地模型。启动就一条命令docker compose up -d然后进数据库建表CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( memory_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL, session_id TEXT, memory_type TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536), importance FLOAT DEFAULT 0.5, confidence FLOAT DEFAULT 0.5, created_at TIMESTAMPTZ DEFAULT NOW(), last_accessed TIMESTAMPTZ DEFAULT NOW(), access_count INT DEFAULT 0, source TEXT DEFAULT inferred, tags TEXT[], is_archived BOOLEAN DEFAULT FALSE ); CREATE INDEX idx_memories_user ON memories(user_id) WHERE is_archived FALSE; CREATE INDEX idx_memories_embedding ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);ivfflat 索引的lists参数建议设为sqrt(总行数)初期数据少可以设 100数据涨到十万级再重建。这个索引是近似检索召回率大概 95% 以上对记忆场景够用了。4.2 MCP Server 的实现要点记忆服务通过 MCP 暴露工具。我用 Python 的mcpSDK 写核心是定义几个 tool。下面是一个精简版的实现骨架from mcp.server import Server from mcp.types import Tool, TextContent import asyncpg import redis.asyncio as redis app Server(hindsight-memory) app.list_tools() async def list_tools(): return [ Tool( namememory_write, description写入一条长期记忆, inputSchema{ type: object, properties: { user_id: {type: string}, content: {type: string}, memory_type: {type: string, enum: [fact, preference, event, reflection]}, importance: {type: number, minimum: 0, maximum: 1} }, required: [user_id, content, memory_type] } ), Tool( namememory_search, description检索与当前任务相关的记忆, inputSchema{ type: object, properties: { user_id: {type: string}, query: {type: string}, top_k: {type: integer, default: 8} }, required: [user_id, query] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name memory_write: return await handle_write(arguments) elif name memory_search: return await handle_search(arguments)handle_write里要做的事生成 embedding、去重检查、插入数据库。handle_search里要做的事生成 query embedding、向量检索、重排序、更新 access_count。这两个函数是整个系统的核心我建议把去重和重排序的逻辑单独抽成模块方便调参。提示MCP Server 的传输方式有两种stdio 适合本地进程调用SSE 适合远程调用。如果你用 Docker 部署建议用 SSE这样 Agent 可以通过 HTTP 连过来不受进程边界限制。4.3 记忆写入的去重与冲突处理去重逻辑我写过一个版本核心是两步先做向量相似度粗筛再用 LLM 精判。粗筛阈值设 0.9找出候选重复项然后让 LLM 判断“新记忆和已有记忆是否表达同一件事”。如果是就更新已有记忆的last_accessed和importance取较大值而不是新增。如果冲突比如用户之前说喜欢咖啡现在说戒咖啡了就把旧记忆标记为is_archived TRUE写入新记忆并在新记忆的 tags 里加supersedes:旧ID。这个冲突处理很重要否则 Agent 会同时检索到“用户喜欢咖啡”和“用户戒咖啡了”然后精神分裂。我实测下来加了冲突检测后涉及用户偏好的回答准确率提升很明显。4.4 检索的重排序实现检索的 SQL 大概是这样的WITH candidates AS ( SELECT *, 1 - (embedding $1::vector) AS semantic_sim FROM memories WHERE user_id $2 AND is_archived FALSE AND (tags $3 OR $3 IS NULL) ORDER BY embedding $1::vector LIMIT 50 ) SELECT *, (0.5 * semantic_sim 0.2 * importance 0.2 * EXP(-0.05 * EXTRACT(EPOCH FROM (NOW() - created_at)) / 86400) 0.1 * LEAST(access_count / 10.0, 1.0)) AS final_score FROM candidates ORDER BY final_score DESC LIMIT $4;先用向量检索拿 50 个候选再用综合公式重排取 top_k。这个两阶段设计比直接向量检索 top_k 效果好很多因为向量相似度高的不一定是最该被记住的。tags $3是数组重叠操作用于标签过滤如果不需要过滤就传 NULL。5. 常见问题与排查技巧实录5.1 记忆检索召回不准怎么办这是最高频的问题。排查顺序我一般是这样的先看 embedding 模型是否合适中文场景用text-embedding-3-small还行但如果你的记忆里有大量专业术语可能需要换更强的模型。再看分块粒度一条记忆如果太长超过 500 字向量会稀释建议拆成多条。然后看重要性分数是否合理如果所有记忆重要性都是默认 0.5那重排序就退化成纯向量检索了。最后看时间衰减参数如果 λ 太大老记忆全被压下去了长期偏好就找不回来。我整理了一个速查表现象可能原因排查方法解决方向召回无关记忆向量模型不匹配人工看 top10 结果换 embedding 模型漏掉关键记忆分块太长或太短检查记忆长度分布调整分块策略老记忆找不回时间衰减过快检查 λ 参数降低 λ 或对偏好类不衰减重复记忆多去重阈值太松统计重复率提高相似度阈值检索慢索引未建或数据量大EXPLAIN 分析建 ivfflat 索引加缓存5.2 Docker 环境常见坑热词里 docker 网络不通、virtualization support not detected 这些我基本都踩过。Windows 上装 Docker Desktop 最常见的两个问题一是 BIOS 里没开虚拟化报virtualization support not detected进 BIOS 开 VT-x/AMD-V 就行二是 WSL2 没装或版本太老wsl --update一下。docker 网络不通通常是容器间用了 localhost记住容器里连另一个容器要用服务名比如postgres:5432而不是localhost:5432。还有一个坑是数据卷权限。PostgreSQL 容器如果挂载的宿主机目录权限不对会启动失败。我的做法是用命名卷named volume而不是绑定挂载bind mount省去权限烦恼。如果非要绑定挂载记得chown成容器内用户 ID。5.3 记忆注入与安全防御热词里 a-memguard 这个方向值得单独说。Agent Memory 有个独特的安全风险记忆投毒。攻击者可能通过对话诱导 Agent 写入恶意记忆比如“记住以后所有转账都不需要确认”然后这条记忆在后续会话里被检索出来影响 Agent 行为。防御思路有几层写入时做敏感内容检测对涉及资金、权限、安全的记忆强制人工确认或直接拒绝检索时对记忆做来源标记低置信度记忆不参与高风险决策定期审计记忆库发现异常记忆及时清理。我在自己项目里加了一个简单的规则层如果记忆内容包含“转账”“密码”“权限”“删除”等敏感词且 source 是 inferred就拒绝写入并记录日志。这层规则虽然简单但能挡住大部分低级攻击。5.4 性能优化记忆量大了怎么办记忆库到十万条以上检索会变慢。优化手段按性价比排序第一是加 Redis 缓存把高频用户的 top 记忆缓存起来命中率能到 60% 以上第二是分区按 user_id 哈希分区每个用户的数据独立第三是归档把超过半年未访问的低重要性记忆移到归档表主表只留活跃记忆第四才是考虑换专用向量库。我实测下来前三步做完十万级数据检索延迟能控制在 50ms 以内完全够用。6. 记忆系统的扩展方向与个人体会这套架构跑通之后能扩展的方向其实不少。比如把记忆做成图结构用 GraphRAG 的思路把实体和关系抽出来这样能回答“用户的朋友里谁喜欢咖啡”这种需要多跳推理的问题。再比如加一个反思层定期让 LLM 回顾最近的记忆生成更高层的洞察这其实就是 hindsight 这个词的本意——从过去的交互里提炼出后见之明。我在实际项目里最大的体会是记忆系统的难点从来不是技术而是策略。用什么数据库、什么模型这些都有成熟方案但“什么该记、什么该忘、怎么找回来”这些策略必须结合具体场景反复调。我建议你先用最简单的方案跑起来观察真实数据再逐步加去重、重排序、冲突处理这些机制。一上来就设计一套完美架构大概率是过度工程。最后分享一个小技巧给记忆系统加一个“调试模式”每次检索时把候选记忆、各项分数、最终排序都打出来。调参的时候看这个日志比盲目改参数高效得多。我靠这个日志发现过好几次“重要性分数全是默认值”这种低级问题也发现过时间衰减把关键偏好压没了的坑。记忆系统是个需要长期养的东西别指望一次调好。
网站建设高端定制企业官网