去年做客服Agent项目时,我碰到过一次很抓狂的线上事故:用户在第30轮对话后说“我刚才不是说过我房子的面积了吗”,Agent回了一段语义含混的话,然后自顾自地推荐起装修套餐。拉日志排查才发现,模型没有抽风,而是上下文窗口被撑满后,最早几轮对话被粗暴丢弃,用户之前明确说过的核心信息跟着一起丢了。这种“大模型上下文窗口用完了”的体验,凡是做过Agent的人应该都熟悉:官方文档把窗口写得很大,100k、200k看着唬人,可真跑起Agent来,系统提示词、工具定义、工具返回结果、模型思考过程全在抢那点空间,真正留给用户真实对话的往往不到一半。所以我现在做Agent规划时,总会先把三个词想清楚:Agent的记忆怎么做、工具调用怎么管、要不要上MCP(Model Context Protocol)。这篇文章就把我这一路的思考和实操完整写出来,适合已经跑通Agent demo、正在为上下文膨胀和工具接入头疼的朋友参考。
1. 上下文窗口为什么总是不够用:一次Agent调用的token账本
1.1 别被100k窗口骗了,可用上下文是这样被瓜分的
先打一个比方:上下文窗口相当于一张工作台面。普通Chat产品里,用户问一句模型答一句,台面上只需要放最近几轮对话。但Agent完全不同,它会往台面上摆一堆东西:系统提示词、当前所有工具的JSON Schema定义、多轮工具调用返回的完整数据、模型自己的推理草稿。我拆过一个实际在跑的Agent,把每次完整请求的日志抓出来逐一数token,一次“用户提问 → Agent调两次工具 → 给出最终回复”的闭环,光工具定义和返回结果就占了5000多个token。如果再叠加几轮历史对话,一次请求轻松破1万token。
有朋友会问:模型上下文不是有200k吗?注意,上下文窗口是“全部占用”的预算池,不是“可用对话长度”。我之前测算过一个典型请求的token分布:
| 组成部分 | 示例内容 | 大概token占用 |
|---|---|---|
| 系统提示词 | 角色设定、业务规则、输出约束 | 800 ~ 3000 |
| 工具定义 | 全部工具的JSON Schema | 2000 ~ 20000 |
| 工具返回结果 | 数据库查询、API响应明细 | 每轮800 ~ 8000 |
| 对话历史 | 用户消息、模型回复、推理过程 | 随轮次线性增长 |
| 模型输出 | 最终回答 | 200 ~ 1000 |
这就能解释为什么窗口明明很大,跑几个工具密集型任务就报警。Agent每次调用都要把工具定义重新注入,连续三轮对话下来,光工具定义和返回结果就反复占了几千token。真正留给“用户说了什么”的窗口空间,远比你想象中少。
1.2 无脑裁历史的代价
上下文满了怎么办?最常见的做法是:谁占地方删谁。早期我项目里也这么干过,直接取最后N轮对话塞进prompt,结果就是开头那场事故。更隐蔽的问题是,模型感知不到“历史被裁过”,裁掉的信息恰好是某个工具调用的前置条件时,后续推理就会建立在错误假设上。
举个例子:用户在第5轮说了“收货地址改成公司”,Agent在第一次工具调用时正确使用了新地址。到了第18轮,历史被裁剪后,Agent再次查快递时拿到的是旧地址。这种错误非常难排查,因为从模型视角看,“旧地址”这个信息并不存在矛盾,它根本不知道自己漏掉了什么。
所以我后来把“如何管理上下文”拆成三个独立问题:记忆怎么存、工具怎么接、每轮往窗口里放什么。下面先讲记忆系统。
2. Agent记忆系统:把“忘记”变成可控策略
2.1 给记忆分三格:工作记忆、语义记忆、情景记忆
做Agent记忆,第一件事是分类。我参考认知科学的思路,把记忆分成三格:
- 工作记忆:当前任务中必须保持在上下文里的临时状态,比如本轮生成的报表参数、已经选好的文件路径。特点是生命周期短、必须实时可见。
- 语义记忆:用户偏好、项目背景、领域知识,比如“用户偏好用顺丰发货”“库存低于10的SKU需要预警”。特点是生命周期长、不需要每轮都放在窗口里,按需取用。
- 情景记忆:发生过的事件,比如“昨天上午查过订单A”。特点是按时间线组织,偶尔需要回溯。
很多人做Agent记忆时只做了一种“用向量库存所有历史”,这等于把工作台上的便签、仓库里的文件、日记本全混进一个抽屉。检索时相关度排序没法区分优先级,经常把一周前的闲聊捞出来当权威信息,还占了大量token。
2.2 窗口加摘要:最省token的短期记忆方案
短期记忆处理的核心是“该退出上下文的先变成摘要再退出”。我给每个会话维护一个运行中的摘要,当对话历史超过阈值(比如3000 token)时触发一次摘要更新。摘要不是简单一句“总结一下上面的对话”,而是固定结构:
## 会话摘要 - 用户目标:... - 已确认信息:... - 已完成事项:... - 未决事项/下一步:... - 关键身份/偏好(如已知):...这样每次注入摘要只需要80到150个token,相比保留3000 token的原始历史,压缩比超过20倍。实测下来,摘要结构化的价值远大于文笔优美。只要保证四个关键字段不丢,细节丢一点完全不影响后续任务。
2.3 长期记忆的正确姿势:先提炼再入库
长期记忆直接拿原始对话分块、embedding、做向量检索,是很多教程的标准做法,但实际效果一般。原始对话噪声大,而且没有时效性概念。我现在的方法是:在Agent完成关键任务节点后,让模型通过一个小工具调用把值得记的内容提取出来,按统一格式写入。记录字段包括:
- 实体:用户/客户/项目名
- 事实:一句话或一个小JSON
- 时间戳:记录发生时间
- 置信度和来源:人工确认还是模型推断
- 失效条件:例如“该偏好仅在用户提到新地址后失效”
检索时也不只算向量相似度,还要做轻量规则过滤:失效条件是否命中、是否距今超过30天降权、是否与当前实体冲突。这几个过滤规则能有效减少“旧信息当前提”的错误。
2.4 记忆写入不要太勤快
记忆写入是有成本的。如果每轮对话结束都调一次“记忆更新”工具,token消耗是其次,更麻烦的是会产生大量重复条目。我后来把写入策略改成两种触发方式结合:
- 节点触发:用户主动修正信息、完成关键步骤、明确表达偏好。
- 阈值触发:每隔5到10轮,检查会话摘要里是否有值得沉淀的新事实再写入。
这个节奏能让记忆库保持干净,也避免给Agent主流程增加太多额外调用。
3. 工具接入是上下文囤积的第二源头
3.1 工具定义:写得越详细,烧得越快
原生Function Calling里,工具定义就是一段JSON Schema,模型每轮都要把所有工具定义读一遍。很多同学为了让模型理解得更准确,把description写得像FAQ一样长,结果工具一多,光定义就超过了系统提示词。我见过一个极端案例,40个工具,定义加起来接近2万token。每次请求还没开始干活,预算已经烧完,模型反而因为选择过多频繁调错。
合理的工具定义应该遵守“API文档思维”:
- description只写边界、触发条件、关键限制,不要写完整教程。
- parameters只写必填字段和少数高频选填字段。
- 能用enum限制的选择尽量用enum,不要开放自由字符串。
一个10个工具的域,工具定义总token尽量控制在2500以内。我在部分项目里还做过“短描述+长说明”的双层设计:短描述用于常驻上下文,长说明通过MCP resource按需获取,效果非常好。
3.2 返回结果:截断、摘要、分页三板斧
工具返回结果是大头。一个“查库存”接口返回200行明细是常有的事,直接把全部返回塞给模型,一次调用就是5000 token左右。我总结了三板斧:
- 字段投影:返回前只留下模型真正需要的字段,把status描述、备注这类大字段去掉。
- 结果分页:列表接口返回前limit到20条,并在返回里注明“共120条,当前展示前20条,如需更多可调用第2页”。
- 自然语言摘要:当模型只需要结论时,在工具端先做一次摘要,只返回“库存42件,低于警戒线的有5个SKU,分别是A01、A02……”。
这些工程手段的本质,是让工具结果从“原始数据”变成“决策材料”。做完以后我发现任务完成质量不降反升,模型对噪声更少的数据更容易做出可靠判断,上下文占用也大幅减少。
3.3 动态工具选择:别把所有工具都塞进窗口
工具数量超过20个以后,可以考虑给Agent加一层“工具路由”。具体做法是:系统提示词里只放一个工具索引,每行一句话,比如“order域可查订单/改订单/开发票,stock域可查库存/设置预警”。Agent每次调用前,先用一个轻量分类函数根据任务关键词选出3到5个候选工具,只把这几个工具的完整schema注入上下文。
这个路由可以是一个规则函数,也可以用一个小模型做分类。我实际用了正则加关键实体匹配,已经能覆盖90%的场景。这一招在工具多的时候省掉的token肉眼可见,而且因为模型不需要大海捞针,工具选错率也下降。
3.4 失败响应也要瘦身
工具调用失败时,直接把原始异常堆栈抛给模型,模型通常会在错误信息里绕圈子,白白消耗好几轮上下文。我有个习惯:所有工具统一返回结构化错误包,包含错误码、一句话原因、建议动作。例如:
{ "code": "ORDER_NOT_FOUND", "message": "订单A001不存在", "suggestion": "请检查订单号是否输入正确,或调用list_orders查看有效订单" }配合最多两次重试策略,上下文开销比满屏的原始error要小得多,排查日志也更清晰。
4. MCP:把工具和记忆移出上下文
4.1 没有MCP时,工具接入有多乱
MCP普及之前,Agent接工具基本是“一家一个标准”:OpenAI有function calling,LangChain有tool装饰器,自研框架还得造一套HTTP回调。换一个Agent框架,所有工具实现都要推倒重写。更麻烦的是,不管用什么框架,工具定义最终都要渲染进系统提示词,工具越多上下文越贵。这个问题一直没被真正解决。
MCP是Anthropic提出的开放协议,思路是把工具、资源、提示词都变成Agent外部的一个个“服务能力”,通过标准接口按需调用,而不是一股脑灌进上下文。现在Claude、OpenAI以及主流Agent框架基本都支持了MCP,它逐步成了Agent生态里事实意义上的“标准插槽”。
4.2 MCP的核心能力:Tools、Resources、Prompts
一个MCP Server对外暴露三类能力:
- 工具:可执行的动作,类似函数调用。Agent可以动态发现有哪些工具、每个工具的参数schema是什么,再按需执行。
- 资源:可读取的数据,比如用户手册、团队知识库、历史订单。Agent按需把resource内容拉取到上下文。
- 提示词模板:预置的“一键流程”,比如“客户投诉处理流程”,把标准步骤注入Agent。
我常用的比喻是:没有MCP时,每个工具都是一台独立家电,每台家电都要自己拉一根专用电源线;有了MCP,家电统一做成标准插头,往标准插座上一插就能用。对Agent来说,插座就是MCP Client,它负责发现工具、传递参数、取回结果。
4.3 一个最小可用的MCP Server
直接给一段可以跑起来的Python示例,用FastMCP最省事:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("order-server") @mcp.tool() def query_order(order_id: str) -> str: """按订单号查询订单状态和金额。参数:order_id字符串,例如A001。""" # 这里接你的数据库或接口 return f"订单{order_id}状态为已发货,金额4200元。" if __name__ == "__main__": mcp.run(transport="stdio")安装依赖并启动:
pip install mcp python order_server.pyAgent侧连上同一个MCP client后,就能自动发现query_order并调用它。从上下文压力角度看,最大的变化是:Agent不再需要把order-server的全部工具定义提前写进系统提示词,它先发一个list_tools协议请求拿到工具名和schema,再按需调用。工具多到几百个时,这种按需发现的优势非常明显。
4.4 MCP真正缓解了哪部分上下文压力
MCP解决的其实不是“token总量”问题,而是“常驻上下文”问题。工具定义不再常驻系统提示词,工具资源按需拉取,工具返回结果该截断还是得自己截断。MCP相当于替你把“工具百科”搬到了台下,Agent在台上只需要演好当前剧本,需要道具时再让场务传上来。
| 维度 | 传统Function Calling | MCP |
|---|---|---|
| 工具定义位置 | 常驻在系统提示词 | 协议动态发现 |
| 工具范围 | 通常是本地函数 | 本地/远程服务 |
| 多Agent复用 | 需要重复实现 | 一次部署,多端接入 |
| 资源/提示词模板 | 自己写死 | resource/prompts标准能力 |
| 跨框架兼容 | 差 | 好 |
这一点很关键:记忆系统同样可以做成MCP Server。比如把用户偏好、团队知识库作为resource暴露给Agent,Agent查询长期记忆和调用订单工具用的是同一种协议。原来“记忆接入”也是各做各的,现在统一成标准通道了。
4.5 生态现状:MCP已经不只是聊天工具的玩具
现在MCP生态已经蔓延到各个方向,远比很多人想象的广:
- Cherry Studio这类客户端可以通过MCP工具把流式输出写到本地文件。
- Dify平台里有人做浏览器MCP,让Agent能读取页面信息。
- 有IDE插件通过MCP连接Oracle数据库做查询分析。
- 逆向分析圈有人给IDA写MCP插件,让大模型辅助分析二进制。
- 设计工具Figma、蓝湖也有MCP通道,编码Agent可以直接拉取设计稿信息。
- 游戏引擎Unreal、工控软件TIA、Altium Designer这类专业软件也陆续出现了MCP接入。
这些案例说明,MCP的价值正在从“聊天工具接小工具”上升到“软件通过统一协议向Agent开放能力”。这也是为什么我把这篇文章的终点定在MCP:它把Agent、记忆、工具之间那层糊在一起的边界重新划清了。
5. 工程化落地:从上下文管理到MCP的演进路径
5.1 先记账:给上下文做预算
无论用不用MCP,我建议先给上下文做预算。拿一个实际Agent任务举例:
| 组成 | 预算上限 |
|---|---|
| 系统提示词 | 500 token |
| 会话摘要 | 800 token |
| 动态工具定义 | 3000 token |
| 工具返回结果 | 2000 token |
| 模型推理与回复输出 | 1500 token |
| 合计 | 7800 token |
预算设计好后,如果模型上下文是32k,你就有大约24k token的空间留给多轮对话和长任务,而不是一开始就被工具定义和历史吃光。预算表同时也是性能监控的基础:每次请求把各部分token数量打进日志,超过红线就报警并触发压缩策略。
5.2 演进路线:先记忆,后工具,再MCP
我把演进路径总结成一句话:先让记忆有序,再让工具苗条,最后让工具和记忆外置。
- 第一步:上下文记账和摘要机制。不管接不接MCP,先把“往窗口里放什么”从拍脑袋变成预算管理。
- 第二步:动态工具选择、返回结果截断、分组schema。工具数量在20个以内时,这些改造收益非常直接。
- 第三步:接入MCP。当工具跨系统复用、同一套工具要服务多个Agent时,MCP的协议收益开始超过改造成本。
我自己的判断标准是:单机demo阶段用原生function calling完全够。一旦出现“同一个工具被多个Agent复用”“工具要在不同项目里反复对接”这两类诉求,再上MCP不迟。
5.3 实践里踩过的坑
第一,不要把100个工具全部堆进同一个MCP Server。Server适合按领域聚合,订单域一个server、知识库一个server。一个server里挂太多工具,会回到schema膨胀和选择过载的老路。
第二,注意MCP Server的部署形态。stdio方式适合本地和开发环境,要处理好子进程的生命周期;远程HTTP server要加认证与限流,否则等于把内部工具暴露给整个网络环境。
第三,MCP调用是有延迟的。序列化、网络往返都要时间,高频且极简单的工具(比如字符串处理、日期转换)用本地闭包函数更合适,MCP更适合跨系统、跨语言、需要复用的工具。
第四,MCP的resource没有实时推送。数据源频繁更新时,要在上游做变更事件标记,或者让Agent按需轮询,不要指望有push消息主动通知。
5.4 一个可参考的轻量架构
最后分享我目前比较顺手的参考架构:
用户输入 → 记忆管理层 → 工具路由 → Agent核心 → MCP Client → 各领域MCP Server
记忆管理层负责在每次请求前准备好“会话摘要+相关长期记忆”;工具路由决定本次用哪一组工具定义;Agent核心只负责推理和决策;MCP Client统一执行工具调用并处理返回截断;各领域Server各自守护具体业务逻辑。
这套架构跑下来,上下文窗口才真正变成了“为当前任务准备的工作台面”,而不是装下所有东西的仓库。我觉得这就是从上下文窗口到MCP这条路上最核心的工程思维转变。
最后再分享一点个人体会。踩过上下文爆掉的坑之后,我越来越觉得,Agent工程本质上是在做结构化:给记忆分级、给工具分组、给上下文做预算、用MCP把工具和资源移出上下文。不要指望换一个更大窗口的模型就能解决所有问题——窗口再大,如果什么东西都往里塞,它最终只会变成一锅粥。先把该外置的外置,该压缩的压缩,然后再谈模型选型。按这个顺序来,Agent在长任务和工具密集型场景下会稳得多。希望这篇实践笔记能帮你少走几条弯路。