
1. 从 Function Calling 到 MCP一场关于“能力边界”的重新思考如果你在过去一年里深度使用过任何主流大模型平台的 Function Calling 能力大概率经历过这样一个过程一开始觉得“哇模型能调工具了”然后很快发现每接一个新工具就要写一遍 JSON Schema每换一个模型供应商就要重新适配一遍参数格式工具一多Prompt 里塞满了各种函数描述模型开始犯迷糊调用准确率断崖式下跌。这不是你的问题这是 Function Calling 这套机制本身的设计边界。Function Calling 解决的是“模型如何表达它想调用某个工具”的问题但它没有解决“工具如何被标准化地描述、发现、连接和治理”的问题。MCPModel Context Protocol要做的恰恰是把后面这半截补上。你可以把 Function Calling 理解成“打电话”MCP 理解成“通讯录加运营商协议”——前者管的是通话内容后者管的是整个通信基础设施。这篇文章适合三类人看一是正在做 AI Agent 应用开发、被 Function Calling 的碎片化折磨过的工程师二是想理解 MCP 到底解决了什么问题、值不值得投入时间学习的技术决策者三是对协议设计本身感兴趣、想从中学习系统设计思路的架构师。我会从 Function Calling 的实际痛点出发逐层拆解 MCP 的设计哲学补充大量实操细节和踩坑经验尽量让你读完就能动手。2. Function Calling 的真实困境为什么“能用”和“好用”之间隔着一道鸿沟2.1 工具描述碎片化每接一个工具就是一次重复劳动Function Calling 的核心工作方式是你在 API 请求里附带一个tools数组每个工具用 JSON Schema 描述它的名称、参数、类型、是否必填。模型根据用户输入决定是否调用某个工具并生成对应的参数 JSON。这套机制在工具数量少的时候没问题。但当你需要接入 10 个、20 个甚至更多工具时问题就来了。每个工具的 Schema 都要手写写完之后还要调优描述文字——因为模型对参数描述的理解直接影响调用准确率。我试过在一个客服场景里接入 15 个工具光是调 Prompt 里每个工具的描述就花了整整两天而且换一个模型版本之前调好的描述又得重新验证。更麻烦的是不同模型供应商对 Function Calling 的支持格式并不完全一致。有的用tools字段有的用functions字段参数结构也有细微差别。你为一个模型写好的工具定义换一个模型可能就要改。这不是某一家的问题而是缺乏统一标准导致的行业性重复劳动。2.2 上下文膨胀工具越多模型越“糊涂”Function Calling 的另一个硬伤是上下文占用。每个工具的 JSON Schema 都要塞进请求的上下文窗口里。一个中等复杂度的工具描述大概 200 到 500 个 token20 个工具就是 4000 到 10000 个 token。这些 token 在每一轮对话中都要重复发送既浪费成本又挤占了本该留给用户内容的上下文空间。而且模型在面对大量工具描述时选择准确率会明显下降。这不是模型“笨”而是信息过载导致的注意力分散。就像你同时给一个人 20 张名片让他记住他大概率一张都记不牢。实测下来当工具数量超过 10 个调用准确率就开始明显波动超过 20 个基本就不可靠了。2.3 状态管理缺失每次调用都是“失忆”状态Function Calling 本质上是无状态的。模型生成一个工具调用请求你的后端执行完返回结果这一轮就结束了。如果工具调用需要多步协作——比如先查数据库拿到用户 ID再用这个 ID 去调另一个服务——你得自己在后端维护这个状态流转逻辑。这意味着什么意味着你的后端代码里充满了各种 if-else 和状态机逻辑而这些逻辑本应由协议层来标准化处理。每次新增一个多步工具链就要写一套新的编排代码。这种重复劳动在 Agent 场景里尤其致命因为 Agent 的核心价值就在于自主编排工具而 Function Calling 把编排的负担全甩给了开发者。2.4 发现机制空白工具从哪来、怎么找、如何更新Function Calling 没有定义工具的发现机制。你的工具列表是硬编码在代码里的或者从某个配置文件里读的。如果工具提供方更新了接口你得手动同步。如果想让模型动态发现可用工具对不起Function Calling 不管这个。这就导致了一个尴尬的局面你想做一个“工具市场”或者“插件生态”但底层协议根本不支持动态发现和注册。每个平台都得自己造一套工具注册中心互不兼容。MCP 的出现很大程度上就是为了填这个坑。3. MCP 协议的设计哲学不是替代 Function Calling而是补全它3.1 分层解耦把“说什么”和“怎么说”分开MCP 最核心的设计思路是分层。它把整个工具调用过程拆成了三层传输层、协议层、能力层。传输层负责数据怎么传支持 stdio标准输入输出和 HTTPSSE 两种方式。stdio 适合本地进程间通信比如你的 IDE 插件和本地工具服务之间的通信HTTPSSE 适合远程服务比如云端 API。这一层完全独立于上层你可以根据部署环境自由选择。协议层定义了消息格式、请求响应模型、错误处理、生命周期管理等。这一层是 MCP 的骨架它规定了 Client 和 Server 之间如何握手、如何交换能力信息、如何发起调用、如何处理异常。能力层定义了具体的工具、资源、Prompt 模板等。这一层是开发者最常打交道的部分你在这里定义你的工具长什么样、接受什么参数、返回什么结果。这种分层的好处是传输方式变了不影响工具定义工具定义变了不影响传输逻辑。对比 Function Calling 把所有东西揉在一个 JSON 里的做法MCP 的可维护性高了一个量级。3.2 双向通信Server 也能主动“说话”Function Calling 是单向的模型发起调用后端执行返回。MCP 支持双向通信Server 可以主动向 Client 发送通知比如工具列表更新了、某个资源发生了变化、采样请求需要处理等。这个能力在实际场景中非常有用。举个例子你有一个 MCP Server 封装了数据库查询工具当数据库表结构发生变化时Server 可以主动通知 Client 更新工具描述而不需要重启整个服务。再比如Server 可以在执行长时间任务时主动向 Client 发送进度通知让用户知道当前进展。这种双向能力让 MCP 更适合构建复杂的、有状态的 Agent 系统。Function Calling 的无状态模型在简单场景下够用但在需要持续交互的场景里就显得力不从心。3.3 能力协商握手阶段就确定“你会什么、我要什么”MCP 在连接建立时会进行一次能力协商。Client 和 Server 互相告知自己支持哪些能力Client 告诉 Server 我支持采样、支持根目录变更通知Server 告诉 Client 我提供工具、提供资源、提供 Prompt 模板。这个握手过程看起来简单但它解决了一个 Function Calling 没有解决的问题动态发现。Client 不需要提前知道 Server 有哪些工具连接之后自动获取。Server 也不需要假设 Client 支持什么协商之后按共同支持的能力来交互。这种设计让 MCP 天然适合构建插件化生态。你可以开发一个 MCP Server 提供某种能力任何支持 MCP 的 Client 都能即插即用不需要为每个 Client 单独适配。3.4 资源与工具分离不只是“调用”还有“读取”Function Calling 只有“调用”这一个概念。MCP 把能力分成了三类Tools工具、Resources资源、Prompts提示模板。Tools 是可执行的操作比如查询数据库、发送邮件、调用 API。Resources 是可读取的数据比如文件内容、数据库记录、配置信息。Prompts 是可复用的提示模板帮助用户快速构建特定任务的输入。这种分类的意义在于不是所有交互都需要“执行”。有时候模型只需要读取一些上下文信息不需要触发副作用。Function Calling 把读取和执行混在一起导致你不得不为“只读”操作也定义一个函数。MCP 的资源概念让只读访问变得自然也更容易做权限控制。4. MCP 核心细节拆解从握手到调用的完整链路4.1 初始化握手一次连接多次协商MCP 连接建立的第一步是初始化握手。Client 发送initialize请求包含协议版本、Client 能力、Client 信息。Server 返回initialize响应包含协议版本、Server 能力、Server 信息。这个过程看起来简单但有几个细节值得注意。协议版本号是必须的它决定了后续交互遵循哪套规范。Client 能力和 Server 能力是双向声明的双方根据对方的能力来决定后续行为。比如 Client 声明支持samplingServer 就可以在需要时向 Client 发起采样请求。握手完成后Client 发送initialized通知表示准备就绪。这个通知是必须的Server 收到后才开始正常处理请求。我见过有人漏掉这一步导致 Server 一直处于等待状态所有后续请求都被阻塞。4.2 工具发现tools/list的返回结构与分页处理握手之后Client 通常第一件事是调用tools/list获取可用工具列表。返回结构包含一个tools数组每个工具包含name、description、inputSchema三个核心字段。name是工具的唯一标识建议用蛇形命名法避免特殊字符。description是给模型看的自然语言描述直接影响调用准确率要写得清晰具体。inputSchema是 JSON Schema 格式的参数定义支持嵌套对象、数组、枚举等复杂类型。如果工具数量很多Server 可能返回分页结果包含nextCursor字段。Client 需要根据这个字段继续请求下一页直到nextCursor为空。这个分页机制在工具数量超过 50 个时特别有用避免一次性返回过大导致传输超时。4.3 工具调用tools/call的请求与响应格式调用工具时Client 发送tools/call请求包含name和arguments两个字段。arguments是一个 JSON 对象键值对对应inputSchema中定义的参数。Server 执行完成后返回结果包含content数组。每个 content 项可以是文本、图片、资源引用等类型。这种灵活的内容格式让工具可以返回富媒体结果而不只是纯文本。如果执行出错Server 返回isError: true和错误信息。Client 需要根据这个标志判断调用是否成功而不是只看 HTTP 状态码。这一点和 REST API 的设计哲学不同需要特别注意。4.4 资源订阅resources/subscribe的实时更新机制MCP 的资源订阅机制允许 Client 订阅某个资源的变更通知。Client 发送resources/subscribe请求指定资源 URI。当资源内容发生变化时Server 主动发送notifications/resources/updated通知。这个机制在需要实时同步的场景里非常有用。比如你有一个 MCP Server 封装了配置文件读取工具当配置文件被外部修改时Server 可以通知 Client 重新读取而不需要 Client 轮询。订阅是 per-resource 的每个资源需要单独订阅。Server 可以在能力声明中告知是否支持订阅Client 根据这个信息决定是否使用该功能。4.5 采样请求Server 反向调用模型的机制采样是 MCP 中一个比较独特的能力。它允许 Server 向 Client 发起请求让 Client 调用模型生成内容。这个机制在 Server 需要模型辅助决策时非常有用。比如你有一个 MCP Server 封装了代码审查工具它可以在审查过程中向 Client 发起采样请求让模型判断某段代码是否存在安全风险。Client 收到采样请求后调用模型生成结果再返回给 Server。采样请求包含messages数组和maxTokens等参数格式和主流模型的对话接口类似。Client 可以根据自己的策略决定是否接受采样请求以及使用哪个模型来处理。5. 实操落地从零搭建一个 MCP Server 的完整过程5.1 环境准备与依赖选择搭建 MCP Server 的第一步是选择开发语言和 SDK。官方提供了 Python 和 TypeScript 两个版本的 SDK社区还有 Go、Rust 等语言的实现。如果你主要做数据类工具Python 生态更丰富如果你做前端工具集成TypeScript 更顺手。以 Python 为例安装官方 SDKpip install mcp这个包提供了 Server 和 Client 的基础类以及 stdio 和 SSE 两种传输方式的实现。建议用虚拟环境安装避免和系统 Python 包冲突。5.2 定义工具从 Schema 设计到描述优化定义工具是 MCP Server 开发的核心工作。每个工具需要三个要素名称、描述、参数 Schema。名称用蛇形命名法比如query_database、send_email。描述要写清楚工具的功能、适用场景、返回什么。参数 Schema 用 JSON Schema 格式支持 string、number、boolean、object、array 等类型。我踩过的一个坑是描述写得太笼统导致模型不知道该在什么场景下调用。比如“查询数据”这个描述就太模糊改成“根据用户 ID 查询订单记录返回订单号、金额、状态”就清晰多了。描述里最好包含一两个使用示例帮助模型理解。5.3 实现 stdio 传输本地进程间通信的最佳实践stdio 传输是 MCP 最常用的方式适合本地工具服务。Server 通过标准输入读取请求通过标准输出写入响应。实现时要注意几点第一不要在 stdout 打印任何非协议内容否则会干扰消息解析。调试信息应该输出到 stderr。第二消息以换行符分隔每条消息是一个完整的 JSON-RPC 对象。第三处理异常时要返回标准的错误响应而不是直接崩溃。一个常见的错误是在工具执行过程中打印了日志到 stdout导致 Client 解析失败。我建议在开发阶段就把所有日志重定向到 stderr养成习惯。5.4 实现 SSE 传输远程服务的部署要点SSE 传输适合远程 MCP Server。Client 通过 HTTP POST 发送请求Server 通过 SSE 流推送响应和通知。部署 SSE Server 时要注意第一需要处理跨域问题配置合适的 CORS 头。第二SSE 连接是长连接需要设置合理的超时和心跳机制。第三要考虑并发连接数避免单机承载过多连接导致资源耗尽。如果部署在容器环境建议用反向代理处理 TLS 终止和负载均衡。SSE 对代理的超时配置比较敏感需要确保代理不会过早断开空闲连接。5.5 调试与测试用 MCP Inspector 快速验证官方提供了 MCP Inspector 工具可以可视化地测试 MCP Server。启动方式npx modelcontextprotocol/inspectorInspector 会启动一个 Web 界面你可以连接 Server、查看工具列表、手动调用工具、查看请求响应日志。这个工具在开发阶段非常有用能帮你快速定位问题。我通常的调试流程是先用 Inspector 验证工具定义是否正确再用真实 Client 做集成测试。Inspector 能帮你排除协议层的问题让你专注于工具逻辑本身。6. 常见问题与排查技巧实录6.1 连接建立失败握手阶段的典型错误连接建立失败是最常见的问题通常表现为 Client 一直等待或直接报错。排查思路如下现象可能原因排查方法Client 无响应Server 未启动或端口错误检查 Server 进程和监听端口握手超时协议版本不匹配检查双方声明的版本号初始化后无响应漏发 initialized 通知检查 Client 是否发送该通知连接立即断开stdout 有非协议输出检查 Server 日志输出位置我遇到最多的情况是 Server 在启动时打印了欢迎信息到 stdout导致 Client 解析第一条消息就失败了。解决办法很简单所有非协议输出都走 stderr。6.2 工具调用返回错误参数校验与异常处理工具调用返回错误时先看错误信息是协议层还是业务层。协议层错误通常是参数格式不对比如该传数字传了字符串。业务层错误是工具逻辑本身抛出的异常。参数校验建议在工具实现的最开始做用 JSON Schema 验证库自动校验。这样能在早期发现格式问题避免错误传播到业务逻辑深处。异常处理要返回结构化的错误信息包含错误码和可读的描述方便 Client 展示给用户。6.3 性能瓶颈大量工具时的响应优化当工具数量超过 50 个时tools/list的响应可能变得很慢。优化思路有几个第一启用分页每次只返回 20 个工具。第二缓存工具列表只在工具变更时重新生成。第三精简工具描述去掉冗余文字。如果单个工具执行很慢考虑异步执行和进度通知。MCP 支持 Server 主动发送进度通知让 Client 知道任务还在进行中避免超时断开。6.4 安全考量权限控制与输入验证MCP Server 的安全问题不容忽视。第一工具执行要有权限控制不是所有 Client 都能调用所有工具。第二输入参数要严格验证防止注入攻击。第三敏感操作要有审计日志记录谁在什么时候调用了什么工具。我建议在 Server 层面实现一个简单的权限模型每个工具标记所需权限Client 在握手时声明自己的权限范围Server 根据这个范围决定是否允许调用。这个模型虽然简单但能挡住大部分误操作和恶意调用。6.5 跨平台兼容不同 Client 的适配经验不同 MCP Client 对协议的支持程度不同。有的 Client 只支持 stdio有的只支持 SSE。有的 Client 支持采样有的不支持。开发 Server 时要考虑这些差异在能力声明中如实告知让 Client 自己决定是否使用某些功能。我通常会在 Server 里做一个兼容层根据 Client 声明的能力动态调整行为。比如 Client 不支持采样Server 就跳过需要采样的逻辑用规则引擎替代。这样能最大化 Server 的适用范围。7. 我对 MCP 落地的一些真实体会MCP 不是银弹它解决的是标准化和互操作性问题不解决模型能力本身的问题。如果你的工具描述写得烂换成 MCP 一样调用不准。如果你的工具逻辑有 bug换成 MCP 一样会出错。但 MCP 确实让工具生态的构建变得可行了。以前你想做一个工具市场得自己定义一套标准然后求着别人适配。现在有了 MCP你只需要实现一个 Server任何支持 MCP 的 Client 都能用。这个网络效应一旦形成工具生态的繁荣速度会快很多。我在实际项目里的做法是先用 Function Calling 快速验证场景确认工具有价值后再封装成 MCP Server 做标准化。这样既保证了开发速度又为后续的生态扩展留了空间。不要一上来就追求完美架构先跑通再优化。最后分享一个小技巧MCP Server 的工具描述可以用模型来生成初稿然后人工调优。具体做法是把你工具的实际功能、参数、返回值喂给模型让它生成描述文字你再根据实际调用效果微调。这样能省不少时间而且模型生成的描述往往比人手写的更符合模型自己的理解习惯。