☰
Agent记忆架构实战:基于MCP与Docker的三层记忆系统设计
2026/10/4 12:53:02 网站建设 项目流程

1. 从“hindsight”说起:为什么我们需要给 Agent 装上“后视镜”

“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在 AI Agent 和 LLM 的语境里,它指向一个非常具体且要命的问题:Agent 的记忆到底该怎么存、怎么取、怎么用?

我接触过不少做 Agent 落地的团队,大家一开始都特别乐观,觉得只要把 LLM 接上工具、挂上 MCP,Agent 就能自己干活了。结果一上生产环境就发现,Agent 像个失忆症患者——上一轮对话里用户明确说过的偏好,下一轮就忘得一干二净;昨天已经排查过的报错,今天遇到一模一样的问题还是从头再来一遍。这不是模型不够聪明,而是记忆架构没设计好。

“hindsight”这个项目标题,我理解它要解决的核心就是:让 Agent 具备“回头看”的能力。不是简单地存聊天记录,而是把历史交互、工具调用结果、环境状态这些信息,经过结构化处理之后,变成 Agent 可以高效检索和复用的“经验”。这背后涉及几个关键技术点:agent memory 的分层设计、LLM 的上下文管理、MCP 协议下的工具编排、以及 Docker 化部署带来的可复现性。

这篇文章适合谁看?如果你正在做 Agent 应用开发,或者你已经在用 Dify、Coze 这类平台搭工作流,但发现记忆模块总是差点意思,那这篇内容应该能给你一些可以直接抄作业的思路。如果你只是刚听说 MCP 和 agent memory 这些词,也没关系,我会从最基础的概念讲起,用生活化的类比把原理说清楚,再一步步拆解实操细节。

我自己的经验是,Agent 记忆这件事,设计阶段多花一天想清楚,后面能省一周的调试时间。下面我就把“hindsight”这个项目涉及的核心思路、技术选型、实操步骤和踩坑记录,完整地梳理一遍。

2. 核心思路拆解:Agent Memory 到底该怎么分层

2.1 为什么“把聊天记录全塞进上下文”是最蠢的做法

很多人第一次做 Agent 记忆,直觉反应就是:把历史对话全部拼接到 prompt 里不就行了?我试过,短对话还行,一旦超过二三十轮,token 消耗直接爆炸,而且模型对超长上下文的注意力会严重衰减。你塞进去 8000 token 的历史记录,模型真正“注意到”的可能只有最后 1000 token。

更麻烦的是,无差别地塞历史记录会引入大量噪声。用户三年前问过的一个无关问题,和当前任务毫无关系,但它占着上下文窗口,挤掉了真正有用的信息。这就像你找一个同事帮忙,他非要把过去三年所有工作邮件都打印出来带在身上,然后每次问你问题都要翻一遍——效率极低,而且关键信息很容易被淹没。

所以“hindsight”的核心设计思路,一定是分层存储 + 按需检索。不是把所有东西都放在一个篮子里,而是根据信息的时效性、重要性、使用频率,分到不同的存储层里,用的时候再精准取出来。

2.2 三层记忆架构:working memory、episodic memory、semantic memory

基于常见实践,我倾向于把 Agent 记忆分成三层,这个分法在认知科学里也有对应概念,落地到工程上非常自然。

第一层:Working Memory(工作记忆)。这是最活跃的一层,存放当前对话轮次、最近几轮交互、当前任务的状态变量。它的特点是容量小、读写快、生命周期短。你可以把它理解成 Agent 的“桌面”——正在处理的文件摊在桌面上,随手就能拿到。技术上通常用内存缓存或者 Redis 来实现,TTL 设置得比较短,比如 30 分钟到 2 小时。

第二层:Episodic Memory(情景记忆)。这一层存放的是“发生过什么”的记录,包括每次对话的摘要、工具调用的输入输出、任务执行的结果。它不是原始日志,而是经过压缩和结构化的版本。比如一次完整的工具调用,原始记录可能有几千 token,但情景记忆里只保留“什么时间、调了什么工具、关键参数是什么、结果成功还是失败、失败原因是什么”这几个字段。这一层通常用关系型数据库或者文档数据库来存,支持按时间范围、任务 ID、工具名称等维度检索。

第三层:Semantic Memory(语义记忆)。这是最高层,存放的是从大量交互中提炼出来的“知识”和“偏好”。比如“这个用户喜欢用简洁的回复风格”、“这个项目的代码规范要求用 4 空格缩进”、“这类报错通常是因为权限配置问题”。语义记忆不依赖于具体某次对话,而是跨会话、跨任务的通用知识。这一层通常用向量数据库来存,支持语义相似度检索。

提示:三层记忆不是互斥的,而是协同工作的。Agent 在处理一个任务时,会先从 working memory 拿当前状态,再从 episodic memory 检索相似历史案例,最后从 semantic memory 获取通用规则和偏好,三者融合之后才形成最终的上下文。

2.3 为什么选 MCP 作为工具编排协议

MCP(Model Context Protocol)最近热度很高,但很多人对它的理解还停留在“又一个协议”的层面。我一开始也疑惑,为什么不用现成的 REST API 或者函数调用?后来实际用下来发现,MCP 解决的是一个很具体的问题:让 LLM 以统一的方式发现和调用外部工具。

在没有 MCP 之前,每接一个工具,你都要写一套适配代码:定义函数签名、写参数校验、处理返回格式、做错误映射。工具一多,维护成本直线上升。MCP 的思路是把工具的描述和调用标准化,LLM 通过 MCP 协议就能知道“有哪些工具可用、每个工具需要什么参数、返回什么格式”,不需要为每个工具单独写适配层。

这和硬件协议的概念有点像——USB 接口标准化之后,你不需要为每个外设单独设计一个接口,插上就能用。MCP 就是 AI 工具调用领域的“USB 标准”。在“hindsight”这个项目里,MCP 主要用来做两件事:一是把记忆存储和检索能力暴露成标准工具,让 Agent 可以主动调用;二是把外部数据源(比如数据库、文件系统、API)接入进来,作为记忆的补充来源。

2.4 Docker 化部署:为什么这是必选项而不是可选项

Agent 记忆系统涉及多个组件:LLM 服务、向量数据库、关系型数据库、缓存、MCP 服务端。如果每个组件都手动安装配置,光是环境问题就能耗掉一整天。Docker Compose 的价值在于,把整个系统的依赖关系和环境配置固化下来,换一台机器只需要docker compose up就能跑起来。

我踩过的一个坑是:在本地开发环境跑得好好的向量检索,部署到服务器上就报错,排查半天发现是两边的向量数据库版本不一致,索引格式不兼容。用 Docker 之后,镜像版本锁定,这种问题基本不会再出现。而且 Docker 的网络配置让各个服务之间的通信变得很清晰,不需要去折腾宿主机的端口和防火墙规则。

3. 核心细节解析:记忆的写入、检索与更新机制

3.1 记忆写入:什么时候存、存什么、怎么存

记忆写入不是越多越好,写入策略直接决定了检索质量。我见过一些实现,把每一轮对话的原始文本全部存进去,结果检索的时候返回一堆无关内容。正确的做法是在写入前做一次摘要和结构化。

具体来说,一次完整的记忆写入流程包括这几个步骤:

  1. 触发判断:不是每轮对话都需要写入长期记忆。我的做法是设置几个触发条件——用户明确表达了偏好或纠正、工具调用产生了重要结果、任务状态发生了关键变化、对话轮次达到一定数量(比如每 5 轮做一次摘要写入)。

  2. 内容摘要:用 LLM 对当前对话片段做摘要,提取关键信息。这里的关键是 prompt 设计,要让模型输出结构化的 JSON,而不是自由文本。比如要求输出{ "event_type": "preference_update", "summary": "...", "entities": [...], "importance": 0.8 }这样的格式。

  3. 分层路由:根据摘要结果决定写入哪一层。偏好类信息写入 semantic memory,任务执行记录写入 episodic memory,当前状态更新写入 working memory。

  4. 向量化与索引:对于需要语义检索的内容,调用 embedding 模型生成向量,写入向量数据库。同时把结构化字段写入关系型数据库,方便做精确查询。

# 记忆写入的伪代码示例 def write_memory(dialogue_segment, context): # 第一步:判断是否触发写入 if not should_trigger_write(dialogue_segment, context): return # 第二步:LLM 摘要与结构化 summary = llm_summarize(dialogue_segment, output_format="json") # 第三步:分层路由 if summary["event_type"] == "preference": store_to_semantic_memory(summary) elif summary["event_type"] == "task_execution": store_to_episodic_memory(summary) # 第四步:向量化 embedding = embed_model.encode(summary["summary"]) vector_db.upsert(embedding, metadata=summary)

注意:摘要 prompt 里一定要明确要求模型保留“实体”信息,比如人名、项目名、工具名、参数值。这些实体是后续检索的关键锚点,丢了就很难精准召回。

3.2 记忆检索:怎么在正确的时间拿到正确的记忆

检索是记忆系统里最考验设计功力的环节。检索的目标不是“找到所有相关记忆”,而是“找到当前任务真正需要的那几条”。返回太多记忆,会挤占上下文窗口,还会引入噪声;返回太少,又可能漏掉关键信息。

我的做法是多路召回 + 重排序:

  • 第一路:向量相似度召回。用当前 query 的 embedding 去向量数据库里找最相似的 top-K 条记忆。K 一般设 10 到 20,不要太大。

  • 第二路:结构化条件召回。根据当前任务的元数据(比如任务 ID、用户 ID、工具名称),从关系型数据库里精确查询相关记录。

  • 第三路:时间衰减召回。最近发生的记忆权重更高,用时间衰减函数给每条记忆算一个分数,越新的记忆分数越高。

三路召回的结果合并之后,用一个轻量级的重排序模型(或者直接用 LLM 做相关性打分)做二次排序,最终选出 top-3 到 top-5 条注入上下文。

这里有个经验:重排序这一步非常关键,但很多人会忽略。向量相似度高不代表真的相关。我遇到过 query 是“怎么配置数据库连接”,向量检索返回了一条“数据库连接池满了怎么排查”的记忆,字面上很相似,但实际场景完全不同。重排序模型能根据更细粒度的语义匹配把真正相关的那条排到前面。

3.3 记忆更新与遗忘:不是所有记忆都值得永久保留

记忆系统如果只写不删,很快就会变成一个垃圾场。遗忘机制和写入机制同样重要。

我的策略是:

  • Working memory 自动过期:设置 TTL,比如 2 小时没有访问就自动清除。

  • Episodic memory 定期压缩:每周做一次批量处理,把相似的情景记忆合并成一条更抽象的记录。比如 10 次“调用天气 API 成功”的记录,压缩成一条“天气 API 调用成功率高,平均响应时间 200ms”。

  • Semantic memory 冲突检测:当新写入的偏好和已有偏好冲突时,不是简单覆盖,而是记录冲突并标记需要人工确认。比如用户之前说“喜欢详细回复”,现在说“太啰嗦了”,系统应该记录这个变化,而不是直接删掉旧偏好。

  • 重要性衰减:每条记忆有一个 importance 分数,随着时间推移和访问频率降低,分数会衰减。低于阈值的记忆会被归档到冷存储,不再参与常规检索,但保留可恢复性。

3.4 MCP 工具层的具体设计

在“hindsight”项目里,MCP 工具层主要暴露这几个能力:

工具名称功能输入参数输出
memory_write写入记忆content, memory_type, metadatamemory_id
memory_search检索记忆query, top_k, filterslist of memories
memory_update更新记忆memory_id, new_contentsuccess/fail
memory_forget删除或归档记忆memory_id, soft_deletesuccess/fail
memory_stats获取记忆统计user_id, time_rangestats object

这些工具通过 MCP 协议注册之后,Agent 可以在需要的时候主动调用。比如 Agent 在回答用户问题之前,可以先调memory_search看看有没有相关历史;在对话结束之后,调memory_write把关键信息存下来。

MCP 的另一个好处是工具发现是动态的。如果后续要加新的记忆存储后端(比如从 Redis 换成别的),只需要在 MCP 服务端做调整,Agent 侧不需要改代码。

4. 实操过程:从零搭建一套可运行的 Agent Memory 系统

4.1 环境准备与 Docker Compose 编排

先把基础环境搭起来。我假设你用的是 Windows 11 或者 macOS,Linux 更简单,步骤类似。

第一步:安装 Docker Desktop。Windows 用户去官网下载安装包,安装过程中如果提示 “Virtualization support not detected”,需要进 BIOS 开启虚拟化支持(Intel VT-x 或 AMD-V)。安装完成后,在设置里确认 WSL 2 后端已启用。

第二步:创建项目目录结构。我的习惯是每个服务一个目录,配置文件和代码分开:

hindsight/ ├── docker-compose.yml ├── .env ├── mcp-server/ │ ├── Dockerfile │ └── src/ ├── memory-service/ │ ├── Dockerfile │ └── src/ └── config/ └── init.sql

第三步:编写 docker-compose.yml。核心服务包括:PostgreSQL(存结构化记忆)、Redis(存 working memory)、Qdrant 或 Milvus(存向量)、MCP 服务端、记忆服务端。

version: '3.8' services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: hindsight POSTGRES_USER: agent POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pg_data:/var/lib/postgresql/data - ./config/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U agent"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru ports: - "6379:6379" volumes: - redis_data:/data qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" volumes: - qdrant_data:/qdrant/storage memory-service: build: ./memory-service depends_on: postgres: condition: service_healthy redis: condition: service_started qdrant: condition: service_started environment: DB_URL: postgresql://agent:${DB_PASSWORD}@postgres:5432/hindsight REDIS_URL: redis://redis:6379 QDRANT_URL: http://qdrant:6333 ports: - "8000:8000" mcp-server: build: ./mcp-server depends_on: - memory-service environment: MEMORY_SERVICE_URL: http://memory-service:8000 ports: - "8080:8080" volumes: pg_data: redis_data: qdrant_data:

注意:.env文件里不要硬编码密码,用环境变量注入。生产环境还要考虑网络隔离,把数据库端口只暴露给内部网络。

4.2 数据库表结构设计

PostgreSQL 里主要建三张表:episodic_memory、semantic_memory、memory_relations。

CREATE TABLE episodic_memory ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64), event_type VARCHAR(32) NOT NULL, summary TEXT NOT NULL, entities JSONB DEFAULT '[]', tool_calls JSONB DEFAULT '[]', importance FLOAT DEFAULT 0.5, created_at TIMESTAMP DEFAULT NOW(), last_accessed_at TIMESTAMP DEFAULT NOW(), access_count INT DEFAULT 0 ); CREATE INDEX idx_episodic_user_time ON episodic_memory(user_id, created_at DESC); CREATE INDEX idx_episodic_importance ON episodic_memory(importance DESC); CREATE TABLE semantic_memory ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL, content TEXT NOT NULL, confidence FLOAT DEFAULT 0.8, source_memory_ids UUID[], created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE memory_relations ( source_id UUID NOT NULL, target_id UUID NOT NULL, relation_type VARCHAR(32) NOT NULL, weight FLOAT DEFAULT 1.0, PRIMARY KEY (source_id, target_id, relation_type) );

entities字段用 JSONB 存,方便后续做 GIN 索引和精确查询。importance和access_count用来做检索排序的加权因子。

4.3 记忆写入的完整实现

写入流程我拆成三个函数:should_write、summarize、route_and_store。

import json from datetime import datetime, timedelta def should_write(dialogue, context): """判断是否触发记忆写入""" # 条件1:对话轮次达到阈值 if context.get("turn_count", 0) % 5 == 0: return True # 条件2:检测到偏好表达 preference_keywords = ["我喜欢", "我习惯", "以后都", "不要再", "记住"] if any(kw in dialogue for kw in preference_keywords): return True # 条件3:工具调用失败 if context.get("last_tool_status") == "error": return True return False def summarize(dialogue, llm_client): """用 LLM 做摘要和结构化""" prompt = f"""请对以下对话片段做摘要,输出 JSON 格式: {{ "event_type": "preference|task_execution|error_resolution|general", "summary": "一句话摘要,不超过50字", "entities": ["实体1", "实体2"], "importance": 0.0-1.0, "tool_calls": [{{"tool": "名称", "status": "success|fail"}}] }} 对话内容: {dialogue} """ response = llm_client.chat(prompt) return json.loads(response) def route_and_store(summary, user_id, session_id, db, vector_db): """分层路由并存储""" if summary["event_type"] == "preference": # 写入 semantic memory db.execute(""" INSERT INTO semantic_memory (user_id, category, content, confidence) VALUES (%s, %s, %s, %s) """, (user_id, "preference", summary["summary"], summary["importance"])) else: # 写入 episodic memory memory_id = db.execute(""" INSERT INTO episodic_memory (user_id, session_id, event_type, summary, entities, tool_calls, importance) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id """, (user_id, session_id, summary["event_type"], summary["summary"], json.dumps(summary["entities"]), json.dumps(summary["tool_calls"]), summary["importance"])) # 向量化存储 embedding = embed_model.encode(summary["summary"]) vector_db.upsert( collection_name="agent_memory", points=[{ "id": str(memory_id), "vector": embedding.tolist(), "payload": { "user_id": user_id, "event_type": summary["event_type"], "importance": summary["importance"], "created_at": datetime.now().isoformat() } }] )

4.4 检索流程的代码实现

检索的核心是多路召回 + 重排序。我写了一个retrieve_memories函数,把三路结果合并后做重排。

def retrieve_memories(query, user_id, top_k=5, db=None, vector_db=None): """多路召回 + 重排序""" # 第一路:向量相似度召回 query_embedding = embed_model.encode(query) vector_results = vector_db.search( collection_name="agent_memory", query_vector=query_embedding.tolist(), query_filter={"user_id": user_id}, limit=20 ) # 第二路:结构化条件召回(最近7天高重要性记忆) structured_results = db.fetchall(""" SELECT id, summary, importance, created_at FROM episodic_memory WHERE user_id = %s AND created_at > NOW() - INTERVAL '7 days' AND importance > 0.6 ORDER BY importance DESC LIMIT 10 """, (user_id,)) # 第三路:时间衰减召回(最近24小时所有记忆) recent_results = db.fetchall(""" SELECT id, summary, importance, created_at FROM episodic_memory WHERE user_id = %s AND created_at > NOW() - INTERVAL '24 hours' ORDER BY created_at DESC LIMIT 10 """, (user_id,)) # 合并去重 all_candidates = merge_and_dedup(vector_results, structured_results, recent_results) # 重排序:用 LLM 做相关性打分 reranked = rerank_with_llm(query, all_candidates) return reranked[:top_k] def rerank_with_llm(query, candidates): """用 LLM 对候选记忆做相关性打分""" prompt = f"""当前查询:{query} 请对以下候选记忆按相关性从高到低排序,只输出 ID 列表: {json.dumps([{"id": c["id"], "summary": c["summary"]} for c in candidates], ensure_ascii=False)} """ response = llm_client.chat(prompt) ranked_ids = json.loads(response) id_to_memory = {c["id"]: c for c in candidates} return [id_to_memory[mid] for mid in ranked_ids if mid in id_to_memory]

提示:重排序用 LLM 虽然效果好,但会增加延迟。如果对响应时间敏感,可以用一个小的 cross-encoder 模型替代,速度更快,效果也能接受。

4.5 MCP 服务端的注册与调用

MCP 服务端用 Python 的mcp库来实现,核心是定义工具描述和处理函数。

from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types server = Server("hindsight-memory") @server.list_tools() async def handle_list_tools(): return [ types.Tool( name="memory_search", description="检索 Agent 历史记忆,返回与查询最相关的记忆条目", inputSchema={ "type": "object", "properties": { "query": {"type": "string", "description": "检索查询"}, "top_k": {"type": "integer", "default": 5}, "user_id": {"type": "string"} }, "required": ["query", "user_id"] } ), types.Tool( name="memory_write", description="写入一条新的 Agent 记忆", inputSchema={ "type": "object", "properties": { "content": {"type": "string"}, "memory_type": {"type": "string", "enum": ["episodic", "semantic"]}, "user_id": {"type": "string"}, "metadata": {"type": "object"} }, "required": ["content", "memory_type", "user_id"] } ) ] @server.call_tool() async def handle_call_tool(name: str, arguments: dict): if name == "memory_search": results = retrieve_memories( query=arguments["query"], user_id=arguments["user_id"], top_k=arguments.get("top_k", 5) ) return [types.TextContent(type="text", text=json.dumps(results, ensure_ascii=False))] elif name == "memory_write": memory_id = write_memory( content=arguments["content"], memory_type=arguments["memory_type"], user_id=arguments["user_id"], metadata=arguments.get("metadata", {}) ) return [types.TextContent(type="text", text=f"Memory written: {memory_id}")]

启动 MCP 服务端之后,在支持 MCP 的客户端(比如 Claude Desktop、Cursor、或者你自己写的 Agent 框架)里配置好服务地址,Agent 就能自动发现这些工具并调用了。

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

5.1 记忆检索返回不相关结果怎么办

这是最常见的问题。我排查下来,原因通常出在三个地方:

第一,embedding 模型选得不对。不同 embedding 模型对中文、英文、代码的语义理解能力差异很大。如果你的 Agent 主要处理中文对话,用英文为主的 embedding 模型效果会打折扣。建议先用 MTEB 榜单上的中文模型做对比测试,选一个在你的数据上表现最好的。

第二,摘要质量太差。如果写入时的摘要就是一堆废话,检索时自然找不到有用信息。检查你的摘要 prompt,确保它要求模型输出具体、可检索的内容,而不是“用户进行了对话”这种空泛描述。

第三,重排序没做好。向量相似度高不等于语义相关。加一个重排序步骤,用 LLM 或者 cross-encoder 做二次筛选,效果提升非常明显。

5.2 Docker 网络不通导致服务间调用失败

Docker Compose 默认会创建一个内部网络,服务之间用服务名作为主机名通信。但有几个坑:

  • 服务启动顺序:用depends_on只能保证启动顺序,不能保证服务就绪。要用healthcheck+condition: service_healthy来确保依赖服务真正可用之后再启动。

  • 端口映射 vs 内部通信:ports映射是给宿主机访问用的,容器之间通信直接用服务名和容器内部端口,不要走宿主机端口。

  • DNS 解析:如果服务名解析失败,检查是否在同一个 network 里。默认情况下 Compose 会创建一个 network,所有服务都在里面,但如果你手动指定了 network,要确保所有服务都加入了。

5.3 向量数据库性能瓶颈

Qdrant 在数据量超过 100 万条之后,检索延迟会明显上升。我的优化经验:

  • 分 collection:按用户 ID 或者按时间分 collection,避免单 collection 过大。
  • 量化压缩:开启 scalar quantization,把 float32 向量压缩成 int8,内存占用减少 75%,检索速度提升 2 到 3 倍,精度损失很小。
  • 索引参数调优:hnsw_ef参数控制检索精度和速度的平衡,默认值 128 可以调到 64 来提速,如果精度不够再往上加。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
检索结果不相关embedding 模型不匹配人工评估 top-10 结果换用中文优化模型
检索结果不相关摘要质量差检查写入的 summary 字段优化摘要 prompt
检索结果不相关缺少重排序对比有无重排序的效果加 LLM 或 cross-encoder 重排
服务间调用超时依赖服务未就绪查看容器日志加 healthcheck 和重试
向量检索慢数据量过大查看 collection 大小分 collection + 量化
记忆写入丢失触发条件太严格检查 should_write 逻辑放宽触发条件
上下文超长检索返回太多统计注入的 token 数减少 top_k,加摘要压缩
Docker 启动失败端口冲突docker compose logs改端口或停掉占用进程

5.5 几个我踩过的坑

坑一:忘记给向量数据库做持久化。第一次部署的时候没挂 volume,容器一重启,所有向量数据全没了。后来在 docker-compose 里加了qdrant_datavolume,问题解决。

坑二:摘要 prompt 里没要求输出 JSON。早期版本让 LLM 自由输出摘要,结果格式五花八门,解析经常失败。后来强制要求 JSON 格式,并在 prompt 里给了示例,解析成功率从 70% 提升到 99%。

坑三:working memory 的 TTL 设得太短。一开始设了 10 分钟,结果用户稍微思考一下,回来继续对话时上下文就丢了。后来改成 2 小时,体验好很多。这个值要根据实际场景调,没有标准答案。

坑四:忽略了对记忆写入的去重。同一件事被反复写入,检索时返回一堆重复内容。后来在写入前加了一个相似度检查,如果和已有记忆的余弦相似度超过 0.95,就跳过写入,只更新访问时间。

6. 记忆系统的扩展方向与个人体会

这套架构跑通之后,可以扩展的方向其实很多。比如跨 Agent 的记忆共享——多个 Agent 共用一套记忆系统,A Agent 学到的经验 B Agent 也能用。再比如记忆的可视化——做一个面板,让用户能看到 Agent 记住了什么、忘了什么,方便调试和信任建立。还有记忆的版本控制——像 Git 一样管理记忆的变更历史,出问题可以回滚。

我个人的体会是,Agent 记忆这件事,工程实现只占三成,七成在于对业务场景的理解。你得清楚你的 Agent 在什么场景下需要记住什么、什么时候需要想起来、什么时候该忘掉。这些判断没有通用答案,只能在实际使用中不断调整。

最后分享一个小技巧:在开发阶段,把每次检索的 query、返回的记忆、以及最终注入上下文的 token 数都打到日志里。上线之后定期 review 这些日志,你会发现很多优化点——哪些记忆从来没被检索到、哪些 query 总是返回不相关结果、哪些记忆占着空间但毫无价值。这些观察比任何理论都管用。

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

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

立即咨询