资讯详情

资讯详情

Function Calling与MCP:大模型工具调用的底层逻辑与实战指南

先说个我最近挺有感触的事。上个月某团队找到我说他们的内部知识助手要做“工单自动处理”模型答完话之后最好能直接调接口、改状态、发通知。我一听就知道绕不开两个词Function Calling和MCP。这几年凡是真正在做大模型应用的人迟早都会碰到这两个东西。很多人把它们当成两个互斥的方案来选其实不是——它俩解决的问题层面压根不一样。这篇文章我想把这两条路的底层逻辑、各自的实战细节、以及怎么组合用一次讲清楚不整虚的。如果你做的是AI应用开发、Agent搭建、或者企业内部流程自动化哪怕只是刚入门这篇文章应该都能帮你省掉不少自己摸索的时间。我会先解释工具调用到底在解决什么问题然后从零手写一套Function Calling的完整管线再落地一个MCP最小示例最后给出一份选型参考和一堆我踩过的坑。1. 先搞清楚一个问题工具调用到底在解决什么1.1 模型只负责“想”不负责“做”几乎所有大模型本质上都是一个“文本生成器”。你给它一段输入它根据训练中学到的规律预测下一段最合理的文字。它能回答问题、写代码、做总结但它不会主动去查数据库、不会去调接口、不会去操作你的工单系统。模型本身是“与世隔绝”的。所以当用户问“帮我查一下库存”模型只能根据训练数据里的常识给你编一个大概的答案而编出来的东西往往不准确。聪明的做法是让模型先理解用户的意图然后输出一个“我想调用某个工具参数是什么”的结构化请求再由应用程序去真正执行这个调用最后把执行结果回填给模型让它基于真实结果继续回答。这个过程就叫工具调用。它解决的其实是“模型从语言走向行动”的核心问题模型负责思考程序负责执行双方通过一个约定好的格式来协作。没有这一步大模型只是聊天玩具有了这一步它才能变成一个能干活的Agent。这也是为什么我说凡是做大模型应用的人迟早都会碰到这两个词。Function Calling是“单个模型能力”层面的工具调用机制MCP则把这种机制上升到了“标准化协议”的高度让工具可以被不同模型、不同应用复用。先看清这个层次差异后面就不容易选错。1.2 Function Calling和MCP的位置差异不少朋友问我的时候会把Function Calling和MCP摆在一起“老师这两个到底哪个好”这种问法本身就把问题问歪了。拿一个不恰当的比喻来说Function Calling是接口形态MCP是接口协议。接口形态决定了“模型返回给我的参数长什么样”协议决定了“我这个工具服务怎么被其他应用调用和发现”。更直白一点Function Calling是模型服务商给你提供的一种“带工具描述发起对话、让模型输出工具调用意图”的API能力。核心动作是模型输出一个结构化JSON标注要调用哪个函数、传什么参数。MCP则是把工具调用这件事打包成一套跨应用的规范工具服务端通过标准协议暴露工具列表和调用入口任何支持该协议的客户端都能直接发现这些工具并调用不需要为每个应用单独写对接代码。你可以把Function Calling理解成“模型和你的应用之间的一次握手”把MCP理解成“你的工具和所有可能的应用之间的一个万能插座”。模型先有Function Calling才能在自己脑子里生成要调用工具的意图而MCP让这个意图能作用于外部世界并且不需要你每次接一个新的宿主应用时就重写一遍工具对接层。弄清楚这一点很多困惑就会解开为什么有些场景用Function Calling就够了因为工具少、调用方单一、改动不频繁。为什么某些场景非要上MCP因为工具多了、有多个应用共享工具、开发团队之间需要解耦。接下来我分别把这两套东西的实际用法拆开讲。2. 手写一套Function Calling管线从工具定义到多轮闭环2.1 工具定义JSON Schema这一步最磨人先不考虑外部框架我们从一个最朴素的实现看起。大模型的API通常允许你传一个tools参数里面用JSON Schema描述每个工具的名称、功能、参数结构。这里最忌的参数就是描述写得含糊。举个我常用来演示的例子——查仓库库存工具{ type: function, function: { name: query_warehouse, description: 查询某区域仓库的某个商品库存余量, parameters: { type: object, properties: { region: { type: string, description: 区域编码例如 east-01、west-03 }, sku: { type: string, description: 商品编号例如 SKU-A100 } }, required: [region, sku] } } }看起来很简单对吧但实际开发中这个大括号里的每个字段都会影响模型输出质量。name要稳定、简短不要用空格和特殊字符description要写清楚“这个工具在什么场景下用、参数格式是什么”因为模型就是靠它来判断什么时候调用这个工具的参数里的description同样重要比如上面写了“例如 east-01”模型就知道这是一个编码而不是一个自然语言区域名。我的经验是写工具描述的时候把自己想象成在写一份给新人看的操作手册。如果描述写得太笼统模型就会在模棱两可的时候胡乱调用如果描述里给了明确的枚举示例模型出错率会明显下降。实测下来多数工具调用不准的案例根因都出在参数描述不够具体而不是模型本身太笨。2.2 模型返回的结构长什么样当你把tools传给模型模型如果认为需要调用工具回来的消息里就会带一个tool_calls字段。一个典型的返回长这样不同类型模型服务商字段名略有差异但思想一致{ id: chatcmpl-demo, message: { role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: query_warehouse, arguments: {\region\: \east-01\, \sku\: \SKU-A100\} } } ] } }这里有一个非常容易踩的坑arguments在很多返回里是字符串不是对象。你需要先JSON.parse或json.loads再把它作为参数传给真正的执行函数。我见过不少新手在这儿直接报“字符串不能当字典用”的错原因就是没做反序列化。另外content位置可能是null这是正常的。因为模型正在“思考如何调用工具”还没开始组织最终回复。你只有拿到工具执行结果之后把它回填给模型模型才会接着生成面向用户的自然语言答案。2.3 循环调度执行、回填、再问的完整闭环工具调用不是一次就完事的。用户问“查一下多个仓库缺货情况”模型可能连续调用三次query_warehouse也可能先查库存、再查订单、再查物流。所以你要做一个“调度循环”把带工具的消息发给模型如果模型返回工具调用就执行并回填然后带着结果再次请求模型直到模型生成普通回复或达到步数上限。一个极简但完整的循环是这样的import json def run_tool_loop(user_input, call_model, dispatch, max_rounds8): messages [{role: user, content: user_input}] for round_index in range(max_rounds): resp call_model(messagesmessages, toolsTOOLS, temperature0.1) msg resp[message] messages.append(msg) if not msg.get(tool_calls): return msg[content] for tc in msg[tool_calls]: fn_name tc[function][name] fn_args json.loads(tc[function][arguments]) result dispatch(fn_name, fn_args) messages.append({ role: tool, tool_call_id: tc[id], content: json.dumps(result) }) raise RuntimeError(tool loop exceeded max rounds)这段代码有几个关键点temperature要调低。工具调用是结构化输出温度太高容易让模型在参数上“自由发挥”编出根本不存在的区域编码。我个人一般设置在0到0.2之间。tool_call_id必须和模型返回的id保持一致否则模型不知道这个执行结果对应的是哪个工具调用。回填消息的角色是tool不是user也不是assistant。用错角色后面的对话上下文就乱了。循环一定要有上限。真实场景里模型可能陷入“反复调用同一个工具”的死循环max_rounds就是兜底保险。dispatch要自己做函数映射不要直接eval。拿到的name必须和内部注册表比对避免执行任何未定义的调用。这套循环看起来不难但它是所有Agent应用的骨架。不管是竞品分析工具、自动化运维助手、还是销售线索生成器本质上都是这个循环的变体。3. 从Function Calling到MCP协议化是迟早的事3.1 MCP拆成三部分看就很清楚了很多讲MCP的文章一上来就砸概念把人绕晕。我的办法是把它拆成三个角色Host、Client、Server。Host就是你的应用比如一个带自然语言对话界面的客户端。用户通过它和整个工具系统交互。Client运行在Host内部负责和Server建立连接、发现工具、发起调用。可以理解为“应用里负责协议通信的那一层”。Server提供工具的进程对外暴露“我能提供什么工具、每个工具的参数是什么、怎么执行”。Server可以跑在本地也可以部署在远端服务器。打个比方MCP Server就是一个标准的USB-C接口的设备Host就是你的电脑。以前你要连接一个硬盘得专门装驱动、用专用线缆现在大家都遵守USB-C标准任何设备插上来就能用。MCP干的就是这件事把“工具提供方”和“工具使用方”的对接方式统一起来。传输层上MCP支持两种主流方式stdio和Streamable HTTP。stdio适合本地进程间通信模型应用把MCP Server当成一个子进程拉起来通过标准输入输出交换消息Streamable HTTP适合跨机器远程调用Server起一个HTTP服务客户端通过网络访问。至于SSE老版本协议里常用新版本里已经并入HTTP传输在新项目里不用再考虑独立的SSE通道了。理解这三个角色之后MCP对你来说就不再是一个黑盒而是一套可以掌控的架构你决定Server跑在哪、Client怎么连、Host怎么展示。出问题的时候也能按角色定位。3.2 把本地文件系统挂成MCP工具一个最小可用示例理论讲再多不如跑通一个最小示例。我们做一个非常接地气的场景把一个本地目录浏览能力挂成MCP工具让模型能“看到”服务器上有哪些文件。先写一个最简Serverimport os from mcp.server.fastmcp import FastMCP mcp FastMCP(local-fs-helper) mcp.tool() def list_dir(path: str) - str: 列出指定目录下的文件和文件夹名称 try: entries os.listdir(path) return , .join(entries) except Exception as e: return ferror: {e} if __name__ __main__: mcp.run()这段代码就是完整的Server注册了一个名为list_dir的工具参数是路径字符串返回目录内容。跑起来后它会在本地启动一个MCP服务通过stdio等待客户端连接。再写一个最小Client负责拉起Server并调用工具import asyncio from mcp import Client, StdioServerParameters async def main(): params StdioServerParameters( commandpython, args[server.py], ) async with Client(params) as session: tools await session.list_tools() print(available tools:, [t.name for t in tools]) res await session.call_tool(list_dir, {path: .}) print(result:, res) asyncio.run(main())跑起来后你应该能看到工具列表里有local-fs-helper的list_dir然后调用它返回当前目录列表。如果这一步通了你就已经理解了MCP的核心链路Client去Server发现工具、发起调用、拿到结果。这个示例的价值不在于功能多强大而在于它揭示了一个关键事实MCP工具不需要和某个具体模型的API绑定。任何Host、任何模型只要实现了MCP客户端就能直接调用你的list_dir。你写一次工具全公司的Agent都能用。3.3 MCP带来的复用红利和新增成本MCP最大的红利就是“复用”。我见过一个真实场景某团队把内部的知识库检索、工单状态查询、会议室预订做成了三个MCP Server之后不管前端做什么新助手只要接MCP客户端三分钟就能拥有这些能力不用针对每个新应用重新写一遍工具对接。但MCP也不是没有成本。第一它引入了新的运行单元Server本身需要部署、监控、管理和单纯的函数调用相比多了一层进程和网络的概念第二本地stdio模式调试起来相对麻烦你需要在客户端日志和Server日志之间来回切换第三如果Server异常退出你的Agent可能直接失去某个能力需要做好健康检查和自动拉起。所以我对MCP的态度是它是解决规模化复用问题的方案不是所有场景都必须上的方案。工具不超过十个、只有一两个应用在用、工具变更不频繁那直接用Function Calling足够一旦你发现自己在为每个应用重复写“调用同一套工具”的适配层或者工具数量涨到20个以上那就到了上MCP的时机。4. 选型决策什么时候该用Function Calling什么时候该上MCP4.1 做个三维评估数量、复用面、部署位置几乎每个人都会问我“我到底该用Function Calling还是MCP”。我给不出一个万能答案但可以给一套判断方法。你就从三个维度去看工具数量、复用面、部署位置。评估维度偏向 Function Calling偏向 MCP工具数量少而精十个以内多而杂几十个起步调用方只有你自己的应用多个应用、多个团队共享工具位置和主应用同进程本地函数独立进程甚至不同机器变更频率极低偶发调整经常有服务要上线下线对接成本写一次JSON Schema就能用需要搭建Server、管理连接标准化要求无所谓自己人能看懂就行需要统一协议避免重复开发简单来说你只是给一个小助手挂两三个查询接口就老老实实写Function Calling别折腾MCP你要做一个Agent平台后面挂十几个不同业务线和不同团队的工具那就直接上MCP。中间地带最理想的做法是混合使用而不是二选一。4.2 混合实践的样板内部Agent的三明治架构我实际参与过的一个项目就是典型的“混合架构”。那是一个内部运营助手主对话链路走Function Calling只挂了三个核心工具用户身份查询、订单状态查询、客服备注写入。这三个工具调用极其频繁且必须和主进程的业务逻辑深度耦合塞进Function Calling里最自然。同时架构里单独起了一个MCP Gateway把另外十几个工具知识库检索、行业资讯搜索、营销素材生成、报表拉取、消息推送挂上去。这些工具有个共同特点不想感知上层业务、希望被多个系统共用、甚至已经是由不同团队提供的独立微服务。这个“三明治”结构的好处是高频核心工具走最短路径性能稳定、易排查低频长尾工具走标准协议新能力只需要新增一个MCP Server就能接入。对话循环里加一个判断——如果模型请求的工具不在Function Calling注册表里就转给MCP客户端去执行然后再把结果回填进消息数组。这样写一次后续扩展一个工具就只需要部署一个Server不用改主程序代码。4.3 从迁移角度想想不要一上来就全量MCP很多人看了MCP的宣传后恨不得把现有系统全部推翻重写。我的建议恰恰相反不要一上来就全量迁移。迁移MCP的成本不只是重写接口更大的消耗在于运维习惯、监控体系、出错排查方式的全面改变。一套线上的Function Calling系统跑得好好的没必要为了“用新协议”而迁。更务实的做法是新项目直接用MCP老项目按需增量迁移。比如先在长尾工具上试验一个Server跑通、稳定、积累排查经验之后再逐步扩大边界。等什么时候你发现“新工具接入流程”已经变成MCP Server的模板化操作了再考虑把核心工具也迁过来。5. 实战中的高频坑和应对办法5.1 模型“编”参数的现场为什么工具描述必须精确实战中最常见的问题是模型明明调用了正确的工具但传进来的参数是“编”的比如它把一个不存在的区域编码east-99传给你或者把一个时间格式从YYYY-MM-DD传成了“下周二”。这几乎是工具调用必踩的坑。我的解决思路分三层。第一层把参数描述写到最具体给枚举、给示例、给格式说明让模型有据可依。第二层用参数约束兜底比如在工具内部做校验不合法的直接返回error: invalid region让模型看到错误后自我修正。第三层在系统提示词里明确“不要猜测参数不确定就问用户”尽量抑制模型的脑补冲动。实测下来这三层叠加效果最好单靠任何一层都拦不住所有幻觉参数。另外工具总数不要一味追求多我给一个常见阈值一次请求里的工具数量超过30个模型的选择准确率会明显下滑因为太长会占据上下文窗口模型自己也容易被绕晕。宁可分拆成多个工具集按路由判断用哪一组也不要堆在一起。5.2 循环失控给调度器加硬上限前面我提到max_rounds这里想专门展开讲一下因为这是Agent上线后最常见的“翻车点”。模型在执行过程中可能陷入某种“死磕”查了库存发现缺货它自己决定去创建采购订单发现没权限又去查权限查到权限后重新查库存然后又创建订单……如此反复一轮请求的token费用哗哗往外流。除了步数上限我自己做系统还会加两个保险一个是总请求超时时间比如整个工具循环最长跑30秒超时就返回“处理超时请稍后再试”另一个是操作幂等性设计即同一个动作被反复执行也不能产生多个重复订单。尤其是涉及写操作的工具接口层必须做幂等校验否则循环失控带来的不只是钱的问题还有脏数据问题。在一个外包客服项目里我就吃过类似的亏模型反复调用“创建退款单”工具好在当时每笔退款都绑定了对外的请求号和订单状态重复调用被拦截了不然后果不堪设想。所以提醒大家循环上限是底线幂等设计是救命稻草缺一不可。5.3 MCP工具出问题的排查顺序MCP的链路比普通Function Calling长出了问题也更难定位。下面这个排查顺序是我实战中反复用并调整过的分享给大家参考。排查步骤大致如下先看Server本身命令行手动拉起Server看有没有报错、有没有正常打印启动日志。用MCP官方调试工具直接对Server做一次list_tools确认Server是活的。再看连接如果是stdio模式确认客户端启动子进程的参数对不对、环境变量对不对如果是HTTP模式确认Server的地址是否可达、认证Token是否有权限。然后看工具名和参数Client能连上但调用报错大多是工具名拼写不对、参数传少了、或者参数类型不匹配。我经常干的一件事是先把list_tools的结果完整打印出来肉眼核对名称和参数定义。接着看执行结果返回格式Server返回的字段有没有按协议包裹模型在解析时是否做了容错。很多“工具结果模型读不懂”的病例其实是Server返回了非标准数据结构。最后才考虑模型侧上下文太长被截断、历史消息里工具结果太多导致模型混乱等等。模型侧问题往往排在最后因为它是整个链路里最不容易主动报错的一环但如果你排查完前四步都没问题那就得回头检查上下文了。按这个顺序排查大多数MCP问题都能在十分钟内定位。我见过不少同学一上来就怀疑模型结果查半天发现是Server启动时工作目录不对连文件都读不到。再说一个容易被忽略的细节MCP的Server日志和Client日志要分开收集。stdio模式下Server的stdout是协议的通信通道不能在里面乱打业务日志否则会干扰Client解析业务日志要么走stderr要么写文件。我刚开始做MCP时就在Server里用print打日志结果Client直接解析失败折腾了快一个小时才反应过来。最后聊两句踩过坑之后的体会我也算踩过来的人了。工具调用这条路上最值钱的不是写了多少行调度代码而是对“模型到底能不能按你的预期行动”这件事有清醒认知。模型不是你的员工它不会老老实实按你的流程走它会编参数、会重复循环、会在工具描述模糊时选择另一个看起来差不多的工具。所有机制本质上都是为了限制模型的这种“自由发挥”把它的能力圈在一个可控的框架里。如果你现在正准备从零搭一套大模型工具调用系统我的建议是先手写一遍Function Calling的循环把消息回填、工具分发、步数上限这几个概念彻底吃透等真的出现了多团队共享工具、或者工具数量明显增长的需求时再把MCP引入从挂一个无关紧要的长尾工具开始积累自己的排查经验。别看别人的架构图有多复杂自己亲手跑通那个最小示例比什么都有用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →