
最近团队里为给AI装手脚吵了一轮。起因是某客户要求把一套内部审批流接入大模型让AI帮忙查单、填表、走流程。起初自然想到Function Calling后来有人提出用MCPModel Context Protocol一把梭说这是AI时代的USB接口。到底哪种方案更值我决定把两个东西拆开揉碎从原理、实操、成本到踩坑完整盘一遍。这篇文章就是这次梳理的产物写给正在评估工具调用方案、犹豫要不要追MCP的开发者看。1. Function Calling先把能调函数这件事讲透1.1 它到底是给模型加了什么能力先澄清一个常见误解Function Calling并不是让大模型直接执行你的代码。模型还是一个文本生成模型它做的事情没有变——根据对话上下文生成文本。真正的变化在于它会多生成一段结构化的调用意图告诉外部系统我现在需要调用某个工具参数是这样的。整个链路是这样的你提前把一系列工具的定义包括函数名、功能描述、参数JSON Schema通过API传给模型。模型在回复用户问题时如果判断需要外部信息或操作就会输出一个标准的tool_calls对象。你的程序拿到这个对象后自己去执行对应的函数拿到结果后再以工具消息的形式塞回给模型模型基于结果继续组织回答。这就是一个闭环。模型本身不会调起你系统里的任何东西真正的执行者是你的业务代码。有朋友拿自动驾驶打比方Function Calling像是给语言模型装了一个方向盘和油门但真正开车的人还是你。它只是告诉你要往哪拐方向盘还是得你打。这也解释了为什么模型能输出调用却永远不可能自己完成接口鉴权或数据库连接。1.2 没有它之前AI是怎么瞎编的在没有Function Calling之前想让模型回答这个账号余额是多少这类问题不外乎两种土办法。第一种是RAG把数据塞进上下文里让模型检索结果就是token消耗巨大数据还可能不新鲜。第二种是提示词工程——如果用户问余额请回复需要查询——然后靠人工去接话体验割裂到不行。Function Calling把意图识别参数抽取动作执行三件事一次搞定。开发者只要定义好工具模型会自动判断当前这个问题需要哪个工具、参数是什么。这在结构化交互场景里带来的体验提升是质变级的从抱歉我无法访问实时数据变成好的我已为你查询到最新余额。1.3 核心收益不能只看前端效果很多团队上Function Calling第一反应是这能让我的AI看起来更聪明。但以我的经验真正的价值要从三个层面看第一交互闭环。用户不用再被引导去别的系统操作AI直接完成查、改、发、签等一系列动作。这对to B产品尤其重要因为B端用户根本不在乎你用了什么模型只关心能不能少点几个按钮。第二降本增效。模型本来要硬背一堆参数格式现在只要给它Schema让它知道有哪些函数比让它记住函数怎么写少消耗大量token。函数越多这个节省越明显。第三可审计可检查。调用意图是结构化数据你可以把每一步操作记录成日志。比起让模型自由发挥生成文本这简直太可控了。出了问题可以追溯模型当时决定调用什么工具、参数对不对这在金融、企业服务这类领域是刚需。2. MCP当装手脚变成一件标准化的活儿2.1 每个Agent都要有二十个转接头吗Function Calling很快带来了一个让人头疼的新问题每家AI应用都要自己实现一套连接外部工具的胶水代码。你要对接内部系统要对接第三方服务要对接数据库每个对接都要写参数定义、鉴权逻辑、错误处理。工具一多代码量就失控了。更头疼的是同一个服务接口在项目A里被封装成查天气在项目B里可能叫获取气象信息。换一个框架或者换一个模型原来写好的工具定义往往得重来。MCP解决的就是这个痛点把工具接入这件事标准化。整个架构分为MCP Server提供服务的一方和MCP Client消费服务的一方两者之间通过一套统一的协议通信。服务方提供工具、资源、提示词三类能力消费方用统一的方式发现和调用它们。用生活案例来理解电脑USB接口一大堆设备都能插因为大家都遵循了一个标准。MCP做的是同一件事——给AI应用和外部工具之间定了一套通用插口协议。2.2 MCP与Function Calling的关系不是替代这件事我必须反复强调MCP不是来埋葬Function Calling的它恰恰是建立在这套机制之上的。你可以把Function Calling理解为模型层面的能力开关——让模型理解我可以调用函数。而MCP是业务层面的接入标准——让N个工具接入N个应用时不用重复造轮子。两者是上下层关系MCP Client把远端工具映射成Function Calling格式模型照常按照Function Calling的机制生成调用意图调用意图再由MCP Client转发给对应Server执行。所以在实际项目里你完全可以把它们叠加使用基础能力内置在自己的代码里直接用Function Calling外部系统、第三方服务、需要共享的工具走MCP。我在某跨平台系统里就是这么干的核心退款逻辑用自建函数天气、汇率、文档解析这类通用能力走MCP Server两边互不干扰。2.3 MCP真正值钱的地方在哪有人说MCP不就是把工具描述JSON化嘛能有那么大价值其实它真正值钱的点有三个。一是一次接入处处可用。一套MCP Server写好了理论上任何一个支持MCP的客户端都能连。你在内部搭一套统一数据查询MCP所有业务线都能直接调用不用每个项目各自对接一遍内部接口。二是能力可以被远程共享。MCP支持远程服务器意味着团队可以把工具服务托管在中心AI应用通过鉴权远程调用能力统一升级、统一监控。这对做企业内部AI中台非常有吸引力。三是生态带来的边际效应。现在各大主流模型客户端都在原生支持MCP网络上已经涌现出大量现成的MCP Server从天气、地图到数据库、代码仓库拿来即用。独立开发者做点什么都能找到现成零件这才是最大的红利。表格对比一下两者各自的本职对比项Function CallingMCP定位模型级别的结构化调用机制工具与AI应用之间的标准化接入协议解决的核心问题让模型表达我要用什么功能、参数是什么让工具接入N个应用时少写胶水代码是否依赖特定模型不依赖服务商各模型基本都支持与底层模型无关但需客户端支持开发成本低定义Schema即可中需要搭建Server/Client学习协议适用场景单个应用内、私有函数调用多应用共享工具、跨系统集成、生态复用与对方的关系MCP Client最终要转换成它构建在Function Calling机制之上3. 实操全记录从手写Function Calling到搭建MCP3.1 一个Function Calling最小闭环考虑场景用户对客服AI说帮我查一下订单SH2024001的物流状态。没有Function Calling时这种请求只能回复模板。有了之后我写了这样一个函数定义。tools [ { type: function, function: { name: query_logistics, description: 根据订单号查询物流状态, parameters: { type: object, properties: { order_id: { type: string, description: 订单号例如SH2024001 } }, required: [order_id] } } } ]然后构造对话消息把用户问题、工具定义一起发给模型。messages [ {role: user, content: 帮我查一下订单SH2024001的物流状态} ] response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, tool_choiceauto ) # 模型如果决定调用工具这里会输出tool_calls tool_calls response.choices[0].message.tool_calls if tool_calls: for call in tool_calls: fn_name call.function.name args json.loads(call.function.arguments) print(f调用 {fn_name}参数是 {args}) # 在这里真正执行你的查询函数拿到真实结果 result query_logistics(args[order_id]) # 把结果作为tool消息塞回会话 messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result) }) # 把带结果的完整消息重新发给模型生成最终回复 final_answer client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools ) print(final_answer.choices[0].message.content)这里面有几个细节特别值得注意。首先是tool_choice参数默认是auto让模型自己判断如果你希望强制某个功能被调用可以传{type: function, function: {name: query_logistics}}。但除非有很强业务理由我不建议设置强制调用因为模型可能上下文里已经包含了答案强制调用反而多一次无意义的往返。其次是描述字段的书写。description质量直接决定模型能不能被正确引导。写根据订单号查询物流状态就比物流查询好用十倍因为模型是透过语义来理解当前对话意图的。多写一点什么场景下该调用、参数的含义、边界情况能少踩很多坑。3.2 组装一个可跑起来的MCP Server我基于官方SDK搭了一个最小服务对外暴露了两个工具一个查天气一个查汇率。代码大致长这个样。from mcp.server.fastmcp import FastMCP mcp FastMCP(internal-toolbox) mcp.tool() def get_weather(city: str) - dict: 根据城市名称获取实时天气 # 假设这里调用了某天气服务的API return {city: city, weather: sunny, temperature: 26} mcp.tool() def get_exchange_rate(currency: str) - dict: 获取指定货币兑人民币的汇率 # 假设这里访问了内部汇率数据库 return {currency: currency, rate: 7.24} if __name__ __main__: mcp.run(transportstdio)用stdio作为传输层时这个Server会以子进程方式被客户端拉起两边通过标准输入输出流交换JSON-RPC消息。我在本地直接用了官方调试工具来连接能看到自动识别出的两个工具并且直接发起调用。mcp dev server.py它能帮你把Server跑起来还会起一个临时测试面板。对新手来说这是最快的验证方式——不用写客户端就能看到服务器暴露了哪些工具、调用结果长什么样。3.3 MCP客户端这边怎么连接服务端搭好客户端接入也不复杂。我用Python写了一个非常朴素的MCP Client核心逻辑是先建立Session然后调用list_tools发现工具再调用call_tool执行。import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params StdioServerParameters( commandpython, args[server.py] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(发现工具:, [t.name for t in tools]) result await session.call_tool(get_weather, {city: 上海}) print(调用结果:, result) asyncio.run(main())跑起来的效果是客户端连上后自动枚举到了get_weather和get_exchange_rate两个工具然后成功调用了其中某个。全程不需要单独处理鉴权、参数格式转换——这就是协议标准化的好处。但注意上面这个示例里的工具返回结果还不是Function Calling可以直接消费的格式。如果要把MCP工具接入到聊天模型需要让客户端把发现到的工具列表翻译成上一节说的tools格式然后按Function Calling的流程走。也就是说MCP帮我们省的主要是工具接入的连接部分最后交给模型生成意图这一步还是得借助Function Calling机制。3.4 两者混跑时参数和超时怎么配我见过不少直接照抄示例配置就出大问题的。这里给出一份经过实际验证的参数建议。参数Function Calling场景建议MCP场景建议temperature0.1到0.3之间偏低防止输出幻觉参数同上tool_choice默认auto特殊场景才强制指定由客户端映射逻辑决定请求超时单个工具执行不要超过15秒MCP远程调用建议10秒内最大递归轮次必须限制建议3到5轮必须限制防止工具互调死循环工具描述长度控制在200字内太长的description反而干扰同理但Server内资源可放宽关于temperature我一直是工具调用场景下的激进的低温派。因为工具参数抽取本质是结构化任务温度高一点参数就可能抽歪。曾经在测试时把温度设成0.7结果模型把订单号里的字母识别成了类似字母排查了半天才发现是温度闯的祸。还有一个容易被忽略的点并行工具调用。现在主流模型支持在一个响应里输出多个tool_calls这在查天气查汇率这种场景很好用但要注意你的业务代码能否并发执行多个函数。如果某个工具有副作用比如下单并行调用会带来未知风险。我习惯在工具定义层面区分只读型和写操作型写操作型的工具永远不放进并行调用集合。4. 常见翻车现场与排查实录4.1 模型返回了根本不存在的函数名这是新手最常见的翻车。我排查过一个案例模型反复输出query_order_detail但系统里根本没有这个工具——真正是get_order_info。查了半天发现是因为tools里同时塞了相似功能的神器加上描述写得太模棱两可模型被带跑了。解决办法有两个方向。第一个是收敛术给每个工具写清楚独特的、排他的触发场景。第二个是兜底术在解析tool_calls时遇到未知函数名别抛异常而是返回一个工具错误信息工具不存在请检查你调用的函数名把这个错误塞回对话。模型有概率自己纠正。我个人的经验是未知函数名出现的频率跟tools数量高度相关。工具少于5个时几乎不会发生一旦超过20个翻车概率直线上升。这时候就该想想是不是该按领域拆分成多个MCP Server而不是让一个工具列表无限膨胀。4.2 工具返回的报错成了死循环燃料有一次对接物流接口模型调用查单工具时运气不好接口刚好超时返回了一个异常信息。结果模型读到报错后又发起同样的调用如此循环往复直到把递归上限打爆。后面查日志发现五分钟内它重试了二十多次同一个查询。根治办法有三招。第一工具内部要对错误分类临时性错误返回请稍后重试永久性错误返回该信息无法查询。第二模型侧必须设置最大工具调用轮次超过就强制结束。第三经验做法是给工具附加一个重试标记同一个工具在三轮内重复调用时直接拒绝并提示模型转用其他策略。4.3 MCP Server连接成功但工具列表是空的这个场景在迁移某旧系统时遇到过。Server能正常启动但客户端枚举不到任何工具。后来定位到是SDK版本不一致Server是用新版FastMCP写的客户端用的却是旧版SDK双方在能力协商阶段出了问题导致工具列表没被正常广播。解决就是统一SDK版本同时我在升级之后按官方推荐的启动模式进行了一次端到端联调。记录一下这个问题在stdio传输模式下尤其容易踩因为你看不到中间到底交换了什么消息。排查时建议临时加一条JSON-RPC日志把进出消息打出来一眼就能看出协商卡在哪一步。4.4 多Agent场景下的工具打架在一个群聊式Agent系统里我遇到的是上下文污染Agent A调用某个工具返回了一大段JSON工具结果并没有被消费干净残留在上下文里被Agent B读取导致B把JSON内容当成了用户发言的一部分。这里的最优解是把这个工具调用过程放在Agent A内部闭环处理只在最终回复里输出精炼后的结论不要把所有中间工具结果都广播到群聊上下文中。或者使用底层能力做好会话隔离确保多个Agent只共享最终结论不共享调试日志。5. 值不值的最终判断我的打分逻辑与实操建议5.1 决策时问自己五个问题不要被MCP是未来这种话带节奏决定投入之前先用下面五个问题过一遍问题回答是的权重我的应用是否只需要3个以内的私有工具倾向于纯Function Calling是否需要接入大量第三方外部服务倾向MCP是否有多个独立的应用/团队要共享同一套工具能力倾向MCP我的技术栈里是否已经成熟支持MCP SDK是则加深选择否则谨慎团队是否有能力维护Server/Client协议层代码否就先用Function Calling快速验证如果你现在的核心诉求就是在单个应用里快速让模型调用几个业务函数那先别折腾MCP。Function Calling足够稳调试链路短对团队心智负担也低。但如果工具数量超出可控范围、或者需要跨系统复用那就值得立刻投入MCP基建。5.2 分阶段演进的实践路径我目前比较推崇的落地路线是这样第一阶段不管最终目标是不是MCP先把手头最频繁的几个能力用纯Function Calling跑通验证业务闭环。第二阶段当工具数量爬到两位数、或者出现明显的重复接入痛苦时再引入MCP Server来托管那些容易泛化的通用能力把Function Calling集中到那几个更隐私、更核心的自有函数上。第三阶段当多系统真的开始共享工具时再把MCP服务器做成统一入口集中管控鉴权和用量。这样做的好处是你不会在一开始就陷入协议层的纠结也能在真正需要标准化时把手头已跑通的Function Calling定义平滑迁移进MCP——因为工具的描述、参数Schema本身就是可复用的。5.3 我个人踩过坑之后的心里话如果只记住一条经验那就是先想清楚在哪里设决策边界再决定用什么技术。Function Calling和MCP都不是魔法它们是让系统有边界、有规则、可验收地调用能力的方法论。技术选型这件事永远是为了能跑通服务而不是为了用上新东西。身边已经有同学把MCP用在了内部知识库工具的开放共享上效果还不错因为团队不再需要每个项目都自己写一遍对接代码。也有同学一上来就整了八个MCP Server最后运维时看得脑壳疼。所以我的建议很朴素手脚要装但先装最常用的那只手跑起来发现真好用再安排工业化批量装机的事。