ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

函数调用实战:从本地部署到多轮调用与避坑指南

函数调用实战:从本地部署到多轮调用与避坑指南 先说一个我自己的直观感受大模型真正开始“值钱”不是从它能聊天开始的而是从它能调用工具、完成任务开始的。你把模型当成一个只会说话的顾问它顶多帮你润色文案但你教会它调接口、发请求、查数据库、操作业务系统它才从“聊天窗口”里走出来变成了一个能帮你干活的数字员工。这个转折点靠的就是 Function Calling函数调用。这篇实战指南我不打算给你堆概念。我直接带你过一遍 Function Calling 从原理到落地的全过程包括本地模型怎么跑、OpenAI 风格接口怎么调、JSON Schema 怎么定义参数、多轮调用怎么处理状态、以及我在真实项目中踩过的那些坑。无论你是刚接触大模型应用开发还是已经做了几个 Demo 但觉得不够“扎实”这篇文章都能给你一条可以照着走的路线。1. 先搞清楚 Function Calling 到底在解决什么问题1.1 没有 Function Calling 的时候大模型有多“笨”很多人第一次用大模型 API 做应用时都会遇到一个尴尬场景你问它“北京今天天气怎么样”它回答得头头是道但实际天气数据是它编的。你问它“帮我查一下订单号 20250101 的物流状态”它要么说“我无法访问外部系统”要么开始一本正经地编造物流轨迹。这背后的原因很简单大模型本身是一个“文本生成器”它没有主动查询数据库、调用 API、操作文件的能力。它的知识截止到训练数据那一刻之后发生的事情它一概不知。所以你让它“查一下”“算一下”“提交一下”它只能靠概率去编一个看起来合理的答案。没有 Function Calling 之前开发者想解决这个问题只能用“提示词硬刚”的方式在 Prompt 里告诉模型“如果用户想查天气你就输出【查天气】北京”然后自己在代码里解析这段文字再调天气接口最后把结果拼接回去。这个方案能跑但极其脆弱。模型稍微换个措辞解析就崩了多几个工具Prompt 就臃肿得没法维护一旦模型在中间步骤输出一些“废话”你的解析逻辑就要跟着崩。1.2 Function Calling 把“意图识别”和“参数提取”从提示词里解放出来Function Calling 的核心思路不是让模型自己决定“要不要调用工具”而是你提前把工具的描述、参数结构告诉模型模型在回答时如果觉得需要调用工具就输出一个结构化的“调用请求”。这个请求里包含工具名称和参数你的代码拿到这个请求后自己去执行真实函数再把结果返回给模型模型最后组织成自然语言回答用户。换句话说Function Calling 做的事是把“意图识别”和“参数提取”这两个关键步骤从“纯文本约定”变成了“结构化协议”。模型不再需要靠输出特定文字来暗示“我要调工具”而是直接输出一个 JSON 结构的 tool_calls开发者解析起来极其稳定。这也是为什么说 Function Calling 是“从聊天到干活”的分水岭——它是第一个让大模型能和外部系统进行确定性交互的官方机制。2. 动手之前本地模型也能跑函数调用2.1 选工具为什么我用 Ollama 而不是直接调云端 API在热词里反复出现“本地部署大模型”“ollama 部署大模型”说明现在很多人都在尝试把大模型拉到本地。我平时做项目验证时也很喜欢先用本地模型跑通流程再切换到云端大模型。原因有三一是数据不出内网适合业务敏感场景二是没有 API 调用费适合反复调试三是可以完全掌握模型行为不会被厂商偷偷改版本搞懵。本地部署工具里我个人最推荐 Ollama。它支持 macOS、Windows、Linux一条命令就能把模型拉下来而且它自带的 OpenAI 兼容接口/v1/chat/completions几乎可以无缝衔接主流 SDK。哪怕你最后生产环境用的是云端 API本地先用 Ollama 做开发联调体验也完全不会差。2.2 MacBook Air M3 16G 实测哪些模型能跑函数调用很多朋友担心自己的笔记本跑不动大模型我拿 MacBook Air M3 16G 实测过几种主流模型结果如下模型参数量量化版本函数调用表现生成速度实测能不能用qwen2.5:7b70亿Q4_K_M能稳定输出 tool_calls参数提取准确率高15~20 token/s推荐日常开发qwen2.5:3b30亿Q4_K_M能输出 tool_calls但复杂参数容易漏字段25~30 token/s轻量场景可用llama3.1:8b80亿Q4_K_M需要提示词引导原生稳定性一般12~18 token/s不推荐做生产mistral:7b70亿Q4_K_M函数调用格式老旧兼容性差15~20 token/s不建议选它实测下来qwen2.5 系列是本地跑 Function Calling 的“性价比之王”。它在训练时就专门强化了函数调用能力输出结构非常规范配合 Ollama 自带的 OpenAI 兼容接口20 分钟就能搭出一个本地函数调用测试环境。2.3 Ollama 部署 Qwen 2.5 7B 的具体步骤这里给出我在 macOS 上实测可行的一套流程。第一步安装 Ollama。直接去官网下载 macOS 安装包或者用 Homebrew 一行命令brew install ollama第二步启动 Ollama 服务并拉取模型。默认情况下Ollama 安装后会自动监听 11434 端口。ollama serve # 如果已经作为服务运行这一步可跳过 ollama pull qwen2.5:7b第三步验证 OpenAI 兼容接口是否可用。打开新终端用 curl 测一下curl http://localhost:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5:7b, messages: [{role: user, content: 你好}] }如果返回正常的 choices 内容说明本地环境已经就绪。这里要注意一点Ollama 的 OpenAI 兼容接口不需要 API Key随便填一个比如“ollama”就能通过认证。3. 核心实现从 OpenAI 到本地模型的 Function Calling 调用全流程3.1 基于 OpenAI 官方 SDK 的完整示例我不喜欢只讲抽象概念直接上一个完整的 Python 示例。这个示例实现了一个“查天气 算运费”的助手你能看到 Function Calling 从“定义工具”到“执行工具”再到“二次生成回答”的完整闭环。from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, # 换成你的 Ollama 地址 api_keyollama # 本地 Ollama 不校验 key随意填 ) def get_weather(city: str) - str: 模拟天气查询接口 weather_map { 北京: 晴气温 25°C, 上海: 小雨气温 22°C, 广州: 多云气温 28°C, } return weather_map.get(city, f{city} 天气数据暂时未收录) tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气情况, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 } }, required: [city] } } } ] messages [{role: user, content: 北京今天天气怎么样}] response client.chat.completions.create( modelqwen2.5:7b, messagesmessages, toolstools, tool_choiceauto, ) # 检查模型是否想调用函数 if response.choices[0].message.tool_calls: tool_call response.choices[0].message.tool_calls[0] print(模型想调用, tool_call.function.name) print(参数, tool_call.function.arguments) # 执行真实函数 import json args json.loads(tool_call.function.arguments) result get_weather(**args) # 把函数调用记录和结果追加到消息列表 messages.append(response.choices[0].message) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) # 让模型基于工具结果生成最终回答 final_response client.chat.completions.create( modelqwen2.5:7b, messagesmessages, toolstools, ) print(最终回答, final_response.choices[0].message.content) else: # 模型没有调用工具直接回答 print(模型直接回答, response.choices[0].message.content)这段代码的核心逻辑不复杂但有几个地方我要特别强调。3.2 messages 列表的“三段式”追加很多人第一次写 Function Calling 会在消息历史的管理上出问题。完整的一次函数调用messages 列表里至少要有四段内容用户原始请求比如“北京今天天气怎么样”模型的 tool_calls 响应也就是模型说要调用 get_weather参数是 {“city”: “北京”}。这条消息的 role 是 assistant但里面没有普通 content而是带一个 tool_calls 字段。工具执行结果role 是 tool必须带上 tool_call_id 来和上一步的调用请求对应。模型基于工具结果生成的最终回复。漏了第 2 条或者第 3 条模型在下一轮就不知道刚才发生了什么可能会重复调用函数或者答非所问。尤其是 tool_call_id必须严格一一对应不然会直接报错。3.3 tool_choice 参数auto、none、required 怎么选在 OpenAI 兼容协议里tool_choice 控制模型“什么时候可以调用函数”。auto模型自己判断是否需要调用工具适合大部分场景。none模型只能用对话回复即使你定义了 tools 也不调用适合“闲聊模式”。required强制模型必须调用一个工具适合“必须走工具”的业务规则。实际项目里我建议默认用 auto然后在业务层面对“模型没有调用工具”的情况做兜底。比如用户问“你好”你就别指望模型调工具直接走普通对话即可。而 required 模式在某些场景里很有用比如“帮我订个酒店”你希望模型无论如何都要先调查询接口而不是直接猜一个答案。4. 让模型会填参JSON Schema 定义与参数工程4.1 参数描述比参数类型更重要很多开发者第一次写 tools 参数时把注意力全放在参数类型上比如 city 是 string、num 是 integer然后发现模型经常填错参数。问题通常出在 description 写得太简单。举个例子如果 description 只写“城市”模型有可能把“首都”这种词直接填进去。但如果你写“需要查询天气的城市名称必须是中文全称例如北京、上海、广州不接受拼音或简称”模型的表现会立刻提升一个档次。这不是玄学是训练数据里真实存在的关联模式模型会优先依据 description 中的示例来生成参数。4.2 用 enum 限制取值范围减少无效调用当参数只有固定几个候选值时一定要用 enum。比如“查询天气”的城市假设你只支持 100 个城市那你最好在枚举里列出来或者至少把常见城市列出来。模型在生成参数时会优先选择 enum 里的值。{ type: object, properties: { city: { type: string, enum: [北京, 上海, 广州, 深圳], description: 城市中文全称 }, date: { type: string, description: 查询日期格式为 YYYY-MM-DD } }, required: [city, date] }有人可能觉得 enum 太死板后面加城市还要改代码。但函数调用本来就是强规范性协议宁可多维护一个枚举列表也不要让模型自由发挥然后返回一堆你处理不了的脏数据。enum 是成本最低的输入校验手段。4.3 复杂嵌套参数让模型学会处理对象和数组除了基础字符串Function Calling 也支持嵌套 JSON。比如“创建订单”这样一个动作需要传递商品列表、收货地址、优惠券 ID参数结构就复杂了。{ type: object, properties: { order_no: { type: string, description: 订单号业务系统生成的唯一编号 }, items: { type: array, items: { type: object, properties: { sku_id: {type: string, description: 商品 SKU ID}, qty: {type: integer, description: 购买数量必须是大于0的整数} }, required: [sku_id, qty] } }, address: { type: object, properties: { province: {type: string}, city: {type: string}, detail: {type: string} }, required: [province, city, detail] } }, required: [order_no, items, address] }我测过 qwen2.5:7b 对这种嵌套结构的支持只要 description 写得清楚它基本能抽出正确的参数树。不过有一点要注意嵌套层级不要太深超过三层以后小参数模型容易抽漏字段。如果你发现模型经常漏掉内层字段优先考虑拍平参数结构而不是继续嵌套。5. 多轮调用与会话记忆真正“干活”的复杂场景5.1 从一个函数到多个函数让模型学会“选择”真实业务不会只有一个函数。我做过一个简单的“物流客服助手”里面同时有查订单、查物流、修改地址、申请售后的四个函数。模型需要根据用户一句话决定调用哪个函数、传哪些参数。多函数的定义方式和单函数差不多就是把多个 JSON 结构放进 tools 数组。真正的问题在于当函数变多之后description 的“辨识度”就变得极其重要。比如“查天气”和“查温度”这两个工具如果不仔细写描述模型很可能总是调用错的那个。我的经验是每个函数的 description 开头第一句就要明确指出这个函数“适合什么请求、不适合什么请求”。5.2 多轮调用中的状态管理模型怎么记住“刚才的订单号”在多轮对话里用户可能先说“帮我查一下订单”你问他订单号他再告诉你“20250101”。如果每次都重新发一个空的 messages模型肯定不知道订单号是什么。所以你必须把历史消息完整地带到下一次请求里。messages [] # 第一轮 messages.append({role: user, content: 帮我查一下订单}) # 模型会反问订单号或者调一个获取订单列表的工具 # 第二轮用户补充信息 messages.append({role: user, content: 订单号是 20250101})这里的关键点在于messages 才是模型“记忆”的唯一来源。只要你不把历史消息丢弃模型就能引用之前的上下文。Function Calling 本身没有独立的“会话状态”它的记忆完全靠 messages 传递所以设计好 messages 的追加规则就是设计好会话状态。我做过的几个项目里踩过最大的坑是“把 tool_call 结果拼错位置”。比如第一轮模型调用查订单函数返回了几个订单列表第二轮用户说“选第一个”结果我把第一轮的 tool 结果丢掉了模型就不知道“第一个”指的是哪条。后来我的统一做法是只要一个会话内的消息全部保留在 messages 里不管中间经过多少次函数调用都不做“裁剪”除非消息长度超过了模型上下文窗口。5.3 循环调用如果第一次工具结果不够怎么继续问有些复杂的任务一次工具调用不够。比如用户问“广州和上海哪个冷”理论上模型需要先调两次 get_weather然后把两次结果对比回答。这时代码里就需要写一个循环不断检查模型是否还想调用工具。while True: response client.chat.completions.create( modelqwen2.5:7b, messagesmessages, toolstools, ) message response.choices[0].message if not message.tool_calls: # 模型不再调用工具输出最终答案 print(最终答复, message.content) break # 逐个处理工具调用 for tool_call in message.tool_calls: messages.append(message) # 注意把 assistant 消息带 tool_calls追加进去 result execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result })在实际运行中这个循环通常 2 到 3 轮就能结束。如果出现模型反复调用同一个函数、参数却不变的情况大概率是 messages 里少了 assistant 那一条模型“忘了刚才已经查过了”。我在代码里特意加了这行注释message 本身有 tool_calls不能只把 content 的内容丢进去必须把整个 assistant message 追加进去。6. 工具选型与避坑从 API 到本地部署怎么选6.1 OpenAI API、Ollama、vLLM 之间怎么选很多初学者会问我到底应该用云端 API 还是本地部署热词里也频繁出现“免费大模型 API”“大模型下载”“本地部署大模型”。我把主流方案放在一起对比一下方便你根据场景选择。方案优点缺点适合场景OpenAI API效果好Function Calling 格式最稳定收费数据出外网可能有延迟生产环境要求高、数据不敏感国内大模型 API千问、GLM 等免备案、中文效果不错各家兼容性略有差异国内业务对合规要求高Ollama Qwen 2.5本地部署、免费、离线可用7B 模型能力有限复杂参数容易出错开发调试、内网数据敏感场景vLLM 开源大模型吞吐高、可并发部署门槛高需要 GPU生产环境的本地推理服务从开发效率角度来说我强烈建议你“先用 Ollama 联调再切云端模型”。因为函数调用的链路和模型关系不大你的代码逻辑只要遵循 OpenAI 兼容协议本地能跑通云端基本上也能跑通。差别只在于模型能力的强弱而不是接口格式。6.2 免费大模型 API 的“坑”有哪些热词里有人问“有可以免费使用的大模型吗”答案是肯定的。很多平台推出了免费额度或限时免费模型。但你在做 Function Calling 项目时必须确认以下几个关键点是否支持 tools 参数。有些入门模型只开放基础对话接口看到 tools 直接忽略或者报错。工具调用的响应格式是什么。有些平台的 tool_calls 结构并不完全标准需要你在代码里做兼容适配。每天/每月的调用次数限制。免费额度往往够你学习测试但不够生产使用。我实际测过几个免费 API最大的问题是模型不按照参数的 JSON Schema 输出导致解析层报错。这种场景下你可以在代码里加一个“参数校验”步骤发现不符合 Schema 就重试一次或者让模型重新提取。重试逻辑看起来简单但在免费模型上极其实用。6.3 别急着上微调先用提示词和函数定义解决问题排行榜和热词里经常出现“大模型微调”很多朋友搞了几天 Function Calling 觉得不完美就想着微调模型。我的建议是不要一上来就微调。函数调用的稳定性和三个因素有关模型本身能力、函数定义质量、代码处理逻辑。很多时候问题出在后两者而不是模型。举个例子小模型总是漏传参数你把 description 写得更详细、把必填字段提到 required 里稳定性会明显提升。只有当你在现有模型上无论如何优化描述、调整提示词都无法达到业务要求时才值得考虑微调。微调是“最后的手段”不是“最佳实践”。7. 常见问题与排查技巧实录7.1 模型不调用函数只直接回答怎么办这种问题经常发生。我建议按以下顺序排查确认 messages 和 tools 传参正确。用最简单的“查天气”示例跑一遍排除代码问题。检查函数 description 里的文字。模型把函数调用当成了一种“语言行为”如果描述里没有明确的触发词它可能不会调用。把“查询指定城市的当前天气情况”改成“当用户想了解某个城市的天气时必须使用此工具”效果立竿见影。换成更强的大模型试试。如果 qwen2.5:3b 不调用换 7b 往往就好了。模型虽然架构相似但在指令遵循能力上有明显差距。7.2 模型调用了函数但参数解析失败参数解析失败最常见的两个原因一是模型返回的 arguments 不是合法 JSON二是参数结构和 Schema 不一致。针对前者我建议在代码里做一层“清洗”比如使用json.loads失败后用正则提取其中的 JSON 片段再解析。针对后者最简单的方法是“用 enum 限定取值 用 required 强制必填”。我写了一个简易的参数解析函数一般在生产环境里足够用def safe_parse_arguments(arguments: str) - dict: 带兜底的参数解析移除代码块标记提取最外层 JSON arguments arguments.strip() if arguments.startswith(): arguments arguments.strip() if arguments.startswith(json): arguments arguments[4:] try: return json.loads(arguments) except json.JSONDecodeError: # 尝试提取 { 到 } 之间的内容 start arguments.find({) end arguments.rfind(}) if start ! -1 and end ! -1: return json.loads(arguments[start:end1]) raise7.3 模型死循环调用同一个函数怎么止损上一节提过死循环一般和 messages 历史不完整有关尤其是漏掉了带 tool_calls 的 assistant 消息。除此之外还有一种场景是模型认为函数返回结果“不满足要求”于是反复调用。这时你在代码里加一个“最大调用次数”限制比如 5 轮之后强制终止并把当前的上下文或多轮结果交给模型让它尝试直接回答。这个兜底逻辑不复杂但能避免生产环境里出现巨额 token 消耗。问题现象可能原因排查方式模型不调用函数description 触发词不明确把“必须使用此工具”写进描述模型调用函数但不填参数参数 description 太模糊增加示例值和初始值参数解析失败模型输出非标准 JSON加 safe_parse_arguments 兜底死循环调用同一个函数messages 历史缺失 assistant 消息将完整 assistant message 追加进历史多函数选择错误description 不具备辨识度每个函数开头写明“适合/不适合”小模型漏传嵌套字段模型能力不足拍平 JSON 结构降低嵌套层级7.4 对于免费 API 和在线平台Function Calling 响应慢怎么办如果你用的是免费 API响应速度往往不稳定比如 30 秒才返回结果。这时你需要注意超时时间要设得足够长避免前端主动断连。如果你在服务端调用建议加一个异步队列不要让用户请求一直挂着。最简单的方式是把调用放到后台任务里轮询拿结果前端只负责展示“处理中”状态。这个方案在老旧的业务系统改造里特别常用。8. 最后分享两个实用小技巧第一个小技巧用 Function Calling 做“参数提取”的时候不一定要真的去执行函数。我经常把 Function Calling 当成一个“结构化信息抽取器”来用——定义好我想提取的字段让模型从用户消息里抽出来然后只取 tool_calls 里的参数不真正执行任何工具。这种用法在表单自动填充、信息录入场景里非常有效本质上是用函数调用的协议来约束模型的输出格式比让模型输出普通 JSON 要稳定得多。第二个小技巧如果你用的是 Ollama可以直接在Modelfile里通过PARAMETER stop或者自定义系统提示词来增强小模型的函数调用稳定性。比如给模型加一句“当用户询问天气时务必调用 get_weather 工具”效果往往比你在 SDK 端用 system prompt 更稳定。这个方法看起来不起眼但在 3B 级别的模型上提升非常明显。坦白说Function Calling 这条路我一开始也走得很曲折。印象最深的一次是把线上客服机器人全部切到函数调用架构结果刚上线那天晚上因为 messages 历史漏了一条 assistant 消息整个机器人在“查订单”场景里疯狂死循环token 费用跑出一个让我肉疼的账单。后来加上了循环次数限制和完整的消息追加逻辑这套系统才真正稳定下来。那次的教训让我明白Function Calling 的门槛不在“会不会调接口”而在“能不能把工具链路、消息状态和异常兜底设计完整”。希望这篇实战指南能帮你少踩几个坑把大模型真正变成你业务里能干活的角色。
返回列表