首页 / 资讯中心 / 文章详情

基于MCP协议的上下文工程:高效解决高Token消耗问题

基于MCP协议的上下文工程:高效解决高Token消耗问题 ★ FEATURED ARTICLE
上个月我帮某团队复盘一个内部Agent的账单发现光是Token费用就比预估高出三倍。翻调用日志时问题一目了然每一轮请求都在反复搬运全部工具定义工具返回的原始JSON也原封不动塞回上下文历史对话越滚越长。这不是模型变贵了而是上下文工程没做对。后来我们把底层通信切到MCP协议整个上下文的组织方式才真正理顺Token消耗直接砍掉一半以上。这篇文章就把这套思路完整写出来包括MCP协议的设计逻辑、具体的Token账目对比、可落地的工程手法以及我实际踩过的那些坑。标题里提到的“上下文工程”我多说一句它跟Prompt Engineering不是一回事。Prompt Engineering管的是“某一次对话怎么写指令”上下文工程管的是“整个系统在每一轮请求里放什么进上下文、放多少、怎么更新”。前者写得好只是下限高后者做得差会直接烧穿预算。本文适合正在做LLM应用、Agent、RAG项目的开发者看尤其是那些每天接到大模型账单就头疼的人。以下内容全是围绕“基于MCP协议的上下文工程高效解决高Token消耗问题”这个标题展开的没有任何隐藏的废话直接上干货。1. Token消耗失控的三宗罪工具定义、历史翻倍与结果轰炸在聊MCP协议能解决什么之前得先把问题拆清楚。我见过太多项目一上来就喊“Token太贵了”实际上是把三件事混在了一起而这三件事的解法完全不同。1.1 静态工具清单每轮都在背整本菜单大多数Agent应用都用了模型自带的函数调用能力。开发者的习惯是把所有工具定义写成一个静态列表每次请求都原样传给模型。早期工具少的时候问题不大等工具数量涨到五六个、七八个麻烦就来了。我算过一笔账一个平均规模的工具定义包含函数名、描述、参数JSON Schema少说150 Token多则500 Token。如果你有10个工具每轮请求就是2000到5000 Token的固定开销而且这还不算模型回复里带出的函数调用内容。打个比方你每次去面馆只打算点一碗葱油拌面但服务员每次都要把整本菜单三页纸全部背诵一遍。更麻烦的是点完这碗面之后你聊了五分钟其他话题服务员又开始背第二遍菜单。模型在每一轮都要重新接收这些工具定义它们占据了上下文窗口的固定空间同时又稀释了真正有效的信息密度。在传统函数调用模式下这几乎是无解的。因为模型需要知道“有哪些工具能用”它才能决定“下一步调哪个”。于是开发者只能牺牲Token效率来换取工具调用的正确率。这个矛盾是上下文工程面临的第一个结构性困难。1.2 对话历史越滚越大正确的上下文反而被稀释第二个问题是多轮对话与多步工具调用叠加造成的历史膨胀。Agent跟传统聊天机器人的区别在于它经常要分多步完成一个任务先查数据库再分析结果再更新工单状态再给相关人员发通知。每一步的中间结果都会作为历史上下文传给下一轮。假设一次数据库查询返回100行JSON约1500 Token。单看不算多但一次Agent任务往往要查五六次加上中间推理产生的文本几轮下来上下文就堆到一两万Token。等任务进行到后面模型真正需要关注的可能是最开始那条告警的时间戳但它已经在过去三轮里被大量JSON字段淹没了。更隐蔽的是这些历史内容并不是一次性全部被模型“想起”的。模型对超长上下文的注意力会衰退越晚的Token越容易让模型忽略早期关键信息。所以历史膨胀不只是贵的问题还直接降低任务完成质量。很多团队以为这是模型能力不够实际上是把上下文工程做砸了。1.3 工具返回原样回灌为无关字段买单最后一个问题在日志里最显眼工具返回什么开发者就把它当消息内容原样塞回模型。数据库查询返回了50个字段模型其实只需要其中3个文件列表返回了300条记录模型关心的可能只有异常状态的几条。这不只是浪费还会把模型的推理带偏。冗余字段里有大量无关信息模型需要额外消耗注意力去分辨哪些该关注。结果就是既要为大量无意义Token买单又要承担推理质量下降的风险。把这三宗罪放一起看核心矛盾就清楚了我们不是在为“模型思考”付费而是在为“上下文的搬运和组织方式”付费。而MCP协议正好给出一套标准化的组织方式。它不改变模型本身的能力改变的是外部工具和数据源接入上下文的方式。2. MCP协议凭什么进入上下文工程标准接口、三个原语、一次拨号MCP全称Model Context Protocol是一个应用层的开放协议目标是把“模型应用与外部数据源、工具之间怎么交换上下文”这件事标准化。我最初以为它只是又一个RPC框架深入了解后发现它的价值在于改变了上下文从哪来、怎么取、取多少。2.1 MCP其实在解决“上下文插座不统一”的问题在MCP出现之前Agent要接一个数据库写一套查询封装接一个办公软件又写一套认证和消息解析每接一个外部系统都是一次定制开发。更关键的是这些外部系统吐出来的数据格式五花八门模型应用的上下文管理逻辑也被这些格式绑架了。MCP做的事情很像是给所有外设统一了一个USB-C口。它定义了Host模型应用主程序、Client协议客户端、Server工具和数据源的服务端三层结构。Host通过Client与Server建立会话Server把自己的资源、工具、提示模板暴露出来Host按需取用。无论Server背后到底是什么系统交互方式都由协议统一描述。这跟上下文工程有什么关系关系非常大。当上下文来源变成“可枚举、可拉取、可协商”的标准化接口之后你就不是在跟一堆杂乱的数据格式搏斗而是在一个清晰的协议框架内做资源调度了。省Token的本质就是在做这种调度优化。2.2 三个原语Resources、Tools、PromptsMCP Server对外暴露的能力可以归成三类理解它们就能理解MCP省Token的基本盘。Resources资源是“有上下文的可读数据”按URI标识比如数据库的表结构、特定项目的说明文档、某个时间段的日志文件。Host可以主动获取一段资源的文本也可以订阅资源的更新。这一块对应的是RAG场景里“动态注入哪些背景知识”的需求。Tools工具是可执行操作模型决定调用Server执行后返回结果。这个跟函数调用接口类似但它放进了协议层并且Server可以在返回时只交回精炼后的文本结果。Prompts提示模板是可复用的提示词片段由Server预置。Host可以把这些片段直接作为上下文的一部分送给模型比如“如何解读慢查询报告”的模板。它解决的是“同一类任务的指令重复编写”问题。这三个原语合起来给上下文工程提供了一套标准词汇表你要背景知识去Resources里挑你要能力调用去Tools里选你要固定话术去Prompts里取。每一项都能独立按需获取不需要像传统函数调用那样把整个能力清单每轮都扛在肩上。2.3 “问一下再取”的按需模式是省Token的重要前提真正让我觉得MCP值钱的地方是它的“能力发现”是协议的一等公民。Host连接Server之后可以发起initialize握手协商双方支持的能力之后可以随时用listTools、listResources之类的标准方法询问Server当前暴露了哪些能力。这个机制本身不强制“按需”它提供了这种可能性。你完全可以在Host端根据当前对话意图只把本轮相关的工具子集注入模型上下文。我实际做的时候在Host和Server之间加了一层意图路由用模型上一步的决策来判断“目前可能用到哪些工具”然后只把这一小撮工具的定义交给模型。效果立竿见影。这个模式之所以重要是因为它把“工具选择的控制权”从全量静态清单变成了基于运行时状态的动态决策。与其说是MCP里有什么魔法功能直接帮你省Token不如说它给了每个开发者一把趁手的扳手让你有地方下手去做原本做不动的优化。3. 同一款Agent的Token账单用MCP前后的对比算账口说无凭直接上账单。我用一个真实改造过的内部Agent项目作为例子项目不方便细说细节但结构跟多数Agent应用一致一个负责运维告警处理的助手能查数据库、查监控指标、发通知、更新工单。改造前用的是传统函数调用改造后切到MCP协议。3.1 场景设定与计算口径模拟场景是这样一段对话用户昨晚有没有大规模告警查一下核心服务最近的QPS和错误率如果有异常就通知值班同学顺便把工单状态改成处理中。Agent完成这个任务大约需要5轮请求交互其中4轮会实际触发工具调用。涉及的可用工具共8个平均每个工具定义约280 Token。我统计的是“输入侧Token”也就是每次请求发往模型的Prompt部分。输出侧Token因为模型能力差异比较大不在这次对比范围。为了让数据可复现我把计算口径写清楚每轮都要带上系统提示词每轮都要带上模型决策所需全部工具定义历史上下文按上一轮生成的完整消息序列计算工具返回结果按实际消息内容进入下一轮计算。3.2 两种方案的Token构成对比先看传统函数调用方案。8个工具定义平均280 Token一个每次请求都要把全部定义发给模型等于每轮固定的2240 Token系统提示词1200 Token加上逐渐累积的对话历史和工具返回结果。5轮下来大致账目如下表。开销项传统函数调用方案5轮MCP方案5轮系统提示词1200 × 5 6000900 × 5 4500工具定义2240 × 5 11200840 × 5 4200历史对话与中间结果约 5600约 4000工具返回结果原样 vs 压缩约 4800约 720合计约2760013420MCP方案里工具定义开销不是2240而是840原因很简单不是每一轮都需要8个工具。第一轮只需要查询权限第二轮只涉及工单更新第三轮才需要通知能力。通过按意图裁剪平均每轮只需要暴露3个工具定义。工具返回结果的差异来自我在Server端做了一层压缩。数据库查询原本返回完整JSON约1200 Token压缩后只保留时间戳、服务名、错误率、QPS几个关键字段的简洁描述约180 Token。4次工具调用算下来从4800压缩到720。3.3 真正省在哪里以及水分在哪里从表格看MCP方案大约省了51%的输入Token。但我必须诚实说一句这里头有一大半功劳不是协议本身的而是“按需暴露”和“返回压缩”这两个动作带来的。MCP的作用是让这两个动作有了规范化、可复用的落点。你完全可以在传统函数调用里手动实现类似的裁剪逻辑只是每换一个工具源就要重新写一套而MCP把这一切变成了标准流程。水分在哪里在于摘要压缩会损失信息。我后面会专门讲这个坑先提醒一句省Token不能无脑省压缩后的内容必须能支撑模型做出正确决策。另外MCP方案的5000 Token左右历史开销里有一部分是握手和工具发现产生的元数据这部分不直接算进模型Prompt但会在实际使用中占用请求连接的时间。如果你频繁开关连接这个固定开销会抵消一部分收益。4. 基于MCP的上下文工程落地按需暴露、服务端压缩、会话亲和聊完理论讲讲真正动手时怎么做。这一节是最核心的实战部分包含一套我验证过的方案Server端工具骨架、按意图裁剪工具定义、上下文分桶、以及会话亲和的上下文管理。4.1 服务端骨架示例工具注册与返回前压缩我用某个开源MCP Python SDK做了一个极简Server骨架逻辑上可以直接迁移到其他语言的SDK。核心就两件事定义工具暴露哪些能力以及在返回结果前把大JSON压缩成精炼摘要。# 伪代码基于开源MCP SDK的服务端骨架 from context_agent import McpServer, tool, resource server McpServer(nameops_agent_server) # 一个普通的工具定义查询慢查询日志 tool(description查询指定时间窗口内的MySQL慢查询返回超阈值SQL摘要) def query_slow_log(start_time: str, end_time: str, threshold_ms: int 200): rows mysql_client.fetch_slow_logs(start_time, end_time, threshold_ms) # 关键不让完整JSON直接进入上下文 summary summarize_slow_logs(rows, max_items5, max_fields4) return summary # 摘要函数只保留模型做决策需要的关键字段 def summarize_slow_logs(rows, max_items5, max_fields4): items [] for r in rows[:max_items]: items.append({ ts: r[ts], sql_len: r[sql_len], exec_ms: r[exec_ms], sample: r[sql_text][:80] }) return json.dumps(items, ensure_asciiFalse)你在真实项目中可能要面对比这复杂得多的工具结果比如云盘文件列表、告警聚合、监控指标序列。我的建议是给每个工具单独写一个_summarize函数不要搞一个万能裁剪器。因为不同业务里“哪些字段对模型决策有用”完全不同万能裁剪的结果往往是保留了一堆无意义字段。4.2 按意图裁剪工具定义ListTools装饰器协议里Host用listTools方法询问Server有哪些工具默认实现通常是全量返回。我在Server端对listTools做了装饰器逻辑根据当前请求附带的意图标签做过滤。这样Host拿到的工具清单本身就已经是缩小的。# 伪代码按意图过滤listTools返回 CURRENT_INTENT_TOOL_MAP { query_performance: {query_slow_log, query_metrics}, handle_alert: {query_metrics, send_notification, update_ticket}, report_and_notify: {send_notification, generate_report}, } def filtered_list_tools(current_intent: str): all_tools server.get_all_tools() if current_intent not in CURRENT_INTENT_TOOL_MAP: return all_tools allowed CURRENT_INTENT_TOOL_MAP[current_intent] return [t for t in all_tools if t.name in allowed]注意这里的实现思路Host在调用listTools时带上当前阶段的目标比如“查询性能”“处理告警”Server返回对应的工具子集。当然如果你的MCP客户端不支持传这个参数你也可以在Host端做同样的过滤逻辑拿到全量清单后在注入模型上下文前只挑一小部分。关键不是放在哪一端而是一定要有一个“根据运行时状态裁剪工具定义”的动作。这个裁剪带来两个直观收益每轮请求的Token基数变小模型在决策时面对的选择集更聚焦误调用无关工具的概率也降低了。4.3 上下文分桶常驻桶、按需桶、临时桶我在服务端维护了一套上下文分桶策略算是这次改造里最稳定有效的部分。分桶的核心思想是不是所有上下文都值得每轮带着把上下文按“时效性”和“重要性”分三类。常驻桶系统提示、核心指令、输出格式要求。这些每轮都要但必须被压缩到极简。我明确要求团队把系统提示控制在500 Token以内超了的砍到只剩行为边界和最必要的运行约束。按需桶当前步骤要用的工具定义、当前任务相关的文档片段、配置信息。这桶数据量最大也是最值得做按需裁剪的地方。它不应该被当作常驻内容而要像“抽屉”一样用到才拉开。临时桶工具返回结果摘要、最近两轮对话历史、模型中间推理内容。这类内容用完即弃我采用滑动窗口只保留最后2到3轮更早的历史做摘要后不再完整保留。这套分桶策略最大的价值是让你在评审时一眼就能看出“什么东西不该出现在这里”。比如某次我发现一个Agent的上下文里混进了一整份项目wiki页面这就是典型的没做分桶管理内容全塞常驻桶了。4.4 会话亲和用状态ID替代重复搬运最后一个是会话亲和的设计。很多Agent应用会在Server端维护内部状态比如当前的告警上下文、已查询过的服务列表。传统的做法是把这些状态塞进对话历史跟着每一轮来回搬运既浪费Token又会因为状态的表述不一致产生混乱。我的做法是在MCP Server端维护一个会话状态对象给这个对象生成一个短ID客户端只需要在下一次调用时带上这个IDServer就能把之前算好的中间状态恢复出来。代码上大概是这样的# 伪代码会话状态ID机制 server_state {} def _get_or_create_state(session_idNone): if session_id and session_id in server_state: return server_state[session_id], session_id new_id generate_short_id() server_state[new_id] {alerts_seen: [], services_monitored: []} return server_state[new_id], new_id tool(description查询服务监控指标支持复用上一次查询上下文) def query_metrics(service: str, session_id: str None): state, sid _get_or_create_state(session_id) metrics metrics_client.fetch(service, sincestate.get(last_since)) state[last_since] metrics[last_ts] return summarize_metrics(metrics), sid这样一来上下文中需要携带的只有一个短ID而不是一大段先前的查询条件与中间结果。要注意的是Session ID会引入额外状态管理的复杂度如果服务端是无状态的部署形态需要考虑把状态落到外部缓存。但大多数内部Agent项目都是单实例部署这个代价很小。5. 别把MCP当银弹实测中踩到的边界与坑MCP协议在这个体系里解决了很多问题但也绝对不是什么银弹。改成MCP之后我踩了好几个坑有些是协议本身的特性有些是自己在设计时没想到的。5.1 Server一多元数据照样会重新膨胀一开始我按“每个工具源一个Server”来组织结果系统里起了四五个Server。本来以为按需裁剪已经很省了没想到握手时每个Server的name、description、工具列表元数据加起来也有大几百Token而且模型要跨Server决策时每个Server的能力背景还得在上下文中解释清楚。解决思路是合并Server粒度。按“业务领域”而不是“数据源类型”来划分把强相关的一组能力放进同一个Server里。比如“运维域”就把查监控、查日志、查告警放一起“沟通域”把发通知、生成报告放一起。这样既保持了MCP的高内聚性也不会让元数据碎一地。5.2 工具描述写太长协议也救不了你MCP只是搬运工不是精简器。很多人以为工具定义进了MCP就自带省Token效果这完全是错觉。工具名称和description该写多简洁还是得多简洁。我在某个Server里见过一个工具描述写了整整三行其中一大半在解释参数的历史原因对模型决策毫无帮助纯属烧钱。我的标准工具描述不超过一句话且必须说清“做什么、何时用”。参数Schema里的描述字段只留必要约束能不放注释就不放。每次评审工具定义时都问一句如果我是模型看到这个描述能不能准确决定调用它不能就改能就赶紧砍。5.3 摘要压缩会伤害决策质量需要设阈值压缩工具返回结果确实能省Token但实操中最容易翻车的地方也在这。我有一次把一个告警聚合结果压得太狠只留了“3条告警最高级别P2”模型没看到告警详情做出的决策是“暂时观望”。实际上那条P2告警关联了核心服务宕机正确动作是立即通知值班。从那以后我定了条规矩压缩摘要之后必须验证一个简单问题——如果我只看摘要能否做出和看全文一样的决定不能就放宽压缩力度。我在代码里给摘要函数加了置信度阈值关键字段如果被截断就明确标注“内容过长已截断请查详情接口”。虽然偶尔会多花一点Token但比决策错误带来的代价便宜得多。5.4 握手与连接开销频繁开关Session不划算MCP会话的建立有握手、能力协商、资源发现这些流程。在请求流量小的时候这部分固定开销占比不高但如果你把一次Agent任务的每一轮都当成一次独立的短连接来处理频繁的 initialize 和 listTools 调用会对响应时间产生肉眼可见的影响。我的做法是复用长连接把整个Agent任务的生命周期绑定到一个MCP会话上。任务开始建立连接任务结束再断开。这样握手开销平摊到每一轮之后基本可以忽略。如果你用的是Serverless环境还要额外注意冷启动带来的耗时波动。6. 可直接复用的落地清单与下一步演进做完了这个改造项目我把整个过程整理成了一张可以照着做的清单。如果你正打算给现有Agent引入MCP协议做上下文工程按这个顺序走会少踩很多坑。6.1 迁移前先量化现状没有数据就没有针对性优化。任何改造之前先花一天时间把现有调用日志统计清楚平均每轮输入Token多少、工具定义占比多少、返回结果占比多少、历史上下文占比多少。我推荐按“系统提示词 / 工具定义 / 历史对话 / 工具返回结果”四类拆分统计然后你会很清楚地看到自己的问题最大聚集在哪一块。这一步很关键。我见过有人一听说MCP好就直接上上线后发现Token没怎么降因为他的系统提示词占了总开销的六成而MCP根本管不到那部分。先量化再决定把力气用在哪是省钱的第一步。6.2 落地检查清单下面这张清单是我在当前项目里迭代出来的每个新工具接入时都会过一遍。工具描述是否控制在40个字符以内是否点明了“做什么、何时用”该工具是否归入正确的Server领域避免元数据碎片化调用该工具前Host是否能按当前意图把无关工具定义裁剪掉返回结果是否有专门的摘要函数摘要后能否支撑同等质量决策大段状态数据是否通过会话ID复用而不是每次都重传系统提示词是否已压到500 Token以内对话历史是否做了滑动窗口和摘要而不是无限累积每一条都好查但放到同一个项目里同时满足需要团队有维度统一的上下文管理意识。MCP的价值就在于给了大家一个共同语言去讨论和检查这件事。6.3 值得留意的协议侧演进采样协商与主动推送最后提一下MCP协议本身还在演进的方向。我目前关注的主要是两块一是协议层面的采样协商机制允许客户端把“生成请求”委托给服务端让Server在能力范围内决定怎么采样二是上下文主动推送能力Server可以在检测到数据变化时主动更新资源而不是总等Host来拉取。这两个方向都直接关系到Token消耗的进一步优化。采样协商能让状态管理更下沉主动推送能减少无意义的重复拉取。不过这些能力还在发展和打磨中对应的SDK支持也不尽相同。我的建议是当前项目先把按需暴露、结果压缩、会话亲和这三板斧打好协议的新能力出来之后再来评估不用急着追新。我个人这段时间的实际体会是真正让项目省下Token的不是MCP协议本身而是协议逼着你想清楚了“每一轮请求到底需要哪些上下文”。如果你偷懒把所有工具、所有历史、所有返回结果都一股脑塞给模型就算套上MCP账单照样爆。反过来只要你把上下文当成一项需要主动管理的资源然后借用MCP的标准管道去落地通常都能把长期运行的Agent开销降一个量级。
阅读完成 · 觉得有帮助?
咨询建站