news 2026/10/21 15:13:24

Function Call实战:让大模型从“只会聊天”到真正执行工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Function Call实战:让大模型从“只会聊天”到真正执行工具

最近在捣鼓AI Agent相关的东西,绕不开一个叫Function Call的能力。不少朋友私信问我说,这东西跟让模型“联网”有什么区别,是不是调个API的事?说实话,光看文档很难体会到它的价值。但当你真正被“模型只会聊天、不会干活”这件事卡住的时候,你就知道需要它了。

这篇文章我打算把Function Call从设计思路到落地代码,再到排查过程完整拆一遍。不整虚的,直接讲清楚它是什么、能解决什么问题、具体怎么接。如果你正在做智能助手、流程自动化或者任何需要让大模型“动起来”的应用,这篇对你会有用。哪怕你是刚入门的新手,只要照着后面给的步骤走,也能跑通。

1. 先搞清楚Function Call到底解决什么问题

1.1 模型不是计算器,也不是工具执行器

先说一个特别容易误解的地方:大模型本质上是一个“文字接龙机器”。你给它一段话,它根据学习过的海量语料,预测最可能接下去的文字是什么。这决定了它擅长写文案、做翻译、总结摘要,但这不代表它能可靠地做数学运算、查实时天气或者替你发一封邮件。

很多刚接触大模型应用的人会有一个朴素的想法:既然模型什么都会,那我直接问它“今天深圳天气如何”不就行了?问题在于,模型的训练数据是过去某个时间点之前的,它没有能力去感知此刻的天气。它能做的只是根据语料中的规律,编造一个看起来合理的天气数据。这就是所谓的“幻觉”。

所以你会发现,单纯靠对话接口,你只能做一个“聊天机器人”,做不了真正解决问题的“助手”。想要让模型真正干活,核心矛盾是:模型的输出是文本,而程序需要的是确定性的数据和动作。你没法指望模型在文本里写一句“我已调用接口发送了邮件”,然后邮件就真的发了。程序不认这个。

1.2 Function Call的本质:把规划交给模型,把执行交回代码

Function Call(函数调用)解决了上面这个矛盾。它的思路是:模型依然只负责“思考”和“决策”,但决策的结果不是一段自由文本,而是一个结构化的调用请求。这个请求里明确写清楚了“你要调用哪个函数”以及“传给这个函数的参数是什么”。

举个例子,当用户问“帮我查一下北京明天的天气”时,模型并不会真的去查天气,它会输出一个这样的结构化结果:我想调用名为get_weather的函数,传入参数city=北京,date=明天。你的后端代码拿到这个结构后,自己去做真实的网络请求、查询天气API、拿到数据,然后再把数据作为新的消息返回给模型。模型根据真实返回的数据,组织成一句自然语言回复给用户。

整个过程就像是你雇了一个聪明的助理,助理负责听懂需求、拆解任务、判断该用什么工具,但他不会亲自去干活,而是把活儿派给真正能干的工人们。代码就是那些工人,Function Call就是助理手里的派工单。这个拆解非常关键——模型不需要是万能的,它只需要会做决策。

1.3 什么时候才真的需要它

严格来说,不是所有场景都需要Function Call。如果你只是做一个简单的问答机器人,输入一段资料让它总结,那直接调对话接口就够了。但当你遇到下面这几种情况时,基本上就必须上Function Call了:

  • 需要获取实时数据:天气、股价、新闻、航班信息。这些数据超出了模型的训练知识范围,必须通过API实时获取。
  • 需要执行业务动作:帮忙订会议、发通知邮件、操作数据库、更新工单状态。这类任务要求有真实副作用,不能靠模型空想。
  • 需要模型控制流程:根据用户的输入,决定走哪个分支、调用哪个内部服务。传统做法是用户手动点按钮选择,Function Call可以让模型自动判断。
  • 需要结构化抽取信息:从一段聊天记录里抽取“用户的姓名、公司、意向等级”,用函数调用的方式可以让模型按你定义的格式输出,而不是自由文本。

我自己的体会是,一旦任务涉及“连接外部世界”或者“产生真实动作”,Function Call就是刚需。它把“能说”变成“能做”,这是AI应用从玩具走向生产力的关键一步。

2. 核心参数拆解与选型逻辑

2.1 tools参数:函数描述才是重头戏

做Function Call开发,大部分时间不是在写调用逻辑,而是在写函数描述。很多第一次接触的人以为tools参数就是把代码里函数的签名搬进去,实际效果却很差。模型并不运行你的代码,它只看你提供的JSON描述来决定何时调用、怎么传参。所以这个描述的准确度和详细度,直接决定了模型“会不会用”你的工具。

一个标准的工具描述长这样:

{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市未来几天的天气情况,包括温度、降水概率和风力。当用户询问天气时使用此工具。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,使用中文全称,例如:北京、上海、深圳" }, "days": { "type": "integer", "description": "查询的天数,可选值为1到7,默认1" } }, "required": ["city"] } } }

这里有几个关键点值得展开说。description字段是写给模型看的说明书,它决定了模型在什么场景下会触发这个工具。写这个描述时,要站在“判断”的角度写:什么情况下用这个工具、参数的单位和范围是什么、有没有默认值。差的描述往往写得像代码注释,比如“获取天气数据”,这种描述模型很难判断什么时候该用。

parameters里每个字段的description也一样重要。模型是通过这些字段描述来生成参数值的。如果你不写明“城市使用中文全称”,模型可能就会输出拼音或者英文。如果days不解释范围,它可能传给你一个“99”,让你后端直接报错。这个参数描述里的每个字,都是模型的“提示词”。

2.2 tool_choice怎么选

默认情况下,模型的tool_choice是auto,意思是让模型自己判断需不需要调用函数。这在大多数场景下是合理的,因为模型会根据用户需求自主决定什么时候需要工具。但有些场景下你需要更精确的控制,tool_choice还有另外两个常用值:

  • none:强制模型不要调用任何函数。这在你只想让模型做纯对话时很有用,可以避免模型偶尔抽风乱调函数。
  • 指定某个函数名称:比如{"type": "function", "function": {"name": "get_weather"}},强制模型必须调用该函数。当你明确知道用户就是要查天气时,这样写可以跳过模型自己的判断环节,省一次对话轮次。

我的建议是,大部分场景用auto就好,不要过早做精细控制。auto模式下模型会根据用户输入的语义灵活决定,泛化能力更强。只有当你的业务路径极其固定、且不允许模型自由发挥时,才考虑指定函数。过早约束会让助手显得死板,用户稍微换一种说法,模型可能就不知道怎么处理了。

2.3 temperature不是越高越好

Temperature控制模型输出的随机性,这个参数在做普通对话时大家习惯调高一些,让回复更有人味。但在Function Call场景下,我的实际经验是把它调低,尤其是在需要精确抽取信息、填充参数的时候。

原因很简单:Function Call的输入参数是要交给后端代码去执行的,如果参数值不够准确,比如城市名字被随机成了“Bei Jing”,或者把date参数写成了不存在的日期,轻则调用失败,重则往数据库里写脏数据。temperature过高时,模型更容易输出非常规表述。把它调到0.2甚至0,虽然回复可能显得呆板一点,但参数极大概率是准确规范的。

同理,top_p也建议调低。这两个参数本质上都在控制采样范围,在工具调用场景下稳定比有趣重要得多。

2.4 参数命名的小学问

写参数名时,不要用过于简略的缩写,也别用带空格的标签。模型阅读的是JSON里的字段名和描述,如果字段名叫c,就算描述里写清楚了“城市”,模型照样会把它的值填得五花八门。字段名尽量用完整英文单词,同时每个字段补上中文描述。

另一个容易被忽略的点是required字段。如果你把某个参数设为非必填,但你的后端代码里又默认它是必传的,那么模型可能在用户没提到相关信息时就不传这个参数,后端直接报KeyError。所以,要么把参数设为required,要么在后端代码里写清楚默认值处理逻辑。别指望模型永远按你心里想的方式传参,它只会按你定义的方式传参。

3. 落地实操:从零完成一次函数轮询

3.1 写两个真正能跑的函数(查询 + 占位)

纸上谈兵没意义,直接上实操。我们先定义两个Python函数,模拟一个天气查询工具和一个“关灯”控制工具。为了演示,天气函数用假数据返回,控制函数直接记录状态。

# tools.py from datetime import datetime def get_weather(city: str, days: int = 1) -> dict: """模拟天气查询,实际项目中这里应该调用真实天气API""" print(f"[调用] 查询{city}未来{days}天天气") # 这里返回模拟数据,为了演示效果固定写死 return { "city": city, "days": days, "forecast": [ {"date": "2025-01-15", "condition": "晴", "temperature": "23°C"}, {"date": "2025-01-16", "condition": "多云", "temperature": "20°C"} ][:days] } def control_device(device: str, action: str) -> dict: """模拟智能家居控制,真实场景这里会调用MQTT或硬件接口""" print(f"[调用] {action}设备: {device}") return {"success": True, "device": device, "action": action, "time": datetime.now().isoformat()}

这两个函数相当简单,但已经覆盖了最常见的两种工具类型:一个是查询类,一个是控制类。查询类工具本身不改变系统状态,控制类工具会触发实际动作。在复杂的业务系统里,这两类工具往往需要不同的安全策略:查询可以放行,控制则需要权限校验。这个咱们后面会再说。

3.2 完整调用链路:用户提问到返回答案

接下来是核心部分,写一个函数,负责把用户的自然语言问题发给模型,收到模型返回的工具调用请求后,执行本地函数,再把执行结果回传给模型,让模型做最终回答。这段代码是整个Function Call流程的骨架:

# main.py import json import openai client = openai.Client(api_key="xxx") # 换成你的key # 把工具描述定义成列表,方便复用 TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市未来几天的天气情况。当用户询问天气时使用此工具。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市中文全称,如:北京"}, "days": {"type": "integer", "description": "查询天数,1-7,默认1"} }, "required": ["city"] } } }, { "type": "function", "function": { "name": "control_device", "description": "控制智能家居设备的开关。当用户要求打开或关闭设备时使用此工具。", "parameters": { "type": "object", "properties": { "device": {"type": "string", "description": "设备名称,如:客厅灯、空调"}, "action": {"type": "string", "enum": ["打开", "关闭"], "description": "执行动作"} }, "required": ["device", "action"] } } } ] # 本地函数注册表:名称到函数的映射 FUNCTION_MAP = { "get_weather": get_weather, "control_device": control_device, } def run_conversation(user_input: str, messages: list = None): if messages is None: messages = [{"role": "system", "content": "你是一个智能助手,可以利用工具回答用户问题。请根据用户意图调用工具,并根据工具返回结果组织回答。"}, {"role": "user", "content": user_input}] response = client.chat.completions.create( model="gpt-4o", # 不同平台的模型名称以实际为准 messages=messages, tools=TOOLS, tool_choice="auto", temperature=0.2 ) msg = response.choices[0].message messages.append({ "role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls if msg.tool_calls else None }) # 判断模型是否请求调用工具 if msg.tool_calls: for tc in msg.tool_calls: fn_name = tc.function.name fn_args = json.loads(tc.function.arguments) print(f"模型请求调用: {fn_name}({fn_args})") if fn_name in FUNCTION_MAP: result = FUNCTION_MAP[fn_name](**fn_args) else: result = {"error": f"未找到函数 {fn_name}"} # 把工具执行结果作为一条工具消息追加到对话中 messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False) }) # 将加了工具结果的完整会话再次发给模型,让模型生成最终回答 final_response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=TOOLS, tool_choice="auto", temperature=0.2 ) return final_response.choices[0].message.content, messages else: return msg.content, messages if __name__ == "__main__": answer, hist = run_conversation("帮我查一下北京明天天气") print("最终回答:", answer)

这段代码里有一个容易被忽略却很关键的细节:当模型返回tool_calls时,你需要把原始的工具调用信息原封不动地追加进messages里,然后再把工具执行结果以tool角色消息追加进去。这一步的顺序非常重要,必须先加assistant消息,再加tool消息,否则模型会搞不清楚哪个结果对应哪次调用。

还有个细节是tool_call_id。这是平台提供的关联ID,一条tool消息必须用它来对应某次函数调用。如果漏了tool_call_id或者写错了,接口直接报错。我一开始就踩过这个坑,后来写成程序化地读取tc.id就没再犯过。

3.3 多轮连续调用的状态管理

上面那个例子只做了一次工具调用,真实场景下模型可能需要连续调用多个工具才能回答一个问题。比如用户说“帮我看看北京的天气,然后提醒我明天带伞”,模型可能先调用get_weather,然后在判断天气的基础上再调用另一个通知工具。这种多轮调用的场景,代码逻辑要设计成循环。

关键在于:不要把“收到工具结果”当成流程的终点,而要继续把它作为新消息发给模型,让模型判断是否还需要再次调用工具。直到模型不再返回tool_calls,而是给出正常的文本回复,才说明任务真的结束了。上面的run_conversation函数目前只处理了一轮工具调用,完整的做法是包一层循环:

def run_conversation_with_loop(user_input: str, messages: list = None): if messages is None: messages = [{"role": "system", "content": "你是一个智能助手..."}, {"role": "user", "content": user_input}] for _ in range(5): # 限制最大循环次数,防止死循环 response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=TOOLS, tool_choice="auto", temperature=0.2 ) msg = response.choices[0].message messages.append({ "role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls }) if not msg.tool_calls: return msg.content, messages for tc in msg.tool_calls: fn_name = tc.function.name fn_args = json.loads(tc.function.arguments) print(f"模型请求调用: {fn_name}({fn_args})") result = FUNCTION_MAP.get(fn_name, lambda **_: {"error": "函数不存在"})(**fn_args) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False) }) raise RuntimeError("超过最大轮转次数,可能进入了死循环")

这个循环最多执行5轮,是个安全阀。因为模型在auto模式下偶尔会出现反复调用同一个工具的情况,比如它拿到了天气数据,却因为对结果不满意,又去调一次。如果不加限制,你的账单会先哭出来。根据我的经验,真实的业务场景里,最多三轮工具调用就能解决绝大多数问题,5轮作为上限已经是比较宽松的了。

这个状态管理方式在后续接入Web应用、对话机器人时非常有用。你只需要把messages这个列表存下来,放在Redis或者数据库里,用户每次说话时把它取出来传入,就能实现跨轮次的对话记忆和工具调用。这也是为什么上面代码里run_conversation_with_loop接收messages作为参数的设计原因——它不是一次性函数,而是可继续的会话处理器。

4. 踩坑实录与排查对照表

4.1 模型一直不返回tool_calls

这是新手最容易遇到的问题:代码写好了,但模型就是不用你准备好的工具,只直接回复文本。排查思路很简单,先从描述入手。描述写得太含糊,比如“查询天气”四个字,模型确实不好判断;但更不能容忍的是描述跟用户问题的表述习惯不匹配。

我在实际项目里遇到过一件事:我写了一个查询商品库存的工具,描述里写的是“查询SKU库存量”,但用户习惯说“还有货吗”,模型死活不触发工具。后来我把描述改成了“当用户询问商品是否有货、库存是否充足、什么时候补货时使用”,模型立刻就能正确触发了。所以碰见工具不触发,先给描述来一轮“唠家常式”的改写,把用户可能的说法都写进去。

还有一种情况是tool_choice被设置成了none,这个最坑,因为代码里看起来一切正常,但工具永远不会被调用。检查一下是不是不小心赋值了。最后,确认你的接口版本是不是支持工具调用,有些旧版本的接口参数名是functions而不是tools,传参方式完全不一样。

4.2 参数解析老出错:json.loads不背锅

模型返回的arguments是一个JSON字符串,比如{"city": "北京"}。你的代码要执行json.loads(fn_args_str)把它变成字典。这个过程看着简单,实际上有个致命细节:模型生成的JSON字符串偶尔会带有多余的换行、转义符,甚至在某些模型上会输出成带有解释性文字的格式。

比如模型可能返回一个字符串:{"city": "北京"}这是正常的,但偶尔也会见到\n{"city": "北京"}\n或者Here's the JSON:\n{"city": "北京"}。直接用json.loads会抛异常。我自己的处理方式是先做一轮清理,再去解析:

import re def parse_arguments(raw_str: str) -> dict: cleaned = raw_str.strip() # 去掉可能存在的解释性前缀,只保留第一个{到最后一个}之间的内容 start = cleaned.find("{") end = cleaned.rfind("}") if start != -1 and end != -1: cleaned = cleaned[start:end+1] return json.loads(cleaned)

这层容错很重要。平时模型都乖乖输出,但偶尔抽一次风,如果代码没做容错,整个流程就断在那里了。函数调用场景里,全局异常捕获加一个友好的错误提示,比追求“一次通过”更靠谱。

4.3 工具描述被模型误解:一个真实的“关灯”教训

有次我做智能家居接入,定义了一个工具控制灯光,描述写的是“根据用户指令打开或关闭灯光”。结果用户说“晚上回家自动开灯”,模型就调用了这个工具,并且传了action=打开。但实际上用户表达的是一个自动化场景,不是现在立刻打开,模型完全不理解里面细微的语境差别。

这个例子说明,工具描述里除了写“什么时候用”,还必须写明“什么时候不用”。后来我把描述改成了“仅当用户明确要求立即执行打开/关闭动作时使用。如果用户提到‘以后’、‘每天’、‘自动’等计划性词汇,不要调用此工具”。模型的表现立刻改善了。这个点很少有人提到,但极其重要——负向描述跟正向描述同等重要。

工具描述的边界越清晰,模型的表现就越好。你甚至可以加上“如果条件不够明确,可以向用户追问”这类描述,模型会在信息不充足时主动提问,而不是瞎猜。

4.4 工具调用结果丢了

还有种常见症状是:工具明明执行成功了,但最终回答驴头不对马嘴,甚至模型根本不知道工具的结果。这基本都出在消息序列的问题上。你要记住,工具消息必须紧跟着参数里指定的tool_call_id去关联。每次循环里,如果你一次追加了多条tool消息,顺序不能乱,必须跟tool_calls列表里的顺序一一对应。

另一个容易踩的坑是messages.append({"role": "tool", ...})之前,没有把包含tool_calls的那条assistant消息先加进去。每次把模型输出追加到历史时,都要把tool_calls字段一起存进去,否则下一次发送给模型时,它看到的工具请求跟实际的工具结果对不上,自然就“失忆”了。这属于状态管理里最容易犯的低级错误。

4.5 安全与权限校验:别让工具裸奔

Function Call让模型可以执行业务操作,这意味着只要有人能操纵用户输入,就有人能间接操纵你的工具。所以工具函数里的参数校验绝对不能省。比如前面定义control_device(device="客厅灯", action="打开"),如果用户说“把所有设备都关掉”,模型很可能会把device填成“所有设备”,你的代码里就得有映射表,把它翻译成具体的设备列表。

更要紧的是,如果工具涉及发短信、扣费、删除数据,不能只靠模型判断,必须做用户级的权限校验。我的做法是在每个工具函数内部先检查会话上下文,确认当前用户是否有权限执行这个操作,再把命令真正打出去。这样即使模型被误导或者被注入攻击,恶意操作的等级也被拆解成了“单个用户的无权限尝试”,而不是“全局业务事故”。

5. 使用中的几点心得

Function Call这套机制,我用下来最大的感受是,它的设计哲学是“让模型做它擅长的事,让代码做模型不擅长的事”。两者配合,才能构成一个真正可用的智能体。模型不擅长确定性计算,那就把计算包在函数里;函数不擅长理解自然语言,那就把理解交给模型。这个分工一旦想明白,所有参数的意义就一目了然了。

实际落地时不要贪多。一次接入十几个工具,模型的决策难度会显著上升,误调工具的概率也会变大。我建议从一个业务闭环最少需要的两三个工具开始,跑通流程,再慢慢往上加。每加一个工具,都要观察它会不会跟已有工具“抢活”。如果用户问天气时模型去调了“今日推荐”,那说明两个工具的描述边界不清,需要微调。

最后说个小技巧:上线后一定要给整个链路加日志。除了正常的业务日志,我更习惯把模型每次返回的tool_calls完整记录下来。这一步成本极低,但在排查问题时价值极大。你可以清楚看到模型做了哪些决策、为什么调了某个工具、参数是怎么填的。没有这些日志,你在线上面对一个“模型乱调用工具”的反馈时,只能靠猜。这个习惯帮我省了无数个小时的排查时间,推荐你从一开始就做好。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 15:12:38

纯CSS实现五彩导航栏:从flex布局到渐变交互的完整实战

导航栏是前端入门绕不开的坎。很多同学学CSS时,单个属性都认识,display:flex、:hover、border-radius这些都能说出来,但真让你独立做一个像样的导航栏,却容易卡住——要么布局乱掉,要么hover效果生硬,要么换…

作者头像 李华
网站建设 2026/10/21 15:12:08

Agent技能可验证中枢:基于HTTP契约的可靠能力治理

1. 项目概述:这不是一个“网站”,而是一套面向 Agent 的可验证能力中枢 “为了让 AI 更可靠,我做了个给 Agent 用的网站(含 skills)”——这句话乍看像极了某位开发者在朋友圈发的轻描淡写式总结,但实际拆…

作者头像 李华
网站建设 2026/10/21 15:11:12

REA模型实战:用资源、事件、代理重构订单库存系统

做业务系统开发的朋友,多半都经历过这样的时刻:一开始库存就一张表,加上一张进出的流水表,开发速度飞快;等业务慢慢起来,店铺多了、促销多了、退款多了,那张流水表开始变得不堪重负。你查一个“…

作者头像 李华
网站建设 2026/10/21 15:10:26

从指针到链表:核心操作、手写实现与调试排查指南

很多人学链表时都有一种感觉:上课听懂了,课后一写就懵。明明代码不长,但一运行就出错,不是空指针就是死循环,改了半天还不知道问题出在哪。这个情况我在带新人、帮同学调试时见得太多了。链表看起来简单,本…

作者头像 李华
网站建设 2026/10/21 15:09:32

Hybrid 架构下 H5 动态修改原生导航栏的 JS API 实战指南

1. 先说清楚:这条 JS API 到底在改谁的导航栏1.1 原生导航栏与 H5 自绘导航栏的本质差别很多前端同学第一次接触"导航栏动态化修改"时会有个误解:以为是改页面里那个div包裹的导航条。但实际上,标题里强调的jsAPI 方式,…

作者头像 李华
网站建设 2026/10/21 15:09:32

从零搭建LLM代码自动修复流水线:缺陷定位、验证与落地实践

“LLM 代码自动修复”这个话题,我最近半年反复搭过好几套方案。说实话,真正跑起来之后你会发现,让大模型给出修改代码反而是整条链路里最轻松的一环,真正的难点在于缺陷怎么精确定位、上下文怎么组织、修复完怎么验证、以及怎么防…

作者头像 李华