上周五团队周会,一个新来的同学问了我一个问题:我们调用一个查询库存的接口,为什么要同时改模型配置、函数定义、工具执行器、日志系统四处代码?这个问题问得特别准。它背后藏着AI应用开发里一个长期被忍下来的痛点——Agent和外部系统之间的连接,一直处于"每家都在自己焊管道"的状态。
MCP协议(Model Context Protocol,模型上下文协议)出现以后,这个局面开始松动。它把模型要理解的外部工具、数据源、交互约定,做成了AI应用开发里的"标准集装箱"。你现在接入一个新工具,不再需要为每个模型单独定制一套接口描述,也不再需要为每个业务系统重写一遍工具注册逻辑。这篇文章不打算复述官方文档,而是从我在AgentEarth这个企业级Agent平台建设过程中的实操视角,聊聊MCP为什么配得上"集装箱革命"这个说法,以及真正落地到生产环境时,哪些设计决策最重要。
1. MCP到底在解决什么问题:AI应用连接外部世界的"集装箱"
1.1 没有MCP的日子:每个人都在焊自己的管道
在MCP普及之前,把一个外部系统接进AI应用,常规路径大概是这样的:先给大模型写一份函数调用Schema,说明这个工具有什么参数、返回什么结构;然后在业务侧写一个真正执行这段逻辑的函数;再把返回结果拼成模型能读懂的文本;最后还得处理鉴权、超时、错误缓存、日志。这套流程本身不难,难的是每接一个新场景都要重复一遍,而且换个模型供应商,前面这份Schema可能又要重新写。
我见过最夸张的项目,团队内部自己定义了一套叫"星舰"的工具协议,前前后后写了三个月,支持了十几个系统。后来大家冷静下来一对比,发现要解决的问题和MCP完全一样:工具发现、参数传递、结果返回、错误表达。区别只在于,他们这套协议全世界只有自己用,而MCP有社区、有多家厂商支持、有不断演进的标准。
还有一个很隐蔽的成本是思想负担。每个团队都觉得自己对该不该用某个协议心里有数,但对新人来说,进来第一天就要学习"我们公司的自定义工具格式",学习成本非常高。MCP的好处是,哪怕你从来没接触过某个系统,只要它暴露了一个MCP Server,你就能通过一套统一的语义去理解它有哪些工具、每个工具怎么调、返回什么。
1.2 MCP的三根支柱:Tools、Resources、Prompts
MCP协议设计上最核心的不是"调一个函数",而是定义了Agent和外部世界之间的三类交互原语。
**Tools(工具)**对应"Agent能执行的动作",比如查询订单、创建工单、发送通知。它通常是读写操作,需要模型根据用户意图决定何时调用。每个工具用JSON Schema描述参数,模型据此生成结构化的调用请求。
**Resources(资源)**对应"Agent能读取的上下文",比如一份文件、一条数据库记录、一个项目的当前状态。它的特点是只读,更像眼睛,负责让模型看到它需要理解的信息。
**Prompts(提示)**是一个很容易被忽略但很实用的原语,它本质上是可复用的提示模板。比如你写了一个"周报生成"Prompt,任何连接到这个Server的Host都可以调用它,而不是每次都从零拼一段指令。
打个比方:Tools是手,Resources是眼睛,Prompts是嘴。一个Agent要完成复杂任务,这三样缺一不可。MCP把它们全部标准化,等于把"手、眼睛、嘴"都换成了通用的USB接口,设备可以随便插。
一个工具描述的Schema大概是这样的:
{ "name": "check_stock", "description": "根据商品SKU和仓库编码查询当前可用库存量,若库存低于安全水位,在返回中附带建议补货量", "inputSchema": { "type": "object", "properties": { "sku": { "type": "string", "description": "商品SKU,例如MCP-2024-001" }, "warehouse": { "type": "string", "description": "仓库编码,例如sh、bj、gz" } }, "required": ["sku"] } }注意description这一栏,这里不是写给人类看的文档,而是写给模型看的"决策依据"。描述写得好不好,直接决定模型在合适场景下会不会选中这个工具。关于这一点,后面单独展开讲。
1.3 为什么会是"协议"而不是SDK
很多人第一次接触MCP时会问:这不就是一个跨进程通信框架吗?跟gRPC、JSON-RPC有什么区别?
区别在于定位。MCP不只是一个通信框架,它定义的是"模型应用"和"外部工具"之间的协作语义。它不关心你是用Python还是Java实现Server,也不关心底层走的是本地管道还是HTTP,它关心的是:工具如何被发现、参数如何描述、结果如何返回、错误如何表达。这套约定是厂商中立、语言中立的。
集装箱革命之所以能重塑全球贸易,不是发明了一种更快的船,而是统一了箱子尺寸和吊装标准。船可以归不同公司,港口可以归不同国家,但箱子在全世界都能互通。MCP做的事也一样:模型厂商可以不同,Agent框架可以不同,业务系统可以完全不同,但只要大家都遵守MCP这套"箱子标准",连接成本就会从"每条航线定制"降为"统一搬运"。
从生态来看,MCP最初由Anthropic提出并开源,随后很快有大量厂商跟进。社区还出现了大量公开的MCP Server实现,覆盖数据库、浏览器、设计工具、开发环境、办公套件等。这标志着它已经从一个公司内部规范走向了开放标准。
2. MCP的架构拆解:从initialize到tools/call的完整链路
2.1 Host、Client、Server:谁在跟谁说话
MCP的架构里只有三个角色,但很多人一开始会搞混。
Host是用户直接面对的应用,比如Claude Desktop、IDE插件,或者你在后台跑的Agent服务。它负责承载对话界面、决策逻辑和整体业务流程。
Client是嵌入在Host内部的连接器,负责和MCP Server建立会话、发送请求、接收响应。同一个Host里可以同时挂多个Client,每个Client连接一个Server。
Server是工具和资源的提供方,可以是一个本地Python进程,也可以是一个远程HTTP服务。它是真正访问数据库、调用内部API、执行动作的地方。
关键点在于:模型本身是不直接连接Server的。模型只和Host/Client这一侧交互,由Client把模型的工具调用意图翻译成标准的MCP请求,再发给Server。这个边界非常重要,它让安全控制、鉴权、审计都有了落脚点。
2.2 一次库存查询:工具调用的协议级旅程
我们在AgentEarth里经常用"查询库存"做新人培训的端到端样例,因为它的链路短,但能覆盖MCP所有关键环节。完整过程大致是这样的:
- Host启动,Client与Server建立连接,发送
initialize请求,携带协议版本和客户端能力。 - Server返回支持的协议版本、Server能力(比如支持哪些工具、资源、提示)。
- Client发送
notifications/initialized通知,握手完成。 - 需要工具列表时,Client发送
tools/list请求,Server返回当前所有工具的定义。 - Agent根据用户问题和工具定义,由模型决定调用哪个工具。
- Client发送
tools/call请求,参数里携带工具名和模型生成的参数。 - Server执行工具逻辑,返回
content数组,里面是文本或结构化结果。 - Agent拿到结果后再交给模型,模型据此生成最终回复。
其中tools/call的请求和响应,用JSON-RPC 2.0格式来表达大致是这样:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "check_stock", "arguments": { "sku": "MCP-2024-001", "warehouse": "sh" } } }{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "SKU MCP-2024-001 在上海仓可用库存为 320 件,安全水位为 100 件,无需补货。" } ], "isError": false } }有些工具需要返回图片或者文件,MCP的content数组还支持image、resource等类型。实际开发中我建议尽量先只用text类型,保持简单,等模型对工具调用稳定了再扩展其他返回类型。过早引入复杂返回类型,会让模型解析结果的难度变大。
2.3 stdio还是Streamable HTTP:部署形态怎么选
MCP协议本身不绑定传输层,目前实际使用最多的有两种:stdio和Streamable HTTP。
stdio方式是Server作为Host的子进程启动,双方通过标准输入输出通信。这种方式在本地开发、桌面应用、CLI工具里非常方便。你不需要开启端口,不需要处理鉴权,一条命令就能把本地脚本暴露给Agent。
Streamable HTTP方式则是Server作为一个远程服务,通过HTTP提供JSON-RPC调用。这种方式适合部署在服务器、Kubernetes里,多个Agent实例可以共享同一个Server,也更容易做负载均衡、监控和鉴权。
两种方式的取舍,我用一张表格总结:
| 维度 | stdio | Streamable HTTP |
|---|---|---|
| 部署位置 | 本地子进程 | 远程服务 |
| 适用场景 | 本地调试、桌面Agent、单用户 | 多租户、生产集群、平台化 |
| 优点 | 简单、无需鉴权、无网络暴露面 | 可水平扩展、便于统一治理 |
| 需要注意 | 生命周期跟随Host,无法独立存活 | 需要处理鉴权、超时、限流、SSE流式响应 |
在AgentEarth里,我们采取的是"开发用stdio,生产用Streamable HTTP"的双轨策略。开发者本地起一个MCP Server做联调,体验非常顺滑;一旦要上测试或生产环境,就容器化部署,由API网关统一暴露。后面所有关于企业级实践的讨论,都默认是生产环境走Streamable HTTP这种形态。
3. AgentEarth的企业级实践:接入、权限与灰度
3.1 MCP适合接什么系统,不适合接什么系统
很多人一听说MCP就把所有系统都往上塞,这是一个典型的初期误区。我们在AgentEarth里走过一段弯路之后,总结了一套判断标准。
适合接的系统有三类:第一类是知识类和查询类系统,比如知识库、客户信息、订单状态查询,这些场景模型需要"看懂"数据,工具返回相对轻量;第二类是需要模型帮用户发起操作的系统,比如创建工单、发通知、更新状态,这类场景天然适合用自然语言触发;第三类是跨系统的编排场景,模型需要同时调度多个内部服务完成任务。
不适合或者需要改造后再接的系统也有三类。第一类是超低延迟的硬实时调用,比如高频交易、毫秒级缓存读取,MCP的协议解析和模型决策开销不值得;第二类是批量数据管道,你绝不应该让模型通过工具一次拉取几十万行数据再塞进上下文,正确的做法是提供"提交导出任务"和"查询任务状态"两个工具,让数据在系统后台流转;第三类是强一致性的异构系统间同步,比如直接同步主数据库,应该走专门的数据集成通道。
在AgentEarth里就发生过一次典型事故:我们接一个报表系统的数据源时,设计了一个叫get_full_report的工具,返回整个月的所有订单明细。结果模型一调用,上下文直接爆掉,单次调用费用高得离谱。后来我们把这个工具拆成了三个:get_report_metadata、query_report_summary、create_export_task。数据仍然留在报表侧,Agent拿到的只是摘要和任务结果。
3.2 统一接入设计:注册、鉴权、审计三件事
企业级落地和写Demo最大的区别,不是工具本身有多复杂,而是"接入治理"这层要怎么做。AgentEarth实践下来,有三件事是逃不掉的:注册、鉴权、审计。
注册要做的是工具元数据管理。每个MCP Server上线前,必须先向内部注册中心上报工具清单,包含工具名称、语义版本、负责人、所属团队、环境(dev/staging/prod)、数据敏感级别。Agent平台启动时从注册中心拉取工具清单,而不是直接去连所有的Server。这样即使某个Server临时下线,Agent侧也能提前知道该哪里降级。
鉴权要做的是一套完整的链路。MCP协议本身不规定怎么鉴权,什么方案都可以,但你不能不做。我们采用的模式是:Agent平台网关负责终端用户的身份认证,拿到用户令牌之后,再通过MCP Server的路由机制把用户上下文传给下游。每个Server内部还会再做一次角色权限校验,防止跨权限访问。
审计是经常被砍掉、但出事时才知道多重要的一项。每一次tools/call都必须落一条审计日志,包括哪个用户、通过哪个Agent、调用了哪个Server的哪个工具、传了什么参数(脱敏后)、返回状态、耗时多少。我们线上出过一次问题:某个Agent把"删除操作"理解错了,删了一批不该删的数据。当时因为没有审计日志,排查了两天才定位清楚。补上完整审计后,类似问题基本能在一小时内定位。
这里有一个小建议:审计日志和业务日志分开存储。业务日志会滚动清理,审计日志至少保留半年以上,因为它要应对安全事件复盘和合规检查。
3.3 高危操作与权限边界:别让Agent"想删就删"
把工具暴露给Agent,不等于让Agent拥有和人类用户一样的全部权限。我们在AgentEarth里把工具分成了三个等级。
只读工具是最安全的,比如查询订单、读取知识库、获取系统状态,可以直接开放给Agent调用。低风险写工具,比如保存草稿、创建普通工单,可以放行,但服务端要做好参数校验。高风险写工具,比如批量删除、转账、发布生产配置,必须加人工审批环节。
人工审批在技术上是这样实现的:MCP Server收到高风险工具调用时,不直接执行,而是返回一个"pending_approval"状态,同时创建一条审批任务。Agent平台把审批任务推送给指定负责人,负责人确认后,Server才会真正执行。Agent这边轮询任务状态,拿到最终结果后再继续下一步。
还有两个细节必须提。第一,模型生成的参数永远不能直接信任。服务端必须重新做枚举校验、范围校验、格式校验。比如删除接口只接受特定前缀的ID,拒绝通配符和空值。第二,写操作一定要支持幂等键。模型在超时后重试、在多轮对话里重复描述同一个意图,都非常容易导致同一个操作被执行多次。给每个写请求带上client_request_id,服务端按这个ID去重,能避免很多"Agent帮我下了三笔订单"的乌龙。
伪代码大概是这个思路:
def handle_order_create(args: dict) -> dict: # 幂等键必须存在 request_id = args.get("client_request_id") if not request_id: return error("缺少幂等键") if redis.exists(f"order:{request_id}"): return redis.get(f"order:{request_id}") # 返回已创建的订单 # 参数校验 if args["amount"] > 100000: return error("金额超过单笔限额,请拆单或申请审批") order = create_order(args) redis.set_ex(f"order:{request_id}", order, ttl=86400) return success(order)3.4 工具版本与灰度:tools/list不是静态的
很多人觉得MCP Server上线之后工具就固定了,但企业里不是这样。业务在变,工具参数在变,返回结构也在变。而工具变更对Agent来说,比对普通API消费者来说影响更大,因为模型对工具的理解完全依赖tools/list返回的描述,一旦描述变了,模型行为就可能跟着变。
这里最容易踩的坑是缓存。很多MCP Client会缓存tools/list的结果,你在Server端改了工具定义,Client那边可能还在用旧版本,导致调用404或者参数不匹配。刚开始我们遇到这个问题时,第一反应是"是不是缓存没刷新",后来发现背后其实是缺少版本管理意识。
现在AgentEarth里的做法是:每个工具定义里加一个version字段,注册中心保留历史版本;工具变更先在staging环境部署,让测试Agent跑一套固定评估用例,确认模型调用行为没有回归后再上生产;生产环境按租户灰度,先放一部分流量到新版本,观察日志里的错误率和调用分布,再逐步扩大。
还有一个操作层面的建议:不要随便改工具名称。模型的规划是基于历史观察和工具描述进行的,一个已经被模型"记住"的工具名,突然消失或者改名,会造成一段混乱期。如果确实要改,保留旧名字做一段时间的跳转,同时在新工具的description里写清楚"这是替代xxx的升级版本"。
4. 生产环境必须面对的四个问题:超时、追踪、限流与幻觉
4.1 超时与重试:别让Agent等太久
Agent调用工具和普通API调用不太一样。普通API超时后,用户看到报错自己处理;Agent调用工具超时后,大模型不会干等着,它要么开始编造一个看似合理的结果,要么重复发起一次调用。这两种情况都很危险。
所以超时策略要按工具类型分别设计。我们在AgentEarth实际使用的配置大概是这样的:
| 工具类型 | 超时时间 | 重试策略 | 备注 |
|---|---|---|---|
| 只读查询 | 8秒 | 最多重试2次,指数退避 | 如查询订单状态 |
| 低风险写操作 | 15秒 | 不自动重试,依赖幂等键 | 如保存草稿 |
| 异步任务提交 | 3秒 | 不重试 | 只负责提交,返回task_id |
| 异步任务状态查询 | 10秒 | 由Agent按业务逻辑决定 | 轮询间隔至少2秒 |
长耗时任务不要直接在工具调用里同步跑完,否则HTTP连接会长时间占用,中间任何一点波动都可能导致整个Agent流程失败。正确做法是:先提交任务,返回一个task_id,然后提供另一个工具让Agent轮询任务状态。虽然这让工具数量变多了,但每个工具的职责清晰,超时可控,整体稳定性会好很多。
限流同样重要。Agent在循环推理中可能连续发出几十次工具调用,如果不做并发限制,轻则把内部系统压垮,重则触发对方的封禁。AgentEarth的网关层对每个Agent实例、每个Server、每个用户分别做了配额管理,超限时返回一个"rate_limited"错误,让模型决定是等待还是换一条路径。
4.2 traceId贯穿:一次失败要能一小时定位
MCP调用链路比传统API长:用户输入进Agent,模型生成意图,Client发起工具调用,Server执行,结果再回到模型,模型生成最终回复。任何一个环节出问题,排查起来都很折磨人,除非你从一开始就建立全链路可观测性。
我们在AgentEarth里的做法很简单但很有效:每个Agent请求创建一个trace_id,通过MCP的请求元数据传给Server;所有Server的日志、审计记录、指标上报都带上这个trace_id。同时接入OpenTelemetry,把Agent的模型调用、工具调用、上下文组装都做成span。这样在链路追踪系统里,一次完整的用户请求可以展开成一棵调用树,你能清楚看到模型调用耗时是多少、工具执行耗时是多少、哪一步返回了错误。
刚开始跑数据时,我们发现了一个非常隐蔽的问题:某个工具返回了十多万字符的JSON,模型在生成回复时把这堆数据全部塞进上下文,导致单次调用的token消耗和费用暴涨。如果没有全链路追踪,这种问题只会以"这月的模型账单怎么翻倍了"的形式出现,根本定位不到根因。后来我们在工具返回值上做了长度限制,超过阈值就对结果做摘要截断,费用立刻降了下来。
可观测性还要关注的指标包括:工具调用延迟分布、工具错误率、参数大小、返回大小、模型重试次数、工具被选中频率。工具被选中频率这个指标特别有意思,它能直观反映工具描述写得好不好。如果一个工具长期不被选中,大概率不是模型的问题,而是描述和实际场景不匹配。
4.3 错误信息与模型幻觉:服务端要守住底线
MCP协议里,工具执行失败有两种表达方式:一种是直接返回非零的isError,另一种是返回正常但内容里包含错误描述。我们强烈建议所有Server统一使用isError字段,同时错误文本要尽量结构化、可消费。
一个合格的错误返回是这样的:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "库存服务当前不可用,错误码:STOCK_5003,可稍后重试或改用仓库查询接口" } ], "isError": true } }注意,错误信息要告诉模型三件事:出了什么错、能不能重试、有没有替代方案。模型拿到这个信息后,它会像一个负责任的助手一样向用户解释,或者自动换个方式尝试。如果你只返回一个"Server Error",模型大概率会开始编造细节。
但也别走另一个极端,把内部堆栈信息直接返回给模型。堆栈里往往包含服务器路径、内部类名、依赖版本,这些信息经过模型加工后可能原样泄露给终端用户,非常不合适。服务端要做一次错误翻译,把内部异常转换成对模型友好的业务错误。
还有一类幻觉问题来自"模型以为调用成功了"。比如工具实际执行失败,但因为Server的实现有bug,返回结构里isError是false,内容却是错误提示。模型会把这个提示当作正常业务结果,继续向用户输出"操作已完成"。这类问题没有捷径,只能通过完善的测试、协议级别校验和人工巡检来兜底。收到工具返回后,Agent侧建议增加一道校验逻辑,检查返回结构是否合法。
5. 实践中的经验教训与学习路径
5.1 不要把MCP Server做成"包了一层REST"
这是我在AgentEarth里最想提醒后来者的一点。很多人第一次写MCP Server时,会直接把内部REST API的字段原样搬过来,做成一个"透传层"。从技术角度它确实能用,但从Agent使用效果来看,往往很糟糕。
原因在于,REST API是给人类开发者设计的,它默认调用者知道"先查这个再查那个";而MCP工具是给模型设计的,模型对业务上下文的理解完全依赖工具名和description。同样是查库存,REST风格可能是GET /inventory/{sku}?fields=all,返回一堆人类才知道怎么解析的嵌套JSON;而MCP工具应该设计成"输入是什么、输出是什么意思、什么时候用"都一目了然。
工具粒度的把握也需要注意。粒度太细,比如把"获取用户姓名"和"获取用户手机号"拆成两个独立工具,模型需要调用好几次才能拿到完整信息,既慢又费token;粒度太粗,比如提供一个"执行任意SQL"的工具,又太危险,模型很可能生成一条完全不符合业务规则的SQL。我们在实践中得出一个经验:一个工具应该对应一个完整的业务动作,而不是对应一个数据库表或一个HTTP端点。
5.2 工具契约先行:写清楚description比写代码更重要
MCP Server的开发,本质上是在做"给模型看的接口设计"。写代码只是其中一小步,真正决定成败的是工具契约文档。
一个好的工具description应该包含:这个工具在什么场景下使用、核心参数的含义、返回值里关键的字段、副作用(比如会不会发消息、会不会改数据)、限制条件(比如只能查未来30天)。这些信息不是给用户看的,是给模型做规划用的。描述不完整,再聪明的模型也会用错。
我举一个对比:
- 差的描述:"查库存"
- 好的描述:"根据商品SKU和仓库编码查询当前可用库存量;库存低于安全水位时,返回中会附带建议补货量;当前仅支持查询未来30天内的库存数据"
同样的底层实现,描述不同,模型在复杂对话中的表现会差很多。我们团队现在把工具契约文档当作Code Review的一部分,任何工具变更必须先过契约评审,再写实现。
5.3 学习路线与高频面试问题
如果你刚接触这个领域,我建议的学习路线是这样的:先彻底搞懂Function Calling,这是MCP的认知基础;然后读一遍MCP规范里的Tools、Resources、Prompts三部分,不需要读所有细节;接着用FastMCP或者官方Python SDK写一个最小的Server,用MCP Inspector工具调试;再把它接到一个真实业务数据源上,比如查公司内部的知识库;最后套上HTTP传输、鉴权、日志,模拟企业环境跑一遍。
现在很多AI应用开发岗位的面试都会问到MCP,常见问题包括:MCP和Function Calling的区别是什么、MCP Server一般怎么部署、如何保证工具调用安全、为什么tools/list会被缓存、工具粒度应该怎么设计。这些问题其实都不难,只要亲手写过一次Server,基本都能答到点上。怕的是只背概念,一让写代码就露馅。
5.4 生态演进与未来可能性
MCP还在快速演进中。除了模型和工具之间的连接,它已经开始被用在与模型无关的Agent协作场景里。未来可能会出现类似"公共MCP注册表"的东西,像Docker Hub一样,你需要什么能力就拉一个对应的Server下来,即插即用。
这当然是好事,但也会带来新的问题。恶意或者不合格的MCP Server可能窃取数据、执行危险操作,供应链安全会变成新的挑战。企业如果要用MCP,最好还是自建信任列表,只允许内部经过审核的Server接入,不要盲目使用来源不明的社区Server。
6. 我在AgentEarth里最后想说的话
真正把一个MCP Server从零部署到生产环境之后,我对"集装箱革命"这个比喻有了更深的体会。集装箱不一定是运输方式里最优雅的方案,但它的价值在于统一了接口,让船、港口、卡车之间的配合成本大幅降低。MCP也是,它不保证每个工具都设计得完美,但它让Agent和工具之间的协作有了一个可以依赖的公约数。
如果让我给一个最具体的行动建议,那就是:不要一上来就接最复杂的业务系统。AgentEarth里最先让我们尝到甜头的,不是那些花哨的智能助手功能,而是一个不起眼的"环境信息查询"工具。它只做一件事,让Agent可以查询当前部署环境的配置、版本号、功能开关状态。这个工具非常简单,但它大大提升了排查问题的效率。你也一样,第一次做MCP实践,稳稳地从一个只读小工具开始,端到端跑通比追求覆盖面重要得多。