资讯动态

Agent失忆根源与解决:从上下文窗口到MCP的完整演进

发布时间:2026/10/26 14:58:31 来源:尧图企业网站定制
如果你的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 SDKB工具提供的是HTTP APIC工具只能通过命令行。每个都得写适配层连接成本是线性的甚至是指数级的。这让我越来越觉得工具调用缺的不是让模型能选工具而是一套统一的接入协议让工具以同一种方式暴露给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、promptsMCP定义的核心原语我理解下来就三种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到逆向工具都开始接MCP4.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_threadFalse) 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_k3) 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的代码要尽量只依赖基础原语toolsresource属于加分项。6. 一些后续可以继续深挖的方向把上面这套跑通之后我又想了几个可以继续扩展的方向留着以后折腾。一个是让Agent自己管理记忆索引。现在是我手动把对话记录切片、向量化、入库后续可以让Agent在每次会话结束时自动生成本次对话值得记住的事再写回记忆库。这样记忆质量会更高也更像一个正在慢慢了解你的助手。另一个是把工作流沉淀成可复用的prompts。比如我写周报这套链路已经稳定了完全可以把读取最近记录、查git log、按模板生成周报这一步流程做成一个MCP prompt。以后只要触发写周报Agent自动按这个流程走一遍省掉每次重复描述任务的时间。还有一个更实际的把记忆库按业务域拆分。比如工作笔记和项目备忘分属不同collection减少跨域检索时的噪声。我在一个项目上试过把两堆笔记混在一个库里检索时经常串味分开之后准确率高了很多。做Agent这一行感受最深的一点是给它一个更大的上下文窗口和让它真的能记住是两码事。窗口是舞台记忆是后台的道具库工具是登场的手脚。MCP的价值在于它第一次让这三者有了统一的交流和衔接方式。下一步不管你是准备给Agent加记忆、加工具、还是加工作流从MCP入手都是成本最低的一条路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑