资讯详情

资讯详情

Agent落地实践:上下文窗口、记忆分层与MCP协议全解析

做了大半年 Agent 落地我几乎每天都会被同一个问题卡住上下文窗口。最初我把精力都放在怎么把窗口用满、怎么塞更多历史后来才发现真正让 Agent 变聪明的并不是窗口本身而是记忆和工具这两件事。等我把外置记忆和工具调用都梳理清楚又发现它们最终都指向 MCP 这套统一协议。这篇文章不念叨概念按我在真实项目里的踩坑顺序聊聊为什么上下文窗口永远不够用、记忆怎么拆层、工具怎么设计以及 MCP 在这个链路里到底扮演什么角色。1. 为什么上下文窗口越改越大Agent 却越来越笨1.1 窗口是“输入上限”不是“有效记忆上限”先明确一个容易混淆的点上下文窗口只代表模型一次能接收的 token 上限不代表它能把窗口里的每一行都同等对待。早期我们总以为只要把窗口从几千扩到几万Agent 就会像人一样“记得更多”事实恰恰相反。我在一次测试里把项目资料放到对话中部把用户问题放到最后模型竟然完整忽略了中间那段看似关键的资料。这不是模型坏的而是注意力天然偏向开头和结尾越长的上下文中间内容被“稀释”得越严重。这种效应在业界有个很形象的说法迷失在中间。窗口 10 万 token 的时候你实际有效利用的可能只有一半另一半是在给模型制造噪音。更要命的是一笔经济账。每轮请求都要把之前所有对话重新发送一遍输入 token 数随对话轮数线性增长。长会话跑到五十轮单次请求的成本和耗时已经很可观但回答质量并没有同步提升甚至因为到处是冗余信息而开始跑偏。所以“扩大窗口”只能算被动扩容它不是解决 Agent 记忆问题的手段。1.2 一股脑塞历史的代价成本、噪音、指令漂移有朋友看完我上面的话会说“那我把窗口初始化时就把系统提示词压短把所有历史按时间顺序丢进去总行吧”听起来合理踩过的坑都知道不行。第一是成本。客服机器人场景里用户反复问同样的售后规则历史记录里同一段产品说明被粘贴了七八遍。每次请求都带着这七八遍重复文本既浪费 token又让模型把注意力放到“这句话怎么在各种上下文里长得不太一样”上。第二是噪音。历史里往往夹杂着用户当时的情绪、管理员误操作、测试消息、甚至用户贴错的日志。这些内容一旦进了上下文模型分不清主次常常被带偏。我遇到过 Agent 因为把历史里几句闲聊当成工具调用的辅助信息结果连续两次输出了错误的查询参数。第三是指令漂移。早期在提示词里写死的规则随着用户消息不断插入会被模型逐渐“遗忘”。尤其当一条用户消息写得很长时模型更倾向于服从用户消息里的表述而不是最开始的系统约束。这种情况在长上下文里比想象中更常见几乎是在提示词里堆规则的大忌。1.3 一次对比实验让我彻底放弃“无限加窗口”的路线为了实际评估我做过一个对照实验同样的问答测试集分别用“全部历史入上下文”和“只保留摘要加最近 10 轮”两种方式跑。结果让我挺意外。全部历史的方式在 20 轮内准确率尚可但到 50 轮之后明显下滑模型开始混淆两个相似的历史问题而只保留摘要和最近对话的方式虽然前期准确率略低但全程曲线平稳最终反超了第一种。更关键的是后者的单轮 token 消耗下降了约 60%。从那之后我的观点就变了上下文窗口应该只装“当前任务需要的东西”历史信息和外部知识应该放到 Agent 的外部记忆里按需取用。这个思路直接决定了后面整套架构的走向。2. Agent 的三种记忆别混在一个筐里2.1 工作记忆、情景记忆、语义记忆的分工在具体工程设计里我习惯把 Agent 记忆拆成三层借用认知科学里的概念但并不严格照搬工作记忆对应当前上下文窗口里的内容处理完一轮就消失主要用来做推理。情景记忆对应完整的会话历史保留事情发生的先后顺序适合按时间和语境回溯。语义记忆对应从历史中提炼出来的事实和知识比如“用户偏好 Python”“项目的部署地址是内网某域”这类脱离原始对话也能独立使用的信息。三者的存储方式完全不同。情景记忆适合 JSON 或文档型存储按 session 维度组织语义记忆需要经过抽取、清洗后写入结构化表或向量库工作记忆则只是运行时拼装出来的一段 prompt。很多项目一开始就把所有历史一股脑写入向量库结果检索出来的全是对话碎片缺少事实归纳这在后面会花很大精力还债。2.2 检索不是装个向量库就完事语义记忆的标配是向量检索但只用向量库会漏掉两类重要信息时间和来源。举个例子用户上个月说“我喜欢在晚上运行定时任务”这个信息本身是语义记忆但如果你没有记录时间戳下个月用户说“改成白天跑”时新记忆和旧记忆冲突你根本不知道应该信哪条因为没有新鲜度可判断。我现在的做法是给每条记忆都附带元数据包括创建时间、更新时间、来源消息 ID、可信度评分。检索时除了向量相似度还会做时间衰减和来源过滤。查询脚本里还可以同时考虑结构化属性的精确匹配比如用户 ID、项目环境、对话主题。混合检索的效果通常比纯向量检索更稳。下面是一条典型语义记忆记录的示例{ memory_id: mem_20240701_001, type: semantic, content: 用户偏好 Python 3.11依赖管理用 uv, metadata: { user_id: u_10086, created_at: 2024-07-01T10:24:00Z, updated_at: 2024-07-02T09:12:00Z, source: session_7788, confidence: 0.92, topics: [language, tooling] } }“先过滤、再相似、后排序”是我常用的检索流程先用结构化字段把无关记忆排除再做向量相似度召回最后按时间衰减和置信度重排。这样既保证了精度又不会让无关用户的记忆串进上下文。2.3 给记忆加“保鲜期”和“可信度”记忆不是越多越好很多 Agent 项目死在“什么都记”。用户偶尔说一句“这个功能有点笨”你就把它当成长期偏好写进语义库之后系统在每次决策时都可能被这条噪音干扰。我的经验是写入语义记忆前先做一道门槛判断这条信息是否稳定、是否会影响后续行为、是否已经存在等价表述。如果只是闲聊情绪不写入如果是明确的偏好或事实写入前还要做一次去重合并避免同一条信息在库里膨胀成十几个变体。同时每条记忆都要有可信度。可信度来自统计来源比如被用户明确纠正过一次的事实可信度下调被用户多次复述的事实可信度上调。检索排序时可信度是排在相似度之后的权重项。这套机制不复杂但能让记忆库长期保持干净。3. 工具调用从提示词里“猜”到结构化调用的转变3.1 为什么提示词里让模型自己拼参数不靠谱刚开始做 Agent 时我用的方式是告诉模型“如果需要查天气就生成一个包含 city 和 date 字段的 JSON然后调用 weather API”。看起来简单实际跑起来全是问题。模型不是代码执行器它会猜测字段名把date写成datetime把city写成city_name也会在 API 需要数字 ID 时把用户输入里的中文名称原样塞进去。更麻烦的是它偶尔会编造成功响应明明 API 调用失败了模型反而编出一个像模像样的结果。这个问题在系统提示词里写得越隐晦翻车概率越高。结构化工具体系解决的就是这个“最后一公里”的问题。模型不需要自己拼 HTTP 请求它只需要按照工具 schema 输出参数由运行时负责校验、执行、返回真实结果。3.2 工具清单名字、描述、参数缺一不可一个工具被模型选中的概率很大程度取决于描述写得好不好。所谓的“好”不是给人类看的运维文档而是给模型看的功能说明。描述里要写清楚这个工具什么时候用、什么时候不用、参数怎么填、边界条件是什么。我常用的工具定义格式是这样{ name: get_customer_order, description: 根据订单号查询客户订单的当前状态、金额和物流信息。仅在用户提供订单号且需要查询订单详情时使用。, parameters: { type: object, properties: { order_id: { type: string, description: 客户订单号例如 ORD20240001 } }, required: [order_id] } }参数描述要具体到格式示例因为模型非常吃“看得懂的样例”。我还习惯把可能的值枚举在描述里。比如状态字段直接写明“取值范围为 pending、shipped、completed、cancelled”模型基本不会乱填。没有这些细节模型会自由发挥你只能靠出错日志去猜哪里出了错。工具数量也要克制。我见过项目把几十个工具一次性全给模型结果模型频繁选错工具。后来我把工具按场景分组在一个场景里只暴露 8 到 15 个真正相关的工具效果立刻改善。工具不是越多越好模型需要的是“在正确时间看到正确工具”。3.3 工具执行层的可靠性建设模型把参数选对只是第一步真正决定 Agent 是否可靠的是工具执行层。我在这里吃过亏的系统性工程问题有三个超时与重试。外部 API 慢起来没底线我给工具执行统一加超时控制超时后的错误也会作为结果反馈给模型让它尝试替代方案。结果截断与摘要。工具返回大段 JSON 时如果原样塞回上下文一次调用就吃掉上万 token。正确的做法是把结果做字段裁剪再针对模型需要的信息生成一段摘要。幂等与防重。模型可能因为网络抖动连续调用同一个扣费型工具这就要求底层接口具备幂等控制或者在运行时做同参数去重。工具执行层的另一个重点是反馈闭环。调用失败时错误信息要原样返回给模型而不是在运行时悄悄吞掉。模型看到明确的报错才能修正自己的下一步动作。这个设计看似简单却是很多 Agent“卡在坏循环里出不来”的根源。4. MCP记忆、工具、数据源共用一套“插座”4.1 MCP 到底解决什么问题当我们同时维护记忆服务、向量库、公司内部 API、第三方工具时最头疼的其实不是单一工具的开发而是几十种接口协议各自为政一个工具用 REST另一个用 WebSocket还有一个要老旧的 XML 接口。Agent 每接一个新数据源都要写一套适配器维护成本直线上升。MCPModel Context Protocol是一套开放的通信协议规范它的目标就是把“模型如何访问数据和工具”这件事标准化。在 MCP 的框架里Agent 是 Host模型运行环境里的客户端负责发起请求外部的数据源和工具则通过 MCP Server 提供能力。一个 MCP Server 可以被多个 Agent 复用一个 Agent 也可以同时连接多个 Server。从上下文窗口到处是 prompt 的混沌状态走到“统一协议 标准化接入”这一步非常关键。4.2 三种原语Tools、Resources、PromptsMCP 里最核心的抽象是三类原语Tools可执行的动作比如“创建工单”“查询订单状态”模型按 schema 传参由 Server 执行并返回结果。它跟前面讲的结构化工具体系是天然对齐的。Resources可读取的数据比如“用户画像”“知识库中的某篇文章”可以由模型主动读取也可以由客户端注入上下文。记忆数据完全可以包装成 Resource。Prompts可复用的提示词模板比如“工单总结模板”“周报生成模板”。它们把常用任务的结构固定下来减少每次都在 prompt 里现编的随机性。这套设计的好处是同一个 MCP Server 既可以暴露业务数据也可以暴露业务操作。Agent 侧不再关心数据源是 MySQL 还是 S3它只跟协议打交道。4.3 一个最小 MCP 交互长什么样MCP 基于 JSON-RPC 2.0 风格的请求响应。我不打算贴完整协议只说最常用的两个动作。客户端发现工具列表时会发一个tools/list请求Server 返回工具描述列表需要调用工具时客户端发送tools/call带上工具名和参数。一条典型的调用请求大致长这样{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: get_customer_order, arguments: { order_id: ORD20240001 } } }Server 返回的结果同样遵循统一结构里面包含执行结果或错误信息。模型通过客户端拿到结果后再决定继续执行工具还是生成最终回答。整个过程里模型本身不感知协议细节它只看到“我有一个工具可以用、它给出了结果”。这套抽象让 Agent 的扩展成本大幅降低。4.4 安全边界才是 MCP 落地的主战场MCP 把工具和数据接入门槛降低了也意味着攻击面变大了。一个 MCP Server 如果有文件读写能力那它就能读服务器上的文件如果它直连数据库那条连接就是潜在的数据泄漏通道。所以接入 MCP Server 时我坚持四条纪律使用最小权限身份连接数据库或云 API不把最高权限令牌交给 Agent。对资源和工具做路径白名单限制读写范围。记录所有tools/call和resources/read审计日志出现异常时能定位。优先使用本地进程内的 MCP Server远程连接时走安全通道并做认证。MCP 本身只是个协议它不会替你做安全决策。真正决定系统安不安全的是你给每个 Server 赋了多大的权限。5. 我的落地架构上下文窗口只装当前任务5.1 一次带回忆、带工具调用的完整流程把记忆、工具、MCP 拼起来之后我的 Agent 调用流程变成了一条明确的流水线用户输入到达先做意图识别判断是普通问答、检索记忆还是调用工具。从记忆系统召回相关信息包括最近对话摘要、语义记忆 TopK、结构化用户画像。把这些信息连同系统约束、上一轮工具结果一起组装成当次上下文。模型只围绕当前上下文推理如果决定调用工具输出结构化调用请求。运行时通过 MCP 统一通道执行工具拿到结果必要时截断或摘要。工具结果回到上下文模型继续推理直至生成最终回答。流程结束后异步执行记忆写入把本轮的稳定事实更新进语义库。这里面最容易忽略的是第 7 步。很多项目做完一轮对话就把结果丢了下一次用户重新说起同一件事Agent 就像失忆一样。记忆写入必须落在任务完成后而不是模型回答结束时——因为模型可能在最后一步才得到关键信息。5.2 记忆管理器的设计能让我直接“抄作业”的版本我提供一个简化但可直接扩展的记忆管理器伪代码方便理解核心逻辑class MemoryManager: def __init__(self, semantic_store, vector_index, profile_store): self.semantic_store semantic_store self.vector_index vector_index self.profile_store profile_store async def recall(self, user_id, query, top_k5): # 1. 结构化画像优先 profile await self.profile_store.get(user_id) # 2. 向量召回语义记忆 candidates await self.vector_index.search(query, top_k) # 3. 按时间衰减和可信度重排 ranked self._rerank(candidates, profile) return {profile: profile, memories: ranked} async def remember(self, event): if not self._should_store(event): return merged await self._deduplicate(event) await self.semantic_store.upsert(merged) await self.profile_store.merge(merged)重点在_should_store和_deduplicate前者过滤低价值消息后者合并重复表达。这两步做不做决定了你的记忆库是越用越聪明还是越用越乱。很多项目一开始图省事所有消息都写入结果库越滚越杂检索出来的全是噪音。5.3 效果怎么衡量指标比感觉可靠做记忆和工具改造最怕没有量化验证。我自己固定看几个指标指标名计算方式反映问题任务完成率用户任务被成功完成的占比Agent 整体有效性工具调用成功率工具成功执行次数 / 调用次数工具 schema 与执行层质量有效记忆召回率人工标注的“应召回记忆”中真正被召回的比例记忆系统质量单任务 token 消耗完成任务平均消耗 token 数上下文压缩效率无意义工具调用率未经用户确认就触发的高风险工具次数决策边界是否合理这些指标不要求一次全上但至少要在迭代前先建立起几个。否则你改一版记忆策略只会觉得“好像聪明了”无法判断是真进步还是偶然。6. 这些坑我踩过写出来希望你绕开第一个坑是把“向量库”当成记忆系统本身。向量库只解决“相似检索”它不会帮你归纳、合并、去重。真正的记忆系统是“抽取层 存储层 检索层 更新策略”的组合向量索引只是存储层里的一个组件。项目初期低估抽取层的复杂度后期几乎都要回头补课。第二个坑是给模型的工具太多但描述太差。我曾把二十多个工具全量暴露给模型模型竟然选了名字看起来像但功能完全不相关的工具。原因就是我在描述里用了太多抽象词比如“处理用户需求”模型理解不了边界。后来我把描述改成类似“当用户明确要求取消订单时使用且必须提前确认退款金额”选择准确率立刻上来。第三个坑是工具返回结果不裁剪就往上下文里塞。MCP 接入一个数据库查询工具后返回值一次性把几十行记录全带回来一次工具调用就占掉大半窗口模型后续几乎无法展开推理。解决办法是给 MCP Server 的输出做一层“结果摘要中间件”只保留模型需要的字段和结论。第四个坑是忽视记忆写入的时效性。我有一版方案把记忆写入做成同步操作结果用户对话高峰期时Agent 响应变慢因为每条消息都先去重、再嵌入、再写入。后来改成异步队列把写记忆放到后台响应速度恢复记忆质量也没有明显下降。异步写记忆时要注意时序同一个信息可能先后有两个版本入队合并在写入侧再处理一次避免旧值覆盖新值。最后分享一个我很常用的测试技巧每种记忆策略或工具配置上线前我会固定用一个包含 30 个真实用户问题的回归集跑 5 轮取平均对比任务完成率和 token 消耗。如果任务完成率上升且 token 下降说明这次改动方向对了如果只降 token 不降准确率说明还能再压如果 token 涨了但准确率没涨那基本就是方向错了。这个朴素的办法帮我避开了很多“自我感觉良好”的无效优化。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →