ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

深入解析Function Calling:从协议本质到手写实现大模型工具调用

深入解析Function Calling:从协议本质到手写实现大模型工具调用 1. 从“笨办法”开始为什么Function Calling不是你想的那样如果你最近在折腾大模型应用尤其是想搞点能联网、能查天气、能操作数据库的“智能体”那你大概率被“Function Calling”这个词刷屏了。各种教程、框架文档里都在提它好像不会这个你的AI应用就缺了灵魂。但说实话我第一次看到这个词的时候脑子里也是一团浆糊。它听起来像是一个具体的函数调用动作又像是一种协议还像是一种开发模式。网上的文章要么讲得太玄乎要么直接甩给你OpenAI API的JSON格式定义看完之后的感觉是懂了但又没完全懂。今天我们不搞那些云里雾里的概念堆砌就从最“笨”的办法开始亲手写一遍。我的核心观点是Function Calling本质上是一种“约定”是大模型LLM和我们写的程序你的代码之间的一种“对话协议”。它的目的极其单纯让大模型这个“黑盒子”能明确地告诉我们——“嘿人类我现在需要你帮我执行某个具体操作这是操作的名字和参数你准备好就告诉我结果。”很多人误以为Function Calling是大模型“主动”调用了我们的代码这是一种错觉。大模型本身只是一段概率预测程序它没有手不会执行curl命令也不会写数据库。它唯一能做的就是根据我们的“约定”在对话的某个时刻输出一段符合特定格式的文本。而我们作为程序的真正控制者需要去“监听”这段文本解析它然后才去真正地执行函数最后把执行结果再“喂”回给大模型让它继续思考。所以这个“笨办法”就是我们先忘掉LangChain、Dify、各种Agent框架。我们就用最原始的HTTP请求和字符串处理来模拟一遍这个“约定”是如何从无到有建立起来的。当你亲手用几十行代码把这个流程跑通你就会发现那些高级框架只不过是把这个流程包装得更优雅、更自动化而已其核心骨架我们今天就能把它搭出来。2. 拆解核心一次完整的Function Calling对话回合要理解Function Calling我们必须把它放到一次完整的“人-模型-工具”交互回合中去看。这个回合通常包含四个清晰的步骤我把它画成了一个简单的循环用户提问 - 模型思考并请求工具 - 程序执行工具 - 模型整合结果并回复让我们用一个最经典的例子贯穿始终用户问“北京今天天气怎么样”2.1 第一步我们如何“告诉”模型有哪些工具可用大模型不是全知全能的它不知道你的程序里有个叫get_weather的函数。所以对话开始前我们必须先“注册”工具。在Function Calling的约定里这通常是通过在发送给模型的系统提示System Prompt或用户消息中附带一个“工具列表”来实现的。这个列表不是一个简单的函数名数组而是一个结构化的描述。我们来看一个最“笨”的表示方法假设我们用纯文本约定你可以使用的工具 1. 函数名get_weather - 描述查询指定城市的天气情况。 - 参数city字符串必需表示城市名称例如“北京”。在实际的OpenAI API中这个列表会被格式化成更机器可读的JSON Schema。但本质没变我们是在用自然语言或结构化数据向模型描述一个“能力”的接口规范。这就像你给一个新同事一份操作手册告诉他“如果你需要查天气就告诉我你要调用‘get_weather’手册并且把城市名告诉我。”2.2 第二步模型如何“请求”调用工具模型收到了用户的问题“北京今天天气怎么样”以及我们提供的工具列表。它开始“思考”即进行概率计算。根据训练数据中对“天气”和“北京”的关联它很可能判断出需要调用get_weather这个工具。关键来了模型不会直接说“帮我执行get_weather(‘北京’)”。在Function Calling的约定下它会输出一段严格符合我们约定格式的文本。如果我们约定用JSON它可能会在回复中生成一个特殊的结构块{ “function_call”: { “name”: “get_weather”, “arguments”: “{ \”city\”: \”北京\” }” } }请注意这里的arguments是一个字符串里面包裹着一个JSON对象。为什么是字符串因为对于模型来说它的一切输出都是文本流。它只是在按照我们描述的格式生成一段看起来像JSON的文本。它并不“理解”JSON也不“执行”JSON解析。这个步骤模型仅仅是在说“根据我们的约定我现在需要get_weather功能相关参数是city北京我把这个请求按格式写好了请你指我们的程序来处理。”注意这是最容易产生误解的地方。模型输出的{“city”: “北京”}只是一个文本片段不是可操作的字典。我们的程序必须主动去解析这段文本将其转化为真正的数据结构。2.3 第三步我们的程序如何“执行”并返回结果我们的程序一直在监听模型的回复。当检测到回复中包含”function_call”这个关键字段时就知道模型在请求调用函数了。这时我们的程序需要解析从arguments字符串中提取出city的值“北京”。分派根据name字段的值“get_weather”在我们的代码中找到对应的真实函数。执行调用真实的get_weather(“北京”)函数。这个函数内部可能去调用一个天气API比如心知天气、和风天气等获取到真实的天气数据例如{“temperature”: “22°C”, “condition”: “晴”, “humidity”: “40%”}。格式化将执行结果再次按照约定格式准备好以便发回给模型。通常这个结果也需要被包装一下。2.4 第四步模型如何“消化”结果并给出最终回复我们将上一步得到的天气结果连同最初的对话历史再次发送给模型。这次发送的消息很关键通常包含一个“角色”为”function”的消息内容就是我们的执行结果。例如角色: function 名称: get_weather 内容: {“temperature”: “22°C”, “condition”: “晴”, “humidity”: “40%”}模型看到这条消息后就明白了“哦我之前请求的get_weather工具已经执行完毕结果是北京晴22度。” 于是它就能基于这个真实、具体的数据生成面向用户的、自然流畅的最终回复“北京今天天气晴朗气温22摄氏度湿度40%是个好天气。”至此一个完整的Function Calling回合结束。你会发现模型始终在它的文本世界里工作而“调用函数”这个物理世界的动作是由我们的程序根据约定“代理”执行的。这就是为什么它更准确的叫法应该是“工具调用”或“函数调用请求”。3. 抛开框架手写一个极简Function Calling流程现在我们不用任何AI框架只用Python标准库和requests库来模拟这个流程。我们会创建一个“虚拟大模型”——一个简单的判断逻辑来模拟LLM的决策过程。3.1 第一步定义工具与虚拟模型首先我们定义真实的工具函数和工具描述# 真实的工具函数 def get_weather(city: str) - str: 模拟查询天气的API调用 # 这里应该是真实的HTTP请求我们模拟数据 weather_data { “北京”: “晴朗25°C”, “上海”: “多云28°C”, “深圳”: “阵雨30°C” } return weather_data.get(city, “未找到该城市天气信息”) # 工具描述我们的“约定” tools_description “”” 你可以使用的工具 - get_weather(city): 查询城市天气。参数city是字符串如‘北京’。 “”” # 一个极其简陋的“虚拟大模型” def virtual_llm(user_query: str, tools_desc: str) - str: 模拟LLM根据用户输入和工具描述决定是否调用工具。 # 简单的关键词匹配来模拟LLM的“思考” if “天气” in user_query: # 模拟提取城市实际LLM会用更复杂的方式 city “北京” if “北京” in user_query else “上海” # 简单演示 # 按照我们约定的格式返回工具调用请求 return f”FUNCTION_CALL: get_weather ARGS: {city}” else: return f”ANSWER: 我不知道如何回答这个问题。”这个virtual_llm函数模拟了核心环节它看到了“天气”关键词结合工具描述决定调用get_weather并以我们自定义的简单格式FUNCTION_CALL: func_name ARGS: arg输出了请求。3.2 第二步构建主控循环接下来我们编写主程序它负责协调整个对话流程def main(): print(“系统启动工具已就绪...”) print(f“工具列表\n{tools_description}\n”) # 模拟用户输入 user_query “北京今天天气怎么样” print(f“用户: {user_query}”) # 第一步将用户问题和工具描述一起交给“模型” llm_response virtual_llm(user_query, tools_description) print(f“虚拟模型回复: {llm_response}”) # 第二步解析模型回复检查是否是工具调用请求 if llm_response.startswith(“FUNCTION_CALL:”): # 解析出函数名和参数 _, func_info llm_response.split(“FUNCTION_CALL: “) func_name, args_str func_info.split(“ ARGS: “) func_name func_name.strip() args_value args_str.strip() print(f“\n检测到工具调用请求”) print(f“- 函数名: {func_name}”) print(f“- 参数: {args_value}”) # 第三步根据函数名映射并执行真实的函数 if func_name “get_weather”: # 执行真正的函数 result get_weather(args_value) print(f“- 执行结果: {result}”) # 第四步将结果格式化成“函数响应”的格式准备反馈给模型 func_response f”FUNCTION_RESULT: {result}” print(f“\n将结果反馈给模型: {func_response}”) # 这里本应再次调用virtual_llm传入原始问题和函数结果让模型生成最终回答。 # 为了简化我们直接模拟最终回复。 final_answer f”根据查询结果{args_value}的天气是{result}。” print(f“\n模型最终回复用户: {final_answer}”) else: print(f“错误未知的函数 {func_name}”) else: # 如果不是工具调用直接输出模型回复 print(f“模型直接回复: {llm_response.replace(‘ANSWER: ‘, ”)}”) if __name__ “__main__”: main()运行这段代码你会看到如下输出系统启动工具已就绪... 工具列表 你可以使用的工具 - get_weather(city): 查询城市天气。参数city是字符串如‘北京’。 用户: 北京今天天气怎么样 虚拟模型回复: FUNCTION_CALL: get_weather ARGS: 北京 检测到工具调用请求 - 函数名: get_weather - 参数: 北京 - 执行结果: 晴朗25°C 将结果反馈给模型: FUNCTION_RESULT: 晴朗25°C 模型最终回复用户: 根据查询结果北京的天气是晴朗25°C。3.3 第三步理解这个“笨办法”的价值虽然这个例子简陋到可笑但它完整还原了Function Calling的核心骨架约定我们和“模型”约定了工具描述格式和调用请求格式FUNCTION_CALL:。请求“模型”按约定格式输出请求文本。解析与执行我们的主程序主动解析这段文本并主动调用对应的真实函数。回调将执行结果按约定格式返回完成闭环。OpenAI的Function Calling、Google的Tool Calling乃至所有AI Agent框架如LangChain的Tools、Dify的Workflow都是在将这个流程标准化、自动化、鲁棒化。它们定义了更严谨的JSON Schema来描述工具提供了自动解析和分派调用的机制并管理复杂的多轮对话状态。但万变不离其宗底层模式就是我们刚才手写的这个循环。4. 迈向实战对接真实大模型API理解了本质我们再来看看如何与真实的OpenAI API协作。这里的关键是使用正确的消息格式。4.1 定义符合OpenAI格式的工具OpenAI要求工具定义是一个JSON Schema列表。我们升级之前的工具描述tools_for_openai [ { “type”: “function”, “function”: { “name”: “get_weather”, “description”: “Get the current weather in a given location”, “parameters”: { “type”: “object”, “properties”: { “location”: { “type”: “string”, “description”: “The city and state, e.g. San Francisco, CA”, }, “unit”: {“type”: “string”, “enum”: [“celsius”, “fahrenheit”]}, }, “required”: [“location”], }, }, } ]这个结构比我们的纯文本描述复杂得多但它提供了机器可读的严格规范包括参数类型、是否必需、枚举值等这让模型能更准确地生成参数。4.2 发起包含工具定义的对话请求当我们向ChatGPT API发送请求时需要将tools参数传入import openai response openai.chat.completions.create( model“gpt-3.5-turbo”, messages[ {“role”: “user”, “content”: “What‘s the weather like in Boston?”} ], toolstools_for_openai, # 关键告诉模型可用的工具 tool_choice“auto”, # 让模型自行决定是否调用工具 )4.3 解析模型的工具调用请求模型的回复response.choices[0].message会包含一个tool_calls字段如果它决定调用工具的话。我们需要检查这个字段message response.choices[0].message if message.tool_calls: # 遍历所有工具调用请求理论上一次可以请求多个 for tool_call in message.tool_calls: func_name tool_call.function.name # 注意arguments 是字符串需要解析成字典 import json func_args json.loads(tool_call.function.arguments) location func_args.get(“location”) print(f“模型请求调用函数: {func_name}”) print(f“参数: {func_args}”) # 接下来根据func_name执行你的真实函数... # result get_weather(location)4.4 执行函数并将结果返回给模型执行完函数后我们必须将结果以特定的“function”角色消息追加到对话历史中并再次请求模型# 假设我们已经获取了结果 weather_result {“temperature”: “22”, “unit”: “celsius”, “condition”: “sunny”} # 将函数执行结果作为一条新消息 messages.append(message) # 先把模型的上一条消息包含tool_calls加入历史 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, # 必须对应之前的调用ID “content”: json.dumps(weather_result), # 结果需要是JSON字符串 }) # 再次调用API让模型基于结果生成最终回复 second_response openai.chat.completions.create( model“gpt-3.5-turbo”, messagesmessages, ) final_answer second_response.choices[0].message.content print(f“最终回答: {final_answer}”) # 例如: “It‘s currently sunny and 22°C in Boston.”这个流程和我们手写的“笨办法”逻辑完全一致只是消息格式和API调用被标准化了。框架的价值就在于帮你自动化管理messages数组的拼接、tool_call_id的匹配、错误处理等琐碎细节。5. 深入原理大模型是如何“学会”调用函数的你可能会有疑问大模型是怎么知道在什么时候、以什么格式来请求调用函数的它并没有被“编程”啊。这背后的核心是指令微调。在模型训练的最后阶段研究人员会使用大量精心构造的“对话-工具调用”配对数据对模型进行微调。这些数据的格式就像我们前面演示的完整回合用户: 北京天气 助手: 思考我需要调用get_weather工具。调用{name: get_weather, arguments: {city: 北京}} 系统(模拟函数执行): {temp: 22°C} 助手: 北京今天气温22摄氏度天气晴朗。模型通过学习海量这样的例子逐渐掌握了模式“当用户问题涉及某个领域如天气而我自身知识无法给出具体实时数据时我应该输出那个特定的JSON结构来‘请求帮助’然后在收到一段结构化的数据后再将其组织成自然语言回答。”所以Function Calling能力不是魔法而是通过数据训练出来的、一种对特定文本模式的条件反射。模型学到的是一种“沟通协议”而不是真正的代码执行能力。6. 避坑指南Function Calling实战中的常见问题在实际项目中集成Function Calling你会遇到一些教科书里不提的坑。这里分享几个我踩过的雷。6.1 参数格式的“幻觉”与校验大模型在生成arguments的JSON字符串时有时会产生“幻觉”输出格式错误或参数值不合理。例如要求数字它给字符串要求枚举值它给一个不在列表里的值。避坑策略永远不要信任模型输出的参数。必须在你的代码中进行严格的二次校验。JSON解析容错用try-except包裹json.loads()准备好处理格式错误的JSON。参数类型转换与验证即使Schema里定义了type: “integer”模型也可能输出”twenty two”。你需要做类型转换和有效性检查。对于枚举值检查输入是否在允许列表中。设置默认值与回退对于非必需参数如果模型未提供或提供错误应使用合理的默认值。def safe_execute_function(func_name, arguments_str): try: args_dict json.loads(arguments_str) except json.JSONDecodeError: return {“error”: “Invalid JSON format from model”} if func_name “get_weather”: location args_dict.get(“location”) unit args_dict.get(“unit”, “celsius”) # 提供默认值 if not location: return {“error”: “Missing required parameter: location”} if unit not in [“celsius”, “fahrenheit”]: unit “celsius” # 纠正错误的枚举值 # 然后才调用真正的API return call_real_weather_api(location, unit)6.2 工具描述的“艺术”工具的描述description和参数描述至关重要它直接指导模型的行为。描述不清会导致模型不理解何时调用或如何传参。实操心得描述要具体、场景化不要写“获取数据”要写“查询用户最近30天的订单列表”。好的描述能让模型准确判断调用时机。参数描述要明确对于location参数描述成“城市名如‘北京’或‘San Francisco, CA’”比单纯写“地点”要好得多。使用枚举限制选项如果参数只有几个固定值一定要用enum列出。这能极大减少模型出错的概率。6.3 处理模型的“拒绝”与“多轮”调用有时即使用户问题相关模型也可能不触发工具调用而是尝试直接回答可能答错。或者一次对话中可能需要连续调用多个工具。应对方案控制tool_choice参数如果你确定必须调用某个工具可以设置tool_choice{“type”: “function”, “function”: {“name”: “get_weather”}}来强制模型调用。通常设为”auto”即可。设计系统提示词在系统消息中明确指令如“你是一个天气助手当用户询问天气时你必须使用get_weather工具来获取最新信息。”管理对话历史在多轮工具调用中务必完整、准确地维护messages列表确保每次请求都包含完整的上下文用户消息、之前的工具调用、工具返回结果。这是Agent框架如LangGraph的核心价值之一。6.4 错误处理与用户体验工具执行可能失败如网络超时、API限流。你不能把一个Python异常堆栈直接返回给模型。技巧设计友好的错误信息格式返回给模型。例如不要返回{“error”: “HTTP 500”}而是返回{“error”: “Weather service is temporarily unavailable. Please try again later.”}。模型可以基于这个信息生成面向用户的友好提示如“抱歉天气服务暂时不可用请稍后再试。”这比直接抛出技术错误要优雅得多。7. 超越基础从Function Calling到AI Agent理解了Function Calling你就拿到了构建AI Agent的基石。一个简单的Agent可以看作是一个永不停歇的循环While (问题未解决): 1. 将当前状态用户目标、历史、可用工具送给LLM。 2. LLM决定下一步行动思考、调用工具、或给出最终答案。 3. 如果调用工具则执行并更新状态。 4. 重复步骤1。Function Calling就是这个循环中“调用工具”的标准化接口。而更复杂的Agent框架如LangGraph, AutoGen则在此基础上增加了规划拆解复杂目标为子任务。记忆管理长期和短期对话历史。工具路由从大量工具中自动选择最合适的一个。多Agent协作让多个具备不同技能的Agent共同完成任务。例如一个“旅行规划Agent”可能内部流程是先调用“搜索航班”工具再调用“查询酒店”工具然后调用“计算预算”工具最后调用“生成行程摘要”工具。每一次调用都是通过Function Calling协议来完成的。所以当你下次看到“AI Agent开发”时可以把它理解为基于LLM大脑和Function Calling手和脚通过一套调度逻辑Agent框架来协调完成一系列复杂任务目标的系统。而这一切的起点就是今天我们从最底层搞明白的这次“请求-执行-回调”的握手。亲手写一遍那个简陋的流程胜过读十篇概念文章。它让你看清了华丽外衣下的简单骨架。接下来无论是使用LangChain快速搭建原型还是深入研究如何用Dify Workflow编排复杂的业务逻辑你都会心中有数知道每一步背后究竟在发生什么。这就是“笨办法”的力量——它给你的是理解而不仅仅是使用。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表