如果你的Agent在第10轮对话里突然不认账,前面已经敲定的方案转头就说"没听你说过",先别急着骂模型太笨。十有八九,问题出在你根本没给它设计"记忆",只是把上下文窗口当记忆用。
这个月我在调一个批量文档处理Agent时又踩了一遍这坑,折腾下来反而把"上下文窗口-记忆-工具调用-MCP"这条技术演进线理清楚了。这篇东西不打算写教程式的漂亮话,就是想把我这几周实测的结论、踩过的坑、以及最终落地的方案原原本本讲出来。适合正在做Agent开发、或者想把自己的工作流交给Agent但老被"失忆"困扰的朋友参考。
1. 上下文窗口不是记忆:Agent"失忆"的真正根源
先说个我自己的真实翻车现场。项目其实很简单:让Agent读取一批合同文档,按我给的模板提取条款并输出JSON。前几轮一切正常,到第21轮左右,它突然把输出格式换成了Markdown表格,而且坚持说"这是最初的约定"。
我去翻日志,发现原因特别蠢:我做了last-N循环截断,把最前面那几条"输出格式"约束给截掉了。模型不是不想遵守,是那几条指令根本不在它的上下文里。这不是模型能力问题,是我把上下文窗口当成记忆来用了——窗口再大,截断策略一出错,该忘的一样忘。
1.1 上下文窗口的本质:一张必须反复重写的白板
很多刚接触Agent的朋友会问类似"CodeGeeX的上下文记忆长度是多少"这种问题,潜意识里觉得上下文窗口越大,Agent记性就越好。但上下文窗口的本质,是模型单次前向计算时能看到的全部token。它更像一张摆在桌上的白板,而不是硬盘——LLM没有"自己记得上一轮我输出了什么"的机制,每一轮问答,你都必须把历史文本重新拼起来喂给它。
这意味着三件事:
- 窗口再大,内容也是"这次看得见",不是"永久存下来"。
- 每次重新拼接历史,时间和钱都花在重复的token上。
- 一旦超出窗口上限,你想保住哪个信息,取决于你的截断/压缩策略,而不是模型有多聪明。
我见过有人把历史对话循环塞进新的请求,结果token数从几千涨到几万,最后要么被硬截断,要么直接把API调用成本推高。这不是"记忆长度"问题,是工程上的管不管得住问题。
1.2 长对话里一定会出现的四个毛病
把大量历史硬塞进上下文,表面上"什么都看得到",实际会踩四个坑:
token膨胀直接烧钱。每轮对话都带着完整历史去计费,对话轮数一多,成本曲线就是线性的还带加速度。有人问"AI Agent token是什么意思",通俗讲,token就是计费字数的切割单位,历史越长,每轮消耗越大。
**信息稀释。"lost in the middle"**这个现象在长上下文里特别常见:模型对开头和结尾的内容关注度高,中间部分经常被忽略。你之前在第8轮说的关键细节,到了第30轮可能已经沉在中间,模型根本"想不起来重点在哪"。
上下文粘滞。模型一旦沿某个方向输出久了,会形成惯性。即便后面出现了矛盾的新信息,它也倾向于延续旧模式去生成内容。这有点像团队里老员工习惯了旧流程,新规下来一时半会儿改不掉。
会话结束即清零。这是最容易被忽略的。关闭浏览器、换一个会话,Agent对你是谁、你上次做到哪、项目背景是什么,一片空白。跨会话记忆完全不存在,所有"记住"都是假的,只是那段时间窗口没关而已。
1.3 一场实测:20轮对话后的"失忆现场"
我后来专门做了个对比实验。同一个Agent,第一组用"完整上下文"策略,每轮都尽可能把所有历史塞进去;第二组用"摘要+关键事实抽取"策略,只保留最初需求摘要、最近5轮原始对话、以及用脚本抽出来的结构化事实。
结果第二组在30轮后还能准确复述第一轮指定的输出格式和样式要求,第一组在第18轮就开始出现"记混了"的情况。原因很简单:第一组的历史里夹杂了大量中间过程的中间态版本,模型分不清哪个才是最终约定。
这个实验让我定下一条原则:上下文窗口负责"当前工作需要看到的",记忆系统负责"长期需要记住的",两者不能混为一谈。
2. 给Agent分层做记忆:工作记忆、长期存储与遗忘机制
既然窗口本身不提供记忆,那就得我们自己搭。我目前实践的方案,可以拆成三层:工作记忆(当前任务状态)、长期记忆(事实与偏好)、以及一个很多人忽略的遗忘/剪枝机制。
2.1 工作记忆:在窗口内做"缓存管理"
工作记忆对应的是"当前这个会话里,Agent必须时刻知道的东西"。它仍然活在上下文窗口里,但我们需要有意识地经营它,而不是无脑堆历史。
我的做法是维护一个"任务状态板块",放在每轮请求最近的prompt前面。结构大概长这样:
[系统定位] 你是一个合同审查助手。 [当前任务状态] - 任务目标:提取乙方义务条款,输出JSON - 已确认的约定:输出字段必须包含 party_b, obligations, risk_level - 当前进度:已完成第3份合同,正在处理第4份 - 最近发现:第4份合同中违约金条款存在缺项 [用户本轮输入] ...这个板块由一段脚本动态生成,每一轮结束后,用一个小模型(或者干脆用代码规则,比如正则加关键词)从新内容里抽取"状态更新",覆盖到板块里。成本低,效果立竿见影。相比之下,网上常见做法是只做"把历史对话压缩成摘要"——摘要也有用,但摘要太平滑,关键约束(比如输出格式)容易在压缩中丢失。状态板块和摘要的区别,相当于便签和日记的区别:一个是"正在办的事",一个是"发生过的事"。
这套思路和传统"长短期记忆网络"(LSTM)里的门控思路有异曲同工之处:我们需要一个显式的、持续更新的"细胞状态",而不是把所有信息都原封不动传下去。LLM时代不用写门控公式,但"什么该更新进状态、什么该遗忘"这套思想是直接可以借用的。
2.2 长期记忆:向量库加结构化档案
跨会话的记忆完全交给窗口就不现实了,必须落盘。我现在的方案是"向量库+结构化档案"双通道。
向量通道负责存储和检索对话片段。每次会话结束后,把有信息量的段落切块、做embedding、存入向量库。等下一轮需要时,通过相似度检索召回相关片段,然后作为上下文注入。这一层对应的是"我记得我们之前聊过一个什么方案,细节想不起来了,类似的那种感觉"。
结构化档案负责存高确定性的事实。比如用户的内容偏好、项目背景、业务术语表、历史决策记录。这些我建议用JSON、SQLite这类结构化格式存,不要一股脑丢进向量库。原因是向量检索是"模糊匹配",而事实需要精确调用。你不想让Agent在"用户是否要求JSON输出"这种问题上去做相似度推测,应该是直接从档案里查。
这个结构再往上抽象,就是网上说的"双网络记忆模型":一条网络负责临时工作状态(对应工作记忆),一条网络负责长期事实存储(对应长期记忆)。实测下来,两条通路分开管理,比把所有东西塞进一个Vector DB里再检索,准确率高得多。检索的目标更明确,召回率、准确率都能分别调优。
2.3 遗忘与剪枝:记忆不是塞得越满越好
这块是我认为最容易被忽视、但影响最大的部分。一开始我把所有历史全量存进向量库,结果用了一周后,召回准确率肉眼可见地下降。原因很简单:旧记录、临时记录、过期决策混在一起,每次检索都会召回一堆无关内容,噪声淹没了信号。
后来我加了三条规则:
- 时间衰减:每条记忆记录带上时间戳,检索时按权重计算,越新的记录权重越高。特别适用于"临时结论"类记忆。
- 置信度标记:从用户明确确认过的信息标记为"高置信",模型推测出来的标记为"低置信"。低置信信息召回时默认排在后面,除非没有任何高置信信息能匹配。
- 定期剪枝:每天跑一次清理任务,删除超时未更新的临时状态、合并重复事实、把不再相关的旧检索条目归档。
有个实际教训:不加时间权重的时候,用户一个月前提过的一条临时偏好,经常把本周刚确定的新结论挤掉。加上时间衰减之后,这类冲突大幅度减少。记忆系统的价值不在于存得多,而在于查得准。存储是手段,决策质量才是目的。
3. 工具调用怎么走到今天的:从写死逻辑到Function Calling
记忆问题解决了,下一个要处理的是"动手干活"。Agent再聪明,不能调用工具也是空谈。工具调用的进化路径,其实和记忆进化的轨迹很像:从全手写,到部分自动化,再到协议化。
3.1 第一代工具调用:什么都得开发者写死
早期的Agent,或者说更早的聊天机器人,调用工具的逻辑基本是写死的。比如用户说"查天气",代码里就做个意图匹配,命中"天气"关键词,就去调天气API,把结果拼到回复里。整个过程不存在"模型自己决定",模型充其量是个生成器,动作流程都在if/else里。
这种做法的最大问题不是"不够智能",而是扩展成本极高。每加一个工具,就要写一套意图匹配规则、参数解析代码、调用逻辑、错误处理,而且这些逻辑和具体工具的API强耦合。工具一多,代码里全是分支判断,维护的人想死。
我早期做的一个内部QA机器人就是这样:二十来个工具,几百个if/else,加一个工具要花半天,改一个接口要排查所有引用。那是第一次真切感受到,工具调用的"组织问题"比"调用问题"更严重。
3.2 Function Calling:把工具选择权交给模型
后来的Function Calling机制,本质是换了一个思路:不再由代码决定"该调用哪个工具",而是由模型自己决定。
做法是把每个工具的"说明书"塞进prompt,包括工具名、功能描述、参数schema。模型根据用户请求,自己判断该用哪个工具,输出一个结构化调用请求,比如:
{ "name": "get_weather", "arguments": "{\"city\": \"上海\"}" }框架拿到这个结果后,去执行真实函数,再把返回值交给模型继续生成回复。
这个方法能奏效,其实依赖一个点:LLM在预训练时看过海量"API调用文档+调用示例",它已经天然具备"根据文档选择函数"的能力。你不需要教它,只需要把说明写得足够清楚。
但实际用了几个月后,我发现Function Calling只是往前走了一半:
- 各家模型的工具定义格式不统一。OpenAI、Claude、国内各家大模型的tool schema细节不一样,换一个模型,函数定义、调用格式基本要重写。
- 工具多了,prompt被工具说明占满。我有一次接了几十个工具后,工具schema占了20000多token,把真正干活的空间挤没了。
- 工具之间互相调用、串联使用时,中间结果全堆在上下文里,这又回到了上一章说的"上下文被垃圾占满"问题。
3.3 工具一多就乱:新碎片化的出现
其实工具数量一旦超过十几个,准确率下降是必然的。模型要在几十个工具里挑一个,每个工具的说明都那么长,它很容易混淆两个名字相似的函数,或者读到一半就忘了前面看过什么。这是"信息稀释"在工具场景的复现。
更头疼的是接入方式碎片化。A工具提供的是Python SDK,B工具提供的是HTTP API,C工具只能通过命令行。每个都得写适配层,连接成本是线性的,甚至是指数级的。这让我越来越觉得,工具调用缺的不是"让模型能选工具",而是一套统一的接入协议,让工具以同一种方式暴露给Agent。这个想法,正好和MCP的定位撞上了。
4. MCP:给Agent的"手"和"脑"统一接口
MCP全称Model Context Protocol,模型上下文协议。我一开始看到这个名字以为只是个新SDK,实际用下来,它解决的问题要比"封装API"大得多。
4.1 MCP到底解决什么问题
一句话:MCP相当于给Agent和各种外部能力之间,规定了一个通用的"USB接口"。
回忆一下USB出现之前的场景:打印机有打印机的接口,键盘有键盘的接口,鼠标有鼠标的接口,每台外设都要专用接口板。USB统一了物理接口之后,任何设备插上就能用,电脑不需要为每种外设定制主板。
MCP做的就是这个事。它定义了一套标准协议,让工具、数据源、记忆存储等能力以统一的方式暴露给Agent。任何遵循MCP的服务,无论后端是什么语言、什么技术栈,都可以被MCP客户端一键连接。你不再需要为每个工具写一套胶水代码,只需要让提供方实现一个MCP Server。
这正好补上了上一章说的碎片化痛点。以前接Playwright做浏览器自动化、接数据库、接本地文件、接记忆查询,四套接入方式;现在统一走MCP,配置一下就能用。
4.2 三大原语:tools、resources、prompts
MCP定义的核心原语,我理解下来就三种:
- tools:可执行的动作,Agent自主决定调用。比如"查询数据库""发送邮件""执行Python脚本"。对应的是"动手做某件事"。
- resources:可读取的数据内容,Agent按需读取。比如"读取一份文档""查询某个用户的历史记录""读取当前项目说明"。对应的是"查阅某份资料"。
- prompts:可复用的提示词模板,相当于预设工作流。比如"合同审查模板""周报生成模板"。对应的是"按照某个流程干活"。
这里有个很多教程没强调的点:记忆系统在MCP里,既可以做成resource,也可以做成tool。如果你希望Agent在回答问题前主动检索历史,那就把"搜记忆"实现成一个tool;如果你希望Agent能直接读某段历史记录,那就实现成resource。两种形态侧重点不同:
| 形态 | 优点 | 适用场景 |
|---|---|---|
| resource | 语义是"读取数据",模型理解成本低 | 情报型记忆:查看档案、查看最近记录 |
| tool | 可带参数、可执行更复杂逻辑 | 交互型记忆:搜索、写入、更新、调用外部库 |
实际做的时候,我建议"搜索"和"读取"都暴露。搜索是tool,因为带参数做检索;指定ID的历史记录读取是resource,因为就是拿一段数据。
4.3 用MCP把记忆和工具串成一条完整链路
纸面上讲原语有点抽象,直接说我目前的一个实际场景:让Agent每周写一份"项目进展周报"。
没有MCP之前,我得写一堆胶水代码:拉取git提交记录、读取项目文档、查历史周报格式、拼接prompt。现在流程完全不一样:
- Agent先调用
memory://recent这个resource,读取近两周的工作日志摘要。 - Agent调用
git_log这个tool,拉取真实的代码提交记录。 - Agent调用
search_notes这个tool,检索和本周项目相关的笔记片段。 - 最后,Agent读取
report_template这个prompt,按模板组织内容,生成周报。
整个链路里,模型自己决定"我需要读记忆、需要查git、需要搜笔记",我不需要写一行对接代码。新增一个数据源,只需要部署一个新的MCP Server,然后在客户端配置上加上即可。
这就是"从上下文窗口到MCP"的完整演化:不是把记忆塞进窗口,而是让Agent按需通过协议获取外部记忆和工具能力。
4.4 生态快照:从Playwright到逆向工具都开始接MCP
4.4 生态快照:从Playwright到逆向工具都开始接MCP
现在MCP生态扩张很快。我用浏览器自动化时,直接接playwright的MCP server就够了;社区里甚至有ida mcp、x32dbg的mcp插件,把逆向调试器也接进了MCP体系;不少桌面AI客户端(比如Cherry Studio这类)已经内置了MCP配置面板,选好server填上地址就能用。趋势很明显:工具接入正在从"每家独立对接"走向"统一协议,一次接入到处复用"。对做Agent开发的人来说,这能省下大量维护胶水代码的时间,把精力放到业务逻辑本身。
5. 实操:搭一个带记忆服务的MCP Server
理论说得再多,不如跑起来。这一节是我最近一次完整实操记录,从选型到代码到踩坑,全都写出来。
5.1 选型与架构
我的目标很简单:做一个"记忆服务",让Agent可以存笔记、查笔记、读取最近记录,并且通过MCP协议暴露给客户端。
选型上:
- 语言用Python,生态成熟,MCP SDK支持好。
- MCP服务端用FastMCP库,API简洁,接口清晰。
- 存储后端用SQLite加一个轻量向量索引。对个人项目来说,不需要上独立的向量数据库,太重了;SQLite一张表存文本,再加一个用
sqlite-vec或chromadb的向量表就够用。
架构层面其实就两个角色:MCP Client(也就是Agent的宿主),和MCP Server(也就是我们写的记忆服务)。客户端连接Server,通过协议调用tools和resources。
5.2 手写一个最小可用的记忆Server
直接看代码。以下代码基于FastMCP的常见写法,实现两个核心功能:保存笔记、搜索记忆,外加一个读取最近记录的资源接口:
from fastmcp import FastMCP import sqlite3 import json mcp = FastMCP("memory-server") # 初始化 SQLite conn = sqlite3.connect("memory.db", check_same_thread=False) conn.execute(""" CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, tags TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) conn.commit() @mcp.tool() def save_note(content: str, tags: list[str] = None) -> str: """保存一条笔记到记忆库。tags用于分类检索。""" payload = json.dumps(tags or []) conn.execute( "INSERT INTO notes (content, tags) VALUES (?, ?)", (content, payload) ) conn.commit() return "笔记已保存" @mcp.tool() def search_memory(query: str, top_k: int = 5) -> str: """ 在记忆库中搜索与query相关的笔记。 返回最近相关的top_k条记录,供Agent参考。 """ rows = conn.execute( "SELECT content, created_at FROM notes " "WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?", (f"%{query}%", top_k) ).fetchall() if not rows: return "未找到相关记录" return "\n".join(f"- [{created_at}] {content}" for content, created_at in rows) @mcp.resource("memory://recent") def get_recent_memo() -> str: """返回最近保存的十条笔记,供Agent快速了解近期记录。""" rows = conn.execute( "SELECT content, created_at FROM notes ORDER BY created_at DESC LIMIT 10" ).fetchall() if not rows: return "暂无笔记记录" return "\n".join(f"- [{created_at}] {content}" for content, created_at in rows)这里我把检索做成了最简单的SQL模糊匹配,只为了演示链路。真实项目里,search_memory里应该换成向量检索。工具描述我特意写得比较详细——这是让模型学会"什么时候该用啥工具"的关键一环,后面踩坑部分还会细说。
5.3 完整调用链路演示
写完Server后,在任意一个支持MCP的客户端里加一个连接配置,指向本地运行的Server,然后开始对话。
我用一个通用MCP客户端跑了一次真实交互,Agent的日志大致如下:
用户输入: 我昨天是不是记录过关于输出格式的想法? Agent处理路径: 1. Agent决定调用 search_memory(query="输出格式", top_k=3) 2. MCP Client 发起 tools/call, 参数: {"name": "search_memory", "arguments": {"query": "输出格式", "top_k": 3}} 3. MCP Server 执行 SQL 检索, 返回: [{content: "用户要求最终报告必须输出为JSON", created_at: "2025-12-03 10:02:21"}] 4. Agent 拿到结果, 组织回复: 是的,您在12月3日提到过,最终报告要求输出为JSON格式。这个日志很有代表性:模型并不是"读到"了所有记忆,它是判断自己需要查记忆,然后主动调用工具去查。整个过程中,记忆内容直到检索成功后才以tool结果的形式注入上下文,没有再傻乎乎地把整个库塞进窗口。
再跑一个"存笔记"的例子。用户说"记住下周三和客户开会对齐需求",Agent会判断这是"需要写入记忆",于是调用save_note。这其实就打开了"对话即记忆"的入口:不用做任何干预,Agent自己决定什么该存。
5.4 我踩过的几个坑
这套方案跑了快两周,踩过的坑不少,直接写出来帮大家绕路。
第一个坑:工具描述写得太简略,模型根本不调用。一开始我把save_note的描述写成"保存笔记",结果整个对话里它一次都没调用过。后来我把描述补成"保存一条笔记到长期记忆库,用于记录用户明确要求记住的事实、偏好、约定,比如'记住我习惯用JSON格式输出'",调用频率立刻上来了。原因就是模型靠描述判断工具用途,描述不充分,它不知道该在什么场景下用。工具描述里最好带上"在什么情况下使用"的说明,而不是只写"这个工具是干什么的"。
第二个坑:工具返回内容太多,反而把上下文冲垮。我让get_recent_memo一次性把最近100条笔记都返回过,结果上下文里全是笔记,任务被冲没影了。后面凡是resource和tool,都严格限制返回条数,能返回3条就不返回5条。工具结果是注入上下文的,返回量越大,污染越重。
第三个坑:MCP Server不是安全边界。我一开始以为MCP自带鉴权,后来查文档发现MCP Server默认信任所有连接它的客户端。本地开发无所谓,但如果你把Server部署到远程,或者接到公网服务上,必须自己加认证,否则任意客户端都能读写你的记忆库。我的做法是:本地用Unix Socket,远程用带token的header校验。别把记忆库裸奔出去,这里面存的可都是用户隐私和项目敏感信息。
第四个坑:不同客户端对MCP协议的支持程度不完全一致。有些客户端对resources支持不好,只走tools;有些老版本对部分协议字段解析有差异。我自己就遇到过,同一套Server在A客户端正常,在B客户端连接时报错。排查下来是客户端实现的协议版本差异。做法是:选一个主流客户端作为基准,Server的代码要尽量只依赖基础原语(tools),resource属于加分项。
6. 一些后续可以继续深挖的方向
把上面这套跑通之后,我又想了几个可以继续扩展的方向,留着以后折腾。
一个是让Agent自己管理记忆索引。现在是我手动把对话记录切片、向量化、入库,后续可以让Agent在每次会话结束时自动生成"本次对话值得记住的事",再写回记忆库。这样记忆质量会更高,也更像一个正在慢慢了解你的助手。
另一个是把工作流沉淀成可复用的prompts。比如我写周报这套链路,已经稳定了,完全可以把"读取最近记录、查git log、按模板生成周报"这一步流程做成一个MCP prompt。以后只要触发"写周报",Agent自动按这个流程走一遍,省掉每次重复描述任务的时间。
还有一个更实际的:把记忆库按业务域拆分。比如工作笔记和项目备忘分属不同collection,减少跨域检索时的噪声。我在一个项目上试过,把两堆笔记混在一个库里,检索时经常串味;分开之后准确率高了很多。
做Agent这一行,感受最深的一点是:"给它一个更大的上下文窗口"和"让它真的能记住"是两码事。窗口是舞台,记忆是后台的道具库,工具是登场的手脚。MCP的价值在于,它第一次让这三者有了统一的交流和衔接方式。下一步,不管你是准备给Agent加记忆、加工具、还是加工作流,从MCP入手都是成本最低的一条路。