从零搭建 LLM Agent 记忆系统:MCP 协议与 Docker 化部署实战
1. 从“hindsight”说起为什么我们需要给 Agent 装一个“后视镜”“hindsight”这个词本身挺有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在 LLM Agent 的语境里它指向一个非常具体且要命的问题Agent 的记忆到底该怎么存、怎么取、怎么用。我接触过不少做 Agent 的团队大家一开始都特别乐观觉得只要把对话历史一股脑塞进上下文窗口就完事了。结果跑不了几轮就发现token 烧得飞快模型还经常“失忆”——明明上一轮刚说过的事情下一轮就忘得一干二净。更麻烦的是当你想让 Agent 跨会话记住用户偏好、记住之前踩过的坑单纯靠 context window 根本撑不住。这就是“hindsight”这个项目标题背后真正要解决的核心矛盾Agent 需要一种能够跨越时间、跨越会话、跨越任务边界的记忆机制而且这种机制不能只是简单的“存下来”还得能在需要的时候精准地“想起来”。热搜词里出现的 agent memory、working memory、MCP、Docker 这些关键词其实已经把技术栈的轮廓勾勒出来了——这是一个围绕 LLM Agent 记忆系统展开的工程实践涉及记忆的存储结构、检索策略、协议对接和部署方式。我打算把这篇文章写成一份“从零到一搭建 Agent 记忆系统”的实操记录。不管你是刚接触 LLM 应用开发的新手还是已经在做 Agent 产品但被记忆问题折磨过的老手下面这些内容应该都能让你少走一些弯路。我会把记忆系统的设计思路、核心数据结构、MCP 协议的对接方式、Docker 化部署的完整流程以及我在实际调试中踩过的坑全部摊开来聊。2. Agent 记忆系统的整体设计与核心思路拆解2.1 为什么“把历史对话全塞进去”是最蠢的做法先算一笔账。假设你的 Agent 每轮对话平均产生 500 个 token 的文本用户和 Agent 一来一回就是 1000 token。如果用户连续聊了 50 轮那就是 50000 token 的上下文。现在主流模型的上下文窗口虽然标称 128K 甚至 200K但你真把 50K 的历史全塞进去先不说成本问题模型对中间部分的注意力衰减是非常明显的——这就是著名的“lost in the middle”现象。更关键的是这 50 轮对话里真正有价值的信息可能只占 10%。用户说“我明天要去北京出差”这是有价值的事实用户说“嗯嗯好的”这是噪音。你把噪音和事实一起塞进去模型提取有效信息的难度反而增加了。所以记忆系统的第一个设计原则就是存储要全但注入要精。原始对话可以完整落盘但每次调用模型时只把最相关的记忆片段检索出来注入上下文。这个“检索”的过程就是 hindsight 的核心价值所在。2.2 记忆的分层结构working memory、episodic memory、semantic memory我在设计记忆系统时习惯把它分成三层这个分层方式借鉴了认知科学里的记忆模型但在工程上非常好用第一层是 working memory工作记忆。这就是当前对话轮次的上下文直接放在 prompt 里容量有限通常控制在 2K 到 4K token。它的作用是保证当前对话的连贯性让 Agent 知道“刚才用户说了什么我正在回答什么”。第二层是 episodic memory情景记忆。这是跨会话的对话摘要和关键事件记录。比如用户昨天问过“Docker 怎么安装 MySQL”今天又问“MySQL 的端口怎么改”Agent 应该能想起来“这个用户昨天在搞 Docker 环境”。情景记忆通常以时间线的方式组织每条记录包含时间戳、摘要、涉及实体和原始对话的引用。第三层是 semantic memory语义记忆。这是从大量对话中抽取出来的结构化知识比如用户的偏好、常用工具、项目背景等。语义记忆不依赖于具体的时间点而是以“事实”的形式存在。比如“用户偏好使用 Python 而不是 JavaScript”、“用户的项目部署在 Ubuntu 22.04 上”。这三层记忆的读写策略完全不同。工作记忆是每轮都重写的情景记忆是每轮追加的语义记忆是定期归纳更新的。热搜词里提到的“agent 存储 working memory”和“llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”其实就是在说记忆的键值设计——key 是身份标识query 是检索意图value 是实际内容。2.3 为什么选 MCP 作为记忆系统的对接协议MCPModel Context Protocol是最近一年在 Agent 圈子里讨论度非常高的一个协议。它的核心思路是把“模型能调用的能力”标准化成一个个 server每个 server 暴露一组工具tools和资源resources模型通过统一的协议去调用。我选择用 MCP 来对接记忆系统主要基于三个考虑第一解耦。记忆系统的实现可以独立于 Agent 框架。今天你用 LangChain 做 Agent明天换成自己写的调度逻辑记忆系统不需要改只要 MCP 接口不变就行。第二可组合。一个 Agent 可能同时需要记忆、需要浏览器操作比如 Playwright MCP、需要代码执行这些能力都通过 MCP 挂载互不干扰。第三可观测。MCP 的调用是有明确日志的每次记忆的读写都能追踪到调试的时候非常方便。热搜词里出现的“playwright mcp”、“chrome devtools mcp”、“browser use mcp 跟 playwright mcp 有什么区别”这些说明 MCP 生态正在快速扩张。记忆系统作为其中一个 server和这些工具 server 是平级关系Agent 根据需要调用即可。2.4 Docker 化部署为什么不用裸机跑记忆系统涉及向量数据库、关系数据库、缓存服务如果直接在裸机上装环境依赖能把你逼疯。Docker 的价值在于把整个技术栈打包成可复现的镜像换一台机器docker compose up就能跑起来。热搜词里“docker 安装”、“docker desktop 安装教程”、“windows 安装 docker”、“virtualization support not detected docker desktop failed to start”这些说明很多人在 Docker 环境准备阶段就卡住了。我会在后面的实操部分详细讲 Windows 和 Ubuntu 两种环境下的安装要点。3. 核心细节解析记忆的存储、检索与更新机制3.1 记忆的存储结构不只是向量数据库很多人一提到 Agent 记忆第一反应就是“上向量数据库”。向量数据库确实重要但它不是全部。我的记忆系统用了三种存储存储类型用途选型建议数据量级关系数据库存储原始对话、会话元数据、记忆索引PostgreSQL / MySQL百万级记录向量数据库存储记忆的语义向量支持相似度检索Qdrant / Milvus / Chroma十万级向量键值缓存存储工作记忆和热点记忆Redis千级键值对关系数据库是“真相来源”所有记忆的原始文本都存在这里。向量数据库是“检索加速器”它存的是记忆的 embedding用于快速找到语义相关的记忆。Redis 是“热数据层”当前会话的工作记忆和最近访问过的记忆放在这里避免每次都查数据库。热搜词里“docker 安装 redis 主从”、“docker 安装 mysql8.0 并使用”这些正好对应了这套存储架构的部署需求。Redis 做主从是为了保证缓存层的高可用MySQL 8.0 则是关系数据库的常见选择。3.2 记忆的写入什么时候该记什么时候不该记不是所有对话都值得写入长期记忆。我的策略是必记的内容用户明确表达的偏好“我喜欢用 VS Code”、用户提供的项目背景“我在做一个电商后台”、用户纠正过的错误“不对我说的是 PostgreSQL 不是 MySQL”、任务的关键结论“最终方案选的是方案 B”。不记的内容寒暄和客套“你好”、“谢谢”、重复确认“好的”、“明白了”、临时性的中间状态“让我想想”。选择性记录的内容技术讨论中的知识点、用户提到的工具和框架、用户的问题模式。实现上我会在每轮对话结束后用一个轻量级的 LLM 调用做一次“记忆抽取”判断这轮对话是否包含值得长期记忆的信息。这个判断的 prompt 大概长这样MEMORY_EXTRACTION_PROMPT 分析以下对话轮次判断是否包含值得长期记忆的信息。 对话内容 用户{user_message} 助手{assistant_message} 如果包含以下类型的信息请提取并返回 JSON 1. 用户偏好preference 2. 项目背景context 3. 关键事实fact 4. 错误纠正correction 如果不包含值得记忆的信息返回 {{should_remember: false}}。 返回格式 {{ should_remember: true, memory_type: preference|context|fact|correction, content: 提取的记忆内容, entities: [涉及的实体], importance: 1-10 }} 这个抽取步骤会增加一次 LLM 调用但它的成本远低于把全部历史塞进上下文的成本。而且抽取出来的记忆是结构化的后续检索和更新都更方便。3.3 记忆的检索key、query、value 的三元组设计热搜词里有一句很精辟的总结“llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”。这其实就是记忆检索的核心逻辑。Key我是谁这是记忆的归属标识。在多用户或多 Agent 场景下每条记忆都必须绑定到一个明确的身份上。可以是用户 ID、会话 ID、项目 ID或者它们的组合。没有 key 的记忆系统检索时会把别人的记忆也捞出来那就乱套了。Query我在找什么这是检索的意图表达。用户当前的问题会被转换成一个检索 query通常是一个 embedding 向量也可能包含结构化的过滤条件比如“只要最近 7 天的记忆”、“只要 preference 类型的记忆”。Value我能提供什么这是检索返回的记忆内容。它不应该是原始对话的全文而应该是经过摘要和结构化的记忆片段。每条 value 还应该附带元数据时间戳、重要性评分、来源会话 ID、相关实体等。检索的流程我一般这样设计把用户当前输入转换成 embedding在向量数据库中做相似度搜索取 top-K通常 K10 到 20用结构化条件做二次过滤时间范围、记忆类型、重要性阈值对候选记忆做重排序可以用一个小的 cross-encoder 模型也可以直接用 LLM 打分取最终 top-N通常 N3 到 5注入上下文这里有个经验相似度分数不是唯一标准。一条记忆可能和当前 query 的语义相似度不高但它是用户的核心偏好那就应该被检索出来。所以我在重排序阶段会加入重要性权重和时间衰减因子。3.4 记忆的更新与遗忘不是所有记忆都值得永远保留记忆系统如果只增不减迟早会变成一个垃圾场。我的做法是合并如果新记忆和已有记忆表达的是同一个事实就合并它们更新置信度和时间戳。比如用户先说“我用 Python”后来说“我主要用 Python 做数据分析”这两条应该合并成一条更完整的偏好记忆。衰减每条记忆有一个“新鲜度”分数随着时间推移逐渐降低。检索时新鲜度低的记忆会被降权。但核心偏好类记忆的衰减速度要慢得多甚至可以设置为不衰减。淘汰当记忆总量超过阈值时淘汰那些重要性低、访问频率低、时间久远的记忆。淘汰不是删除而是归档到冷存储万一以后需要还能找回来。热搜词里“a-memguard: a proactive defense framework for llm-based agent memory”这个方向其实就是在解决记忆系统的安全问题——防止恶意注入的记忆污染 Agent 的行为。虽然我的项目没有直接实现这个框架但在设计记忆写入接口时我加了一层校验所有写入的记忆都要经过一个“一致性检查”如果新记忆和已有高置信度记忆矛盾就标记为待审核不直接生效。4. 实操过程从零搭建一个可运行的 Agent 记忆系统4.1 环境准备Docker 安装与常见坑先说 Docker 的安装。Windows 用户和 Ubuntu 用户的路径不太一样我分别说一下。Windows 环境Windows 上装 Docker Desktop 是最省事的方案。去官网下载安装包双击运行一路下一步。但有几个坑要注意第一个坑是虚拟化支持。热搜词里“virtualization support not detected docker desktop failed to start”就是这个问题。Docker Desktop 需要 WSL2 或者 Hyper-V 支持。你需要在 BIOS 里开启虚拟化Intel VT-x 或 AMD-V然后在 Windows 功能里启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。第二个坑是 WSL2 的内存占用。默认情况下 WSL2 会占用大量内存你可以在用户目录下创建一个.wslconfig文件来限制[wsl2] memory8GB processors4 swap2GB第三个坑是镜像拉取速度。国内网络环境下建议配置镜像加速器。在 Docker Desktop 的设置里找到 Docker Engine添加 registry-mirrors 配置。Ubuntu 环境Ubuntu 上装 Docker 用官方脚本最方便# 卸载旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release # 添加 Docker 官方 GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加仓库 echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装 Docker Engine sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin # 把当前用户加入 docker 组避免每次都要 sudo sudo usermod -aG docker $USER newgrp docker装完之后用docker run hello-world验证一下。如果拉取镜像很慢同样需要配置镜像加速。4.2 用 Docker Compose 编排记忆系统的全套服务我的记忆系统需要四个服务PostgreSQL、Redis、Qdrant向量数据库、以及记忆系统本身的应用服务。用 Docker Compose 编排是最干净的方案。version: 3.8 services: postgres: image: postgres:16-alpine container_name: memory-postgres environment: POSTGRES_USER: memory_user POSTGRES_PASSWORD: memory_pass_2024 POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U memory_user -d agent_memory] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: memory-redis ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru qdrant: image: qdrant/qdrant:latest container_name: memory-qdrant ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage memory-service: build: ./memory-service container_name: memory-service ports: - 8080:8080 environment: DATABASE_URL: postgresql://memory_user:memory_pass_2024postgres:5432/agent_memory REDIS_URL: redis://redis:6379/0 QDRANT_URL: http://qdrant:6333 EMBEDDING_MODEL: text-embedding-3-small depends_on: postgres: condition: service_healthy redis: condition: service_started qdrant: condition: service_started volumes: postgres_data: redis_data: qdrant_data:这个 compose 文件有几个设计点值得说明PostgreSQL 加了 healthcheck确保应用服务在数据库真正就绪后才启动。Redis 配置了allkeys-lru淘汰策略和 512MB 内存上限防止缓存把内存吃满。Qdrant 暴露了 6333HTTP和 6334gRPC两个端口。应用服务通过depends_on控制启动顺序。启动命令很简单docker compose up -d查看日志docker compose logs -f memory-service4.3 记忆系统的核心代码实现记忆系统的应用服务我用 Python 写基于 FastAPI 暴露 HTTP 接口同时通过 MCP 协议对外提供工具调用能力。先看数据库表结构-- 原始对话表 CREATE TABLE conversations ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_conversations_session ON conversations(session_id); CREATE INDEX idx_conversations_user ON conversations(user_id); -- 记忆表 CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, entities JSONB DEFAULT [], importance INTEGER DEFAULT 5, confidence FLOAT DEFAULT 1.0, source_session_id VARCHAR(64), access_count INTEGER DEFAULT 0, last_accessed_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_memories_user_type ON memories(user_id, memory_type); CREATE INDEX idx_memories_importance ON memories(importance DESC);记忆的写入逻辑import json from datetime import datetime from openai import OpenAI client OpenAI() def extract_memory(user_message: str, assistant_message: str) - dict: 从对话轮次中抽取值得记忆的信息 response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: MEMORY_EXTRACTION_PROMPT}, {role: user, content: f用户{user_message}\n助手{assistant_message}} ], response_format{type: json_object}, temperature0.1 ) return json.loads(response.choices[0].message.content) def write_memory(user_id: str, session_id: str, memory_data: dict): 将抽取的记忆写入数据库和向量库 if not memory_data.get(should_remember): return None # 写入 PostgreSQL with get_db_connection() as conn: with conn.cursor() as cur: cur.execute( INSERT INTO memories (user_id, memory_type, content, entities, importance, source_session_id) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id , ( user_id, memory_data[memory_type], memory_data[content], json.dumps(memory_data.get(entities, [])), memory_data.get(importance, 5), session_id )) memory_id cur.fetchone()[0] conn.commit() # 生成 embedding 并写入 Qdrant embedding get_embedding(memory_data[content]) qdrant_client.upsert( collection_nameagent_memories, points[{ id: memory_id, vector: embedding, payload: { user_id: user_id, memory_type: memory_data[memory_type], content: memory_data[content], importance: memory_data.get(importance, 5), created_at: datetime.now().isoformat() } }] ) return memory_id检索逻辑def retrieve_memories(user_id: str, query: str, top_k: int 5) - list: 检索与 query 相关的记忆 query_embedding get_embedding(query) # 向量检索 search_results qdrant_client.search( collection_nameagent_memories, query_vectorquery_embedding, query_filter{ must: [ {key: user_id, match: {value: user_id}} ] }, limittop_k * 3 # 多取一些用于重排序 ) # 重排序结合相似度、重要性、时间衰减 now datetime.now() scored_memories [] for result in search_results: payload result.payload similarity result.score importance payload.get(importance, 5) / 10.0 created_at datetime.fromisoformat(payload[created_at]) days_old (now - created_at).days time_decay 1.0 / (1.0 days_old * 0.05) final_score similarity * 0.6 importance * 0.25 time_decay * 0.15 scored_memories.append((final_score, payload)) scored_memories.sort(keylambda x: x[0], reverseTrue) return [m[1] for m in scored_memories[:top_k]]4.4 通过 MCP 协议暴露记忆能力MCP 的核心是让模型能够以标准化的方式调用外部工具。我用 Python 的mcp库来实现记忆服务的 MCP serverfrom mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(memory-service) app.list_tools() async def list_tools() - list[Tool]: return [ Tool( nameremember, description将重要信息写入长期记忆。当用户表达了偏好、提供了项目背景、或纠正了之前的错误时调用。, inputSchema{ type: object, properties: { content: {type: string, description: 要记忆的内容}, memory_type: { type: string, enum: [preference, context, fact, correction], description: 记忆类型 }, importance: { type: integer, minimum: 1, maximum: 10, description: 重要性评分 } }, required: [content, memory_type] } ), Tool( namerecall, description检索与当前对话相关的长期记忆。在回答用户问题前调用以获取用户的背景信息和偏好。, inputSchema{ type: object, properties: { query: {type: string, description: 检索意图}, top_k: {type: integer, default: 5} }, required: [query] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict) - list[TextContent]: if name remember: memory_id write_memory( user_idcurrent_user_id, session_idcurrent_session_id, memory_data{ should_remember: True, memory_type: arguments[memory_type], content: arguments[content], importance: arguments.get(importance, 5) } ) return [TextContent(typetext, textf已记忆ID: {memory_id})] elif name recall: memories retrieve_memories( user_idcurrent_user_id, queryarguments[query], top_karguments.get(top_k, 5) ) if not memories: return [TextContent(typetext, text没有找到相关记忆。)] formatted \n.join([ f[{m[memory_type]}] {m[content]} for m in memories ]) return [TextContent(typetext, textf找到以下相关记忆\n{formatted})] async def main(): async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这个 MCP server 暴露了两个工具remember和recall。Agent 在对话过程中可以主动调用recall来获取相关记忆也可以在发现重要信息时调用remember来写入记忆。4.5 与 Agent 框架的集成如果你用的是支持 MCP 的 Agent 框架比如 Claude Desktop、Trae IDE 等只需要在配置文件里加上这个 MCP server 的启动命令即可。以 Claude Desktop 为例配置文件大概长这样{ mcpServers: { memory-service: { command: docker, args: [exec, -i, memory-service, python, -m, mcp_server], env: {} } } }如果你是自己写 Agent 调度逻辑那就需要在每轮对话前调用recall在对话后调用remember。这个调度逻辑可以封装成一个装饰器def with_memory(func): async def wrapper(user_message: str, user_id: str, session_id: str): # 检索相关记忆 memories retrieve_memories(user_id, user_message, top_k5) memory_context \n.join([m[content] for m in memories]) # 注入到 prompt enhanced_message f相关背景记忆 {memory_context} 用户当前输入{user_message} # 调用原始处理函数 response await func(enhanced_message) # 抽取并写入新记忆 memory_data extract_memory(user_message, response) write_memory(user_id, session_id, memory_data) return response return wrapper5. 常见问题与排查技巧实录5.1 Docker 网络不通怎么办这是最高频的问题之一。热搜词里“docker 网络不通”排在前列说明很多人被坑过。常见原因和排查步骤症状一容器之间无法互相访问。先检查它们是否在同一个 Docker network 里。默认情况下docker compose创建的服务都在同一个 network 下可以用服务名互相访问。如果你手动docker run的容器需要显式指定--network。症状二容器内无法访问外网。检查宿主机的 DNS 配置。可以在docker-compose.yml里给服务加上 DNS 配置services: memory-service: dns: - 8.8.8.8 - 114.114.114.114症状三端口映射不生效。检查宿主机端口是否被占用。netstat -tlnp | grep 8080看一下。另外注意Docker Desktop for Windows 有时候会有端口转发的延迟重启 Docker Desktop 可以解决。排查网络问题的万能命令是docker exec -it container sh进入容器然后用ping、curl、nslookup逐个排查。5.2 记忆检索不准确怎么调检索不准确通常有三个原因Embedding 模型不合适。如果你用的是通用的 embedding 模型它在技术领域的语义区分度可能不够。可以考虑换一个在技术文本上表现更好的模型或者用领域数据做微调。分块策略有问题。一条记忆如果太长embedding 会丢失细节如果太短又缺乏上下文。我的经验是每条记忆控制在 50 到 200 字之间超过 200 字的拆成多条。重排序权重不合理。相似度、重要性、时间衰减这三个因子的权重需要根据你的场景调。如果用户偏好类记忆经常被漏掉就提高重要性的权重如果最近的信息更重要就提高时间衰减的权重。我一般会用一个小的评测集来调这些参数准备 20 到 30 个 query每个 query 标注哪些记忆应该被检索出来然后看不同参数组合下的召回率和准确率。5.3 记忆冲突怎么处理当新记忆和旧记忆矛盾时比如用户先说“我用 MySQL”后来说“我改用 PostgreSQL 了”系统需要能识别这种冲突并更新。我的做法是在写入前做一次冲突检测def check_conflict(user_id: str, new_memory: dict) - dict: 检查新记忆是否与已有记忆冲突 similar retrieve_memories(user_id, new_memory[content], top_k3) for old in similar: if old[memory_type] ! new_memory[memory_type]: continue # 用 LLM 判断是否冲突 judgment client.chat.completions.create( modelgpt-4o-mini, messages[{ role: user, content: f判断以下两条记忆是否冲突 旧记忆{old[content]} 新记忆{new_memory[content]} 返回 JSON{{conflict: true/false, resolution: keep_old|keep_new|merge, merged_content: 如果合并合并后的内容}} }], response_format{type: json_object} ) result json.loads(judgment.choices[0].message.content) if result[conflict]: return {has_conflict: True, old_memory: old, resolution: result} return {has_conflict: False}如果检测到冲突根据 resolution 决定是保留旧记忆、用新记忆替换、还是合并两者。对于“改用 PostgreSQL”这种情况正确的处理是用新记忆替换旧记忆同时把旧记忆标记为“已过时”。5.4 常见问题速查表问题现象可能原因排查方法解决方案Docker Desktop 启动失败虚拟化未开启检查 BIOS 虚拟化设置开启 VT-x/AMD-V启用 WSL2容器间网络不通不在同一 networkdocker network ls使用 docker compose 统一编排记忆检索返回空向量库未索引检查 Qdrant collection确认写入时 embedding 已生成记忆重复写入缺少去重逻辑查看 memories 表写入前做相似度检查检索速度慢向量数量过大监控 Qdrant 查询延迟加索引、限制 top_k、启用缓存上下文超长注入记忆过多统计 prompt token 数限制注入记忆条数和长度Redis 内存溢出未设淘汰策略redis-cli info memory配置 maxmemory 和 LRU 策略PostgreSQL 连接超时连接池耗尽查看 pg_stat_activity增大连接池、优化慢查询5.5 几个我踩过的坑坑一embedding 维度不一致。我一开始用 OpenAI 的 text-embedding-ada-0021536 维后来换成 text-embedding-3-small也是 1536 维以为无缝切换结果发现两个模型的向量空间不兼容检索质量暴跌。换 embedding 模型时必须重新生成所有向量。坑二时间戳时区问题。PostgreSQL 存的是 UTC 时间但我在 Python 里用datetime.now()生成的是本地时间导致时间衰减计算错误。统一用datetime.utcnow()或者带时区的datetime.now(timezone.utc)。坑三MCP server 的 stdio 阻塞。MCP 的 stdio 传输模式下如果 server 里有阻塞操作比如同步的数据库查询会导致整个 MCP 连接卡死。所有数据库操作都要用异步驱动或者放到线程池里执行。坑四Docker volume 权限问题。PostgreSQL 容器默认以 postgres 用户运行如果你挂载的宿主机目录权限不对容器会启动失败。解决办法是在宿主机上把目录 owner 改成 UID 999PostgreSQL 容器内的用户 ID或者用 named volume 而不是 bind mount。6. 记忆系统的扩展方向与个人体会这套记忆系统跑通之后我陆续加了一些扩展。一个是记忆的可视化面板用简单的 Web 页面展示某个用户的所有记忆支持按类型、时间、重要性筛选调试的时候非常直观。另一个是记忆的导入导出支持把某个用户的记忆打包成 JSON迁移到另一个环境。还有一个方向是多 Agent 共享记忆。当多个 Agent 协作完成一个任务时它们需要一个共享的记忆空间来同步状态。我的做法是在记忆表里加一个scope字段区分“私有记忆”和“共享记忆”检索时根据 Agent 的角色决定能访问哪些 scope。热搜词里提到的“rag graphrag llm wiki 本体 rag”这些其实指向了记忆系统的另一个进化方向从扁平的向量检索走向结构化的知识图谱。向量检索擅长找“语义相似”的内容但不擅长做多跳推理。比如用户问“我上周说的那个项目用的什么数据库”向量检索可能找不到因为这需要先定位到“上周说的项目”再查这个项目的数据库选型。知识图谱可以解决这类问题但构建和维护成本高得多。我的建议是先用向量检索跑起来等确实遇到多跳推理的需求时再考虑引入图谱结构。最后分享一个我在调试记忆系统时的小技巧给每条记忆加一个“来源追溯”字段记录它是从哪轮对话、哪个 session 抽取出来的。当检索结果不对劲时你可以顺着这个字段回溯到原始对话看看是抽取环节出了问题还是检索环节出了问题。这个字段在排查问题时能省你很多时间。这套系统目前在我自己的几个 Agent 项目里跑得挺稳单用户千级记忆量下检索延迟在 50ms 以内注入上下文的记忆控制在 500 token 以内对整体响应速度的影响可以忽略不计。如果你也在做 Agent 记忆相关的开发希望这些经验能帮你少踩几个坑。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →