ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP入门实战:Tool和LLM解耦,跨进程跨语言调用工具

MCP入门实战:Tool和LLM解耦,跨进程跨语言调用工具 MCP 入门实战Tool 和 LLM 解耦跨进程跨语言调用工具原来是这么回事如果你最近在折腾 AI Agent、智能体或者“让大模型自己用工具”大概率已经被 MCP 这个词刷屏了。MCPModel Context Protocol模型上下文协议是 Anthropic 在 2024 年底开源的一套开放协议官方的说法是“给 AI 应用接上标准化的工具接口”但我更愿意把它理解成一次对 Tool 和 LLM 关系的重新梳理——让模型不再被工具绑死让工具也不再被模型限制。今天这篇实战文章我就从“Tool 和 LLM 到底怎么解耦”这个角度出发用尽可能直白的话把我自己从头实现一个 MCP Server、跨进程跨语言调用工具的全过程拆给你看。这套东西适合谁如果你正在做 Agent 类项目或者公司内部有一堆老系统Java、Go、Python 混着来想让 LLM 调用又不想为每个模型单独写一套适配层那 MCP 几乎是目前最值得先研究的方案。读完这篇你应该能搞懂 MCP 的定位、Tool 的标准化格式、stdio 和 HTTP 两种传输方式以及“跨语言调用”到底是靠什么魔法实现的。1. MCP 到底解决了什么问题1.1 先从一次“手写函数调用”的痛说起假设你是一个后端工程师老板说“让 ChatGPT 能查我们公司内部订单数据”。你第一反应大概是给模型接一个 function call函数调用注册一个query_order(order_id)工具进去。这在单个模型、单台机器上问题不大。痛点从第二个系统就开始了。今天接 ChatGPT明天接文心一言后天又接一个开源的 Qwen每个模型的提示词模板、function calling 格式、参数解析方式都不一样。为了同一个业务工具你至少得适配三套协议。更麻烦的是工具本身常常不在 LLM 所在的进程里订单系统在 Java 微服务集群用户画像在另一个 Python 服务里你总不能为了让 LLM 调用把所有工具的远程调用逻辑全部揉进一个 Agent 进程里。还有一个容易被忽略的坑如果你把 AI 聊天服务和业务系统部署在不同的机器、不同的语言栈里传统的 function calling 根本管不了“跨进程的调度”。你常常需要自己写 RPC 框架、自己设计鉴权、自己定义参数格式——这些代码不是不能写只是写完之后你会发现它跟你接哪个 LLM 毫无关系纯属重复造轮子。我当初就是在做了三套类似适配之后开始认真思考一个问题工具和模型之间能不能有一层“所有对话双方都遵守的标准插座”1.2 MCP 的定位给工具调用一个“USB-C”接口MCP 的官网文档里把自己定义为“应用与工具之间的开放协议”。如果拿我们最熟悉的硬件来类比它做的事情非常像 USB-C过去你给手机充电要分 Micro-USB、Lightning、圆孔叫“各搞各的”今天只要设备支持 Type-C一根线走天下。MCP 就是想让 LLM 应用和外部工具之间按照一套标准接口插上就能用。具体到技术结构上MCP 采用了客户端-服务器模式MCP Server对外暴露工具、资源、提示词的一方负责真正干活。比如“查询天气”“读写数据库”都可以作为一个 Server 里的工具。MCP Client通常是 LLM 应用内嵌的组件负责发现 Server 上有哪些工具、调用这些工具并把结果交给 LLM。Host承载 Client 的宿主程序比如 Claude Desktop、VS Code 插件或你自己写的 Agent。这种分层设计的好处是工具提供方不需要知道上层用的是哪个模型上层也不关心工具的底层是怎么实现的。两边只认 MCP 协议。也就是说“Tool 和 LLM 解耦”不是一句口号而是被协议强制出来的结果——LLM 看到的是统一的“工具列表”Tool 看到的是一套通用的“请求/响应”格式。注意MCP 不是某个模型专属格式它是中立协议。你可以用 OpenAI 的模型做 Host也可以接 Anthropic 的模型底层 Server 不用改。2. Tool 和 LLM 解耦拆的是哪一层2.1 MCP 里的三个角色我们要理解“解耦”得先把 MCP 里的角色分清楚。按官方规范MCP 协议在抽象上定义了三种能力但最容易混淆的是前两种Tools工具可被 LLM 主动调用、执行某个操作的函数式能力比如“查天气”“下单”“发短信”。工具一般都有输入参数执行完会返回结果。在实际 Agent 场景里这是用的最多的一类。Resources资源可以被读取的上下文数据相当于给 LLM 提供“背景资料”。比如你要让 LLM 生成营销文案可以先把某些品牌规范文档作为 Resource 暴露出来。Prompts提示词模板预置的对话模板方便复用常用指令。在工程落地时Agent 框架无论是 LangChain 还是自研的最关心的就是 Tools。所以下面会用 Tools 做主线索来讲。2.2 Tool 是怎么被“说清楚”的MCP 之所以能跨语言、跨进程工作是因为它把“工具的描述”和“工具的执行”分离了。先看描述侧一个 MCP 工具在协议层面会被序列化成 JSON Schema 形式的定义。大致长这样{ name: get_weather, description: 查询指定城市的实时天气, inputSchema: { type: object, properties: { city: { type: string, description: 城市名例如 北京 } }, required: [city] } }这段 JSON 是给 LLM 看的或者说给“连接器”看的。模型读完以后“知道有一个工具能做这件事”然后会在合适的时机发起一次带参数的调用请求。你不需要在 Server 端进行任何模型相关的 Prompt 拼接也不需要关心当前到底是大语言模型A还是B。再看执行侧MCP Server 里的工具函数才是真正干活的它接收参数、执行逻辑、返回结构化输出。一次完整的工具调用大概是LLM 侧决定调用get_weatherHost 通过 MCP Client 发送tools/call请求参数为{name: get_weather, arguments: {city: 北京}}MCP Server 找到对应工具函数并执行Server 返回{content: [{type: text, text: 晴25℃}], isError: false}整个过程用的都是 JSON-RPC 2.0 风格的消息和具体语言无关。这是“解耦”的关键细节。2.3 Tool 与 LLM 解耦以后谁在“控制”谁有的朋友可能第一次接触时觉得别扭既然 LLM 会“主动调用”工具那到底是 LLM 控制工具还是工具在反过来约束 LLM我的理解是从实现层面看是 Host 在中间当调度器——它负责把工具定义、用户的指令一起喂给 LLM在 LLM 返回“我要调用工具”的意图后由 Host 真正发起调用再把结果回填给 LLM。这可以避免模型“幻觉出一段工具执行结果”。从架构层面看工具和 LLM 是完全对等的“服务方”耦合只发生在协议层的那一瞬谁也不用为了谁去改业务实现。也就是说MCP 把原来“又要选择模型、又要写工具注册代码”的复杂网络压成了一个“竖切”结构你有多少个 MCP Server就可以给多少个 Agent 用你有多少个 Agent也可以共享同一套 Server。这种架构在团队协作时很有价值——通常后端组只负责维护 MCP ServerAI 应用组只负责连 Host 和 Prompt两边可以通过接口文档并行开发。3. 5 分钟跑通第一个 MCP Server手写一个计算器工具3.1 环境准备讲完理论我们进入实战环节。这里我选用 Python因为 MCP 的官方 Python SDK 做得比较成熟并且 FastMCP 这套高层封装写起来很省事。需要的东西Python 3.10 以上MCP Python SDK安装命令pip install mcp[cli]如果你愿意折腾 TypeScript用modelcontextprotocol/sdk也是一样的逻辑。为了让后面“跨语言”的示例有对比本文服务端用 Python客户端也会用 Python 示例最后我再说说怎么用非 Python 客户端去连。3.2 用 FastMCP 快速实现一个工具新建一个文件weather_server.py先做最简单的版本from mcp.server.fastmcp import FastMCP # 创建 MCP Server名称会在 Client 端显示 mcp FastMCP(DemoServer) mcp.tool() def add(a: float, b: float) - float: 两个数字相加返回和 return a b mcp.tool() def get_weather(city: str) - str: 查询城市天气演示用随机返回一个固定结果 Args: city: 城市名比如 北京 # 这里可以替换成真实天气接口 return f{city}晴气温 26℃空气良好。 if __name__ __main__: mcp.run(transportstdio)这段代码看着轻但背后已经把不少事情处理好了mcp.tool()装饰器会自动从函数签名和docstring中提取工具名称、描述、参数 Schema。所以写工具时参数类型和说明文字一定要认真因为模型是靠它们来理解怎么调用的。接着启动服务python weather_server.py你会看到程序在等待标准输入。注意别直接回车因为 MCP 走的是 stdio 通道所有消息都是通过标准输入/标准输出用 JSON 传递的。这里回答一个常见疑问为什么默认是 stdio因为最简单、最安全两个进程之间只需有父子关系用管道通信就能完成整个协议交互。如果你的环境需要走网络也可以通过挂载额外的 transport 实现 HTTP。这部分后面再展开。3.3 用 MCP Client 发起一次调用现在再写一个客户端模拟 Agent 场景中的“Host”它向 Server 查询有哪些工具然后调用工具。新建文件client_demo.pyimport asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params StdioServerParameters( commandpython, args[weather_server.py] ) # 用 stdio 与子进程通信 async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # 1. 初始化连接 init_result await session.initialize() print(初始化:, init_result) # 2. 列出服务端提供的工具 tools_result await session.list_tools() for tool in tools_result.tools: print(发现工具:, tool.name, -, tool.description) # 3. 调用工具 result await session.call_tool(get_weather, {city: 上海}) print(调用结果:, result) asyncio.run(main())运行python client_demo.py你应该能看到类似下面的输出初始化: InitializeResult(protocolVersion2024-11-05, capabilities..., ...) 发现工具: add - 两个数字相加返回和 发现工具: get_weather - 查询城市天气演示用随机返回一个固定结果 调用结果: [TextContent(typetext, text上海晴气温 26℃空气良好。)]到这一步你已经完成了第一个完整的 MCP 工具调用闭环。整个交互过程工具和 LLM 之间没有直接连接全是 Client 代理转发这正好呼应了“解耦”这个词。4. “跨进程、跨语言”到底是怎么做到的4.1 进程边界与传输层启动上面的客户端后其实已经发生了一次典型的跨进程调用client_demo.py进程和weather_server.py进程是两个独立进程甚至如果你把command填成docker exec ...或者指向远程服务器上的命令还能进一步拉大进程间的物理距离。mcp 的跨进程底层主要靠两套 transportTransport通信方式适用场景stdio父进程启动子进程用标准输入/输出交换 JSON 消息本地进程最简单无需网络配置HTTPSSEClient 与 Server 通过 HTTP 请求 Server-Sent Events 响应流远程调用、跨主机、Web 服务集成stdio 是最容易理解的一种“进程边界”Client 拉起 Server往子进程的 stdin 里写 JSON-RPC 消息从 stdout 里读返回。因为消息格式是文本所以两个进程用什么语言写完全不影响对方。对于 HTTPSSE大致流程是客户端先通过 HTTP POST 初始化会话然后建立一个 SSE 流服务端通过该流回传数据客户端再次发起调用时再走 POST。现在新版规范里还开始推 Streamable HTTP本质上是把双向消息收敛到单条 HTTP 连接上做起来更简单。跨进程的真正收益我实际操作下来有几点非常直观不需要和 LLM 进程共享内存或线程某个工具崩溃了不容易拖垮整个 Agent。工具服务可以独立部署、扩容。权限可以收敛在 Server 侧比如数据库连接字符串、API Key 不用暴露给 LLM 应用层。4.2 跨语言的本质靠标准协议不靠 SDK很多朋友第一次听到“跨语言调用工具”下意识以为是 SDK 内部做了什么黑魔法。其实真相朴素得很大家约定用同一种消息格式JSON-RPC JSON Schema和传输通道stdio/HTTP两边按规范读写语言差异就消失了。你完全可以拿 Python 写 MCP Server然后用 Java 写 Client因为两者对“线格式”的理解是一致的。官方 SDK 之所以重要只是帮你省去了手写协议编解码的功夫即便没有 SDK只要按规范文档手工拼 JSON 也能实现客户端。举个例子如果我想用 Node.js 去调用上面的 Python Server在 MCP TypeScript SDK 里写一个 Client逻辑几乎一样import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: python, args: [weather_server.py] }); const client new Client({ name: demo-client, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(tools); const res await client.callTool({ name: get_weather, arguments: { city: 广州 } }); console.log(res);这个例子就能说明“语言不是障碍”——只要运行环境能启动 Python 子进程任何语言都能通过一条管道拿到同样的工具结果。这也是 MCP 在设计上最漂亮的一点它不是给某一种语言写的库而是一套协议。4.3 既然如此还有必要用 Function Calling 吗那么新的疑问就来了OpenAI 等厂商的 Function Calling 也很成熟为什么还需要 MCP我的看法是Function Calling 更像“模型侧的一种原生能力”它告诉你“模型倾向于用哪种 JSON 结构来发起工具调用”但仍然依赖你框架层去绑定具体的工具执行逻辑。工具一旦变成独立服务模型无法直接触碰数据库或外部 API你又得自己写一层授权和转发这层代码没有任何公共标准项目间没法复用。MCP 则把“工具开发”从“模型应用开发”里彻底拉了出来。你甚至可以先完全不考虑模型把业务系统的工具封装成 MCP Server后面谁要用谁去连。你还可以测试同一个 MCP Server 接 Claude、接自研模型、接多智能体框架全都走相同协议。不过也得承认MCP 不是银弹。它本身不解决“模型多聪明”的问题只解决“工具如何被标准化访问”的问题。模型对工具描述的语义理解、工具编排策略还是要靠上层 Prompt 和 Agent 算法做。4. 实操中值得注意的设计模式这一节算是加餐主要讲当 MCP Server 从玩具走向真实业务时我踩过或者看人踩过的一些坑以及现在比较常用的一套结构设计。4.1 一个 Server 还是多个 Server这里面有个很容易踩的坑有人把整个业务系统全部工具都塞进同一个 MCP Server导致 Server 越来越重而且向所有客户端暴露了全套工具。更合理的做法是按“领域”拆分 Server比如订单 MCP Server、商品 MCP Server每个 Server 管理某一类资源与工具。这有点像微服务领域里的 Bounded Context限界上下文好处是权限边界清晰上层 Agent 想连接什么就连接什么。如果你的团队有多个仓库最好提前在文档中规定好命名规范和工具命名的统一前缀比如订单域的工具统一以order_.*开头。4.2 保持工具函数的纯粹性在 MCP 工具实现里我强烈建议不要在工具函数内部做任何 LLM 相关的事比如让工具自己返回一段自然语言总结。最好让工具返回结构化数据由上层 Host/LLM 负责组织语言。这样才能让工具足够通用接不同的模型不会因为语言风格问题造成干扰。比如get_weather直接返回{temp: 26, condition: sunny}比返回一串“上海今天天气不错”更利于后续处理。MCP 虽然不强制工具返回的类型但整洁的数据结构肯定是上策。另外如果某个工具内部依赖网络、数据库要设计好超时和重试策略。因为 MCP Server 基于进程通信子进程不退出的话超时会波及上层模型调用。4.3 鉴权与安全问题MCP 协议本身尤其是 stdio 模式默认建立在“进程可信”的前提下。真实系统中如果走 HTTPSSE一定要考虑鉴权不能裸奔在公网。我建议一开始就要求 inbound/outbound 都带 token或者在网关层做 mTLS。凡是涉及数据库、文件系统敏感操作的工具在 Server 端务必做用户级授权校验不能只看“是不是本内网调用”。等到模型能自由调起你系统中的工具时传统的“系统边界”会被打破这时候工具开发者的安全意识得上线。引用一句行话“LLM 给了攻击者一把更长的杠杆而 MCP 给这把杠杆配了多个操纵杆接口。”5. 常见问题与排查实录5.1 “The tool call could not be parsed” 是啥意思很多人都见过这么一段报错The models tool call could not be parsed (retry also failed).尤其在用 Claude 或各种 OpenAI 兼容接口时遇到。这大概率并不是 MCP 协议的问题而是 LLM 输出“想调用的工具”时生成的 JSON 不符合调用规范。比如参数里多了注释、少了引号、或者使用了模型本身不支持的 function calling 格式。排查时我先建议看 Host 收到的原始消息是不是模型输出了 Markdown 代码块包裹的 JSON甚至把工具名拼错了。可以适当降低模型温度或者在 Prompt 里给出更严格的格式示例。如果确定格式没问题那再检查 MCP Server 端注册工具时是不是参数字段有缺失或类型不对。比如工具的inputSchema里写了必填字段city但调用请求没有传Server 端就直接 reject 了。5.2 Server 启动了但工具列表为空如果你用list_tools()查看不到任何工具先看代码里装饰器有没有生效。特别是当你把mcp.tool()写错了位置或者把函数定义放在if __name__ __main__之后注册时机就会不对。另一个常见情况是 FastMCP 在解析 docstring 格式时过于宽松导致描述为空但不报错。建议用python -c from weather_server import mcp; print(mcp.list_tools())这种方式先在本进程里检查服务器侧工具列表。5.3 使用 HTTPSSE 时连接不稳定我在开发远程 MCP 服务时遇到过一种坑本地 stdio 一切正常换成 SSE 后总是隔一会儿断连。排查下来发现是部署环境里的反向代理没开 SSE 所需的响应缓冲关闭设置数据流被缓冲池吃掉客户端迟迟收不到事件流。如果你用 Nginx 反代 SSE记得对相关路径设置proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s;另外SSE 是单向的它和 HTTP POST 一起配合状态管理比 stdio 复杂建议先用官方 SDK 的调试工具走通再联调。5.4 跨语言调用时中文乱码因为 stdout 传输协议的消息都是 UTF-8但你 Windows 控制台默认可能用 GBK。子进程打印乱码后Client 解析 JSON 就会失败。解决办法是服务端启动时强制 UTF-8比如在 Python 脚本里加上import sys sys.stdout.reconfigure(encodingutf-8)或者在主程序调用时设置环境变量PYTHONIOENCODINGutf-8。6. 从 Demo 走向生产三个进阶思路如果你已经消化了上面的内容可以开始尝试下面三个方向。它们是我自己在落地 MCP 时觉得最有价值的延伸也能帮你从“能用”走向“真正用起来”。第一把 MCP Server 容器化。这样 Server 可以跑在与应用隔离的容器里再通过 HTTPSSE 对外提供服务。团队共享一个 MCP 服务时尤其建议做成容器镜像避免每个人本地 Python 环境不一致导致“我这边能跑你那边起不来”。第二用 MCP 接入已有的内部 API。假设公司已经有一个 RESTful API 网关了不要急着用代码重写业务逻辑可以写一层 MCP Server 适配器把 API 的方法和参数映射成工具定义。适配器内部主要做参数校验和权限校验数据源不动风险最小。第三把 Agent 的“规划”和“执行”解耦。MCP 目前主要解决执行侧的工具标准化规划还是靠 LLM。你可以在 Host 侧引入一个简单的 Agent 循环根据用户意图多次调用模型规划 → 调用多个 MCP 工具 → 汇总结果。工具本身不需要知道“谁在编排它”它可以被任何 Agent 框架复用。另外提醒一句多智能体系统里工具上下文的管理也值得注意。每个智能体如果都连接几十个 MCP Server工具列表会非常长。模型上下文窗口有限最好做一层按需发现的机制别把所有工具描述一股脑塞给 LLM。就我个人近期实践的感受来说MCP 真正带来的改变并不是“多了一种协议”而是让工具的开发思维从“围绕模型 API 设计”转向“围绕通用协议设计”。做 MCP Server 端时我可以先不考虑接哪个模型不考虑未来的 Agent 长什么样只需要把一个业务动作定义得足够清晰然后交给协议去分发。无论是跨进程还是跨语言它的底层逻辑都是标准化消息交换而这恰恰是软件开发里最朴素也最持久的原则。最后再分享一个小技巧调试 MCP 时把协议收发日志全部打开能省掉一堆“疑似客户端问题其实是服务端挂掉”的无效排查时间你在实战中遇到的大部分疑难杂症最后都能从消息日志里找到答案。
返回列表