Function Calling 实战指南:从原理到落地,让大模型真正动手干活 1. 从“只会聊天”到“动手干活”Function Calling 到底解决了什么问题大模型刚火起来那阵子我跟很多人一样觉得它就是个超级能聊的搜索引擎——你问它什么它都能接上话但也就仅此而已了。你让它帮你查一下明天的天气它只能回你一句“抱歉我无法获取实时信息”你让它帮你订张机票它给你编出一段看似合理但完全虚构的航班号。这种“只会说不会做”的状态就是 Function Calling 要解决的核心痛点。Function Calling中文一般叫“函数调用”或“工具调用”是大模型与外部世界交互的一套标准化机制。说白了它让大模型从一个“只会动嘴的顾问”变成了一个“能动手的执行者”。你告诉模型“帮我查一下北京现在的天气”模型不再瞎编而是输出一个结构化的调用请求比如get_weather(city北京)你的程序拿到这个请求后去调真实的气象接口把结果再喂回给模型模型最后用自然语言告诉你“北京现在晴气温 28 度”。这套机制为什么重要因为大模型本身有三个硬伤知识有截止日期、无法执行副作用操作、不擅长精确计算。Function Calling 相当于给模型装了一双手——它负责理解和决策“该调用什么工具、传什么参数”具体的执行交给外部程序。分工明确各司其职。这篇文章适合谁看如果你是想把大模型接入自己业务系统的开发者或者是对 Agent、智能助手底层原理好奇的技术爱好者再或者你只是想知道“为什么 ChatGPT 能查天气而我本地部署的模型不能”那这篇内容应该能帮你把整条链路捋清楚。我会从设计思路讲到代码实操从参数细节讲到踩坑经验尽量做到你看完就能自己动手搭一个最小可用的工具调用 Demo。2. 核心设计思路拆解模型不执行只做“决策翻译官”2.1 为什么不让模型直接执行工具很多人第一次接触 Function Calling 时会有个误解以为是大模型内部真的去调用了某个 API。其实不是。大模型从头到尾只做一件事生成文本。它生成的是一段符合特定格式的文本这段文本描述了“我想调用哪个函数、传什么参数”。真正执行函数的是你写的代码。这个设计非常关键理解它才能理解后面所有的实现细节。为什么这么设计三个原因第一安全隔离。如果模型能直接执行任意代码或网络请求那风险不可控。让模型只输出“意图”由开发者在中间层做校验、鉴权、限流才能保证系统安全。第二模型无关性。函数的具体实现跟模型没关系你用 Python 写、用 Java 写、用 Go 写都行模型只负责输出结构化的调用意图。这样同一套工具可以接不同的模型同一套模型也可以接不同的工具。第三可观测可调试。模型输出的调用请求是纯文本你可以打日志、可以做审计、可以在中间加一层人工确认。如果模型直接执行了你连它干了什么都很难追踪。打个比方模型就像一个餐厅里的点菜员它负责听懂客人要什么然后写一张点菜单交给后厨。点菜员不需要会炒菜后厨也不需要懂客人说了什么方言。Function Calling 就是那张标准化的点菜单。2.2 一次完整的工具调用经历了什么把整个链路拆开看一次典型的 Function Calling 包含四个阶段第一阶段工具注册。你在调用模型 API 时除了传用户的消息还要额外传一个tools参数。这个参数是一个数组里面每个元素描述了一个可用的工具——包括工具的名字、功能说明、以及参数的结构定义。这个描述用的是 JSON Schema 格式后面会详细讲。第二阶段模型决策。模型收到用户消息和工具列表后判断是否需要调用工具、调用哪个工具、传什么参数。如果需要它不会返回普通的文本回复而是返回一个特殊的结构里面包含tool_calls字段标明函数名和参数。第三阶段本地执行。你的程序解析模型返回的tool_calls找到对应的本地函数把参数传进去执行拿到结果。这一步完全在你的掌控之中你可以做任何校验和处理。第四阶段结果回传与最终回复。你把执行结果按照特定格式追加到对话历史里再次调用模型。模型这次拿到真实数据后生成最终的自然语言回复给用户。这四个阶段里模型被调用了两次。第一次是“决策”第二次是“总结”。中间的执行环节完全在模型之外。理解这个两段式调用是理解 Function Calling 的关键。2.3 与传统 API 调用的本质区别有人会问这不就是让模型帮忙拼个 API 请求吗有什么稀奇的区别在于决策的灵活性。传统 API 调用是你写死的if 用户问天气 then 调天气接口。但用户可能说“我明天要出门该穿什么”这句话里没有“天气”两个字但隐含了查天气的需求。传统规则引擎很难覆盖这种模糊表达而大模型可以理解意图自主决定“我需要先查天气再根据温度给建议”。更复杂的情况是多工具协作。用户说“帮我看看北京和上海明天哪个更适合跑步”模型可能需要先调两次天气接口拿到两个城市的温度、湿度、空气质量然后综合判断。这种动态的、多步的决策能力是 Function Calling 相比传统硬编码调用的核心优势。当然代价是模型可能判断错误——该调的时候不调不该调的时候乱调或者参数传错。所以实际工程中工具描述的写法、参数的约束、以及兜底逻辑的设计都有不少讲究。3. 核心细节解析工具描述、参数 Schema 与调用协议3.1 工具描述怎么写才能让模型“看懂”工具描述是 Function Calling 里最容易被忽视、但影响最大的部分。模型能不能正确调用工具八成取决于你的描述写得好不好。一个工具描述通常包含三个部分name函数名、description功能说明、parameters参数定义。我见过太多人把description写成“查询天气”四个字就完事了然后抱怨模型老是不调用或者调错。问题就出在这里。好的description应该像写给一个新员工的操作手册要说明这个工具做什么、什么时候该用、什么时候不该用、有什么限制。比如{ name: get_weather, description: 查询指定城市当前及未来三天的天气情况。当用户询问天气、温度、是否下雨、穿衣建议、出行建议等与气象相关的问题时使用。不适用于查询历史天气数据。, parameters: { type: object, properties: { city: { type: string, description: 城市名称使用中文例如北京、上海。不要传省份名或区县名。 }, days: { type: integer, description: 查询天数1 表示仅今天3 表示未来三天。默认 1。, enum: [1, 3] } }, required: [city] } }注意几个细节description里明确写了“什么时候用”和“什么时候不用”这能大幅减少误调用。参数描述里写了格式要求和示例能减少参数传错。enum限制了取值范围模型就不会传个days7进来。实操心得工具描述不是写给你自己看的文档是写给模型看的“提示词”。你写提示词时会考虑措辞对模型理解的影响工具描述也一样。我一般会把工具描述当成 prompt engineering 的一部分来打磨改一版测一版。3.2 参数 Schema 的类型系统与约束parameters用的是 JSON Schema 标准支持的类型包括string、integer、number、boolean、array、object。对于嵌套结构可以用object套object或者用array配合items定义元素类型。几个实用的约束关键字enum限定取值列表适合状态、类型这种有限选项的参数。required标记必填参数。模型有时会漏传标了 required 能提高传全的概率但不能保证。default默认值。不过要注意模型不一定理解 default 的含义它可能还是会显式传值。minimum/maximum数值范围限制。pattern正则约束适合格式固定的字符串比如日期、编号。我个人的经验是参数越简单越好。如果一个工具需要传七八个参数模型出错的概率会明显上升。更好的做法是拆成多个小工具每个工具只做一件事参数控制在三个以内。这跟函数式编程里“单一职责”的原则是一样的。另外参数类型尽量用基础类型。我试过用嵌套很深的 object 做参数模型经常把结构搞乱。如果确实需要复杂结构可以考虑让模型传 JSON 字符串然后在本地解析这样反而更稳。3.3 模型返回的调用请求长什么样不同厂商的 API 格式略有差异但核心结构类似。以常见的对话补全接口为例模型决定调用工具时返回的消息里会有一个tool_calls数组{ role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: get_weather, arguments: {\city\: \北京\, \days\: 3} } } ] }注意arguments是一个JSON 字符串不是对象。这是很多人第一次写代码时会踩的坑——直接当对象用会报错得先JSON.parse或json.loads解析。为什么要设计成字符串因为模型生成的是文本字符串是最自然的输出形式而且不同语言的 JSON 解析库都能处理。id字段是这次调用的唯一标识回传结果时需要带上让模型知道哪个结果对应哪个调用。如果模型一次返回了多个tool_calls并行调用每个都有自己的 id你需要分别执行、分别回传。还有一种情况是模型返回了content和tool_calls同时存在。这通常意味着模型在调用工具前先说了一句话比如“我来帮你查一下天气”。处理时两者都要保留回传结果时把 assistant 消息原样带上。3.4 结果回传的格式与角色定义执行完工具后你需要构造一条role为tool的消息追加到对话历史里{ role: tool, tool_call_id: call_abc123, content: {\city\: \北京\, \forecast\: [{\date\: \2024-06-01\, \temp_high\: 32, \temp_low\: 22, \condition\: \晴\}]} }content通常是字符串。如果你的工具返回的是结构化数据建议序列化成 JSON 字符串再传。有些实现支持直接传对象但为了兼容性字符串更稳妥。tool_call_id必须和之前模型返回的 id 对应。如果模型一次调了三个工具你就要回传三条 tool 消息每条对应一个 id。顺序不重要但 id 不能错。回传之后再次调用模型模型就会基于这些真实数据生成最终回复。这时候模型的角色又变回了“总结者”它会把工具返回的原始数据翻译成用户能看懂的自然语言。注意工具返回的内容也会占用上下文长度。如果工具返回了一大坨数据比如一个几百行的表格模型可能处理不过来或者成本飙升。实际工程中我通常会在工具层做一次“摘要”或“截断”只把关键字段回传给模型。4. 实操过程从零搭一个可用的工具调用 Demo4.1 环境准备与最小依赖要跑通一个 Function Calling 的 Demo你不需要什么重型框架。核心就是两样东西一个能调用模型 API 的 HTTP 客户端一个 JSON 解析库。Python 的话requests加内置的json就够了。如果你用官方 SDK那就更省事。我这里的示例用 Python 写因为生态最成熟、代码最直观。其他语言逻辑完全一样只是语法差异。你需要准备一个模型 API 的访问凭证具体怎么获取这里不展开各家平台都有自己的流程。先装依赖pip install requests如果你用的是某家平台的官方 SDK按它的文档装对应的包就行。我建议先用最原始的 HTTP 请求跑一遍把整个链路的每个字节都看清楚再去用封装好的 SDK。这样出了问题你知道去哪找。4.2 定义工具函数与 Schema我们先定义一个真实的本地函数比如查天气。为了 Demo 能跑这里用模拟数据实际项目中替换成真实的气象 API 调用import json import requests def get_weather(city: str, days: int 1) - dict: 模拟天气查询实际项目中替换为真实 API 调用 mock_data { 北京: {condition: 晴, temp_high: 32, temp_low: 22}, 上海: {condition: 多云, temp_high: 29, temp_low: 24}, 广州: {condition: 阵雨, temp_high: 33, temp_low: 26}, } result mock_data.get(city, {condition: 未知, temp_high: None, temp_low: None}) result[city] city result[days] days return result然后定义这个工具对应的 Schematools [ { type: function, function: { name: get_weather, description: 查询指定城市的天气情况。当用户询问天气、温度、穿衣建议、出行建议时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称中文如北京、上海 }, days: { type: integer, description: 查询天数默认 1, enum: [1, 3] } }, required: [city] } } } ]这个tools列表就是你要在 API 请求里传的东西。注意它是个数组意味着你可以一次注册多个工具。模型会在所有工具里选最合适的那个。4.3 发起第一次调用让模型做决策构造请求体把用户消息和工具列表一起发出去def call_model(messages, toolsNone): url https://api.example.com/v1/chat/completions headers { Authorization: Bearer YOUR_API_KEY, Content-Type: application/json } payload { model: your-model-name, messages: messages, } if tools: payload[tools] tools payload[tool_choice] auto resp requests.post(url, headersheaders, jsonpayload) return resp.json()tool_choice参数控制模型是否必须调用工具。auto表示让模型自己决定none表示禁止调用也可以指定强制调用某个工具。大多数场景用auto就行。第一次调用messages [ {role: user, content: 北京明天天气怎么样适合跑步吗} ] response call_model(messages, tools) assistant_msg response[choices][0][message] print(assistant_msg)如果模型判断需要调用工具assistant_msg里就会有tool_calls。如果它觉得不需要就直接返回文本。这里的关键是不要假设模型一定会调用工具你的代码要能处理两种情况。4.4 解析调用请求并执行本地函数拿到tool_calls后解析参数、执行函数if assistant_msg.get(tool_calls): messages.append(assistant_msg) for tool_call in assistant_msg[tool_calls]: func_name tool_call[function][name] func_args json.loads(tool_call[function][arguments]) if func_name get_weather: result get_weather(**func_args) else: result {error: f未知函数: {func_name}} messages.append({ role: tool, tool_call_id: tool_call[id], content: json.dumps(result, ensure_asciiFalse) })这里有几个细节值得说。json.loads解析参数时可能失败如果模型生成了不合法的 JSON你得有 try-except 兜底。func_args里的键可能跟函数签名不匹配比如模型传了个city_name而不是city这时候要么做映射要么返回错误让模型重试。ensure_asciiFalse是为了让中文正常显示不然会变成\uXXXX转义。4.5 第二次调用生成最终回复把工具结果追加到消息历史后再次调用模型final_response call_model(messages, tools) final_msg final_response[choices][0][message] print(final_msg[content])这次模型拿到真实的天气数据会生成类似“北京明天晴气温 22 到 32 度适合跑步但注意防晒”这样的回复。注意第二次调用时tools参数可以传也可以不传。传的话模型理论上还能继续调用工具多轮调用不传的话它就只能基于已有信息回复。我一般会传保留多轮调用的能力。整个流程跑下来你会发现核心代码不到 50 行。Function Calling 的机制本身并不复杂复杂的是工程化——错误处理、多轮调用、并发控制、成本优化这些才是实际项目中真正花时间的地方。4.6 多轮调用与并行调用的处理有些任务需要模型连续调用多个工具。比如用户问“北京和上海明天哪个更适合跑步”模型可能先调get_weather(city北京)拿到结果后再调get_weather(city上海)。这种多轮调用需要你在代码里加循环每次模型返回tool_calls就执行、回传直到模型不再调用工具、直接返回文本为止。max_rounds 5 for _ in range(max_rounds): response call_model(messages, tools) msg response[choices][0][message] messages.append(msg) if not msg.get(tool_calls): break for tool_call in msg[tool_calls]: # 执行并回传结果 ...max_rounds是必须的防止模型陷入无限调用循环。我见过模型因为工具返回错误、反复重试同一个调用的情况没有上限的话会一直烧 token。并行调用是指模型在一次返回里给出多个tool_calls比如同时查北京和上海的天气。这时候你可以并发执行这些函数提高效率。但要注意如果工具之间有依赖关系并行会出问题。模型一般能正确处理依赖但保险起见我建议对无依赖的工具才做并行。5. 常见问题与排查技巧实录5.1 模型不调用工具怎么办这是最高频的问题。用户明明问了天气模型却直接编了一段回复根本没调工具。排查思路按优先级来第一检查工具描述是否清晰。如果description写得太模糊模型可能觉得“这个问题我自己能答不需要工具”。把使用场景写具体比如“当用户询问实时天气、温度、降水概率时使用”。第二检查tool_choice参数。如果设成了none模型被禁止调用工具当然不会调。确认是auto或指定了具体工具。第三检查用户消息是否真的需要工具。有时候用户问的是“天气一般怎么预测”这是知识性问题不需要实时数据模型不调用工具是对的。别把正常行为当 bug。第四模型能力问题。一些小模型或旧模型对 Function Calling 的支持不好可能压根不理解工具描述。换一个明确支持工具调用的模型试试。5.2 参数传错或漏传的排查模型传的参数跟你的函数签名对不上常见的有参数名拼错、类型不对、必填项缺失、多传了不存在的参数。排查方法很直接把模型返回的arguments打印出来看。我一般会在解析前先 log 一下原始字符串确认模型到底生成了什么。如果是参数名问题在工具描述的properties里把名字写得更明确或者在description里加示例。如果是类型问题检查type定义是否准确比如该用integer的地方别写成string。还有一种情况是模型传了null或空字符串。这通常是因为它不确定该传什么。把参数标成required能减少这种情况但根治办法是让描述更明确。5.3 工具返回结果太长导致上下文爆炸工具返回的数据量不可控比如查数据库返回了几百条记录。全塞给模型轻则成本飙升重则超出上下文限制直接报错。解决办法是在工具层做处理。我通常会在函数返回前做三件事截断只取前 N 条、摘要只保留关键字段、分页告诉模型还有更多数据需要的话再查。比如查订单列表不要返回完整订单对象只返回订单号、金额、状态三个字段模型足够用了。实操心得工具返回给模型的内容目标是“让模型能做出正确判断”而不是“完整还原数据”。想清楚模型需要哪些信息才能生成好的回复只回传那些信息。5.4 常见问题速查表问题现象可能原因排查方向解决建议模型不调用工具描述模糊、tool_choice 设置错误检查 description 和 tool_choice细化使用场景描述确认 auto参数解析失败arguments 不是合法 JSON打印原始字符串加 try-except提示模型重试参数名不匹配模型理解偏差对比 schema 和函数签名在描述中加参数示例无限调用循环工具返回错误模型反复重试加调用轮次上限max_rounds 限制错误时返回明确提示上下文超限工具返回数据过大检查返回内容长度截断、摘要、分页调用结果被忽略回传格式错误检查 role 和 tool_call_id确保格式符合协议5.5 几个容易踩的坑坑一忘记把 assistant 消息加入历史。模型返回tool_calls的那条 assistant 消息必须原样追加到 messages 里否则回传 tool 结果时模型不知道对应哪个调用。我一开始就漏了这步导致模型报错说找不到对应的 tool_call。坑二tool_call_id 对不上。并行调用时多个 tool 结果的 id 必须和模型返回的 id 一一对应。顺序可以乱但 id 不能错。建议用字典按 id 索引别靠顺序。坑三工具函数抛异常没处理。如果本地函数执行时抛了异常你没捕获整个流程就断了。正确做法是捕获异常把错误信息作为 tool 结果回传给模型让模型决定是重试还是告诉用户出错了。坑四忽略模型的文本回复。有时候模型在tool_calls之外还带了content比如“我来帮你查一下”。这条 content 也要保留不然对话会显得突兀。6. 工具选型与工程化建议6.1 什么时候该用 Function Calling不是所有场景都需要 Function Calling。如果你的需求是固定的、可枚举的比如“用户点按钮触发某个操作”那直接写代码调用就行没必要绕一圈让模型决策。Function Calling 的价值在于处理模糊的、开放的、需要理解意图的输入。适合的场景智能客服需要根据用户描述查订单、改地址、退款个人助手需要根据自然语言指令操作日历、发邮件、查资料数据分析工具需要根据问题自动选择查询维度和聚合方式。不适合的场景流程完全固定、输入格式严格受控、对延迟极度敏感。这些场景用传统编程更可靠、更便宜、更快。6.2 工具粒度怎么把握工具设计有个核心原则一个工具只做一件事但要把这件事做完整。太粗的工具比如一个do_everything函数模型不知道怎么传参太细的工具比如把查天气拆成查温度、查湿度、查风速三个模型要调好几次容易漏。我的经验是按“用户意图”来划分工具。用户说“查天气”对应一个get_weather用户说“订机票”对应一个book_flight。每个工具的参数控制在 2 到 4 个超过就考虑拆分。另外工具之间尽量正交不要有功能重叠。如果两个工具都能完成同一件事模型会纠结选哪个增加不确定性。6.3 错误处理与降级策略生产环境里工具调用失败是常态——网络超时、接口限流、数据格式变化什么都有可能。你的代码要能优雅处理这些情况。基本策略是捕获所有异常把错误信息结构化后回传给模型。比如{error: timeout, message: 天气服务暂时不可用}。模型拿到这个可以决定是重试、换工具、还是告诉用户稍后再试。同时要有降级方案。如果模型调用工具失败能不能用缓存数据能不能给一个兜底回复这些要在设计阶段就想好别等线上出问题了才临时加。6.4 成本与延迟的权衡每次 Function Calling 至少调用模型两次决策一次、总结一次token 消耗是普通对话的两倍以上。如果涉及多轮调用成本还会翻倍。延迟方面两次模型调用加上工具执行时间用户等待时间可能到好几秒。优化方向有几个工具描述精简别写太长减少输入 token工具结果摘要别回传大段数据缓存对相同参数的调用结果做缓存并行无依赖的工具调用并发执行小模型做决策用便宜的小模型判断该调什么工具用大模型做最终总结。这些优化不是一开始就要做但心里要有数。等业务量上来了这些细节决定成本能不能扛住。7. 从 Demo 到生产还需要补哪些课跑通 Demo 只是第一步。真正上线还要考虑权限控制——不是所有用户都能调用所有工具得在中间层做鉴权。还要考虑审计日志——谁在什么时候调了什么工具、传了什么参数、返回了什么结果这些都要记录方便排查问题和合规审查。另外是工具版本管理。工具的参数和返回值可能会变模型描述也要跟着更新。如果工具升级了但描述没改模型可能传旧参数导致调用失败。建议把工具定义和实现放在一起管理改的时候同步更新。最后是测试。Function Calling 的测试比普通接口测试麻烦因为模型的输出有随机性。我的做法是准备一批典型用户输入跑多次看调用成功率对失败的 case 逐个分析是描述问题还是模型问题。这个测试集要持续维护每次改工具描述都跑一遍。这个领域变化很快各家平台的 API 格式、支持的能力都在迭代。但核心机制——模型做决策、本地做执行、结果回传总结——这套逻辑是稳定的。把这套逻辑吃透换任何平台都能快速上手。我自己从最早的手写 HTTP 请求到后来用各种框架底层理解一直没变过这也是我觉得值得花时间讲清楚的原因。