2026/11/3 8:37:39

从Function Calling到MCP:构建标准化的AI工具服务协议

从Function Calling到MCP:构建标准化的AI工具服务协议 1. Function Calling 的三堵墙没有协议、没有边界、没有生命周期如果你和我一样是从 Function Calling 开始接触 AI 工具链的大概率会在某个项目做到三分之二时感到不对劲模型确实能输出结构化的 JSON 参数你在代码里写了一个分支去执行对应函数再把结果塞回对话让它继续决策。看起来是一条顺畅的调用链但等你要把这套能力搬到第二个应用、第三个终端或者只是换一个模型供应商就会发现在 Function Calling 里积累的东西很难带走。MCPModel Context Protocol刚出现时很多人把它解释成“AI 界的 USB-C”这个类比不算错但真正动手做集成时又会发现它的价值不止于“统一接口”。它重新划定了工具的位置工具不再是调用方内存里的一个普通函数而是独立运行、自我描述、可被发现的服务。今天这篇我想把这两者放在同一张表里从协议设计而不是框架功能的角度拆一拆 Function Calling 为何不够用以及 MCP 用哪些决定补上了缺口。1.1 先退一步Function Calling 到底是什么Function Calling 是模型供应商提供的一种结构化输出能力。你在请求里附带一组工具描述模型生成回答时不再说“我需要查下库存”而是输出一个{name: query_stock, arguments: {...}}结构。调用方拿到这个结构之后在本地代码里路由到真实函数执行完再把它作为消息传回去模型再根据执行结果给出最终答复。这个过程对开发者友好调试也直观适合把一些小工具快速暴露给模型。但麻烦也藏在这种便利里。工具列表依附在“某一次模型请求”上真正的执行发生在你的应用进程里。这意味着同样一个函数如果旁边另一个服务也想调用它不能通过一次 API 请求里的 tool 列表就单独访问必须把整个执行环境搬过去复制代码、引入共享库或者另包一个 HTTP 服务。走到最后一步你就会发现自己其实在一遍又一遍地造轮子而且每个轮子的接口风格还都不一样。1.2 三堵墙的具体含义第一堵墙是“没有协议只有厂商样例”。不同模型供应商的 Function Calling 字段并不兼容。OpenAI 的调用格式、Anthropic 的工具描述、Google 的函数声明底层都会用 JSON Schema 描述参数但外层封装各有各的说法。今天你按 A 家的字段写了工具层明天想换 B 家就得写一层转换逻辑。小项目里这是成本大项目里这是负担。MCP 没打算定义一个“模型该怎么理解工具”而是定义了一套独立于模型的报文规范让工具在模型层之外也能被同一个客户端复用。第二堵墙是“执行没有边界”。Function Calling 的每个函数都跑在调用方进程内模型拿到的是函数返回结果而调用方必须自己处理密钥、环境变量、运行上下文。更现实的问题是权限原函数如果直连数据库或生产环境它所用的凭证往往就暴露在应用代码里。调用方进程权限过大工具本身又没有独立的执行沙箱这会成为隐性的操作风险。第三堵墙是“生命周期不存在”。多数 Function Calling 场景是一次请求、一次调用、拿到结果就结束。但如果工具本身有状态呢比如一个持续监听文件变化的工具一个需要分步执行的长任务或者一个要跟外部系统建立会话的查询Function Calling 都没有合适的承载形态。它适合“即调即走”的短操作却无法表达一个长期存在的服务。1.3 为什么简单场景下它仍然够用说了这么多并不是要让所有项目立刻抛弃 Function Calling。如果你的场景是单体应用、工具数量少、调用频率不高而且没有第二个消费端那么 Function Calling 依然是性价比最高的选择。它不需要额外部署服务不需要考虑进程间通信一个函数文件就能跑通。MCP 的价值点是“当工具开始被多个应用、多个模型、多个场景复用时需要一层独立于应用的服务边界”这时候再上协议才划算。维度Function CallingMCP工具定义格式随模型厂商变化统一 JSON Schema 描述执行位置调用方进程内独立 server 进程凭证粒度集中在调用方各 server 本地持有跨应用复用需要重复接入或改造一次接入多处消费生命周期随请求结束可建立长连接会话2. MCP 的设计出发点把工具调用从“参数协议”升维成“服务协议”Function Calling 解决的是“模型如何吐出一段参数”MCP 解决的是“工具如何作为独立服务存在”。这个区别是整个设计的分水岭。2.1 一次连接两端都要亮出能力MCP 的消息基座是 JSON-RPC 2.0。你可能已经熟悉 REST API用不同的 URL 表达不同资源用 GET/POST 表达操作。MCP 走的不是这条路它更像一个方法总线——无论你是列工具、读资源、发起调用还是推送日志都通过同一套method和params结构完成。客户端连接一个 MCP server 时通常先发一条初始化消息{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, clientInfo: { name: demo-client, version: 1.0.0 }, capabilities: {} } }服务端返回自己的协议版本和能力声明后客户端再发送 initialized 通知随后才进入正常的工具发现与调用阶段。这套流程比 Function Calling 重但它带来了一个重要能力双方可以协商。客户端告诉服务端自己支持哪些能力服务端也可以只暴露自己愿意开放的能力。这是一种“能力协商”而不是假设所有端都一定会长成同一套模型。2.2 服务端成为独立的执行边界MCP 里最值得琢磨的一个决策是工具不再以一个纯函数存在而是由一个服务器进程对外暴露。这个服务器可以跑在本地也可以跑在远端可以由客户端拉起也可以由部署平台托管。调用方拿到的永远是一个接口描述和一次调用结果而不是一段可以直接嵌入自己进程的代码。这带来一个很实际的改变工具的运行责任被分离了。原来工具跑在应用里应用挂了工具就没了现在工具跑在自己的 server 进程或者远端节点里调用方只是一个消费方。对于工具持有方来说它可以单独发布、单独升级、单独鉴权对于调用方来说它不需要看到工具背后的数据库连接串、凭证路径和操作系统依赖。2.3 传输可以变报文必须一致MCP 允许不同的传输方式本地开发常用 stdio也就是客户端直接拉起一个 server 进程通过标准输入输出传递 JSON-RPC 消息跨进程或者远端场景则可以用 HTTP、SSE 这类传输。为什么强调“传输可以变”因为协议层真正的资产不是网络通道而是消息格式本身。同样一条tools/call请求不管 server 是本地进程还是远程服务报文结构是一致的。开发者调试本地工具时用的客户端到生产环境连接远端 MCP server 时依然适用。这也是它和“厂商 API 字段”最大的区别字段规则随厂商走消息规则随协议走后者才具备跨模型迁移的可能。2.4 从语言实现解耦Function Calling 里你写的是一个语言函数调用方和实现方的依赖关系紧密到难以拆分。MCP 里你写的是一个可描述的服务客户端只关心你暴露的接口。于是当你想把某个工具迁移给另一个团队或者把同一个工具同时接入 IDE、网页端、命令行客户端时不需要重新处理原函数所在项目的依赖链。这就是“参数协议”和“服务协议”的分界线。3. 拆开 MCP 骨架Resources、Tools、Prompts 三个原语各司其职MCP 并没有简单地把 Function Calling 复制一遍而是定义了三个互相补充的原语。理解它们就能理解 MCP 为什么值得单独成为一个协议。3.1 Resources把上下文接到对话里Resources 在语义上类似于“只读的数据面”。一个 MCP server 可以把本地文件、数据库 schema、项目文档、监控指标都暴露成带 URI 的资源。客户端可以主动列举资源、读取资源也可以由服务端在合适的时机把资源变化推送出来。我之前做过的项目里有一个内部知识库工具就直接被包装成了一个 Resources 源。它的工作不是“执行某个操作”而是“提供当前版本的关键上下文”。模型在回答时不需要调用复杂工具只要把资源内容作为背景信息注入即可。从工程角度看这比把大段文档拼进 prompt 更可控服务端决定数据的生产与更新客户端决定什么时机读取而不是每次漫无目的地全部塞给模型。3.2 Tools可以被调用的执行能力Tools 是离 Function Calling 最近的组件。它的名字、描述、参数 schema 都可以通过tools/list暴露给客户端客户端再根据模型的需要发起tools/call。与 Function Calling 不同的地方在于MCP 对工具返回的结构做了规范化要求返回一组 content block而不是一个裸字符串。这样做的好处是渲染解耦。普通文本、图片、结构化数据都能在 content block 里表达客户端拿到后可以按照自己的界面能力渲染而不是只把字符串拼进对话。比如一个截图分析工具可以返回一张 base64 图片和一个描述文本块前端就能同时展示图片与结论。这个小设计看似不起眼却让工具的结果具备了跨端复用能力。3.3 Prompts把流程模板沉淀成资产Prompts 这个原语最有想象力。它不是一个“提示词字符串”而是一段可复用的多步对话模板。服务端可以定义 “代码审查” 这个 prompt客户端触发后服务端返回一组消息序列引导模型按照既定流程执行。这对团队协作价值很大。过去提示词散落在各个客户端、各个工程师的配置里版本更新靠复制粘贴。MCP 把流程模板放到 server 端统一维护客户端只是触发和执行模板本身成为可管理的资产。项目的操作规范、分析流程、报告模板都能以 Prompts 形式沉淀下来。3.4 最小 server 长什么样这里用 Python 的 FastMCP 库写一个极简示例方便你直观感受from mcp.server.fastmcp import FastMCP mcp FastMCP(demo-server) mcp.tool() def add(left: int, right: int) - int: 两个整数相加 return left right mcp.resource(note://today) def get_today_note() - str: return 今日重点是迁移 MCP if __name__ __main__: mcp.run(transportstdio)启动这个脚本后它便成为一个可以通过 stdio 通信的 MCP server。用兼容 MCP 的客户端连接后客户端会看到add这个 tool 和note://today这个 resource。整个过程里模型厂商并不感知它只是从一个符合协议的端点获取能力。这个最小模型足以说明MCP 的核心不是某个宏大平台而是一套把“能力”打包成标准服务的方法。4. 被大多数教程忽略的设计哲学上下文聚合、会话生命周期与权限边界讲 MCP 的教程很多但真正值得深入玩味的是它背后的设计取舍。我理解下来有三个点最容易被低估。4.1 上下文聚合连接多个数据源而不是只连接多个函数Function Calling 一次调用通常只解决一个具体动作。MCP 的设计却把“连接多个数据源”放在更优先的位置一个 MCP 客户端可以同时连接多个 server每个 server 提供自己的 resources 和 tools。客户端在拿到这些能力列表后由模型判断应该使用哪些。我在实际项目里同时挂了一个数据库 server 和一个浏览器自动化 server。数据库 server 提供只读的查询工具浏览器 server 负责打开页面并获取渲染结果。模型在一个对话里既查询了数据库又收集了页面实时数据最后把两边的结果合在一起给出结论。这种多源上下文聚合是 Function Calling 难以做到的因为你很难在一套工具定义里既承载数据读取又承载跨进程编排。4.2 会话生命周期从 initialize 到 shutdownMCP 引入了明确的会话生命周期。连接建立时先初始化协商连接存活期间可以持续交互关闭时通过特定报文结束。这套机制让 server 能够在会话内维持状态比如记录客户端偏好、暂存中间结果、连续推送进度。这看起来是协议细节实际上直接影响工程实现。一个长耗时任务在 MCP 场景里可以和客户端保持连接服务端用进度通知持续汇报状态而在 Function Calling 场景里你只能阻塞等待函数返回或者自己改造一套异步机制。MCP 把异步和状态变成协议的一部分让制造复杂工具的团队不需要重新发明轮子。4.3 权限边界从函数级走向服务级MCP 的另一个哲学是“让工具拥有自己的运行边界”。server 可以持有自己的密钥、读取自己的配置、访问自己需要的外部系统而客户端只看到接口和返回值。这种隔离在治理层面很关键因为它让权限粒度从一个函数缩小到一个服务单元。举例来说如果一个银行应用的工具需要访问用户账户数据Function Calling 模式下密钥要么存放在应用环境里要么被注入到工具执行的进程里边界模糊。MCP 模式下密钥可以放在独立的账户服务进程里调用方只发送参数服务端在自身环境内完成鉴权和访问。你需要控制的节点从一个巨大的应用进程变成一个可独立审计的 server。4.4 关于它做不到的事MCP 不是安全层也不是万能胶水。它规范了工具之间、工具与客户端之间的对话格式却不改变工具本身的推理能力。你的工具返回的内容含糊MCP 不会自动把它变清晰你的 server 存在漏洞MCP 同样不会替你挡住。它的价值是“让工具更容易被组合、替换和治理”而不是“让工具变得更聪明”。如果一开始就把这个边界画清楚后续不会对它产生不切实际的期待。5. 从热搜里翻出三个高频翻车点超时、日志、幂等最近社区里关于 MCP 的问题越来越多除了“MCP 是什么”这种入门问题真正让开发者卡住的多半是实际运行时的小环节。我翻了翻手头碰到过的案例三个高频翻车点值得单独拿出来讲。5.1 超时客户端默认快速失败很多 MCP 客户端为了避免请求悬挂会设置一个较短的超时时间常见的是 30 秒。对于大多数查询类工具30 秒够用但一旦你的 server 要做 Blender 渲染、Unity 场景导入、浏览器大规模页面抓取这种事30 秒根本不够。我见过一个把几何计算封装成 MCP 工具的项目模型请求一次后客户端直接超时日志里什么都没留下。排查后发现不是工具逻辑问题而是客户端默认超时太短。解决办法有两个方向如果任务确实需要长时间计算最稳的方式是让工具立即返回一个任务 ID再提供另一个状态查询工具如果任务本身具备分阶段能力就用 MCP 的进度通知机制持续上报。单纯把客户端超时调大只能临时缓解并不能解决长任务的管理问题。5.2 日志别污染 stdio 通道如果你用 stdio 方式运行本地 MCP server那么 stdout 被协议消息占用。这时候你如果图省事在代码里console.log打印调试信息这些文本会被客户端当成 JSON-RPC 报文来解析轻则日志错乱重则直接中断通信。正确做法是把日志写到独立通道。本地调试时至少要用标准错误输出而不是标准输出更规范的方式是使用 MCP SDK 自带的日志能力通过协议层的logging/setLevel和消息通知上报让客户端统一收集和展示。自定义日志管理也是一样的原则你选任何日志框架都可以但必须保证日志不混入 stdout 的协议流。import logging from mcp.server.fastmcp import FastMCP logger logging.getLogger(demo-server) logging.basicConfig(levellogging.INFO) mcp FastMCP(demo-server) mcp.tool() def ping() - str: logger.info(someone called ping) return pong if __name__ __main__: mcp.run(transportstdio)这样运行后日志会出现在标准错误或独立日志系统里不会干扰协议通信。很多新上手 MCP 的人第一周就栽在这个点上检查代码逻辑找不出问题最后发现是日志把 stdout 挤爆了。5.3 幂等客户端重试服务端不能重复生产副作用网络请求存在不确定性MCP 客户端在超时或连接异常时可能会重试。如果你的工具不是只读操作而是“生成报告”“发送通知”“创建资源”这类有副作用的动作就必须考虑幂等。我踩过的坑是一个报表生成工具。客户端第一次调用时服务端已经生成了文件但响应在网络上丢失客户端重试后又生成了一份重复报表。解决办法是在工具层支持幂等键客户端传一个请求 ID服务端用 ID 判断是否已处理已处理就直接返回之前的结果。这跟普通微服务设计的思路一致但放到 MCP 里容易被忽略因为很多人还停留在“函数调用不会重复”的直觉里。5.4 生态为什么热闹Blender、Playwright、BurpSuite 都不缺从热搜词里可以看到Blender MCP、Playwright MCP、Chrome DevTools MCP、BurpSuite MCP、Unity MCP 这些名词在快速出现。你会发现这些工具差异很大有的是设计软件有的是浏览器自动化有的是安全测试工具。但它们的共同点是被封装成标准 MCP server 之后客户端只需要一套接入逻辑就能消费这些完全不同的领域能力。这其实是协议价值最直接的体现。Figma、Blender、Unity 这些软件各有自己的脚本接口和本地生态如果没有 MCP你每接入一个工具都得写一套专门的适配代码。MCP 出现后工具作者只要写一个 bridge server把软件的脚本接口翻译成标准报文任何支持 MCP 的客户端便都能使用。生态热闹不是因为某个模型特别好用而是标准协议降低了所有工具的接入成本。6. 从 Function Calling 迁移到 MCP 的落地路径以及不做迁移的理由如果你已经有一定 Function Calling 基础迁移到 MCP 并不需要推翻重写。真正需要的是一次思路转换从“写函数”变成“写服务”。6.1 第一周把现有函数包成标准 server最稳妥的迁移方式是不要一上来就动核心业务而是挑一个不敏感、调用频率低的工具先试水。我的建议分四步列出当前用 Function Calling 实现的工具清单明确输入输出结构。为每个工具用 JSON Schema 描述参数尽量沿用原有定义。用 MCP SDK 写一个最小的 server把这些函数注册成mcp.tool()。用 MCP Inspector 或任意兼容客户端连接验证调用链路跑通。这一步的目的不是马上替换生产环境而是让团队熟悉“工具作为一个独立进程”的心智模型。你可能会发现有些函数里引用了全局变量、隐式依赖调用方环境这些问题在 Function Calling 里不明显一旦拆成独立 server 就暴露了。早暴露反而是好事。6.2 第二步把输入输出从“面向模型”改成“面向协议”Function Calling 的参数定义往往以模型理解为优先而 MCP 的参数定义要兼顾客户端展示和其他消费端。写出更准确的描述、为返回内容增加结构化字段、区分只读工具和写操作这些都会提升迁移后的质量。我通常建议在工具命名上就把副作用写清楚。比如get_user_profile可以只读update_user_profile就要标注为写操作。客户端的权限策略可能基于工具名称或能力分组做判断清晰的命名比后面加一堆文档更有效。6.3 什么情况不做迁移不是所有场景都应该迁移。如果工具只在一个应用内使用、没有跨端复用诉求而且要求极低延迟、极低资源开销那么 MCP server 的额外部署和通信开销确实没必要。MCP 的价值建立在复用和组合上单点使用的工具直接留在 Function Calling 里反而更高效。另一个不该迁移的场景是“临时脚本”。如果你只是在一个 Jupyter Notebook 里让模型帮忙调个函数增加一个 server 进程纯属多余。把握好边界它被几个消费端需要未来会不会被其他模型调用答案是“不确定”时可以先不上 MCP。6.4 我踩过一圈之后留的三个判断第一MCP 最适合作为“团队级基础设施”来建设而不是每个人各自加一个 server。给 server、凭证、资源这三类东西找一个集中管理的位置否则会从一个编程问题演变成配置管理问题。第二协议层要尽早把日志、幂等、鉴权当一等公民。很多项目先用 demo 跑通上线后才发现日志采集不了、重试导致重复操作、密钥散落在多个环境里。MCP 的协议给了一半的支持另一半需要工程纪律来补。第三不要把所有工具都做成一个 server。我更习惯按领域拆分数据库一组、浏览器一组、设计软件一组。表面上多部署几个进程实际上每个 server 的权限边界更清晰出问题时影响面也更小。这个决策在项目第一天看不出区别等工具数量增长到两位数时你会感谢当初的克制。MCP 和 Function Calling 的争议本质上是“一次性调用”和“基础设施化”的之争。如果你正在构建的不只是一次对话而是一套会持续演化的 AI 能力平台那么与其继续在 Function Calling 里补协议、补边界、补生命周期不如早点按服务协议的方式来设计。把这些边界想清楚你的工具才经得起多次消费端的轮番调用。