ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent记忆系统实战:从架构设计到Docker部署的完整指南

Agent记忆系统实战:从架构设计到Docker部署的完整指南 1. 为什么“记忆”才是Agent落地的真正分水岭做Agent开发的人都有一个共同的体感模型能力本身早就不是瓶颈了。你拿一个中等规模的LLM配上好的提示词和工具链完成单轮任务的效果已经相当能打。但一旦把任务拉长到十几轮、跨天、跨会话整个系统就开始“失忆”——上一轮确认过的参数下一轮它当没发生过用户三天前说过的偏好它完全不知道。这不是模型不行是记忆架构没搭对。“hindsight”这个词本身就点破了问题的本质后见之明。人在做决策时最有价值的往往不是当下的信息而是过去发生过什么、当时怎么处理的、结果如何。Agent要真正像人一样工作就必须具备这种“回头看”的能力。而当前绝大多数Agent项目记忆层要么是空的要么就是简单粗暴地把所有对话历史塞进上下文窗口token烧得飞快效果还一塌糊涂。这篇文章围绕“hindsight”这个项目标题展开聊的是Agent Memory智能体记忆从设计到落地的完整思路。涉及的核心技术点包括LLM的上下文管理、MCP协议在记忆读写中的角色、Docker化的部署方案、以及working memory与长期记忆的分层策略。适合正在做Agent应用开发、想让自己的Agent“记住事”的工程师也适合对LLM应用架构感兴趣、想了解记忆系统怎么设计的技术人。不管你是刚接触Agent的新手还是已经踩过几轮坑的老手下面这些内容应该都能让你少走一些弯路。2. 记忆系统的整体架构设计思路2.1 为什么不能只靠上下文窗口硬撑很多人一开始做Agent记忆思路特别朴素把所有历史对话拼成一个超长prompt一股脑丢给模型。短会话还行一旦超过十几轮问题就全暴露了。首先是token成本每次请求都要把全部历史重新编码一遍费用是线性增长的。其次是注意力稀释上下文越长模型对关键信息的召回率反而越低中间部分的内容基本等于没写。最后是延迟长上下文的推理时间肉眼可见地变慢。所以记忆系统的第一原则是上下文窗口是工作台不是仓库。工作台上只放当前任务需要的东西仓库里存的才是全量历史。Agent每轮对话开始时从仓库里检索出相关记忆放到工作台上用完就清掉。这个“仓库”就是我们需要设计的记忆层。2.2 三层记忆模型working memory、episodic memory、semantic memory参考认知科学的分类我在实际项目中把Agent记忆分成三层Working Memory工作记忆当前会话的即时上下文存在内存里生命周期就是一次会话。它决定了Agent“现在在想什么”。Episodic Memory情景记忆具体发生过的事件记录比如“用户在3月5日让我查了某只股票的收盘价”。它决定了Agent“记得做过什么”。Semantic Memory语义记忆从多次交互中提炼出的抽象知识比如“这个用户偏好简洁的回答风格”。它决定了Agent“知道什么”。这三层的读写频率、存储介质、检索方式完全不同。Working memory用内存或Redis就行读写极快episodic memory需要持久化通常用关系型数据库或文档数据库semantic memory则需要向量化存储支持语义检索。2.3 MCP协议在记忆系统中的角色定位MCPModel Context Protocol本质上是一套标准化的工具调用协议它让LLM能够以统一的方式访问外部资源。在记忆系统里MCP的价值在于把“记忆的读写”抽象成标准工具Agent不需要关心底层用的是Redis还是Postgres只需要调用memory_store和memory_recall两个工具就行。这样做的好处是解耦。记忆后端的选型可以随时替换Agent侧的代码不用动。而且MCP的工具有明确的schema定义LLM在调用时能准确理解参数含义减少了“模型乱传参数”的问题。我在实际项目里试过用MCP封装记忆读写之后工具调用的成功率比裸写function call高了大概两成。2.4 Docker化部署让记忆服务独立可扩展记忆服务不应该和Agent主进程耦合在一起。原因很简单Agent可能频繁重启、扩缩容但记忆数据必须持久可靠。用Docker把记忆服务单独跑起来好处有三个一是数据隔离容器挂了数据还在volume里二是独立扩展记忆检索压力大的时候可以单独加副本三是环境一致开发、测试、生产用同一个镜像不会出现“我本地能跑”的尴尬。下面这张表对比了三种常见部署方式的优劣部署方式数据持久性扩展性运维复杂度适用场景进程内嵌差差低原型验证独立服务Docker好好中生产环境云托管服务好好低快速上线对于大多数团队我建议从独立服务Docker开始既有足够的可靠性又不会引入太多运维负担。3. 核心细节解析与实操要点3.1 Working Memory的窗口管理策略Working memory的核心问题是当前上下文里到底放什么。我的做法是维护一个固定大小的滑动窗口但窗口里的内容不是简单的“最近N轮对话”而是按优先级动态调整的。具体来说每条消息有一个优先级分数由三个因素决定时间衰减越新的消息分越高、任务相关性和当前query语义相似度高的分高、信息密度包含关键实体、参数的消息分高。每轮对话开始前按分数从高到低填充窗口直到达到token预算上限。这个策略听起来复杂实现起来其实不麻烦。时间衰减用一个指数函数就行相关性用embedding算余弦相似度信息密度可以用简单的规则比如是否包含数字、专有名词。我实测下来相比朴素的滑动窗口关键信息的召回率提升了大概35%。注意token预算不要卡得太死留出20%的余量给模型输出。否则模型还没说完就被截断了体验很差。3.2 Episodic Memory的存储结构设计情景记忆的存储关键是结构化的字段设计。如果只是把对话原文存进去检索效率会非常低。我通常会把每条记忆拆成这几个字段timestamp事件发生时间用于时间范围过滤session_id会话标识用于按会话聚合event_type事件类型比如“查询”“确认”“修改”entities涉及的实体列表比如股票代码、人名summary一句话摘要用于快速预览raw_content原始内容用于需要细节时回查embedding向量表示用于语义检索这样设计之后检索可以走多条路径按时间查、按实体查、按语义查灵活得多。存储介质我一般选PostgreSQL因为它的JSONB字段能很好地支持半结构化数据而且pgvector扩展可以直接做向量检索不用额外引入向量数据库。3.3 Semantic Memory的提炼与更新机制语义记忆不是手动写的而是从情景记忆里自动提炼出来的。我的做法是每隔一段时间比如每10轮对话或者每天定时跑一个提炼任务把这段时间的情景记忆拿出来让LLM总结出用户的偏好、习惯、常用参数等抽象知识然后更新到语义记忆库。这里有个关键问题新提炼的知识和旧知识冲突怎么办。比如用户之前偏好简洁回答后来又说“你多解释一点”这时候不能简单覆盖而是要记录知识的时效性。我的方案是给每条语义记忆加一个confidence分数和last_confirmed时间戳新知识进来时如果和旧的冲突就降低旧知识的confidence而不是直接删除。这样Agent在检索时能拿到带权重的知识行为更自然。3.4 MCP工具的参数设计细节用MCP封装记忆读写工具的参数设计直接决定了LLM能不能用对。我踩过的坑是参数名太抽象模型理解不了。比如memory_recall工具如果参数叫query和top_k模型经常不知道该传什么query。后来我改成what_am_i_looking_for和how_many_results调用准确率明显提升。这背后的逻辑是LLM对参数的理解依赖于参数名的语义。参数名越接近自然语言模型越容易填对。当然也不能太长控制在3-5个单词比较合适。另外每个参数都要有清晰的description说明什么情况下该传什么值。{ name: memory_recall, description: 从长期记忆中检索相关信息。当需要回忆用户之前的偏好、历史事件或已确认的参数时调用。, parameters: { what_am_i_looking_for: { type: string, description: 描述你要找什么信息用自然语言比如用户偏好的回答风格 }, how_many_results: { type: integer, description: 返回多少条结果默认5条最多20条 }, time_range: { type: string, description: 时间范围过滤格式如last_7_days不传则不限 } } }3.5 Docker Compose编排记忆服务栈记忆服务通常不是单个容器而是好几个组件配合记忆API服务、PostgreSQL带pgvector、Redis做working memory缓存。用Docker Compose编排最方便一个文件搞定所有依赖。version: 3.8 services: memory-api: build: ./memory-api ports: - 8080:8080 environment: - DATABASE_URLpostgresql://user:passpostgres:5432/memory - REDIS_URLredis://redis:6379 depends_on: - postgres - redis volumes: - ./logs:/app/logs postgres: image: pgvector/pgvector:pg16 environment: - POSTGRES_USERuser - POSTGRES_PASSWORDpass - POSTGRES_DBmemory volumes: - pgdata:/var/lib/postgresql/data ports: - 5432:5432 redis: image: redis:7-alpine volumes: - redisdata:/data ports: - 6379:6379 volumes: pgdata: redisdata:这个编排文件里pgvector/pgvector:pg16镜像自带向量检索能力省去了单独部署向量数据库的麻烦。Redis用来存working memory设置合理的过期时间就行。提示生产环境记得给PostgreSQL和Redis加上密码并且不要把端口暴露到公网。我见过太多因为数据库裸奔被扫的事故了。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。假设你用的是Ubuntu 22.04Docker和Docker Compose的安装步骤如下# 安装Docker curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh # 安装Docker Compose插件 sudo apt-get install docker-compose-plugin # 验证安装 docker --version docker compose versionWindows用户直接装Docker Desktop就行注意安装时勾选WSL2后端。如果启动时报“Virtualization support not detected”去BIOS里把虚拟化打开这个坑我踩过好几次不是软件问题。Python侧需要装几个关键库pip install fastapi uvicorn psycopg2-binary redis openai tiktoken pgvectortiktoken用来算token数做窗口管理的时候必须用。pgvector的Python客户端用来做向量检索。4.2 记忆API服务的核心代码实现记忆API服务用FastAPI写暴露几个核心接口/store存记忆、/recall检索记忆、/working_memory管理工作记忆。先看存储接口的实现from fastapi import FastAPI from pydantic import BaseModel import psycopg2 import redis import json from datetime import datetime app FastAPI() # 数据库连接 conn psycopg2.connect(postgresql://user:passlocalhost:5432/memory) redis_client redis.Redis(hostlocalhost, port6379, decode_responsesTrue) class MemoryItem(BaseModel): session_id: str event_type: str entities: list[str] summary: str raw_content: str embedding: list[float] app.post(/store) async def store_memory(item: MemoryItem): cur conn.cursor() cur.execute( INSERT INTO episodic_memory (timestamp, session_id, event_type, entities, summary, raw_content, embedding) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id , ( datetime.now(), item.session_id, item.event_type, json.dumps(item.entities), item.summary, item.raw_content, item.embedding )) memory_id cur.fetchone()[0] conn.commit() return {id: memory_id, status: stored}检索接口要支持多种过滤条件app.post(/recall) async def recall_memory( query_embedding: list[float], session_id: str None, time_range: str None, top_k: int 5 ): cur conn.cursor() # 构建查询条件 conditions [] params [] if session_id: conditions.append(session_id %s) params.append(session_id) if time_range last_7_days: conditions.append(timestamp NOW() - INTERVAL 7 days) where_clause AND .join(conditions) if conditions else 11 # 向量相似度检索 cur.execute(f SELECT id, summary, raw_content, 1 - (embedding %s::vector) as similarity FROM episodic_memory WHERE {where_clause} ORDER BY embedding %s::vector LIMIT %s , [query_embedding] params [query_embedding, top_k]) results cur.fetchall() return {memories: [ {id: r[0], summary: r[1], content: r[2], similarity: r[3]} for r in results ]}这里的是pgvector的余弦距离操作符1 - distance就是相似度。注意embedding的维度要和建表时一致我一般用1536维对应常见的embedding模型输出。4.3 Working Memory的Redis实现Working memory用Redis的List结构存每个session一个keydef push_working_memory(session_id: str, message: dict, max_tokens: int 4000): key fwm:{session_id} # 获取当前窗口内容 current redis_client.lrange(key, 0, -1) current_tokens sum(count_tokens(m) for m in current) # 如果超预算从最旧的开始删 while current_tokens count_tokens(message) max_tokens and current: removed current.pop(0) current_tokens - count_tokens(removed) redis_client.lpop(key) # 推入新消息 redis_client.rpush(key, json.dumps(message)) redis_client.expire(key, 3600) # 1小时过期 def count_tokens(message: dict) - int: import tiktoken enc tiktoken.get_encoding(cl100k_base) return len(enc.encode(json.dumps(message)))这个实现里有个细节过期时间设为1小时。因为working memory是会话级的会话结束就没用了。设太长浪费内存设太短又可能导致会话中断后上下文丢失。1小时是个比较平衡的值你可以根据实际会话时长调整。4.4 记忆提炼任务的定时调度语义记忆的提炼任务用APScheduler跑定时任务from apscheduler.schedulers.background import BackgroundScheduler from openai import OpenAI scheduler BackgroundScheduler() client OpenAI() def extract_semantic_memory(): # 取出最近的情景记忆 cur conn.cursor() cur.execute( SELECT summary, raw_content FROM episodic_memory WHERE timestamp NOW() - INTERVAL 1 day ORDER BY timestamp DESC LIMIT 50 ) memories cur.fetchall() if not memories: return # 让LLM提炼 prompt f从以下交互记录中提炼用户的偏好和习惯。 只输出JSON格式包含preferences和habits两个数组。 记录 {chr(10).join([m[0] for m in memories])} response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], response_format{type: json_object} ) result json.loads(response.choices[0].message.content) # 更新语义记忆 for pref in result.get(preferences, []): upsert_semantic_memory(pref, preference) scheduler.add_job(extract_semantic_memory, interval, hours6) scheduler.start()提炼频率我设的是6小时一次。太频繁浪费API调用太稀疏又会导致语义记忆更新不及时。这个值可以根据你的业务节奏调整高频交互的场景可以缩短到1小时。4.5 与Agent主流程的集成最后一步是把记忆系统接进Agent主流程。核心逻辑是每轮对话前检索记忆对话后存储记忆。async def agent_chat(session_id: str, user_input: str): # 1. 获取working memory wm get_working_memory(session_id) # 2. 检索相关长期记忆 query_embedding get_embedding(user_input) recalled await recall_memory(query_embedding, session_idsession_id) # 3. 组装上下文 context build_context(wm, recalled, user_input) # 4. 调用LLM response await call_llm(context) # 5. 存储本轮交互 await store_memory(MemoryItem( session_idsession_id, event_typedialogue, entitiesextract_entities(user_input), summarysummarize(user_input, response), raw_contentfUser: {user_input}\nAssistant: {response}, embeddingget_embedding(user_input response) )) # 6. 更新working memory push_working_memory(session_id, {role: user, content: user_input}) push_working_memory(session_id, {role: assistant, content: response}) return response这个流程里第2步的检索和第5步的存储是异步的不阻塞主流程。实测下来整个记忆读写增加的延迟在100ms以内对用户体验基本没影响。5. 常见问题与排查技巧实录5.1 记忆检索召回率低怎么办这是最常见的问题。Agent明明存了记忆但需要的时候就是检索不出来。排查思路按优先级来第一检查embedding质量。如果embedding模型本身对中文支持不好检索效果肯定差。建议用专门优化过中文的embedding模型或者至少用多语言版本。我试过用纯英文模型处理中文内容召回率直接腰斩。第二检查分块策略。如果一条记忆太长embedding会稀释关键信息。建议单条记忆的summary控制在100字以内raw_content可以长但检索时用summary的embedding。第三调整相似度阈值。默认返回top_k条但可能有些条目的相似度很低混进来反而干扰。加一个阈值过滤比如相似度低于0.7的直接丢弃。第四混合检索。纯向量检索对精确匹配不敏感比如用户搜“股票代码600519”向量检索可能返回一堆股票相关但不含这个代码的记忆。这时候要加上关键词检索用BM25做混合排序。5.2 Docker容器网络不通的排查记忆服务和Agent服务分在不同容器里网络不通是高频问题。排查步骤确认在同一网络。Docker Compose默认会创建一个网络所有服务都在里面。如果你手动docker run记得加--network参数。用服务名而不是localhost。容器内访问另一个容器要用Compose里定义的服务名比如postgres:5432不能用localhost:5432。检查端口映射。容器间通信不需要端口映射但如果你从宿主机访问需要-p参数。看日志。docker compose logs memory-api能看到具体的连接错误信息。我遇到过一次诡异的问题容器网络正常但Python连PostgreSQL就是超时。最后发现是PostgreSQL的pg_hba.conf没配置允许容器网段的连接。默认配置只允许localhost需要加一行host all all 172.16.0.0/12 md5。5.3 记忆数据膨胀的处理跑一段时间后episodic memory表会变得非常大检索变慢。处理方案定期归档超过90天的记忆移到归档表主表只保留近期数据。摘要压缩对同一session的连续多条记忆合并成一条摘要。索引优化确保embedding字段有IVFFlat或HNSW索引时间字段有B-tree索引。-- 创建向量索引 CREATE INDEX ON episodic_memory USING ivfflat (embedding vector_cosine_ops) WITH (lists 100); -- 创建时间索引 CREATE INDEX idx_timestamp ON episodic_memory (timestamp DESC);IVFFlat的lists参数建议设为数据量的平方根。比如有10000条记忆lists设100左右比较合适。5.4 LLM调用记忆工具时的参数错误模型调用MCP工具时传错参数这个问题的根源通常是工具描述不够清晰。排查和优化方法问题现象可能原因解决方案参数名传错参数名太抽象改成自然语言风格参数类型错误schema定义不明确加type和example该调用时不调用description没写清触发条件补充何时使用说明不该调用时乱调用工具太多模型混淆精简工具数量合并相似功能我个人的经验是工具数量控制在7个以内超过之后模型的调用准确率会明显下降。如果功能确实多就做分层先让模型选大类再选具体工具。5.5 常见问题速查表问题排查方向快速修复记忆存不进去数据库连接、表结构检查连接串和migration检索结果不相关embedding质量、分块策略换模型、缩短summary容器启动失败端口冲突、依赖未就绪加healthcheck和depends_on响应变慢记忆检索阻塞主流程改异步、加缓存token超限working memory窗口太大调小max_tokens语义记忆不更新定时任务没跑检查scheduler日志6. 几个我踩过的坑和实操心得第一个坑是过度设计。一开始我想把记忆系统做得特别完善又是图数据库又是多级缓存结果开发了两周还没跑通。后来砍掉一半功能先用PostgreSQLRedis跑起来反而一周就上线了。记忆系统这东西先跑通再优化不要一上来就追求完美架构。第二个坑是忽略token计算。working memory的窗口管理如果不精确算token很容易出现“以为没超实际超了”的情况。不同模型的tokenizer不一样中文的token数大概是字符数的1.5倍左右英文是0.75倍。一定要用对应模型的tokenizer来算不能拍脑袋估。第三个心得是记忆的写入要带元数据。一开始我只存内容后来发现检索时没法按来源过滤。加上source字段区分是用户说的还是Agent自己生成的、confidence字段之后检索的精准度提升了很多。第四个心得是定期review记忆质量。我每周会抽一批记忆出来人工看看检查摘要是否准确、实体提取是否完整。这个习惯帮我发现了好几个系统性问题比如某个类型的实体总是提取失败后来加了专门的规则才解决。记忆系统不是一锤子买卖它需要持续调优。但只要你把基础架构搭对了后面的优化就是渐进式的不会推倒重来。hindsight这个词提醒我们Agent的价值不仅在于当下能做什么更在于它能从过去学到什么。把记忆层做扎实你的Agent才能真正从“工具”变成“助手”。
返回列表