最近圈子里聊 MCP(Model Context Protocol)的人明显多了起来。不管是 IDE 里接数据库、让 Agent 调 Figma,还是用 LangGraph 编排所谓“能让 AI 下地干活”的多工具链路,MCP 几乎已经成了接入外部工具时的默认协议。网上讲概念的帖子很多,但真正从协议层握手讲到LangGraph 里多 Server 调用的实操文章并不多。这篇就把我实际跑通的一条完整链路分享出来,从最底层的 initialize 握手开始,到用 LangChain 生态把多个 MCP Server 接进 LangGraph Agent,每一步都附代码和踩坑记录。想搞懂 MCP 不只是在文档里复制粘贴配置的人,这篇文章应该能帮你省不少时间。
1. MCP 到底是什么,它到底解了什么结
1.1 从“工具接口混乱”说起
先说一个最现实的问题:在没有 MCP 之前,如果我想让 AI 帮忙查数据库、搜文件、调用公司内部 API,需要给每个数据源单独写一套“连接器”。LLM 本身只认识文本,要让它操作外部系统,要么走 Function Calling,要么自己在代码里拼 prompt 和工具调用。Function Calling 解决的是“模型如何决定调什么函数”,但函数从哪来、怎么规范化、怎么让多个不同应用共用一个工具能力,它管不到。
结果就是每个项目都在重复写类似的胶水代码:数据库一个封装、文件系统一个封装、第三方 API 一个封装,换个模型厂商可能还要再适配一遍。MCP 的诞生就是为了终结这种混乱。它把“AI 应用(客户端)”和“外部工具/数据源(服务端)”之间的通信方式标准化了。协议说白了就是一套规则:客户端怎么连服务器、怎么列工具、怎么调用工具、怎么拿结果。一旦大家都遵守这套规则,同一个 MCP Server 可以被任意支持 MCP 的客户端复用,客户端也不用关心 Server 内部是 Java、Python 还是 Node 写的。
MCP 的官方定位是“上下文协议”,因为它不只是可以暴露函数(Tools),还可以暴露资源(Resources)和提示词(Prompts)。信息源和数据访问方式统一了,模型拿到的“上下文”就不再只是一段文本,而是整个可编程的工具环境。这个定位,和单纯做“远程函数调用”的 RPC 本质上拉开了距离。
1.2 协议组成和运行模式
从部署模式看,MCP 目前主力有两种 Transport(传输方式):
- stdio:MCP Server 以子进程方式启动,客户端通过标准输入输出和它交换 JSON 消息。本地开发最常用,LangGraph 本地集成、FastMCP 默认模式基本都是这种。
- HTTP/SSE:MCP Server 作为独立服务暴露 HTTP 接口,客户端通过网络访问,适合跨机器、跨容器的部署。
一个 MCP 连接里有三个角色:
| 角色 | 说明 | 例子 |
|---|---|---|
| Host | 承载 AI 应用的进程,通常是你写的 Agent 主程序 | LangGraph 应用、Claude Desktop、IDE 插件 |
| Client | 负责和 Server 建连、收发协议消息的组件 | MCP Python SDK 里的 ClientSession |
| Server | 暴露能力的一方,提供方法供 Client 调用 | 写好的文件搜索服务、数据库查询服务 |
运行模式上,客户端先启动传输层,然后发起握手、能力协商,最后进入正常工作状态。理解这个时序非常关键,很多人用 LangGraph 接入 MCP 时遇到的怪问题,有相当一部分就出在“没按协议时序来”这上面。
2. 协议握手:MCP 连接的第一步
2.1 通信基础:JSON-RPC 2.0
MCP 协议的消息层建立在 JSON-RPC 2.0 之上,也就是客户端和服务器之间发的是一个又一个 JSON 对象,包含 jsonrpc 字段、method、params。请求需要带 id,响应需要匹配这个 id,通知则不需要响应。这套规范非常成熟,理解起来也简单:它就像两个人约定好对话格式,一个人开口说“帮我做什么”,另一个按编号回复“结果是什么”。
MCP 的每一次方法调用最终都落到 JSON-RPC 的三大类消息上。比如客户端发:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0.0"}}}服务端回:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"sql-server","version":"0.1.0"}}}很多读者看到这种 JSON 就头疼,但实际开发中,官方 SDK / FastMCP 已经帮你把这些底层逻辑封装好了,你不需要手工编 JSON。但为什么我还要强调理解 JSON-RPC?因为排查问题时要看协议日志,一旦看到Method not found、Invalid params、JSON-RPC error,你必须能立刻反应过来是协议层出了问题,而不是业务代码出错。
2.2 initialize 握手的完整过程
MCP 的连接不是建立 TCP 连接就算完事,它有一个类似 TCP 三次握手的“协议握手”过程。流程是这样的:
- 客户端发起
initialize请求,带上自己支持的协议版本和能力描述。 - 服务端返回自己的协议版本、能力列表(支持 tools、resources、prompts 中的哪些)和服务标识。
- 客户端收到后,发送一个
notifications/initialized通知,表示“我知道你的能力了”。 - 服务端收到通知后,双方才进入“正常工作状态”。
这里有个很关键的细节:在notifications/initialized发出之前,客户端是不允许调用tools/list、tools/call等业务方法的。协议上管这叫生命周期状态机。我在项目里看到过一种低级错误:刚连上 server 就立刻调工具,结果请求被 server 端无情拒绝,报错是Connection not initialized。原因就是 SDK 的initialize在后台异步进行,而你用asyncio.create_task提前并发发请求了。解决方法是确保连接流程走完,再进入业务逻辑。
版本协商也是一个重点。MCP 的 protocolVersion 是带日期的字符串,比如 2025-06-18。client 和 server 各自声明版本,最终协商后的版本取两者的交集。如果你手写协议对接,一定要处理好版本不一致的情况,别直接把服务端版本原样当成本地版本,否则后续能力解析会错位。用官方 SDK 则不用太担心,它会自动按兼容策略处理。
2.3 能力发现:tools/list 和 tools/call
握手结束,第一件真正意义上的“业务”操作就是能力发现。客户端发tools/list,服务端返回工具列表。每个工具包含名称、描述、输入参数 schema。这个 schema 是 JSON Schema 格式,也就是模型端做 Function Calling 时最需要的那份“说明书”。
拿 SQL 查询服务举例,工具返回会长这样:
{"tools":[{"name":"query_order","description":"按订单号查询订单信息","inputSchema":{"type":"object","properties":{"order_id":{"type":"string"},"status":{"type":"string","description":"订单状态,默认全部"},"page":{"type":"integer"}},"required":["order_id"]}}]}LangGraph 里的模型看到这份 schema 后,会决定“什么时候调用工具、传什么参数”。工具描述写得好不好,直接影响模型的理解准确率。MCP Server 把工具描述写清楚,比在 Agent 代码里再加一堆 prompt 更管用。
真正的执行动作是tools/call。客户端发:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_order","arguments":{"order_id":"SO-001"}}}服务端返回的结构里包含content数组,内容类型默认是文本,也支持图片等资源。SDK 里这个返回会被封装成工具调用结果,LangGraph 的 ToolNode 拿到后直接写回状态。
除了 tools,MCP 还有 resources 和 prompts 两类能力:
| 能力 | 用途 | 类比 |
|---|---|---|
| tools | 可执行的函数,模型自主选择调用 | 给 AI 配的“手” |
| resources | 暴露只读数据,比如配置文件、数据库记录 | 给 AI 看的“资料室” |
| prompts | 预置的提示词模板,客户端可调用 | 给 AI 准备的“话术库” |
这三样组合起来,MCP Server 才能被称为“完整的上下文提供者”,而不仅仅是“工具仓库”。
2.4 心跳与连接生命周期
建立连接后,MCP 还有一套心跳机制。客户端可以对 server 发ping请求,server 必须响应;反过来 server 也可以 ping 客户端。目的是探测连接是否健康,尤其在 HTTP/SSE 模式下,长时间不通信可能导致网络中间设备把连接回收。我遇到过一个现象:MCP 服务部署在远程服务器上,Agent 空闲几分钟后调用工具失败,排查发现连接已经被中间代理超时断开了。解决办法是在 Agent 的空闲分支里加定期 ping,保持活跃。
协议层面的连接状态还有 shutdown、aborting 等。正常关闭时双方会走shutdown流程,释放资源。在 LangGraph 的异步环境里,如果直接杀进程而不走关闭流程,可能留下僵尸子进程,尤其是 stdio transport 下,子进程没有父进程回收,会一直赖在系统进程列表里。
3. LangGraph 里多 Server 到底怎么玩
3.1 语言模型框架为什么需要 MCP
LangGraph 本身的定位,是用图的方式编排 Agent 的工作流:节点负责执行,边负责决定下一步去哪儿,状态在节点之间流转。它并不关心工具怎么连外部的数据库、文件、API,它只知道“模型说要调一个叫 xx 的工具,参数是 yy,谁来执行?”这个执行者可以是任意实现了 Tool 接口的对象。
MCP 接入 LangGraph,最直接的好处是:你不必为每个外部系统写 LangChain 专属的 Tool 封装,只需写一个 MCP Server,LangGraph 应用通过 MCP 客户端就能使用它的能力。工具维护、数据源管理、权限控制都下沉到 Server 侧,模型层和应用层保持干净。
实际项目里,我更推荐把 MCP Server 当成“一个 AI 可操作的能力单元”。比如你这个 Agent 需要两件事:查数据库和操作文件系统,那分别在两个 MCP Server 里实现。这样一来,别人想复用时直接起 Server,你想替换实现时也不用动 Agent 代码。
3.2 多 Server 架构选择的三种方式
当你有多个 MCP Server,怎么设计 LangGraph 的调用策略?我总结了三种常见方案。
方案一:全量工具注入。把所有 Server 的 tools 拉出来,一股脑交给 LLM。简单粗暴,适合工具总数量少(10 个以内)、且彼此功能独立的情况。缺点是当工具数量多、prompt 变长后,模型的工具选择准确率会下降,token 成本也高。
方案二:按子 Agent 分组。每个子 Agent 绑定一个 MCP Server,再定义一个 Supervisor 路由节点,根据用户意图决定调用哪个子 Agent。适合企业级的复杂场景,比如“文件管理子 Agent”“数据库操作子 Agent”。代价是实现复杂度上了一个数量级,需要设计拓扑和路由逻辑。
方案三:LLM 路由 + 动态加载。先用一个轻量分类器判断当前任务属于哪个领域,然后在状态里动态挂载对应 Server 的工具。LangGraph 的状态持久化让这一招变得更加可行:可以把已加载的工具缓存到状态中,避免每次重复握手。
我自己在中等复杂度的项目里,默认先做方案一,跑通后再按工具调用冲突或效果衰减来决定要不要升级到方案二。过早容器化、过早拆图,都会让你在调试时多痛十倍。
3.3 两条集成路线:直接接 Protocol 还是用 Adapter
接入 LangGraph 有两条主流路线,很多人纠结选哪个:
- 路线 A:自己写 MCP Client 封装。用官方
mcpPython SDK,创建ClientSession,list_tools()拿工具,然后在 LangGraph 的节点里直接调call_tool()。优点是完全掌控协议细节,适合需要在调用前后加日志、鉴权、限流的场景;缺点是一切手写,代码量明显增加。 - 路线 B:使用
langchain-mcp-adapters。这个库提供了MultiServerMCPClient,能把多个 MCP Server 统一聚合成 LangChain Toolkit,然后直接传给 LangGraph 的 ToolNode。优点是省心,几行代码就连上了;缺点是屏蔽了底层细节,出问题时需要翻库源码。
选哪种,取决于你的项目阶段。如果是验证 demo,绝对选 B;如果要做生产级系统,我的建议是先用 B 跑通,然后把关键的 Server 调用改造成 A,给自己留调试入口。
4. 实操:一个双 Server 调用的 LangGraph Agent
4.1 环境准备与服务端编写
我用一个典型例子来演示:一个 Server 负责查 SQL 数据库里的订单,另一个 Server 负责读取本地文件模板。Agent 的最终任务是这样的:用户输入“查询订单 SO-001 的状态,并用模板 A 生成摘要”,Agent 需要先调数据库 Server 拿订单数据,再调文件 Server 读模板,最后合成为一段话。
先安装环境依赖:
pip install mcp langchain langgraph langchain-mcp-adapters langchain-anthropic我这里的 LLM 用的是 Claude 模型,你换成 OpenAI、通义千问等模型只需要替换 ChatModel 初始化,对 MCP 链路没有影响。
数据库查询 Server(sql_server.py):
from mcp.server.fastmcp import FastMCP mcp = FastMCP("OrderQueryServer") @mcp.tool() def query_order(order_id: str) -> str: """按订单ID查询订单状态,返回订单状态、金额、客户名""" # 这里简化成模拟数据,真实场景连接数据库 mock_db = { "SO-001": {"status": "shipped", "amount": 199.0, "customer": "张三"}, "SO-002": {"status": "pending", "amount": 89.0, "customer": "李四"}, } order = mock_db.get(order_id) if not order: return f"订单 {order_id} 不存在" return f"订单 {order_id} 状态: {order['status']}, 金额: {order['amount']}, 客户: {order['customer']}" if __name__ == "__main__": mcp.run(transport="stdio")文件 Server(file_server.py):
import os from mcp.server.fastmcp import FastMCP import pathlib mcp = FastMCP("FileTemplateServer") @mcp.tool() def read_template(template_name: str) -> str: """读取指定名称的模板文件内容,模板位于当前目录 templates 文件夹下""" base_dir = pathlib.Path("./templates") safe_name = os.path.basename(template_name) # 防目录穿越 file_path = base_dir / safe_name if not file_path.exists(): raise FileNotFoundError(f"模板 {template_name} 不存在") return file_path.read_text(encoding="utf-8") if __name__ == "__main__": mcp.run(transport="stdio")写完后,在项目里建一个 templates 目录,放一个 template_a.txt,内容写上“客户 {customer} 的订单 {order_id} 当前状态为 {status}”。两个 Server 分别在不同终端启动:
python sql_server.py python file_server.py启动后终端会一直不返回,等待标准输入收到协议消息。stdio transport 的特点就是这样,一切交流都在标准输入输出里进行,日志要输出到 stderr,以免污染协议流。第一次写 FastMCP 的人很容易在这里翻车:在工具函数里随手print(),结果把调试信息打到了 stdout,客户端解析直接失败。后面我会再强调一次这个坑。
4.2 用 MultiServerMCPClient 连接两个 Server
现在写主程序,核心是配置两个 Server 的连接信息,并获取聚合工具:
import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent async def main(): client = MultiServerMCPClient( { "sql-server": { "transport": "stdio", "command": "python", "args": ["sql_server.py"], "encoding": "utf-8", }, "file-server": { "transport": "stdio", "command": "python", "args": ["file_server.py"], "encoding": "utf-8", }, } ) async with client: tools = await client.get_tools() model = ChatOpenAI(model="gpt-4o-mini", temperature=0) agent = create_react_agent(model, tools) result = await agent.ainvoke( {"messages": [("user", "查询订单 SO-001 的状态,并读取模板 template_a 生成摘要")]} ) print(result["messages"][-1].content) if __name__ == "__main__": asyncio.run(main())这里有几个要点。get_tools()返回的是 LangChain Tool 对象,它们的名字默认会带上 Server 前缀吗?不会,默认就是 MCP Server 侧声明的工具名。如果两个 Server 里都有同名工具,你会得到两个同名 Tool 对象,LangGraph 的 ToolNode 遇到这种情况极大概率会出问题。所以我建议在 Server 侧就把工具名设计得带好前缀,例如sql_query_order、file_read_template,从源头避免冲突。
async with client这一步真的很重要。它负责启动所有子进程、完成协议握手。如果你在循环里反复创建和销毁MultiServerMCPClient,每次开启都会重新做握手,开销很大;更好的做法是复用一个长连接,手动管理生命周期。
4.3 用 ToolNode 构建带路由的 Agent 图
create_react_agent是 LangGraph 预构建的 ReAct Agent,适合快速验证。但如果你想控制多个 Server 的调用顺序、处理多步依赖,我更推荐手写 StateGraph。下面是一个带工具调用的核心图:
from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode from langchain_core.messages import HumanMessage, BaseMessage from typing import Sequence class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], operator.add] tool_names: list[str] def build_graph(model, tools): tool_node = ToolNode(tools) def agent_node(state: AgentState): # 把当前所有工具绑定到模型,让模型决定调用哪个 model_with_tools = model.bind_tools(tools) response = model_with_tools.invoke(state["messages"]) return {"messages": [response]} def should_continue(state: AgentState): last = state["messages"][-1] if hasattr(last, "tool_calls") and last.tool_calls: return "continue" return "end" graph_builder = StateGraph(AgentState) graph_builder.add_node("agent", agent_node) graph_builder.add_node("tools", tool_node) graph_builder.add_edge(START, "agent") graph_builder.add_conditional_edges( "agent", should_continue, {"continue": "tools", "end": END}, ) graph_builder.add_edge("tools", "agent") return graph_builder.compile()这套结构里:
AgentState里messages是对话历史,所有节点都能读写;agent_node把工具绑定给模型,由模型生成“是否调用工具”的决定;should_continue判断最后一条消息是否包含tool_calls,有就跳进tools节点,没有就收尾;ToolNode收到模型生成的 ToolCall 后,按工具名找到对应的执行函数,执行完把结果作为 ToolMessage 塞回 messages。
多 Server 在这里其实已经被抹平了:不管工具来自哪个 MCP Server,ToolNode 只认工具名。所以真正的多 Server 协调工作发生在工具名设计和模型意图理解上,而不是在图中加特殊节点。
4.4 完整执行流程与验证
把上面代码组装起来,跑一次完整流程。模型的执行逻辑大致是这样的:
- 看到用户消息“查询订单 SO-001 的状态,并读取模板 template_a 生成摘要”。
- 模型判定需要调
sql_query_order(order_id="SO-001"),生成 ToolCall。 - Agent 节点返回带 ToolCall 的消息,条件边把它送进 ToolNode。
- ToolNode 找到 SQL Server 对应的 MCP Client,通过内部会话发
tools/call,拿到订单状态消息。 - 状态更新回 messages,再次进入 Agent 节点。
- 模型看到订单结果,认为还需要读模板,于是生成第二个 ToolCall:
file_read_template(template_name="template_a")。 - ToolNode 执行第二个 Server 的调用。
- 模型拿到模板内容后,合成最终摘要,输出给用户。
整个过程里,两个 MCP Server 的调用顺序和次数由模型动态决定,这就是 Agent 与普通固定流程最大的区别。实际运行到第三步时,终端里能看到 SQL Server 子进程收到了协议请求,但如果你在工具里放了 debug print,注意它不会出现在主进程终端,而会污染 stdio 通道——这个要在开发早期就养成输出到 stderr 的习惯。
4.5 Supervisor 模式与更大规模的编排
如果你的场景复杂度更高,比如多个 MCP Server 分属不同团队、工具量上百,我会建议再升级一层,用 Supervisor 模式。大致思路是:
- 每个 MCP Server 对应一个子 Agent,子 Agent 的 model 只绑定该 Server 的工具;
- 顶层再放一个 Supervisor Agent,它不绑定任何工具,只根据用户指令把任务分配给对应子 Agent;
- 子 Agent 执行完并把结果汇报给 Supervisor,Supervisor 再做最终整合。
LangGraph 对这种情况的支持非常好,因为它天然支持图编排和多 agent 协作。不过要提醒一句:多 Agent 的调试复杂度远高于单 Agent,工具链路的故障传播也更隐蔽。我的建议是先在单图上跑通所有 MCP 工具,确认没有协议层问题,再按领域拆分。拆分后每个人的 Agent 只负责一种 Server,问题定位起来舒服很多。
5. 常见问题与排查实录
5.1 握手失败与协议版本不一致
现象:启动客户端后,server 子进程正常启动,但客户端在get_tools()时报错或一直卡住。
排查思路:先确认 server 单独跑起来有没有启动成功,有没有语法错误。stdio transport 下,server 因为缺依赖、读不到环境变量等任何原因在启动阶段崩溃,客户端都感知不到,只会看到“连接不上”。我遇到过 Windows 环境下python命令指向了 Microsoft Store 的假 python,server 实际没起来,客户端在那傻等。解决办法:手动在终端执行同样的启动命令,看有没有报错;或者把启动命令具体化,像C:/Python311/python.exe这样写绝对路径。
还有一种很常见的情况:协议握手阶段 client 和 server 的 protocolVersion 不一致。新版 mcp 库和旧版 server 之间容易出现版本协商失败。官方 SDK 一般会做兼容,但如果你用的是自研协议实现,这种版本不匹配就会直接告别。此时看协议日志,找到initialize请求和响应的protocolVersion,确认都到了同一天。
5.2 stdio 污染问题
现象:工具明明执行了,但客户端收到的内容全是乱码或奇怪的字符串。
原因:MCP Server 端代码里用了可以改变 stdout 输出的 API,或者打印了日志信息,导致标准输出里混入了非协议内容。协议解析器看到 JSON 中混着别的内容,自然失败或解析错误。
对策:所有查日志、调试信息输出到sys.stderr,不要用print()。FastMCP 里的日志配置也建议单独设置。这个坑无数次在开发阶段坑人,严格执行“stdout 只跑协议”原则可以帮你避开 90% 的诡异错误。
5.3 工具名冲突和多 Server 同名工具
现象:两个 Server 都提供了search工具,LangGraph 模型选择时犹豫不决,或者 ToolNode 执行时报错找不到唯一工具。
原因:LangChain Tool 集合要求工具名唯一,同名时后加入的会覆盖前面一个,或者抛异常。
对策:在 MCP Server 侧把工具名写得具体化,用{domain}_{action}的模式命名,例如sql_query_order、file_read_template。宁可名字长一点,也别让模型在十万八千里外猜错工具。如果实在改不了 Server 侧名称,可以在拿到 LangChain Tool 后通过tool.name重新复制一个新 Tool 对象改名。
5.4 异步环境下的生命周期管理
现象:在 Jupyter 或 FastAPI 里跑 LangGraph Agent,第一次调用成功,第二次就报“connection closed”或者子进程退出了。
原因:MultiServerMCPClient的生命周期在异步环境里没有正确维护。你可能在某个 request 结束后调用了 client 的关闭方法,或者没有用async with包住整个 Agent 生命周期。stdio transport 下的 MCP Server 是子进程,父进程一关连接它也就退出了。
对策:把 MCP 连接的创建和 Agent 的执行放在同一个上下文里,避免在中间穿插耗时操作导致连接被回收。服务化部署时,建议在应用启动阶段建立 MCP 连接,进程退出时再统一关闭,不要在每次请求里新建连接。
5.5 Windows 和容器环境的问题
搜索热词里能看到一堆 Windows Server、Docker Desktop 下面 MCP/Server 的报错,这确实和现实很贴合。Windows 上最常见的坑是:
- 命令名问题:
python、python3在不同环境指向不一致,建议用绝对路径; - 权限问题:某些进程以管理员权限启动,子进程继承权限后访问系统资源受限;
- Docker 环境:容器里跑 MCP Server,stdio transport 下客户端和 server 必须共享文件描述符,所以通常不适合跨容器直接做 stdio,要走 HTTP/SSE transport。
我个人的经验:本地开发阶段一律 stdio,简单直接;一旦要部署成多服务或者上生产,迁移到 HTTP/SSE transport,并在 Server 侧加上鉴权和日志。晚了会后悔。
以上这些坑,几乎都是我在真实项目里一个个踩出来的。MCP 的连接链路本来就长,客户端、SDK、Server、模型 Agent 四层叠加,任何一层出问题都会表现为“Agent 调不动工具”。我的排查顺序永远是:协议层(握手和工具列表)引发的问题 → 传输层(stdio或网络)问题 → Agent 层(工具绑定和模型决策)问题。顺着这个顺序走,你很快能找到根因。
最后说一点个人体会。MCP 的价值,不在于它能把工具调用写得有多炫,而在于它把外部能力接入这个本来很“脏”的活,变成了可以按标准重复做的事。多 Server 调用只是表面现象,真正的核心是让 Agent 不关心工具在哪儿、服务是谁写的,只关心工具叫什么、能干什么。这套思路一旦建立,你会发现从“为每个系统写适配器”到“为每个能力写 Server”,就像是把一堆乱七八糟的充电线换成了统一的 USB-C 口,连接成本一下子就下来了。
后续如果要扩展,我建议从这个方向继续深入:给 MCP Server 加完善的鉴权和审计日志,定义更严格的工具描述规范,再用 LangGraph 的检查点机制把多轮调用状态持久化。我个人在实际操作中的体会是,工具描述写得越接近“人的指令”,模型的理解越准确,比在 Agent 层堆 prompt 有效得多。这篇就当个起点,接下来你大可以自己动手,把第一个 SQL Server 和一个文件 Server 接进你的 LangGraph Agent,跑通之后再谈规模化。