1. 工具接入的乱局:为什么最后选了 LangGraph + MCP
先说说我遇到的实际问题。做过一段时间多智能体应用的朋友应该都有同感:模型本身的能力差距已经越来越小,真正拉开体验差距的,是模型能调用多少外部工具、工具接得好不好用。
我们当时的项目需要让智能体对接十几个不同的服务:有自己内部写的数据查询接口,有第三方的天气、地图、文档解析能力,还有团队里另一条产品线维护的推荐算法服务。一开始的做法非常简单粗暴——每个服务写一个 Python 函数,函数体里直接调用对应的 SDK,然后把函数名、参数说明、返回值格式一起扔给大模型,让模型自己去选。
听上去没毛病,但真正跑起来就是灾难。
第一,工具协议五花八门。内部接口走 HTTP + 自定义签名,第三方有的走 REST、有的走 WebSocket,还有个老业务系统只能走命令行调用。每次模型要调一个新工具,我都要写一段适配代码,把外部差异消化成统一的函数签名。
第二,工具描述和模型推理质量高度耦合。大模型靠我写在 docstring 里的一段话决定该不该调用这个工具、参数怎么填。描述写得不清楚,模型不是漏调就是参数传错。十几个工具堆在一个模型上下文里,token 消耗也会涨得很离谱,而且每加一个工具都要重新测试一轮,回归成本巨大。
第三,换了模型之后,工具调用的"方言"也变了。今天用的模型原生支持某种 function calling 格式,明天换成另一个模型,格式又要改一遍。我们组里的同学每天都在处理这种"从 model A 适配到 model B"的琐碎差事,完全不像在搞业务。
后来我们开始关注两个方向,一是 LangGraph,二是 MCP,也就是 Model Context Protocol。LangGraph 解决的是流程编排的问题——把一次复杂的智能体任务拆成有状态、可回滚、可分支的工作流;MCP 解决的是工具接入标准化的问题——工具提供方只实现一次 MCP 服务端,任何支持 MCP 的客户端都能直接连接使用。两个东西一组合,工具接入侧的工作量肉眼可见地降下来了,模型侧的推理质量也稳了不少。
这篇文章就是我在真实项目里的落地记录,从选型逻辑到代码实现,再到我自己踩过的几个坑,完整过一遍,保证你照着能跑通。
提示:本文适合已经用过 LangChain 或者 LangGraph,但还没把 MCP 接进来的开发者。如果你对这两个东西完全陌生,建议先花十分钟把基础概念过一遍,再看本文实操部分会顺畅很多。
2. 用 LangGraph 的最大理由:状态流比链式调用更能兜住真实场景
很多朋友第一次听说 LangGraph,会直接和 LangChain 的 Chain 对比,觉得 LangGraph 无非就是把多个 LLM 调用串起来。这是个很大的误解。
2.1 链式调用的痛:中途断了怎么办
在 LangChain 时代,我们写智能体最常见的方式是构建一个顺序链:先调用模型 A 做意图识别,再根据意图调用模型 B 生成答案,中间穿插检索等步骤。代码看起来逻辑清晰,但有一个致命问题——链路一旦因为外部服务超时、模型输出格式异常、某个工具报错被打断,整个执行就彻底死了。你没法在步骤 3 报错之后"回到步骤 2 重新问一次模型",因为这些状态根本没有被保存下来,链也不会自动重试中间步骤。真实业务里,一次工具调用失败太常见了,网络抖动、服务端限流、参数校验不通过……任何一件小事都能让整条链废掉。
LangGraph 之所以叫"Graph",是因为它把整个任务定义成了一张状态图。节点是处理逻辑,边是状态转移条件。执行过程不再是"从头串到尾"的一次性调用,而是"在图中跑了很多步,每步都更新一个共享状态对象"。节点之间通过读取和写入共享状态来协作,运行时可以在任意节点停下来,保存快照,恢复执行,甚至根据当前状态动态决定下一步去哪个节点。
这个区别听着只有一句话,但带来的工程能力差异是全面的。拿我们项目里"客户工单自动分类 + 优先级判定"这个场景举例。简单方案就是一步模型调用,让模型把工单归类成几个类型,再判断优先级。但是真实工单经常有歧义,比如一句话里同时提到了账号问题和支付问题,模型给的分类置信度不够高。在链式方案里,你只能把分类结果原样收下,或者多做一次模型调用强行追问,写起来别扭,跑起来也慢。在 LangGraph 里,我可以直接在边条件里做判断:"如果置信度低于阈值,走到追问节点;如果高于阈值,直接去生成最终结果"。哪个分支走不通,状态里都有记录,后续排查一清二楚。
2.2 状态图的可维护性:每个分支都能单独测试
另一个对我这种习惯来回改需求的开发者的好处是,图结构天然适合增量维护。你增加一个新节点,只需要把它接到某个边上,它不会像链式调用那样动不动破坏前后代码的顺序逻辑。我们经常说"智能体工程是一门迭代主导的工程",因为需求变化太快——本周只需要 4 个工具,下周可能就要接 8 个。图结构下加一个新分支只是加一个节点和一条边,原来跑通的分支完全不受影响。这一点在我后面接入 MCP 工具时帮了大忙,因为 MCP 工具往往是一批一批接入的,每个工具本身就自带一个服务状态,把它注册成图节点后,既不和原有逻辑冲突,又能顺着图结构完成条件判断和重试。
2.3 状态共享让"工具调用"不再是黑盒
还有一点值得单独讲,LangGraph 的共享状态对象让我第一次真正看清了"模型调用工具"过程中发生了什么。以前用 AgentExecutor 时,工具返回值只会作为 Prompt 的一部分悄悄塞给模型,过程没法观察。而在 LangGraph 里,我可以定义"调用哪个工具、传了哪些参数、工具返回了什么、模型基于这个返回又思考了什么",把这些全塞进状态里。这个特质对我排查 MCP 工具问题极其关键。后面我会详细讲,MCP 工具接入之后的报错有相当大比例是"工具调用方参数没传对"或"工具返回格式模型没读明白",没有共享状态,这类问题排查起来基本靠猜。
3. MCP 把工具接入变成"即插即用",不再给模型换方言
3.1 MCP 到底改了什么:从一次函数调用到一套完整协议
MCP(Model Context Protocol)本质上是一套基于 JSON-RPC 的通信规范,规定了模型客户端如何向外部工具服务端发起请求、工具服务端如何返回结果、双方如何握手、如何发现能力。这套协议最初的目标是让"支持 MCP 的客户端"和"实现 MCP 的工具服务端"任意组合都能工作。就像 USB 接口之于外设,只要设备都遵守这个接口标准,插上就能用。
在我接入之前,一直觉得 MCP 就是把 OpenAI 的 function calling 格式重新封装了一层。真正动手后才发现,它提供的东西远不止参数约束。MCP 协议里有几个核心概念:
- Tools(工具):对应可被调用的外部能力,包含名称、描述、输入 schema。客户端可以通过协议自动发现服务端支持哪些工具。
- Resources(资源):可以被读取的数据内容,比如文档、数据库查询结果、文件内容。资源是只读的,目的是给模型提供上下文素材。
- Prompts(提示模板):可复用的提示词模板,服务端可以定义一些固定的交互范式,客户端按需加载。
- Sampling(采样):服务端反过来请求模型生成内容的能力,实际业务中用得少,但在一些工具联动场景里很有用。
最初我们的目标很朴素——把十几个工具统一成 MCP 服务端,让 LangGraph 里的 Agent 节点统一走 MCP 客户端去调。跑通之后才发现,MCP 带来的额外收益远超预期。
3.2 模型层与工具层的解耦:换模型不再伤筋动骨
以前我们每换一个模型,就要把工具调用的参数格式、结果解析格式重新适配一遍。每个模型对"如何描述工具、如何传参、如何返回结构化结果"的期望都不一样。接入 MCP 之后,模型层面对工具的能力感知只依赖 MCP 服务端提供的 schema 描述,无论底层换哪个模型,工具调用的协议层是固定的。我给服务端定义好 tools schema,客户端的 LangGraph 代码完全不用改,直接换个 model 配置就能跑。
这一点在团队协作里的价值尤其大。做算法的人不需要关心工具的调用协议,他只需要写好 MCP 服务端,描述清楚工具是干什么的;做应用的人不需要关心工具有哪些后端依赖,他只需要把服务端地址配好,等工具自动出现在工具列表里。我们组以前那种"SDK 版本一升级,工具调用就崩"的情况,再也没发生过。
3.3 工具注册、发现与动态扩展
利用 MCP 协议,可以实现一种很舒服的开发模式:工具服务端以进程独立的方式部署,客户端只需要知道服务端的地址(本地可以是 stdio 管道,远程可以是 HTTP + SSE)。服务端启动时声明自己支持哪些工具,客户端连上去之后自动拉取工具清单,不需要在业务代码里手写工具列表。
这直接解决了我前面提到的"每加一个工具要回归一轮"的问题。以前加一个工具,要改 LangGraph Agent 代码里 tools 数组,还要改 tool 对象里的 description 和参数定义。现在加工具,只需要在 MCP 服务端代码里新增一个 tool 定义,服务端重启后客户端就能发现新工具,图里的节点逻辑甚至都不用动。对我们的实际项目来说,工具接入的边际成本降到了 10 分钟以内,其中大半时间还是花在写工具自己的业务逻辑上。
4. 实战:把一个内部数据查询服务包装成 MCP 工具并接入 LangGraph
下面进入正题。我用一个模拟场景演示完整链路:假设我们的内部系统里有一个"订单数据查询"服务,它接收一个订单号,返回订单状态、金额、商品列表。这个服务原来是一个普通的 Python 函数,现在我要把它包装成 MCP 服务端,再让 LangGraph 里运行的一个 Agent 节点通过 MCP 客户端调用它。
4.1 环境准备与依赖安装
我建议使用 Python 3.10+,并新建一个虚拟环境。下面的命令在我的环境中实测没有问题。
# 创建并激活虚拟环境 python3 -m venv .venv source .venv/bin/activate # 安装核心依赖 pip install langgraph langchain-core mcp # 如果你的 MCP 服务端需要走 SSE/HTTP 方式,可能还需要 pip install "mcp[cli]" httpx我个人的建议是先把 langgraph 和 mcp 分开装,并且尽量不要一次性把 langchain 全家桶都装进来,因为很多依赖版本会打架。实测下来的稳定组合是:langgraph 0.2.x+langchain-core 0.3.x+mcp 1.0+。如果你用的版本比这个大,某些 API 的参数名可能有细微差异,注意对照官方文档调整。
4.2 用一个最简单的 MCP Server 暴露"外部能力"
首先我们定义工具本身。这个工具背后可能调了内部 SDK 或者一个内部 HTTP 服务,但作为消费者,我们只需要对外暴露一个清晰的名字和参数约束。
# order_mcp_server.py from mcp.server.fastmcp import FastMCP mcp = FastMCP("order-query-server")MCP 官方现在提供了FastMCP这个高层封装,写起来很像现有的 Python web 框架。通过@mcp.tool()装饰器就可以快速注册工具。
@mcp.tool(name="query_order", description="根据订单号查询订单基本信息,返回订单状态、金额和商品列表。") async def query_order(order_id: str) -> dict: # 这里简化处理,实际应该是查询内部订单库 if not order_id or len(order_id) < 10: return {"status": "error", "message": "invalid order id"} # 模拟数据 return { "order_id": order_id, "status": "paid", "amount": 99.9, "items": ["书", "笔记本"], } if __name__ == "__main__": # 以 stdio 方式启动,方便本地开发调试 mcp.run()这里有几个细节值得关注。第一,工具描述一定要写清楚"什么时候该用这个工具"。因为模型是根据描述来决定是否调用的,写得太宽泛,模型会在不该用的时候也去查订单;写得太窄,模型又会忽略它。我在项目里的经验是,描述里最好包含触发场景和关键参数含义两个信息。第二,返回的 dict 里尽量用清晰的字段名。MCP 会把 dict 序列化后返回给调用方,模型后续要用这些字段做推理,字段名一乱,推理结果就会受影响。
使用FastMCP的好处之一是不用手写 JSON-RPC 的细节。如果你们项目有特殊需求,也可以直接基于mcp.server.lowlevel.Server手写,但一般情况下不需要。
4.3 在 LangGraph 流程中启动 MCP Client 并与 Agent 节点绑定
现在开始写 LangGraph 这一侧。
# langgraph_agent.py from langchain_core.messages import HumanMessage, AIMessage from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import create_react_agent from langgraph.checkpoint.memory import MemorySaver from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools这里引入的是langchain-mcp-adapters这个适配器包,如果没有装,先执行下面的命令。这个包专门负责把 MCP 客户端返回的工具转换成 LangChain 里的 Tool 对象。
pip install langchain-mcp-adapters接下来写一个函数,负责连接 MCP Server 并获取工具列表。
async def get_mcp_tools(): server_params = StdioServerParameters( command="python", args=["order_mcp_server.py"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await load_mcp_tools(session) return tools等一下,这里有个关键的细节。如果直接在函数里使用async with,工具列表获取后连接就关闭了,后面 Agent 真正调用工具时服务早就断开了。MCP Client 的连接生命周期必须和 Agent 执行生命周期保持一致。
在实际项目里,我的做法是把连接提升到上层维持,让它在整个任务过程中保持打开。这里为了代码简洁,我先把工具加载出来,连接生命周期的问题放到后文"踩坑记录"里专门讲。
然后我们定义 Agent 节点。这里用 LangGraph 的create_react_agent,它内部是一个标准的 ReAct 循环:模型根据现状决定调用哪个工具,拿到结果,再继续推理,直到生成最终答案。
from langchain_openai import ChatOpenAI model = ChatOpenAI( model="qwen-plus", base_url="https://your-endpoint.example.com", # 这里替换成你实际使用的模型服务地址 api_key="your-api-key", temperature=0, )说明一下,这里我用的模型服务只是一个例子。LangGraph 本身支持任意实现了 ChatModel 接口的模型,你可以根据自己公司内网可用的模型服务来替换,关键是让模型能够支持 tool calling 能力。
def make_agent(): tools = asyncio.run(get_mcp_tools()) agent = create_react_agent(model, tools, state_schema=AgentState) return agent为了让状态流更清晰,我们把 Agent 封装成一个普通节点:
from typing import TypedDict, List from langgraph.graph import StateGraph class AgentState(TypedDict): messages: List[HumanMessage | AIMessage] async def agent_node(state: AgentState): agent = make_agent() result = await agent.ainvoke({"messages": state["messages"]}) return {"messages": result["messages"]}然后构建图:
graph = StateGraph(AgentState) graph.add_node("agent", agent_node) graph.add_edge(START, "agent") graph.add_edge("agent", END) app = graph.compile(checkpointer=MemorySaver())这样我们就有了一个可以执行的最简图。在使用时:
result = await app.ainvoke( {"messages": [HumanMessage(content="请帮我查询订单号 ORD-202501-001 的状态")]}, config={"configurable": {"thread_id": "test-1"}}, )等到这个流程跑完,result["messages"]的最后一条就是模型的最终回答。
4.4 从"能跑"到"好用":打印每次工具调用的中间状态
如果只是图能跑,那和普通链式调用没什么区别。LangGraph 真正的调试优势体现在"查看中间状态"。上面代码里加一段,我们就可以完整看到模型的思考链路:
async for chunk in app.astream_events( {"messages": [HumanMessage(content="请帮我查询订单号 ORD-202501-001 的状态")]}, version="v2", ): if chunk["event"] == "on_chain_end": if "model" in chunk["metadata"].get("langgraph_node", ""): # 打印模型每一步的输出 print(chunk["data"]["output"])用这个方法,我们能清楚看到模型是"先决定调用 query_order,拿到结果后再生成回答",还是"跳过工具直接瞎编"。在接入 MCP 工具的初期,这个中间状态几乎是唯一靠谱的排查入口。
这里还有一个部署上的小建议:如果你的 MCP Server 是远程部署的,客户端需要改为 SSE 或者 Streamable HTTP 连接方式。连接参数从StdioServerParameters换成 URL 即可,工具加载方式完全一样。本地开发用 stdio 更简单,生产环境建议走 SSE,因为进程隔离和服务生命周期都好管理。
5. 踩过的坑:MCP 连接生命周期、超时、工具描述、状态污染
上面那套代码看起来顺理成章,但我在实际项目中几乎每一步都踩过坑。下面挑四个最典型的详细讲讲,这些都是文档里不会写但实操必然会遇到的问题。
5.1 坑一:MCP 连接生命周期与 Agent 执行不同步
我第一次接入 MCP 时,很自然地写了类似于下面这种代码:
async def agent_node(state): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await load_mcp_tools(session) agent = create_react_agent(model, tools) return await agent.ainvoke({"messages": state["messages"]})表面上没问题,工具也能调通。但一旦 Agent 思考时间变长,或者一次任务里要多次调用工具,就会不定期报出"session closed"之类的错误。原因很简单:MCP 的 stdio 进程是用管道和客户端通信的,ClientSession 退出时,子进程会被杀,再想调用工具时连接已经没了。把连接放在 Agent 执行期间共同的生命周期范围内,才能保证工具多次调用稳定。
我的最终做法是让 node 函数持有 session 对象,并在整个图执行期间不释放它。示意代码如下:
async def run_agent_with_session(session, state): tools = await load_mcp_tools(session) agent = create_react_agent(model, tools) return await agent.ainvoke({"messages": state["messages"]}) # 在调用层: async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result = await graph.ainvoke({"messages": [HumanMessage(content="...")]})如果你用的是 FastMCP 服务端,还有一个更省心的方案——直接让 FastMCP 暴露的 HTTP 接口承担连接管理,客户端每次请求都重新握手,但那样会牺牲一些延迟。局域网内部延迟不高的时候倒是无所谓,但如果你的 MCP 服务部署在跨机房网络里,建议还是维护一个长连接。
5.2 坑二:默认超时太短,工具稍慢就被 Agent 判定失败
MCP 客户端对工具的调用默认有一个超时时间,这个时间远比你想象的要短。我们内部某个查询工具需要 1.5 秒才能返回结果,但 Agent 端调用时经常失败,日志里只有一行 "Tool call timed out"。当时我排查了很久,一度怀疑是工具服务端的问题,后来才发现是 MCP 客户端的默认超时只有 60 秒还是 30 秒来着我记不清了,总之和我想象的"无限等待"完全不同。
解决办法有两个。一个是在创建ClientSession时,通过底层sampling请求的timeout参数把超时调大。另一个更粗暴但也更好理解的方式,是在工具服务端主动把数据准备好,让工具客户端"秒回"。比如查询类工具,可以在服务端加一层缓存,缓存命中的请求直接返回,避免超时。不过这里要提醒一下,缓存会导致数据陈旧,如果业务对实时性要求高,就不能这么干。
我个人的建议是,先在客户端把超时调到 60 秒以上来做稳定性验证,最后再根据实际耗时的 p99 重新收紧到一个安全值。一上来就保持默认值,很容易误伤团队里的工具开发同事。
5.3 坑三:模型不调用工具?多半是工具描述说得不够直白
这个坑非常常见,也非常隐蔽。很多时候,模型没有调用你的工具并不是模型笨,而是工具描述写得含糊。举个真实的例子,我们的内部工具有一个叫report_data_to_excel,描述是"导出数据到 Excel"。模型在回答用户"把结果整理成表格"时,竟然完全没有调用这个工具,而是直接把 markdown 表格返回给用户。原因是描述里没有说清楚"什么时候该用这个工具,什么时候不该用"。
后来我把描述改成了:"当用户明确提出'导出 Excel'、'生成 xlsx'、'下载表格文件'等请求时,调用本工具。如果用户只是希望看到表格形式的数据,应直接用 markdown 表格回复,无需调用。"
这个改动立竿见影。工具描述这块,我的经验是参考搜索引擎的文档编写思路——把"触发条件 + 不触发条件 + 参数含义"写清楚,长度可以适当放宽。有些开发者担心描述太长占 token,实测下来,一段精心编写的 80 字描述,比三段模棱两可的注释带来的调用正确率提升明显得多,token 成本微乎其微。
5.4 坑四:共享状态被工具副作用污染
LangGraph 的状态对象默认会被所有节点引用。如果工具节点直接在 state 里追加一个很大很大的字段,比如把整个数据库查询结果塞进 messages,那么后续所有节点都会背着这个大包袱跑,token 消耗和内存都会爆涨。这个问题在 LangChain 时代不太突出,因为链式调用的数据传递是显式的。但在 LangGraph 里,节点的输入输出默认都会往同一个状态对象里写。
我遇到的最夸张的一次,某个工具返回了一个几十万字符的 JSON 字符串,工具节点把它写进 state,下一个模型节点再把它全发给模型,结果模型一看到这个超长上下文,直接拉垮了。后来我在工具节点里明确指定了 state 写入的 key,只把工具返回结果放进一个tool_result字段,模型节点只有在必要时才读取它。这样状态对象干净了很多,后面的排查也轻松了不少。
状态设计这一块我的建议是:任何工具返回的数据都不要原样塞进给模型的消息列表。先在节点里做一步精简,比如只保留 Top N 条记录、关键字段。模型推理真正需要的是"维度准确的数据摘要",而不是"完整无缺的原始库表"。这一点做到了,整条链路的 token 消耗能下降 30%-40%。
6. 进阶设计:多个 MCP Server、动态工具发现和必要时的权限兜底
跑通单 MCP Server 之后,你会发现新增工具变成了一种享受。但要真正用上"即插即用"的能力,还有一些工程化设计值得提前规划。
6.1 多 MCP Server 并存:用命名空间隔离工具,避免冲突
我们最后实际生产环境里跑了三套 MCP Server,分别对应订单查询、内容服务、内部文档库三个域。三套服务各自维护自己的工具集合,LangGraph 侧的 Agent 节点分别连三套 Server,把三份工具列表合并后再创建 Agent。这里有个坑:不同 Server 之间完全可能定义同名工具,例如"订单查询"服务和"用户画像"服务可能都有get_info。合并后 Agent 看到两个同名工具,就会晕。
解决办法很简单——在注册工具名时加上服务前缀。比如订单服务里的工具叫order_get_info,用户画像服务里的叫profile_get_info。这样 Agent 不会混淆,日志也好排查。如果你对工具命名有自己的规范,也要注意保持"服务域-动作"的统一风格,别让模型去猜。
6.2 动态工具发现:一把梭式地自动拉取工具列表
MCP 的工具发现机制天然支持动态更新。你可以定期轮询 MCP Server 的tools/list接口,发现新增工具后,自动更新 LangGraph Agent 里的 tools 列表。这一步在开发阶段价值很大。我们当时用了一个很轻量的方案:在 Agent 节点里增加一个缓存,每隔 5 分钟检查一次远程工具清单,有变动就刷新本地工具列表。
需要注意,Agent 节点每次执行都会构造新的 React Agent 实例,如果工具列表更新不及时,可能出现"服务端已经有新工具,但客户端 Agent 里还没有"的情况。为了避免这个问题,缓存检查尽量放在每次 Agent 构建之前,如果增量差异很小,甚至可以忽略刷新带来的性能开销。
6.3 权限兜底:不要让模型在无人监管的情况下调敏感工具
最后聊一个容易忽视但必须考虑的问题——工具权限。MCP 协议本身只解决"调用能力"的标准化,不解决"谁有资格调用哪些工具"的授权问题。如果你的工具涉及内部核心数据或者写操作,直接把这些工具暴露给模型,而模型完全没有权限校验,风险是很高的。
我见过一种朴素的方案:在 LangGraph 的图里加一个权限节点,在调用工具之前检查当前会话的用户角色,如果不匹配就返回"无权限操作"。这个节点可以做成 MCP Client 外层的 wrapper,也可以做成一个独立的图节点。关键是把权限校验放在工具节点的"上游",让任何进入工具节点的调用都被拦一道。
还有一种更细的粒度:给不同的 MCP Server 配置不同的 API Key 或身份角色,客户端连接时声明身份,服务端根据身份决定暴露哪些工具。这个设计需要服务端配合,但技术上完全可行。我们的经验是,权限兜底最好在框架层面做,而不要依赖模型自我约束。模型的判断再好,也不值得拿核心数据去赌。
7. 最后分享两个提升开发效率的小技巧
再聊两个和工程效率相关的小技巧,都是后来沉淀到团队内部文档里的。
第一个是给 LangGraph 图加一个完整的 observability hook。当时我们用astream_events把每个节点的耗时和输出摘要打进日志,包括工具调用的开始和结束时间、参数大小、返回体大小。这样,当某次任务变慢或者工具返回异常时,不用翻半天日志,直接在 trace 面板里定位哪一步出了问题。如果你对公司内部的 tracing 系统有依赖,LangGraph 也支持自定义 callbacks,把事件发送到已有的监控平台。这件事做好之后,接入 MCP 工具的速度会快一个量级,因为你可以从 trace 里直接判断"工具本身慢还是模型决策慢"。
第二个是为 MCP Server 单独写一个自测入口。MCP Server 本身是一个独立进程,你完全可以在不拉起 LangGraph 的情况下,用一个简单的脚本直接调用服务端工具,检查返回格式和耗时。我通常会准备一个debug_client.py,里面只做一件事:连接 Server、列出工具、调用某个工具、打印结果。当业务方怀疑是 Agent 侧的问题时,我先跑这个脚本,能排除掉一半的沟通成本。
这两个技巧听起来都很简单,但它们帮我们省下的时间非常可观。尤其在工具数量多了以后,"定位问题出在哪一层"才是开发中最耗时的一环。
我在整个接入过程中的最大体会是——LangGraph 负责把复杂任务的组织权从开发者手里交还给状态流,MCP 负责把工具的接入从"为每个模型写方言"变成"一次实现、随处可用"。两个工具组合起来,确实解决了真实项目中一堆琐碎但致命的集成问题。你们如果也在多智能体应用里被工具接入折磨过,不妨照着这篇文章的思路从一个小工具开始试水。