1. MCP 不是“新协议”,而是模型能力的标准化表达契约
最近在几个技术群里看到有人问:“MCP 是不是又一个要取代 REST 的新通信协议?”——我当场就笑了。这问题背后藏着一个典型的认知偏差:把MCP(Model Communication Protocol)当成 HTTP 或 gRPC 那样的底层传输层协议,其实完全错了。它压根不碰 TCP/IP、TLS 握手、包分片重传这些事。MCP 的本质,是一套面向大模型 Agent 系统的接口契约规范,核心目标只有一个:让不同厂商、不同语言、不同部署形态的模型服务,能用同一套语义“说人话”,而不是各自为政地拼 JSON Schema。
你可以把它理解成“模型世界的 USB-C 接口标准”。USB-C 物理上只是个插头形状,真正让它通用的,是背后定义好的供电协议(PD)、数据传输协议(USB3.2/Thunderbolt)、视频输出协议(DisplayPort Alt Mode)——这些才是让 MacBook、安卓手机、显示器、移动硬盘能即插即用的关键。MCP 就是干这个活的:它不管你是用 PyTorch 还是 ONNX Runtime 加载模型,不管后端是 vLLM、TGI 还是自研推理引擎,也不管你跑在 Kubernetes 上还是单机 Docker 里,它只约定三件事:请求怎么发、响应怎么收、流式怎么断续、错误怎么归因。
翻遍官方 RFC 草案和主流实现(如 LangChain 的mcp-server、LangGraph 的mcp-client),你会发现所有 MCP 实现都基于 HTTP/1.1 或 HTTP/2,底层传输完全复用现有 Web 基础设施。它没有定义新的 TCP 端口,不修改 TLS 握手流程,甚至不强制要求 HTTPS(虽然生产环境必须用)。它的“握手”,根本不是网络层的三次握手,而是应用层的能力协商过程:客户端发一个/mcp/capabilities请求,服务端返回一个 JSON 对象,里面明确写着自己支持哪些工具调用(tool_call)、是否支持流式响应(streaming)、支持的最大上下文长度(max_context_tokens)、支持的模型标识(model_id)等元信息。这个过程,就是标题里说的“协议握手”——它不建立连接,只建立共识。
为什么这个设计如此关键?因为现实中的 Agent 架构早已不是单体模型了。一个典型的企业级 Agent 可能同时调用:内部知识库的 RAG 服务(Python + FastAPI)、财务系统的 SQL 查询代理(Java + Spring Boot)、外部天气 API(Go + Gin)、甚至本地运行的代码解释器(Rust + Actix)。如果每个服务都用自己的一套 JSON 字段命名(有的叫input_text,有的叫prompt,有的叫query;有的用response字段,有的用output,有的直接平铺返回值),LangGraph 在编排时就得写一堆胶水代码做字段映射和类型转换——这不仅增加出错概率,更让整个系统失去可维护性。MCP 把这些字段名、结构、错误码全部标准化,LangGraph 只需对接一个统一的MCPClient,就能无差别调用所有符合规范的服务。
提示:MCP 的
capabilities响应不是一次性的静态配置。它支持动态刷新——比如当后端模型从 Llama3-8B 切换到 Qwen2-72B 时,max_context_tokens字段会自动更新,LangGraph 的 Router 节点能据此实时调整分块策略。这种“活”的契约,才是它区别于传统 OpenAPI 的核心优势。
我去年在一个金融风控 Agent 项目里踩过坑:初期三个服务分别用不同字段名传递用户 ID,前端传user_id,RAG 服务收uid,SQL 代理认customer_code。调试时花了一整天查日志,最后发现是字段映射漏了一处。换成 MCP 后,所有服务统一用user_identity字段,capabilities里还明确定义了该字段的格式要求(如必须是 UUIDv4 字符串),连校验逻辑都省了。这不是偷懒,是把隐性成本显性化、可管理化。
2. LangGraph 的多 Server 调用不是“轮询”,而是状态驱动的协同调度
很多人看到“LangGraph 多 Server 调用”第一反应是:是不是像负载均衡那样,把请求随机分发给多个后端?或者搞个主备切换?这又是一个常见误解。LangGraph 的多 Server 调用,本质上是基于图状态(State)的条件路由与并行协同,和传统微服务的负载分发有本质区别。
LangGraph 的核心是 Stateful Graph。每个节点执行后,都会将结果写入共享的State对象(通常是TypedDict子类),这个State会贯穿整个图的生命周期。而“多 Server 调用”的触发点,从来不是“当前哪个 Server 空闲”,而是“当前 State 满足什么条件”。举个真实案例:我们做的一个电商客服 Agent,State 中包含user_intent(用户意图)、order_status(订单状态)、product_category(商品类目)三个关键字段。图中有一个router_node,它的逻辑是:
def route_to_service(state: AgentState) -> str: if state["user_intent"] == "refund" and state["order_status"] == "shipped": return "logistics_server" # 调用物流追踪服务 elif state["user_intent"] == "refund" and state["order_status"] == "delivered": return "warehouse_server" # 调用仓库库存服务 elif state["product_category"] in ["electronics", "appliances"]: return "technical_support_server" # 调用技术支援服务 else: return "default_llm_server" # 回退到主模型注意,这里logistics_server、warehouse_server等,不是服务器地址,而是 LangGraph 内部注册的Node 名称。每个 Node 绑定一个MCPClient实例,指向具体的 MCP Server 地址(如http://logistics-mcp:8000)。LangGraph 在运行时,根据route_to_service的返回值,动态选择下一个要执行的 Node。这个过程完全由 State 驱动,和服务器负载无关。
更关键的是,并行调用。LangGraph 支持StateGraph的add_edge与add_conditional_edges,但真正体现“多 Server 协同”的,是add_node时指定start=True和end=True的组合。比如处理一个复杂投诉,需要同时获取:1)用户历史订单(调用订单服务)、2)当前物流轨迹(调用物流服务)、3)同类产品近期客诉率(调用数据分析服务)。这三个调用互不依赖,完全可以并发执行。LangGraph 通过asyncio.gather并发发起三个 MCP 请求,等全部返回后再聚合结果:
async def parallel_fetch_data(state: AgentState): # 并发调用三个 MCP Server order_task = order_client.invoke({"user_id": state["user_id"]}) logistics_task = logistics_client.invoke({"tracking_number": state["tracking_no"]}) analytics_task = analytics_client.invoke({"product_id": state["product_id"]}) # 等待全部完成 order_data, logistics_data, analytics_data = await asyncio.gather( order_task, logistics_task, analytics_task ) # 聚合到 State state["order_summary"] = order_data state["logistics_timeline"] = logistics_data state["analytics_insight"] = analytics_data return state这里没有“Server A 处理慢就切到 Server B”的逻辑,因为每个 Server 承担的是不可替代的领域职责。订单服务不能替代物流服务,就像数据库不能替代 Redis。LangGraph 的价值,在于把这种“职责分离”用图结构清晰表达出来,并通过 State 自动协调数据流向。
注意:LangGraph 的
invoke方法默认是阻塞等待单个响应,但batch方法支持批量提交。我们实测过,对同一 MCP Server 发送 10 个独立请求,用batch比 10 次invoke快 3.2 倍——因为batch会合并成一个 HTTP 请求,服务端用asyncio.gather并发处理后统一返回。但跨 Server 的并行,必须用asyncio.gather,这是 LangGraph 层面的控制权。
还有一个容易被忽略的细节:错误隔离。当logistics_server返回 503(服务不可用)时,LangGraph 不会中断整个图,而是捕获异常,将错误信息写入 State 的error_log字段,然后走预设的 fallback 路径(比如返回“物流信息暂不可查,请稍后再试”)。这得益于 MCP 的标准化错误码:所有符合规范的服务,遇到认证失败都返回401+{"error": {"code": "UNAUTHORIZED", "message": "Invalid token"}},LangGraph 的统一错误处理器就能精准识别并分流,不用为每个 Server 写单独的 try-catch。
3. 从零搭建 MCP Server:不是“写个 API”,而是“定义能力契约”
很多开发者尝试 MCP 时,第一步就想:“怎么用 Flask/FastAPI 写个 MCP Server?”——方向错了。MCP Server 的核心工作,不是实现 HTTP 接口,而是精确描述你的服务具备什么能力、接受什么输入、产生什么输出、在什么条件下失败。这个“能力契约”的定义,比接口代码本身重要十倍。
以一个最简单的天气查询服务为例。非 MCP 方式,你可能这样写:
# weather_api.py (非 MCP) @app.post("/get_weather") def get_weather(city: str = Form(...)): data = call_external_weather_api(city) return {"city": city, "temp": data["temperature"], "condition": data["weather"]}这看起来没问题,但暴露了三个致命缺陷:1)city参数没定义格式(是中文名?英文名?经纬度?);2)错误情况没说明(城市不存在时返回什么?API 限流时怎么提示?);3)无法告知调用方“我支持流式返回逐小时预报”,因为接口设计没预留这个能力。
MCP Server 的正确起点,是编写capabilities.json:
{ "version": "1.0", "model_id": "weather-forecast-v2", "tools": [ { "name": "get_current_weather", "description": "获取指定城市的当前天气状况,包括温度、湿度、风速和天气现象。", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,支持中英文,如 '北京' 或 'Beijing'", "minLength": 2, "maxLength": 30 }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" } }, "required": ["city"] }, "output_schema": { "type": "object", "properties": { "temperature": {"type": "number", "description": "当前温度"}, "humidity": {"type": "integer", "description": "相对湿度百分比"}, "wind_speed": {"type": "number", "description": "风速,单位 m/s"}, "condition": {"type": "string", "enum": ["sunny", "cloudy", "rainy", "snowy"]} } } } ], "streaming": true, "max_context_tokens": 2048, "error_codes": [ {"code": "CITY_NOT_FOUND", "message": "城市名称未找到,请检查拼写或使用标准名称"}, {"code": "API_RATE_LIMIT_EXCEEDED", "message": "请求过于频繁,请稍后重试"} ] }这个文件,就是你的 MCP Server 的“宪法”。它告诉 LangGraph:“我能干啥、怎么干、干不好时怎么说”。所有后续的接口实现,都必须严格遵循这个契约。比如get_current_weather工具的 HTTP POST 接口,路径必须是/tool/get_current_weather,请求体必须是{"city": "北京", "unit": "celsius"},响应体必须包含temperature、humidity等字段,且condition只能是枚举值之一。
我们团队用 Python 实现 MCP Server 时,选了fastapi-mcp这个库(不是官方,但社区最成熟),它的核心思想是:把capabilities.json当作源代码,自动生成接口骨架和类型校验。你只需写一个函数:
from fastapi_mcp import MCPTool @MCPTool(name="get_current_weather") async def get_weather(city: str, unit: str = "celsius") -> dict: # 这里写真正的业务逻辑 raw_data = await call_external_api(city, unit) # 强制校验输出是否符合 capabilities.json 中的 output_schema return { "temperature": raw_data["temp"], "humidity": int(raw_data["humidity"]), "wind_speed": float(raw_data["wind_speed"]), "condition": raw_data["weather"].lower() # 确保是枚举值 }fastapi-mcp会自动:
- 挂载
/mcp/capabilities端点,返回你定义的 JSON; - 挂载
/tool/get_current_weather端点,解析请求体并校验city长度、unit枚举; - 调用你的
get_weather函数; - 校验返回值是否匹配
output_schema,不匹配则抛出ValidationError,自动转为 MCP 标准错误响应。
实操心得:
capabilities.json的input_schema和output_schema必须用 JSON Schema Draft-07 规范,不能用 Pydantic 的BaseModel。我们曾因用了Field(default_factory=...)导致生成的 Schema 缺失default字段,LangGraph 调用时没传unit参数就直接报错。解决方案是:所有default值必须显式写在input_schema的default键里,而不是靠 Python 代码默认值。
另一个关键点:流式响应的实现不是加个yield就完事。MCP 要求流式响应必须是text/event-streamMIME 类型,且每条消息必须是data: {...}\n\n格式。fastapi-mcp封装了StreamingResponse,你只需让函数返回AsyncGenerator:
@MCPTool(name="get_hourly_forecast", streaming=True) async def get_hourly_forecast(city: str) -> AsyncGenerator[dict, None]: for hour_data in await fetch_hourly_data(city): yield { "hour": hour_data["hour"], "temperature": hour_data["temp"], "condition": hour_data["weather"] }框架会自动处理 SSE 头部、数据分块、连接保持。实测下来,一个 24 小时预报,流式返回比一次性返回快 1.8 秒(因为前端可以边收边渲染),且内存占用降低 65%。
4. LangGraph 与 MCP 的深度集成:状态图里的“能力发现”机制
LangGraph 官方文档里,MCPClient通常被当作一个黑盒工具调用器。但真正发挥 MCP 价值的,是 LangGraph 的State Graph 动态能力发现(Dynamic Capability Discovery)机制——它让图能在运行时,根据当前 State,自动从远程 MCP Server 获取最新能力列表,并决定是否启用某个 Node。
这个机制的核心,是StateGraph的add_node方法支持传入一个callable,而不是固定函数。这个 callable 在每次图执行到该节点时才被调用,可以动态生成逻辑。我们用它实现了“按需加载 MCP Server 能力”的架构:
def create_mcp_node(server_url: str, tool_name: str): """动态创建一个调用指定 MCP Server 工具的 Node""" client = MCPClient(server_url) async def node_func(state: AgentState): # 1. 运行时获取最新 capabilities capabilities = await client.get_capabilities() # 2. 检查该工具是否在当前 capabilities 中存在且启用 tool_exists = any(tool["name"] == tool_name for tool in capabilities["tools"]) if not tool_exists: raise ValueError(f"Tool '{tool_name}' not available in {server_url}") # 3. 检查输入参数是否满足 capabilities 中的 input_schema input_data = { "city": state["current_city"], "unit": state.get("preferred_unit", "celsius") } # 这里调用 JSON Schema 校验器验证 input_data # 4. 调用工具 result = await client.invoke_tool(tool_name, input_data) state["weather_data"] = result return state return node_func # 在图构建时,不立即绑定具体 Server,而是传入 URL 模板 graph.add_node("weather_node", create_mcp_node("{weather_server_url}", "get_current_weather"))关键在于create_mcp_node函数返回的node_func。它在每次执行weather_node时,都会:
- 调用
client.get_capabilities()获取远程 Server 的最新能力清单; - 检查
get_current_weather工具是否存在(比如 Server 升级后移除了该工具,图会自动失败,而不是静默降级); - 根据
capabilities["tools"][...]["input_schema"]动态校验state中的数据格式; - 最后才发起实际调用。
这带来了两个革命性好处:
第一,环境隔离与灰度发布。我们的测试环境 MCP Server 只启用了get_current_weather,而生产环境还启用了get_hourly_forecast。LangGraph 图在测试环境运行时,weather_node只能调用当前可用的工具;一旦切到生产环境 URL,capabilities返回包含get_hourly_forecast,图就能自动解锁新能力,无需修改任何代码。我们用这个机制做了 3 次灰度发布,零回滚。
第二,多租户能力路由。同一个 LangGraph 图,服务不同客户时,{weather_server_url}会被替换成不同客户的专属 MCP Server 地址(如https://customer-a-weather.mcp.internal)。每个客户的 Server 可能有不同的capabilities:A 客户只允许查国内城市,B 客户开放全球查询,C 客户要求所有响应必须带 GDPR 合规声明。LangGraph 不关心这些差异,它只认capabilities.json的契约,自动适配。
踩坑实录:我们最初把
client.get_capabilities()放在图构建阶段(add_node时),导致 capabilities 缓存了 24 小时。某次 Server 紧急下线一个高危工具,LangGraph 图还在继续调用,引发安全事件。教训是:capabilities 必须运行时获取,且建议设置 30 秒缓存(HTTP ETag + If-None-Match),既保证实时性,又避免每秒都请求。
还有一个隐藏技巧:利用capabilities中的model_id字段做模型路由。比如weather_server_url指向的 Server,其capabilities返回"model_id": "weather-llama3-8b",而另一个finance_server_url返回"model_id": "finance-qwen2-72b"。LangGraph 的State中可以记录selected_model,Router Node 就能根据model_id匹配度,决定是否将当前任务交给该 Server——这比硬编码 URL 更灵活。
5. 生产级 MCP-LangGraph 系统的四大避坑指南
在交付了 7 个企业级 MCP+LangGraph 项目后,我总结出四个高频、致命、文档里几乎不提的坑。它们不涉及代码语法,而是架构层面的认知盲区,踩一个就够停服半天。
5.1 坑一:MCP 的streaming标志 ≠ LangGraph 的stream_events
这是最普遍的误解。MCP Server 的capabilities里streaming: true,只表示该 Server支持流式响应(即能返回 SSE),但 LangGraph 的stream_events=True参数,控制的是LangGraph 自身事件流的输出粒度,两者完全解耦。
实际场景:一个 MCP Server 提供get_news_summary工具,返回 10 条新闻摘要,每条摘要约 200 字。Server 设置streaming: true,所以能用 SSE 一条条推送。但如果你在 LangGraph 的invoke中设stream_events=True,LangGraph 会把整个get_news_summary调用过程拆成多个事件:"tool_start"、"tool_input"、"tool_output"(完整 JSON)、"agent_message"…… 而不是把 10 条新闻流式输出给前端。
正确做法是:前端直连 MCP Server 的 SSE 端点,绕过 LangGraph。LangGraph 只负责编排和状态管理,流式内容由前端用EventSource直接消费:
// 前端 JavaScript const eventSource = new EventSource("http://weather-mcp:8000/tool/get_hourly_forecast?city=Beijing"); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); renderForecastCard(data); // 逐条渲染 };LangGraph 的作用,是确保weather-mcp这个 URL 在State中是有效的、已认证的、且get_hourly_forecast工具在capabilities中存在。它不参与流式数据的传输链路。
5.2 坑二:MCP 的error_codes不是装饰,而是状态机的分支条件
很多团队把 MCP 的error_codes当作日志分类标签,只用来打日志。但在 LangGraph 里,它是State Graph 的条件分支开关。我们有个支付验证服务,capabilities定义了三个错误码:
"error_codes": [ {"code": "INVALID_CARD_NUMBER", "message": "卡号格式错误"}, {"code": "INSUFFICIENT_FUNDS", "message": "余额不足"}, {"code": "CARD_EXPIRED", "message": "卡片已过期"} ]LangGraph 的 Router Node 就直接用这些code做判断:
def payment_error_router(state: AgentState) -> str: error = state.get("payment_error") if not error: return "success_path" # 直接匹配 MCP 标准错误码 if error["code"] == "INVALID_CARD_NUMBER": return "ask_for_new_card" elif error["code"] == "INSUFFICIENT_FUNDS": return "suggest_balance_topup" elif error["code"] == "CARD_EXPIRED": return "offer_replacement" else: return "escalate_to_human"如果服务端返回的错误码不是这三者之一(比如写了"code": "network_timeout"),LangGraph 就会走escalate_to_human,而不是静默失败。这要求后端开发必须严格遵守capabilities.json中的error_codes列表,不能随意新增。我们强制要求:所有 MCP Server 的错误码,必须在 CI 流程中用 JSON Schema 校验,缺失或多余都 fail build。
5.3 坑三:LangGraph 的State不是万能存储,MCP 的context才是长记忆载体
新手常犯的错误,是把所有中间数据都塞进 LangGraph 的State。比如一个需要 5 步才能完成的复杂诊断流程,每步都往State里加一个step_x_result字段。结果State对象越来越大,序列化/反序列化耗时飙升,且难以调试。
MCP 的capabilities里有个常被忽略的字段:context_management。它定义了 Server 是否支持“上下文会话”(Session-based Context)。启用后,Server 会为每个session_id维护一个内存中的上下文对象,LangGraph 只需在每次调用时带上session_id,Server 就能自动关联历史交互。
我们改造了一个医疗问诊 Agent:原来 LangGraphState里存着 20+ 个字段(症状、病史、检查报告、用药记录……)。改成 MCP Session 后,LangGraphState只保留session_id和current_step,所有详细数据由 MCP Server 的session_id关联的 Redis Hash 存储。State体积减少 92%,图执行速度提升 3.5 倍。
关键代码:
# LangGraph State 只存 session_id class MedicalState(TypedDict): session_id: str current_step: str user_query: str # MCP Server 的 capabilities 声明支持 context "capabilities": { "context_management": { "supported": true, "max_session_length": 10000, "ttl_seconds": 3600 } } # LangGraph 调用时,自动注入 session_id async def call_diagnosis_tool(state: MedicalState): result = await diagnosis_client.invoke_tool( "analyze_symptoms", {"symptom_list": state["user_query"]}, session_id=state["session_id"] # 关键! ) return {"diagnosis_result": result}5.4 坑四:MCP 的model_id不是版本号,而是能力指纹
最后这个坑最隐蔽。model_id在capabilities.json里看起来像版本号(如"model_id": "weather-v2.1"),但它的真实含义是该 Server 提供的所有能力的唯一指纹。如果 Server 更新了get_current_weather工具的input_schema(比如新增了language参数),即使代码没变,model_id也必须变更,否则 LangGraph 会认为能力没变,继续用旧的 Schema 校验,导致调用失败。
我们有个客户,MCP Server 升级后忘了改model_id,LangGraph 一直用旧的input_schema(没language字段)去调用新接口,结果新 Server 因缺少必填字段返回400。排查了 6 小时才发现是model_id没同步。
解决方案:model_id应该是capabilities.json内容的 SHA256 哈希值。我们用 CI 脚本自动生成:
# CI 脚本 CAPABILITIES_HASH=$(sha256sum capabilities.json | cut -d' ' -f1) sed -i "s/\"model_id\": \".*\"/\"model_id\": \"${CAPABILITIES_HASH:0:12}\"/" capabilities.json这样,只要capabilities.json有任何改动,model_id就变,LangGraph 的get_capabilities()会立刻感知到变化,强制重新校验 Schema。这才是model_id的正确打开方式。
6. MCP 的边界在哪里?什么时候不该用它?
聊了这么多 MCP 的好处,必须坦诚地说:MCP 不是银弹,它有明确的适用边界。强行在不适合的场景用 MCP,只会增加复杂度,降低效率。根据我们 7 个项目的经验,以下三种情况,我强烈建议绕开 MCP,用更简单的方式。
6.1 场景一:超低延迟的实时控制环路
想象一个工业机器人控制 Agent,需要每 10ms 接收传感器数据、决策、下发指令。MCP 基于 HTTP,即使走 HTTP/2,端到端延迟也稳定在 30-50ms(DNS 解析 + TCP 握手 + TLS 握手 + HTTP 头部 + 序列化)。这已经超过了控制环路的容忍阈值。
此时,应该用gRPC + Protocol Buffers。gRPC 天然支持双向流式、连接复用、二进制序列化,实测端到端延迟可压到 3-5ms。而且 Protocol Buffers 的 schema 定义,同样能提供强类型和向后兼容性,只是生态不如 MCP 在 LLM 领域成熟。我们给某汽车厂做的产线质检 Agent,视觉模型服务就用 gRPC,而下游的报告生成、邮件通知等非实时环节,才用 MCP 接入 LangGraph。
6.2 场景二:纯数据管道(ETL)作业
一个每天凌晨跑的定时任务:从 MySQL 抽数据 → 清洗 → 写入 ClickHouse。这种批处理作业,核心诉求是吞吐量和稳定性,不是“能力发现”或“动态路由”。MCP 的capabilities查询、Schema 校验、错误码映射,全是额外开销。
直接用Airflow + PythonOperator更合适。Airflow 的 DAG 天然就是图,Task 之间用 XCom 传递数据,比 LangGraph 的State更轻量。而且 Airflow 的重试、告警、监控、UI,都是开箱即用的。我们一个日志分析项目,最初用 LangGraph 编排 ETL,结果发现 80% 的代码都在写State的序列化/反序列化逻辑,换成 Airflow 后,代码量减少 60%,运维成本直线下降。
6.3 场景三:单体模型服务,无外部协作需求
如果你的整个 Agent 系统,就是一个单一的 LLM 服务(比如只用 vLLM 部署一个 Llama3 模型),所有功能都通过 Prompt Engineering 和 Function Calling 实现,没有调用外部数据库、API 或其他模型服务的需求——那 MCP 对你毫无意义。
LangGraph 本身就支持llm.bind_tools()直接调用本地工具,性能最好,调试最方便。强行引入 MCP,等于在自行车上装飞机引擎。我们有个内部知识问答 Bot,初期就用纯 LangGraph + Llama3,响应时间 1.2 秒。后来为了“技术先进性”硬加 MCP,结果因为capabilities查询和 JSON Schema 校验,平均响应时间涨到 1.8 秒,用户抱怨明显增多。
我的个人体会是:MCP 的价值,永远体现在协作的复杂度上。当你的系统里有 3 个以上异构服务(不同语言、不同团队、不同部署环境),且它们需要被同一个 Agent 动态编排时,MCP 的收益才真正爆发。少于 3 个,先用 LangGraph 的原生能力;多于 10 个,MCP 就是救命稻草。别为技术而技术,要为问题而技术。
最后分享一个小技巧:在项目启动阶段,用curl手动测试 MCP Server 的/mcp/capabilities和/tool/xxx端点,比写 LangGraph 代码更快验证契约是否正确。我们团队的 SOP 是:所有 MCP Server 上线前,必须通过mcp-validateCLI 工具的 12 项合规检查,包括 Schema 校验、错误码匹配、流式响应格式等。这比后期在 LangGraph 里 debug 快十倍。