ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent记忆与工具接入:从上下文窗口到MCP的工程实践

Agent记忆与工具接入:从上下文窗口到MCP的工程实践 前段时间我搭了一个会联网查资料、会调用计算引擎的 Agent功能倒是跑通了但一进入多轮对话就原形毕露模型记不住它自己五分钟前搜到的网页结论同一个问题翻来覆去问。我一度以为是上下文窗口不够大后来把窗口从 32k 换到 128k发现治标不治本——垃圾信息倒是都被塞进去了模型反而变笨了。这也是很多人对 Agent 的最大误解把上下文窗口当成记忆把模型当成人。Agent 的记忆与工具本质上是一道工程题模型本身没有记忆所有记得都是靠输入侧构造出来的模型也不自带能力所有行动都要靠外部工具。从最开始硬编码 function calling到后来用向量库做长期记忆再到如今业内逐步形成统一的 MCPModel Context Protocol接入规范这条路径其实非常清晰。这篇文章我想把这条链路完整拆开讲一遍上下文窗口到底在干什么、Agent 的记忆应该怎么分层、MCP 解决的是哪一类问题以及我落地过程中踩过的坑和最终的实现取舍。适合正在搭 Agent、被模型老忘事和工具越接越乱困扰的开发者参考。1. 上下文窗口的账本模型能记住多少取决于你给它看了什么1.1 失忆的本质Attention 覆盖不到的地方就是不存在的很多人第一次做 Agent 时会有一个错觉模型能连续对话说明它有记忆。其实没有。大语言模型本质上是一个无状态函数给定一段 token 序列预测下一个 token。它没有跨请求的硬盘每一次推理都从你提供的上下文开始。你看到的记住上一轮对话只是应用层把历史消息重新拼进 prompt 再发给模型而已。一旦这条历史被裁剪、被摘要、被移出窗口模型对它的记忆就立即归零。这带来一个很少有人细想的推论Agent 的记忆能力其实是 prompt 工程的一部分。你能让模型记住多少取决于你愿意在上下文窗口里放多少东西以及这些东西排列得是否清晰。上下文窗口不是人脑中的长期记忆它更像一块会议白板Agent 每次处理请求时只看得到白板上现在写着什么。你没写上去的内容对它来说等于不存在。我实际做项目时遇到过最典型的情况Agent 执行一个多步骤调研任务第一步从某个知识库返回了十条结论第二步模型需要基于其中第三条做进一步查询。如果中间插入了大量无关的日志、中间输出、工具返回的原始 JSON模型就会把关键结论淹没。它的失忆并不是坏了而是注意力被稀释了。1.2 长上下文是租来的内存不是自己的硬盘那么把上下文窗口做大不就能解决了吗我一开始也是这么想的实测下来发现事情没有这么简单。先说成本账。输入 token 是按量计费的窗口越大你塞进去的内容越多单次调用成本线性上升。128k 窗口不是说你可以随便塞满 128k 还保持体验不变——KV Cache 会显著占用显存服务端处理长上下文的延迟会明显增加。我在项目里对比过同样一个任务塞 2k 上下文时首 token 基本秒回塞 50k 上下文时首 token 延迟直接翻了三四倍。对于高并发的 Agent 服务这个差距会直接把用户体验拖垮。更隐蔽的问题是注意力稀释。自注意力机制虽然理论上可以覆盖任意两个位置但序列越长每个 token 能分配的有效注意力越有限。业界常说的迷失在中间现象指的就是模型对长文本中部位置的依赖能力显著弱于开头和结尾。你可以理解为白板越写越满最新的字盖住了旧内容但旧内容也没被真正擦掉——它们只是变成了一堆难以辨认的笔迹全是噪音。所以我的结论很明确长上下文是租来的内存不是自己的硬盘。它能让你临时处理一大段材料但不能作为 Agent 的记忆基础设施。真正的长期记忆必须放到模型外部按需检索、按需注入。1.3 上下文膨胀是推理质量的隐形杀手这一点我是在一次调试中意外发现的。当时我在一个 Agent 里塞入了非常多历史会话记录想让它记住用户的偏好。结果发现模型在回答一些简单问题时反而开始答错。反复排查后我意识到上下文里塞入了之前某轮模型自己的错误分析新的一轮里它看到这段内容后倾向于顺着这个错误方向继续推理。这其实是 LLM 已知的通病模型非常容易受上下文中的错误中间结论影响甚至会把你不经意写进 prompt 的假设当成事实。上下文越臃肿这种信息污染概率越高。你为了让模型多记住一点塞进去的每一段无关文字都在稀释它真正需要关注的目标信息。这个教训让我开始思考一个问题如果不能无限依赖上下文窗口那 Agent 的记忆到底应该放在哪里、以什么形式组织这就引出了后面要讲的分层记忆方案。2. Agent 记忆的三层结构短期、长期与工作记忆怎么拆我把 Agent 的记忆拆成三层来设计短期记忆、长期记忆、工作记忆。这个拆法不是学术定义是纯工程实践得出的结论——因为它能直接指导我怎么管理 prompt 和外部存储。2.1 短期记忆你正在给模型展示的内容短期记忆就是每次请求时真正进入上下文窗口的那部分内容。它应该尽量精简只包含四类信息系统指令Agent 的角色、目标、行为约束这部分恒定不变。当前用户请求本轮要完成的任务。必要的检索结果从长期记忆或工具侧拿回来的、和当前任务直接相关的片段。最近的推理轨迹模型上一两步做了什么、得到什么结论保证多步任务不重复。很多人在做多轮对话 Agent 时习惯把全部历史消息都塞进上下文这是最典型的过度设计。实际更有效的做法是滚动窗口加摘要压缩保留最近 N 轮完整消息更早的历史先让模型总结成一段摘要随消息一起传入。这样既保留了大方向又不会让上下文无限膨胀。短期记忆的设计目标是让模型每轮看到的上下文都聚焦、干净、够用。它不是简单的时间切片而是一个经过筛选的信息包。2.2 长期记忆外部存储与召回长期记忆是我存放 Agent 积累下来的事实、结论和用户偏好的地方通常放在模型外部按需检索。它解决的问题很直白上下文窗口放不下所有东西但 Agent 又想记住足够多的背景。当前工程上最主流的长期记忆方案是向量检索加关键词检索的混合召回。基本流程是把有价值的对话摘要、任务结论、用户偏好切块用 embedding 模型转成向量存入向量数据库需要时用当前问题去检索相似片段取 top K 回来作为上下文的一部分注入短期记忆。只做向量检索有个明显的坑向量检索擅长语义相似但不擅长精确匹配。用户上次提到一个设备 ID XJH-2309这次问上次那个 XJH 设备怎么配置向量检索很可能抓不到。所以我现在做长期记忆一定是双路召回一路 BM25 关键词一路向量相似度把两路结果合并去重后再统一排序。长期记忆还需要考虑生命周期管理。不是所有内容都值得永久保存。我会给每条记忆打一个重要度分数定期清理低于阈值的内容并对高频出现的重复记忆做合并。否则向量库很快会被大量无价值的碎片填满检索质量直线下降。2.3 工作记忆给 Agent 的草稿纸工作记忆是我在调试 Agent 时单独引入的一层专门存放当前任务进行中的中间状态已经确认的事实、尚未验证的假设、接下来要执行的步骤、已经尝试过但没有走通的路径。这层记忆不一定要进 long-term store它只需要在单次任务会话中存活。实现方式可以是一个结构化 JSON 文件、一个临时缓存或者直接存在任务状态对象里。它的价值在于当 Agent 在工具返回结果后需要重新规划下一步时可以直接读取工作记忆中的已完成清单避免重复劳动。任务结束后我会把工作记忆里值得保留的结论摘要写入长期记忆其余清空。三层记忆的分工可以用下面这个表概括记忆类型存放位置生命周期典型技术使用时机短期记忆上下文窗口内单次请求滚动窗口、摘要压缩每一轮模型调用工作记忆会话状态/缓存单次任务JSON 状态、任务上下文多步任务执行中长期记忆外部存储跨会话持久向量库关键词重排任务开始时主动召回这个分层设计解决了我第一个大问题模型不再失忆。但紧接着暴露了第二个问题——工具接入越来越乱。3. 工具接入的十字路口从 function calling 的临时工到 MCP 的标准化接口3.1 function calling 模式每接一个新工具都要改一遍 Agent早期做 Agent 工具调用主流做法是 function calling给模型声明一组函数的 JSON Schema模型根据对话内容输出一个函数名加参数Agent 侧拿到后分发执行再把结果拿回来拼进上下文。这个模式在小工具数量下很好用。但当一个 Agent 需要同时接十几种工具——查天气、查订单、读数据库、操作文件、调内部 API——问题就开始爆发。首先是协议不统一。每个工具背后的服务都有自己的调用方式有的走 HTTP 接口有的连数据库有的读本地文件。Agent 侧必须为每一种工具写一段专门的接入代码工具一变Agent 代码也得跟着改。其次是描述不统一。每个工具的 function schema 是各写各的命名规范、参数格式、错误返回都随缘模型经常会根据模糊描述选错工具。最头疼的是权限隔离和状态管理不同工具需要的认证方式、上下文环境都不一样全部耦合在一个 Agent 进程里排查问题极其痛苦。这就像你把所有电器直接焊在墙里的电路上而不是用统一的插线板。加一个电器就要重新布线时间久了没人敢动。3.2 MCP 的协议骨架client、server、transport 与 JSON-RPCMCP 想解决的就是这个插线板问题。它的核心思路是定义一套通用协议让 Agent 作为 MCP 客户端外部工具和数据源作为 MCP 服务端两者通过标准消息交互。任何工具只要实现一个 MCP server就能被任何支持 MCP 的 Agent 客户端直接使用不需要为每家单独适配。我理解 MCP 的架构主要看四个概念Client/Server 模型Agent 侧是 MCP client负责发起请求和使用工具工具侧是 MCP server负责暴露能力。传输层MCP 支持本地进程通信stdio和远程 HTTP/SSE 两种模式。本地工具可以直接挂载成子进程远程服务可以通过 HTTP 暴露。消息格式底层用 JSON-RPC 2.0消息结构化便于解析和调试。能力协商初始化阶段 client 和 server 各自声明支持的能力比如 server 支持哪些工具、是否提供资源模板、是否支持采样回调等。一次完整的工具调用流程大致是Agent 发起 initialize 握手协商双方能力然后通过 tools/list 获取当前 server 暴露的全部工具清单需要执行时通过 tools/call 传入工具名和参数server 返回结构化结果Agent 再把结果整合进上下文继续推理。直观感受就是新增一个工具 新增一个 MCP server。Agent 主流程完全不用动它只是通过协议去发现和调用能力。我后来把一批内部接口封装成 MCP server 后再接入新工具时基本只写 server 内部逻辑Client 侧连重新部署都不需要。3.3 MCP 真正解决的三个问题发现、协商、复用很多文章讲 MCP 只讲它是 AI 界的 USB我觉得这个类比不够准确。USB 解决的只是接口统一MCP 解决的是三个更深的问题第一是发现。传统 function calling 模式里一个 Agent 能用什么工具是写死在代码里的。MCP 让工具变成可查询的清单Agent 运行期就能看到 server 提供了什么能力甚至可以根据任务动态加载不同的 server。第二是协商。每个工具服务能力不同MCP 通过能力协商机制让双方明确边界。比如一个 server 可能只支持读取文件而不支持写入初始化时就把这个边界说清楚Agent 就不会试图调用一个不存在的方法。第三是复用。因为协议统一了同一个 MCP server 可以被不同 Agent、不同模型、不同前端复用。社区里已经出现大量通用 MCP server数据库、文件系统、浏览器操作都有现成的不用每次从零造轮子。这一点对工程效率的提升是巨大的。传统 function calling 和 MCP 的差异我列在下面这张表里维度传统 function callingMCP工具清单写在 Agent 代码里server 通过 tools/list 动态暴露协议各家自定义 JSON Schema统一 JSON-RPC 2.0新增工具修改 Agent 代码并重新部署新增一个 MCP server跨客户端复用不可复用一套 server 多端复用能力协商无初始化阶段明确 capabilities4. 记忆系统与工具的握手MCP 生态里的一个正循环4.1 把记忆仓库变成 MCP Server让回忆也成为一个可调用工具当我把工具接入统一到 MCP 之后突然意识到一件事记忆本身也可以作为一种工具暴露出来。传统思路里长期记忆是从外部注入 prompt 的先检索再把结果拼进上下文。但这样做的媒介是prompt 拼接记忆系统和主流程深度耦合。而如果我写一个 memory MCP server把“召回记忆”“写入记忆”“删除记忆”都注册成标准工具Agent 就可以像调用其他工具一样主动去回忆。这个转变的意义在于解耦。我可以在 memory server 内部随意更换存储引擎、检索策略、重排逻辑对 Agent 主流程完全透明。今天用开源向量数据库明天换云上的托管服务只要 tools 的接口不变主流程一行代码都不用改。实际设计中我暴露了这几个工具recall按语义和关键词混合召回相关记忆片段。remember写入一条长期记忆附带重要度评分。update_importance手动调整某条记忆的重要性用于后续清理。forget按 key 删除指定记忆。4.2 工具调用前后的记忆写入什么时候值得记一笔有了记忆 server下一步要考虑的就是写入时机。工具调用本身会产生大量数据如果全部写入长期记忆很快会污染记忆库。我总结了一套自己的写入策略概括成三个值得记值得记的是工具返回的结果摘要。比如查订单接口返回了 200 行数据不应该把 200 行都存进去而是让模型或规则提取出关键字段——订单号、状态、时间、下一步操作——压缩成一两句话存进记忆。值得记的是有明确实体指向的事实。用户说我的服务器编号是 abc123其实在南京机房这就是一条高价值长期事实后续任何提问都可以随时召回。值得记的是关键决策及原因。Agent 在某一步为什么选了这个方案而不选另一个这种推理轨迹在后续多轮任务里非常有用能避免模型重复纠结。不值得记的包括临时性的中间计算结果、一次性的报错详情、已经被后续信息覆盖的过时状态。我给每条待写入记忆设一个重要度阈值低于阈值的直接丢弃。别小看这个过滤动作它决定了记忆库长期可用性。4.3 编排层的两级检索策略先宽后窄有了记忆 server 和 MCP 工具体系最后的关键一步是编排层怎么把它们组织起来。我的实践是采用两级检索策略。第一级是任务启动时的宽召回。用户发来一个请求Agent 先用这个请求去 memory server 里召回 top K 相关片段同时把它转成系统提示词的一部分。这里的 K 可以设得稍大一些比如 20 条确保不遗漏关键背景。第二级是工具调用过程中的窄注入。当模型决定调用某一个工具并拿到返回结果后编排层根据当前任务状态把 20 条宽召回结果压缩到最相关的 5~6 条连同工具结果一起拼成一个上下文包送给模型做最终决策。这样做的原因是宽召回保证信息覆盖窄注入保证决策聚焦。如果模型在决策阶段看到的是 20 条记忆加上大段工具原文它反而不知道怎么取舍。上下文包的最终大小直接影响推理质量这个我在前面讲上下文窗口的账本时已经踩过一遍了。这种记忆服务化、工具标准化、编排轻量化的结构是我目前认为最可持续的 Agent 架构外挂记忆不绑死模型工具接入不绑死代码Agent 主流程始终保持简单。5. 落地参考实现与三个必须接受的教训5.1 最小可用记忆系统的模块清单如果你要照着做一套自己的记忆系统不需要一开始就上特别复杂的架构。我建议从下面这几个模块起步嵌入模型用开源的通用 embedding 模型即可中文场景注意选对分词和向量维度。存储引擎数据量小可以用轻量级向量库数据量上来再考虑分布式方案。关键词索引保留一套倒排索引做 BM25 关键词召回专门补向量检索的短板。摘要服务用同一套 LLM 接口做对话摘要和结果压缩没必要单独引入大模型。MCP server 壳把上述能力封装成标准工具暴露给 Agent 客户端。编排层决定什么时候调 recall、调完放哪里、多长的窗口算合理。这套最小系统搭起来之后大部分问题出在细节调优上而不是架构选型上。这个我很有发言权因为我在调优阶段踩过的坑比搭架构阶段多得多。5.2 一段示意代码MCP 工具暴露记忆查询下面这段是伪代码用来表达 MCP 工具注册的核心逻辑。真实 SDK 的语法有差异但思路完全一致把记忆操作声明为 schema注册进 server。# 伪代码MCP 工具注册的核心逻辑 def register_tools(server): # 召回记忆语义 关键词混合检索 server.register_tool( namerecall, description按语义与关键词混合召回长期记忆片段, input_schema{ query: string, top_k: integer, filter_entities: array }, handlerrecall_handler, ) # 写入记忆先摘要再存避免存入脏数据 server.register_tool( nameremember, description写入一条长期记忆importance 用于后续清理, input_schema{ content: string, importance: number, source: string }, handlerremember_handler, )recall_handler 内部做的事情并不复杂先把 query 转成向量在向量库中做 ANN 检索同时把 query 去掉停用词后做 BM25 检索两路结果合并按分数加权排序取前 top_k 条返回。remember_handler 则会在写入前调用摘要模型把 content 压缩到 200 字以内再用 embedding 模型转向量连同原文、时间戳、重要度一起写入存储。这条设计的核心思想是任何记忆操作都走标准化工具接口Agent 主流程不需要知道记忆系统内部用了什么技术。我在调试阶段也确实因为这种解耦节省了大量时间——换存储引擎、调检索参数都不影响上层逻辑。5.3 必须接受的三个教训最后分享三个我在实际项目中反复踩过的坑每一个都付出了不少调试时间。第一个教训是不要无差别全量注入记忆。最开始我做记忆召回习惯把检索出来的所有相关片段全部塞给模型期望它自己挑重点。结果模型经常被无关记忆带偏回答质量反而下降。后来我严格限制每次注入的记忆片段数量并且每段都标注来源时间和与当前任务的关系效果立刻改善。第二个教训是工具返回的原始数据永远不要直接拼接进上下文。工具接口返回的 JSON 往往又长又乱字段命名也不是给模型看的。直接拼接不仅浪费 token还会让模型在理解字段含义上反复失误。正确做法是返回结果先做字段裁剪和重命名必要时让模型先做一次结果摘要再把摘要放入上下文。第三个教训是embedding 检索对精确 ID 和数字无效。我遇到过很多次用户问刚才那个编号是多少模型却从记忆里召回了别的编号。后来我强制要求所有写入记忆的内容必须保留结构化字段比如 entity_id、order_no检索时先用关键词精确匹配这些字段再结合向量检索问题才解决。这个双路召回的设计不是锦上添花而是刚需。我在实际开发里还有一个很深的感觉先把记忆边界画清楚再去接工具顺序很重要。上下文窗口只是初始白板MCP 只是把工具统一成了插线板真正让 Agent 好用的是背后那个决定该记住什么、该调什么的编排逻辑。我的习惯是先写一份简单的 memory policy——什么进长期、什么留在会话、什么直接丢弃——然后才动手写 Agent 主循环。很多项目做到后期变得不可维护根源往往就是这一步省了。如果你正在搭类似的 Agent不妨也先花半天时间把这层边界想清楚再写代码。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进