1. 为什么“记忆”和“工具”是 Agent 落地的两道坎
做 Agent 的人都有一个共同体会:模型本身的能力其实已经够用了,真正卡住项目的是两件事——它记不住东西,以及它够不着外部世界。前者是记忆问题,后者是工具问题。这两个问题不解决,Agent 就只能停留在演示阶段,永远进不了生产环境。
我最早接触 Agent 开发的时候,天真地以为把对话历史全部塞进上下文窗口就行了。结果第一次跑长任务就翻车:一个需要连续处理二十多步的操作流程,跑到第十五步的时候模型开始胡言乱语,前面确认过的参数它忘了,用户明确说过的偏好它也丢了。排查了半天才反应过来,上下文窗口虽然标称支持很长的输入,但实际有效注意力是有限的,信息一多就开始互相干扰。这就是典型的“记忆没管好”。
工具的问题同样棘手。Agent 要真正干活,就得能调用外部能力——查数据库、发请求、读写文件、操作第三方服务。早期大家的做法是给每个工具写一个函数,然后在提示词里描述这些函数怎么用。工具少的时候还行,一旦超过十个,提示词就臃肿得不像话,模型选错工具、传错参数的概率直线上升。更麻烦的是,每接一个新工具就要改一次提示词、重新测试一轮,维护成本极高。
这两个问题看起来独立,实际上是一体两面。记忆决定了 Agent 在什么时候需要什么信息,工具决定了 Agent 能做什么动作。两者配合不好,Agent 要么“想不起来该干什么”,要么“想起来了但干不了”。所以我把这篇博文的核心定在:怎么用工程化的方式管理 Agent 的记忆,怎么用标准化的协议接入工具,以及这两者怎么协同工作。
这篇文章适合正在做 Agent 应用开发、被上下文管理和工具接入折磨过的工程师,也适合刚入门想搞清楚“记忆”和“工具”到底该怎么设计的同学。我会从整体架构讲到具体实现,把参数选择、踩坑经验、排查方法都摊开来说。读完之后,你至少能搭出一个记忆可控、工具可扩展的 Agent 骨架。
2. 记忆系统的整体设计与选型思路
2.1 上下文窗口不是记忆,它只是工作台
很多人把上下文窗口等同于 Agent 的记忆,这是一个根本性的误解。上下文窗口更像是一张工作台,你手头正在处理的材料摊在上面,方便随时取用。但工作台面积有限,摊满了就得收拾,收拾的时候如果直接扔掉,那这些信息就永远丢了。
我习惯把 Agent 的记忆分成三层来理解。第一层是即时上下文,就是当前这一轮对话或这一步操作需要的信息,放在上下文窗口里,随用随取。第二层是短期记忆,跨越多轮对话但有时效性的信息,比如当前任务的进度、用户在这次会话里说过的偏好,通常存在内存或轻量存储里。第三层是长期记忆,跨会话持久化的知识,比如用户的历史偏好、领域知识库、过往任务的结论,需要落到数据库或向量存储里。
这三层的划分不是为了好看,而是因为它们的读写频率、容量需求、检索方式完全不同。即时上下文要求低延迟、高相关性;短期记忆要求快速读写、支持结构化查询;长期记忆要求大容量、支持语义检索。用一套方案硬扛三层需求,结果一定是每层都做不好。
提示:不要一上来就上向量数据库。我见过不少项目,明明只需要存几十条会话状态,非要搭一套向量检索,结果查询延迟高、维护复杂,收益却几乎为零。先想清楚你的记忆到底需要多“长期”,再决定用什么存储。
2.2 记忆的写入、检索与遗忘机制
记忆系统的核心操作只有三个:写入、检索、遗忘。听起来简单,但每个都有讲究。
写入的时机很关键。不是所有对话内容都值得记。我的做法是设置一个“记忆价值判断”环节,用规则或轻量模型判断当前信息是否值得写入长期记忆。比如用户说“我今天想吃火锅”,这大概率是一次性信息,不值得长期保存;但用户说“我对花生过敏”,这就是必须长期记住的硬约束。判断逻辑可以很简单:包含偏好、约束、身份、长期目标的信息优先写入,闲聊和临时状态不写入。
检索的策略决定了 Agent 能不能在需要的时候想起该想起的东西。最朴素的做法是关键词匹配,但自然语言里同一个意思有无数种表达,关键词匹配召回率很低。所以语义检索几乎是必选项。但纯语义检索也有问题:它容易召回“意思相近但实际无关”的内容。我的经验是采用混合检索——语义相似度占七成权重,关键词匹配占三成权重,再叠加时间衰减因子,越新的记忆权重越高。这样既能召回语义相关的内容,又能保证时效性。
遗忘机制是最容易被忽略的。很多项目只进不出,记忆库越堆越大,检索质量越来越差。遗忘不是简单删除,而是分级处理:低价值记忆降低检索权重,过期记忆归档到冷存储,冲突记忆用新版本覆盖旧版本。我一般会设置一个定期清理任务,每周跑一次,把三个月内从未被检索到的记忆标记为冷数据,把明确过期的记忆(比如“我下周要出差”这种)自动清理。
2.3 从上下文窗口到外部存储的衔接方案
上下文窗口和外部存储之间的衔接,是整个记忆系统最容易出问题的地方。衔接不好会出现两种极端:要么把所有外部记忆都塞进上下文,导致窗口爆炸;要么完全不塞,导致 Agent 对历史一无所知。
我的做法是按需注入。每一轮对话开始前,先用当前输入去检索短期记忆和长期记忆,拿到最相关的若干条,压缩成简洁的摘要,注入到上下文窗口的固定位置。注入的内容要控制长度,我一般限制在总上下文预算的百分之二十以内。剩下的预算留给当前任务的实际内容和工具返回结果。
这里有个细节:注入的位置会影响模型的使用效果。我试过把记忆放在对话开头、放在用户输入之前、放在系统提示词里,实测下来放在系统提示词之后、用户输入之前效果最稳。模型会把它当作“背景知识”而不是“当前指令”,不容易混淆。
另外,注入的记忆要带元信息,比如来源、时间、置信度。这样模型在引用记忆时能判断这条信息是否还可靠。比如一条三个月前的偏好,模型可以主动问用户“您之前提到过偏好A方案,现在还是这样吗”,而不是直接按旧偏好执行。
3. 工具接入的核心难点与 MCP 的解决思路
3.1 传统工具接入方式的三个痛点
在 MCP 出现之前,工具接入基本靠“手写描述加函数调用”。这套方式在工具数量少、场景固定的时候能用,但一旦规模上去就暴露三个痛点。
第一个痛点是描述与实现耦合。每个工具的功能描述、参数格式、调用方式都写在提示词或代码里,工具一改,提示词和代码都得跟着改。我维护过一个有三十多个工具的项目,每次加一个新工具,光改提示词和回归测试就要花半天。
第二个痛点是发现机制缺失。模型怎么知道有哪些工具可用?传统做法是把所有工具描述一股脑塞进提示词。工具少还好,工具一多,提示词里全是工具说明,真正给任务的指令反而被淹没了。而且模型没有“按需发现”的能力,它只能看到你给它的工具列表,看不到列表之外还有什么。
第三个痛点是跨平台复用困难。你在 A 平台写的工具,换到 B 平台就得重写一遍,因为两个平台的工具描述格式、调用协议都不一样。这导致工具生态碎片化,每个平台都要重复造轮子。
3.2 MCP 到底解决了什么问题
MCP 的核心思路是把工具的描述和调用标准化,让工具提供方和使用方解耦。你可以把它理解成工具领域的“通用接口标准”——工具提供方按标准暴露自己的能力,Agent 按标准发现和调用这些能力,双方不需要知道对方的具体实现。
具体来说,MCP 定义了一套描述协议,工具提供方用这套协议声明自己有哪些工具、每个工具接受什么参数、返回什么结果。Agent 端则通过标准化的方式获取这份描述,然后决定调用哪个工具、传什么参数。整个过程不需要把工具描述硬编码到提示词里,而是动态获取。
这带来的直接好处是:工具可以独立于 Agent 开发和部署,Agent 可以在运行时发现新工具,同一个工具可以被不同 Agent 复用。我实测下来,接入一个新工具的时间从原来的半天缩短到十几分钟,而且不需要改 Agent 的核心代码。
注意:MCP 解决的是“标准化接入”问题,不解决“工具设计”问题。工具本身的粒度、参数设计、错误处理还是得你自己想清楚。我见过有人把 MCP 当成万能药,接了一堆设计糟糕的工具,结果 Agent 调用成功率反而下降了。
3.3 工具描述、发现与调用的标准化流程
MCP 的标准化流程可以拆成三步:描述、发现、调用。
描述阶段,工具提供方按协议格式声明工具清单。每个工具需要说明名称、用途、参数列表、参数类型、是否必填、返回值格式。这份描述要写得让模型能看懂,所以用途说明要用自然语言,参数说明要清晰无歧义。我一般会要求团队在写工具描述时遵循一个原则:假设读这份描述的人完全不了解你的系统,他能不能根据描述正确调用这个工具。
发现阶段,Agent 在启动时或运行时向工具提供方请求工具清单。这个请求是标准化的,不依赖具体平台。Agent 拿到清单后,可以根据当前任务筛选出相关工具,而不是把所有工具都塞进上下文。这就解决了“工具太多淹没指令”的问题。
调用阶段,Agent 按标准格式发起调用请求,工具提供方执行后按标准格式返回结果。调用过程中如果出错,错误信息也按标准格式返回,Agent 可以根据错误类型决定重试、换工具还是向用户求助。
这套流程的价值在于可组合性。你可以把多个工具提供方组合起来,Agent 面对的是一个统一的工具池,不需要关心每个工具背后是谁提供的、怎么实现的。这为构建复杂 Agent 系统打下了基础。
4. 记忆与工具的协同:让 Agent 真正能干活
4.1 记忆驱动工具选择,工具结果反哺记忆
记忆和工具不是两个独立的模块,它们必须协同工作。协同的核心逻辑是:记忆帮助 Agent 选对工具,工具的结果又成为新的记忆。
举个例子。用户说“帮我查一下上个月那个项目的进度”。Agent 首先从长期记忆里检索“上个月那个项目”指的是什么——可能是用户之前提过的某个项目名称。拿到项目名称后,Agent 从工具池里选择“查询项目进度”这个工具,把项目名称作为参数传进去。工具返回进度数据后,Agent 把这次查询的结果写入短期记忆,同时判断这个结果是否值得写入长期记忆——如果项目进度是用户持续关注的,就写入长期记忆,下次用户再问类似问题时可以直接引用。
这个过程中,记忆的质量直接决定了工具选择的准确性。如果记忆检索召回的是错误的项目名称,工具调用就会查错对象。反过来,工具返回的结果如果没被正确记忆,下次用户追问时 Agent 又得重新查一遍,效率很低。
我的经验是,在工具调用前后各加一个记忆操作。调用前,从记忆里检索相关上下文,作为工具参数选择的依据;调用后,把结果和上下文一起写入记忆,并标注这次调用的成功与否。这样日积月累,Agent 对“什么情况下该用什么工具”会越来越有感觉。
4.2 多轮任务中的状态保持与恢复
多轮任务是记忆和工具协同最吃力的场景。一个任务可能需要十几步操作,中间涉及多次工具调用和多次用户确认。如果状态保持不好,任务跑到一半就断了,用户得从头再来。
我的做法是用一个任务状态对象来管理多轮任务。这个对象包含任务目标、当前步骤、已完成步骤、待确认事项、中间结果。每执行一步,就更新这个对象,并把它写入短期记忆。如果任务中断,下次用户回来时,Agent 从短期记忆里恢复这个对象,接着上次的进度继续。
这里的关键是状态对象的序列化和反序列化要可靠。我一般用 JSON 格式存储,字段设计得尽量扁平,避免嵌套太深导致恢复时出错。另外,状态对象里要记录时间戳,如果中断时间太久(比如超过一周),恢复时 Agent 应该主动向用户确认任务是否还需要继续,而不是闷头接着跑。
工具调用产生的中间结果也要纳入状态管理。比如一个数据处理任务,第一步工具返回了原始数据,第二步工具需要对原始数据做清洗。如果中间断了,恢复时 Agent 需要知道原始数据在哪、清洗到哪一步了。我的做法是把中间结果存到临时存储,状态对象里只存引用地址,避免状态对象本身过大。
4.3 错误恢复与降级策略
Agent 干活不可能一帆风顺,工具调用失败、记忆检索为空、用户输入模糊,这些情况都会发生。好的 Agent 不是不犯错,而是犯错后能恢复或降级。
工具调用失败时,我的策略是分级处理。如果是参数错误,Agent 尝试修正参数后重试一次;如果是工具暂时不可用,Agent 从工具池里找功能相近的替代工具;如果替代工具也没有,Agent 向用户说明情况并给出建议。整个过程要记录到记忆里,避免下次犯同样的错误。
记忆检索为空时,Agent 不应该硬编一个答案,而应该主动向用户询问缺失的信息。比如用户问“帮我处理一下那个文件”,但记忆里没有“那个文件”的指代,Agent 就应该问“您指的是哪个文件”,而不是随便猜一个。
降级策略的核心是保证任务不彻底失败。哪怕工具全挂了,Agent 至少应该能把当前状态保存下来,告诉用户“任务进行到哪一步了,遇到了什么问题,您可以稍后继续”。这比直接报错崩溃要好得多。
5. 实操过程:从零搭一个带记忆和工具调用的 Agent
5.1 环境准备与依赖选型
先说环境。我用的是一台普通开发机,Python 3.10 以上,内存 16G 起步。记忆存储用 SQLite 加 FAISS 做向量检索,工具调用用 MCP 的 Python SDK。这套组合的好处是轻量、依赖少、本地就能跑通,不需要额外搭服务。
依赖清单如下:
pip install mcp faiss-cpu sentence-transformers sqlite3sentence-transformers用来做文本向量化,选一个小模型就行,比如all-MiniLM-L6-v2,速度快、效果够用。FAISS 用来做向量相似度检索,CPU 版本足够,除非你的记忆库有百万级以上条目才需要考虑 GPU。
SQLite 用来存结构化记忆和任务状态。有人会问为什么不用 PostgreSQL,我的理由是:Agent 的记忆库在早期规模不大,SQLite 零配置、单文件、易备份,开发阶段非常方便。等规模上去了再迁移也不迟。
提示:向量模型的选择会影响检索质量。我试过几个模型,
all-MiniLM-L6-v2在中文场景下表现一般,如果你的记忆以中文为主,建议换成支持中文的模型,比如text2vec-base-chinese。模型大小和检索质量要权衡,不是越大越好。
5.2 记忆模块的实现:写入、检索、遗忘
记忆模块我分成三个类来实现:MemoryWriter、MemoryRetriever、MemoryForgetter。
MemoryWriter负责写入。它接收一条记忆内容,先做价值判断,再决定写入哪一层。价值判断用简单的规则引擎:包含“偏好”“过敏”“必须”“永远”等关键词的,写入长期记忆;包含“这次”“今天”“临时”的,写入短期记忆;其余默认写入短期记忆,并设置较短的过期时间。
class MemoryWriter: def __init__(self, db_path, vector_store): self.db = sqlite3.connect(db_path) self.vector_store = vector_store def write(self, content, metadata): value_score = self._judge_value(content) if value_score > 0.7: layer = "long_term" else: layer = "short_term" embedding = self._embed(content) self.vector_store.add(embedding, metadata) self.db.execute( "INSERT INTO memories (content, layer, metadata, created_at) VALUES (?, ?, ?, ?)", (content, layer, json.dumps(metadata), time.time()) ) self.db.commit()MemoryRetriever负责检索。它接收查询文本,先做向量检索拿到候选集,再用关键词匹配做二次排序,最后叠加时间衰减因子。
class MemoryRetriever: def retrieve(self, query, top_k=5): query_embedding = self._embed(query) candidates = self.vector_store.search(query_embedding, top_k * 3) scored = [] for cand in candidates: semantic_score = cand.score keyword_score = self._keyword_match(query, cand.content) time_decay = math.exp(-0.01 * (time.time() - cand.created_at)) final_score = 0.7 * semantic_score + 0.3 * keyword_score final_score *= time_decay scored.append((cand, final_score)) scored.sort(key=lambda x: x[1], reverse=True) return [item[0] for item in scored[:top_k]]MemoryForgetter负责遗忘。它定期跑,把长期未被检索的记忆标记为冷数据,把过期记忆清理掉。
class MemoryForgetter: def cleanup(self): three_months_ago = time.time() - 90 * 24 * 3600 self.db.execute( "UPDATE memories SET layer = 'cold' WHERE last_accessed < ? AND layer = 'long_term'", (three_months_ago,) ) self.db.execute( "DELETE FROM memories WHERE layer = 'short_term' AND created_at < ?", (time.time() - 7 * 24 * 3600,) ) self.db.commit()这三个类配合起来,记忆系统的基本功能就齐了。实测下来,检索延迟在几十毫秒级别,对 Agent 的响应速度影响很小。
5.3 工具模块的实现:MCP 服务端与客户端对接
工具模块分服务端和客户端。服务端负责暴露工具,客户端负责发现和调用。
服务端我用 MCP SDK 写一个简单的工具提供方,暴露两个工具:一个查天气,一个算数学。
from mcp.server import Server from mcp.types import Tool, TextContent server = Server("demo-tools") @server.list_tools() async def list_tools(): return [ Tool( name="get_weather", description="查询指定城市的当前天气", inputSchema={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } ), Tool( name="calculate", description="计算数学表达式", inputSchema={ "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式"} }, "required": ["expression"] } ) ] @server.call_tool() async def call_tool(name, arguments): if name == "get_weather": result = f"{arguments['city']}今天晴,25度" return [TextContent(type="text", text=result)] elif name == "calculate": result = str(eval(arguments["expression"])) return [TextContent(type="text", text=result)]客户端用 MCP 的客户端 SDK 连接服务端,获取工具列表,然后按需调用。
from mcp.client import Client client = Client() await client.connect("demo-tools") tools = await client.list_tools() # 根据任务筛选工具 selected_tool = "get_weather" result = await client.call_tool(selected_tool, {"city": "北京"})这套流程跑通后,加新工具只需要在服务端加一个函数,客户端不需要改代码,重新获取工具列表就能用。这就是标准化的价值。
5.4 把记忆和工具串起来:一个完整任务流程
现在把记忆和工具串起来,跑一个完整任务。任务场景:用户问“北京今天天气怎么样,适合跑步吗”。
第一步,Agent 从记忆里检索用户偏好。检索到“用户喜欢户外跑步,但空气湿度高时容易不舒服”。这条记忆来自长期记忆,是用户之前提过的。
第二步,Agent 从工具池里选择get_weather工具,传入城市“北京”。工具返回“北京今天晴,25度,湿度80%”。
第三步,Agent 结合记忆里的偏好和工具返回的天气数据,判断湿度偏高,建议用户谨慎跑步。同时把这次查询结果写入短期记忆,标注“用户询问北京天气,已回复”。
第四步,如果用户后续再问“那明天呢”,Agent 从短期记忆里知道用户关注的是跑步适宜度,直接查明天天气并给出同样的判断逻辑,不需要用户重复说明需求。
这个流程里,记忆让 Agent 知道用户的偏好,工具让 Agent 拿到实时数据,两者结合才能给出有针对性的回答。缺了记忆,Agent 只会报天气数字;缺了工具,Agent 只能凭旧知识瞎猜。
6. 常见问题与排查技巧实录
6.1 记忆检索不准的排查思路
记忆检索不准是最常见的问题,表现是 Agent 答非所问,或者该想起的没想起。排查分三步。
先看向量模型是否适合你的数据。如果记忆以中文为主,用英文模型效果肯定差。换一个中文模型试试,往往能解决大部分问题。
再看检索权重是否合理。语义权重太高会召回“意思相近但无关”的内容,关键词权重太高会漏掉“换个说法”的内容。我一般从七三开开始调,根据实际效果微调。
最后看时间衰减因子是否过强。如果衰减太快,旧的重要记忆会被新记忆淹没。可以适当降低衰减系数,或者对标记为“长期重要”的记忆不施加衰减。
6.2 工具调用失败的典型原因
工具调用失败的原因五花八门,我整理了一个速查表。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 模型选错工具 | 工具描述不清晰 | 检查工具描述是否无歧义 |
| 参数格式错误 | 参数类型未约束 | 检查 inputSchema 是否完整 |
| 调用超时 | 工具执行太慢 | 加超时限制,优化工具实现 |
| 返回结果解析失败 | 返回格式不标准 | 统一返回格式为 JSON |
| 工具不可用 | 服务端未启动 | 检查服务端状态和连接 |
我踩过最坑的一次是工具描述里用了“查询数据”这种模糊说法,结果模型在三个功能相近的工具之间反复横跳。后来把描述改成“根据用户ID查询订单状态”,问题立刻消失。工具描述一定要具体到“做什么、输入什么、输出什么”。
6.3 上下文窗口爆满的应急处理
上下文窗口爆满的表现是模型开始胡言乱语、重复输出、或者直接报错。应急处理分两步。
第一步,立刻压缩当前上下文。把历史对话做摘要,只保留关键信息,把工具返回的大段数据截断或摘要。我一般会保留最近三轮对话的完整内容,更早的做摘要。
第二步,检查记忆注入量是否过大。如果每轮都注入大量记忆,窗口很快就会被占满。把注入量控制在总预算的百分之二十以内,超出部分做摘要或只注入最相关的几条。
长期来看,要建立上下文预算管理机制。给系统提示词、记忆注入、当前任务、工具返回各分配固定预算,任何一项超了就压缩。这样能避免窗口被某一项占满。
6.4 多轮任务中断后的恢复技巧
多轮任务中断后恢复,关键是状态对象要完整。我遇到过恢复后 Agent 不知道任务进行到哪一步的情况,排查发现是状态对象没存全,中间结果丢了。
解决方法是:每执行一步就持久化状态对象,不要等任务结束才存。状态对象里要包含足够的上下文,让恢复后的 Agent 能理解当前处境。另外,恢复时先向用户确认任务是否继续,避免用户已经不需要了 Agent 还在闷头跑。
还有一个技巧是给任务状态加版本号。如果任务逻辑改了,旧版本的状态对象可能不兼容,恢复时可以根据版本号做迁移或提示用户重新开始。
7. 一些实操心得和后续扩展方向
做 Agent 的记忆和工具接入,我最大的体会是:不要追求一步到位。我见过太多项目一开始就想搭一套完美的记忆系统,结果复杂度失控,连基本功能都跑不通。正确的做法是先跑通最小闭环——能存能取能调用工具,然后再逐步优化检索质量、扩展工具数量、完善错误处理。
另一个体会是,记忆和工具的设计要一起考虑。单独设计记忆系统容易过度设计,单独设计工具系统容易忽略上下文需求。两者协同设计,才能让 Agent 真正能干活。
后续可以扩展的方向有几个。一是记忆的主动学习,让 Agent 自己判断哪些信息值得记,而不是靠规则。二是工具的自动发现和组合,Agent 能根据任务自动找到并组合多个工具完成复杂操作。三是记忆和工具的跨会话共享,让多个 Agent 实例共享同一套记忆和工具池。这些方向我还在摸索,有进展再分享。
最后分享一个小技巧:在开发阶段,给记忆和工具的每次操作都打日志,记录输入、输出、耗时、成功与否。这些日志在排查问题时非常有用,能帮你快速定位是记忆检索错了还是工具调用错了。等系统稳定了再降低日志级别,避免日志本身成为负担。