4.2 Function Calling 概念介绍#

我们在使用大模的时候,不怕大模型不会回答这个问题,最怕的是它一本正经地胡说八道,而且关键是此时你还根本分不清大模的回答到底是正确的还是错误。但遗憾的是,大多情况下大模型并不会承认自己不会这个问题,因为连它自己也不知道自己不会这个问题,最终也就只会生成一些似是而非的答案,这就是大家常说的大模型幻觉。

4.2.1 大模型中的幻觉#

大模型幻觉(Hallucination) 是指大型语言模型在生成内容时,一本正经地胡说八道的现象。具体来说,模型生成的文本看起来非常流畅、逻辑自洽且自信满满,但其中的事实、数据、引用或逻辑推理却是错误的、虚构的或与现实世界不符的。这就好比人类在做梦或产生幻觉时,“看到”了实际上不存在的东西。

例如你问大模型:”我想去洗车,洗车店距离我家50米,你说我应该开车过去还是走路过去?“

图 4-2. 千问大模型回答结果

从图4-2中可以看出,尽管模型说了一大堆自己认为正确无误富有逻辑的回答,但事实上却是错的,而且在我们看来错得既离谱又好笑。

那怎么解决呢?解决方法就是打开模型的联网功能,并再次提问。

图 4-3. 使用联网工具时千问大模型回答结果

从图4-3中的结果可以看出,此时的回答才是正确且符合逻辑的。类似的例子还有问模型当前的时间、某个城市的天气、市场行情等具有实时性的信息。

所以,为了解决这样类似的问题就必须让大模型:① 知道什么时候调用工具;② 知道调用哪个工具;③ 知道参数怎么填写。而这正是函数调用所要解决的问题。

4.2.2 Function Calling 概念定义#

函数调用(Function Calling) 指的是一种让大模型能够调用外部函数或工具的机制。简单来说,函数调用让大模型具备"调用工具"的能力,并且在这种机制下大模型不再只是生成文本,而是可以:① 根据用户问题,判断需要调用哪个函数;② 自动根据用户提问生成函数参数;③ 调用函数并返回结构化调用结果。④ 根据结果生成回答内容。

所以 Function Calling 的本质是把"理解问题"交给大模型,把"执行任务"交给程序,它提供了模型链接外部世界的能力,最终使得回答变得可验证、可信赖,而不再局限于训练数据和上下文。借助这一能力,大模型可以连接数据库、API、搜索引擎以及各种内部系统,从而实现查询订单、获取天气、分析股票甚至执行 SQL 等实际操作。

更进一步,Function Calling 其实也是构建 AI Agent 的基础能力。例如类似“总结今日 AI 新闻并发送邮件”这样的任务,Agent 可以自动完成搜索、总结、生成内容并调用邮件接口发送,本质上其实就是多个工具调用的串联。因此,无论是 AI Agent、RAG 还是智能助手,其底层都离不开 Function Calling,所以未来的大模型系统大概也将逐步演进为 LLM + Tools + Memory + Planning 的组合架构,例如 OpenClaw 这类产品。

4.2.3 Function Calling 调用逻辑#

在介绍完 Function Calling 的基本定义和作用以后,我们再来看它在整个用户与大模型交互中的生命周期。

图 4-4. 大模型工具调用流程图

如图4-4所示,红色线条和蓝色线条分别是大模型在不使用工具和使用工具与用户交互时的调用流程图。

(1)不使用工具调用流程

对于不使用工具时的流程最为清晰,这是最基础的 LLM 交互模式,本质上就是一次调用完成对用户问题的回答。

例如,用户提问:“中国的首都在哪里?”,程序调用大模型把用户问题传给模型,然后大模型直接基于已有知识生成回答,接着大模型输出结果返回给程序,最后程序再将结果返回给用户。

此时可以看出,不使用工具时的特点便是只有一次模型调用、没有外部信息、完全依赖模型训练数据,因此它所适用的场景有常识问答(历史、概念)、写作、总结、翻译等不需要实时信息的问题。

(2) 使用工具调用流程

对于使用工具时的流程来说,它则是模型与外部能力协同的体现,在回答用户问题时至少会涉及到两次调用大模型。

例如,用户提问:“上海今天天气如何?”。

第1步:程序调用大模型(第一次)把用户问题传给模型;

第2步:大模型识别到回答该问题需要调用两个工具(时间查询工具和天气查询工具)并同时解析得到工具名和对应参数(例如 get_weather(location = '上海')get_current_date());

第3步:程序接收到工具名和参数信息,调用工具并得到运行结果,然后将结果返回给程序;

第4步:程序根据结果和用户提问调用大模型(第二次),然后得到大模型给到的回复;

第5步:程序接收到来自大模型的回复,并将其返回给用户。

可以看出,使用工具时的特点便是多次模型调用,并且程序负责“执行工具”提供真实世界的能力,而模型则负责“决策 + 整合”。

一句话总结,无工具调用时是模型根据用户提问自己回答,而有工具调用则是模型根据工具输出结果结合用户提问总结回答。

在清楚 Function Calling 的基本概念以及在整个大模型调用中的生命周期以后,你是不是已经开始好奇应该如何定义一个工具,并能同时在代码中进行使用了?

下面,我们来开始一步步深入介绍这部分内容。

4.2.4 工具的定义#

从上面的内容我们可以知道,工具的本质其实就是一个函数或者外部 API ,因此自然是需要定义一个函数来完成这部分功能。但是,对于大模型来说,它不可能接受一个函数作为输入,因此我们还需要根据一定的规则将函数转换为大模型能够看懂的描述。以下完整示例代码参见 Code/Chapter04/C02_function_calling.py 文件。

现在,我们来定义上面提到的两个工具,查询指定城市的添加和今天的日期。首先,分别定义这两个函数,如下所示:

1 def get_weather(location: str):
2     weather_conditions = ["晴天", "多云", "雨天"]
3     random_weather = random.choice(weather_conditions)
4     return f"{location}今天是{random_weather}。"
5 
6 def get_current_date():
7     date = '随便写一个测试时间,2025年3月5日'
8     return date

由于这里是用于模拟测试,所以上述两个函数的返回结果都是固定的。

根据这两个函数的定义,生成一个大模型能够看懂的工具描述,并在调用大模型时将其作为输入。对于 get_weather() 函数来说,其对应的工具描述为:

 1 {
 2   "type": "function",
 3   "function": {
 4       "name": "get_weather",
 5       "description": "当你想查询指定城市的天气时非常有用,可以通过该工具返回天气结果。",
 6       "parameters": {
 7           "type": "object",
 8           "properties": {
 9               "location": {
10                   "type": "string",
11                   "description": "城市或县区,比如北京市、杭州市、余杭区等。",
12               }
13           },
14           "required": ["location"],
15       }
16   }
17 }

对于上面这个工具定义描述来说,它整体上包括两个部分: LLM 厂商约定描述部分和 get_weather() 函数本身的 JSON Schema 描述部分。上面 parameters字段对应的内容中,除了对字段的描述 description 外,其余部分便是 get_weather() 函数本身的 JSON Schema 描述,其中 "type": "object" 为固定写法, properties 表示对函数字段信息的描述,required 表示是否为必填。第2行中的 "type": "function" 为固定写法,function 字段用于对工具进行描述,name 表示工具名称,第5行 description 则是对工具作用的描述,这个字段对于大模型来说是最重要的。

同样的,对于 get_current_date() 来说我们也可以得到类似的工具描述信息。最后,我们可以将多个工具的描述信息以列表的形式放到一起,如下所示:

 1 tools = [
 2     {
 3         "type": "function",
 4         "function": {....}
 5     },
 6     {
 7         "type": "function",
 8         "function": {
 9             "name": "get_current_date",
10             "description": "当你想知道今天的日期时非常有用,可以通过该工具获取今天的日期",
11             "parameters": {
12                 "type": "object",
13                 "properties": {}
14             }
15         }
16     },
17 ]

4.2.5 发起 Function Calling 调用#

在完成工具的定义以后再来看如何让大模型在回答问题时使用这些工具。首先我们需要创建一个消息数组,它包含有系统消息 System Message 和 用户消息 User Message,本质上都是输入模型的提示词,如下所示:

 1 messages = [
 2     {
 3         "role": "system",
 4         "content": """你是一个很有帮助的助手。如果用户提问关于天气的问题,请调用 ‘get_current_weather’ 函数;
 5      如果用户提问关于时间的问题,请调用‘get_current_time’函数。
 6      请以友好的语气回答问题。""",
 7     },
 8     {
 9         "role": "user",
10         "content": "上海今天的天气如何?今天是什么日期?"
11     }
12 ]

这里值得一提的是,尽管上面在创建 tools 数组时已经对工具的作用与何时使用工具进行了描述,但在 System Message 中强调何时调用工具通常会提高工具调用的准确率。

进一步,可以将创建好的 toolsmessages 一起喂给大模型,这样便发起了一次 Function Calling 调用流程,示例代码如下:

1 def dashscope_function_calling(messages, tools=None):
2     response = dashscope.Generation.call(
3         api_key=os.getenv('DASHSCOPE_API_KEY'),
4         model="qwen-flash", messages=messages, tools=tools)
5     return response

注意:不是所有模型都支持使用工具,具体需要看对应模型的官网介绍。对于千问大模来说,可以参见 [2] 中的描述。

在上述代码运行结束以后,可以得到类似如下结果:

{"status_code": 200, "request_id": "95e54ee6-c1f8-498c-8910-c1715e3c640c", "code": "", "message": "", "output": {"text": null, "finish_reason": null, "choices": [{"finish_reason": "tool_calls", "message": {"role": "assistant", "content": "", "tool_calls": [{"function": {"arguments": "{\"location\": \"上海市\"}", "name": "get_weather"}, "id": "call_aafb567f7e05422c8dede7", "index": 0, "type": "function"}, 
{"function": {"arguments": "{}", "name": "get_current_date"}, "id": "call_916855efd1a844518abd2e", "index": 1, "type": "function"}]}, "index": 0}]}, "usage": {"input_tokens": 289, "output_tokens": 36, "prompt_tokens_details": {"cached_tokens": 0}, "total_tokens": 325}}

从输出结果可以看出,由于用户既问了天气也问了日期,所以大模型将两个工具都识别了出来,并且可以发现,工具信息存在于 choices 字段中的 tool_calls 里面。

4.2.6 运行工具函数#

此时根据大模型的输出结果我们可以知道回答该问题应该使用什么样的工具。因此,下一步要做的就是定义一个函数名到函数实体的映射关系,根据从大模型回答中得到的函数名和参数发起函数调用获取结果。

 1 def conclusion(response=None, messages=None):
 2     messages.append(response.output.choices[0].message)
 3     tool_calls = response.output.choices[0].message['tool_calls']
 4     function_mapper = {'get_weather': get_weather, 'get_current_date': get_current_date}
 5     for tool in tool_calls:
 6         func_name = tool['function']['name']
 7         func_para = tool['function']['arguments']
 8         call_id = tool['id']
 9         result = function_mapper.get(func_name)(**json.loads(func_para))
10         print(f"工具运行结果为: {result}")

在上述代码中,第3行是从大模型第一次返回结果中获取得到函数工具的相关信息。第4行是定义一个函数名与函数的映射关系。第5~10行是分别取大模型结果中每个函数的名称和参数,并发起运行得到结果。

上述代码运行结束会得到类似如下结果:

工具运行结果为: 上海今天是多云。
工具运行结果为: 随便写一个测试时间,2025年3月5日

这里需要注意的是,对于千问系大模型来说,需要确保工具的输出为字符串格式。

4.2.7 大模型总结回复#

由于函数工具的输出格式较为固定,如果直接返回给用户,可能会有语气生硬、不够灵活等问题。因此,可以让大模型综合用户输入以及工具输出结果,生成自然语言风格的回复,可以将工具输出提交到模型上下文并再次向模型发出请求。

进一步,在上面代码中继续实现后续逻辑:

 1 def conclusion(response=None, messages=None,tools=None):
 2     ......
11     messages.append({"role":"tool","content": result,"tool_call_id": call_id})
12     print(f"拼接好的 messages:\n{messages}")
13     response = dashscope_function_calling(messages)
14     print(f"总结输出结果:\n {response}")

在上述代码中,第11行中 tool_call_id 是系统为每一次的工具调用请求生成的唯一标识符。因为模型可能一次性要求调用多个工具,将多个工具结果返回给模型时 tool_call_id可确保工具的输出结果能够与它的调用意图对应。

上述代码第12行的运行结果为:

[{'role': 'system', 'content': '你是一个很有帮助的助手。如果用户提问关于天气的问题,请调用 ‘get_current_weather’ 函数;\n         如果用户提问关于时间的问题,请调用‘get_current_time’函数。\n         请以友好的语气回答问题。'}, {'role': 'user', 'content': '上海今天的天气如何?今天是什么日期?'}, Message({'role': 'assistant', 'content': '', 'tool_calls': [{'function': {'arguments': '{"location": "上海"}', 'name': 'get_weather'}, 'id': 'call_7e868ef43b8c4899bcf6b1', 'index': 0, 'type': 'function'}, {'function': {'arguments': '{}', 'name': 'get_current_date'}, 'id': 'call_2bdd25a7b3e843229e9952', 'index': 1, 'type': 'function'}]}), {'role': 'tool', 'content': '上海今天是多云。', 'tool_call_id': 'call_7e868ef43b8c4899bcf6b1'}, {'role': 'tool', 'content': '随便写一个测试时间,2025年3月5日', 'tool_call_id': 'call_2bdd25a7b3e843229e9952'}]

可以看出,此时我们将把用户的原始问题、系统预置提示词以及工具返回的结果作为整体喂给大模型。最后,第14行的输出结果如下所示:

 {"status_code": 200, "request_id": "b41ade97-de64-4f99-94a8-0619c4703166", "code": "", "message": "", "output": {"text": null, "finish_reason": null, "choices": [{"finish_reason": "stop", "message": {"role": "assistant", "content": "今天上海的天气是多云,气温较为舒适,建议出门时带件薄外套哦~  \n今天是2025年3月5日,星期三,祝你有美好的一天! 🌞"}, "index": 0}]}, "usage": {"input_tokens": 150, "output_tokens": 46, "prompt_tokens_details": {"cached_tokens": 0}, "total_tokens": 196}}

从上述结果可以看出,最终生成的结果的确是根据工具返回的内容总结而来。

4.2.8 小结#

到此,对于 Function Calling 的基本原理及使用方式就介绍完了,包括什么是大模型中的幻觉、Function Calling 的定义与使用流程等。但是大家有没有发现一个问题?整个过程有点繁琐,需要我们自己实现的内容太多。那有没有更好的方法呢?

引用#

[1] https://help.aliyun.com/zh/model-studio/qwen-function-calling#72d1e52737f6x

[2] https://help.aliyun.com/zh/model-studio/qwen-function-calling#c8feada2328us

附录#

打开任意大模型Web端,输入如下内容输入:

messages = [
        {
            "role": "system",
            "content": """你是一个很有帮助的助手。如果用户提问关于天气的问题,请调用 ‘get_current_weather’ 函数;
         如果用户提问关于时间的问题,请调用‘get_current_time’函数。
         请以友好的语气回答问题。""",
        },
        {
            "role": "user",
            "content": "上海今天的天气如何?今天是什么日期?请输出对应工具的名称及参数"
        },
        {
            "role": "tools" ,
            "content": [
                        {
                            "type": "function",
                            "function": {
                                "name": "get_weather",
                                "description": "当你想查询指定城市的天气时非常有用,可以通过该工具返回天气结果。",
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "location": {
                                            "type": "string",
                                            "description": "城市或县区,比如北京市、杭州市、余杭区等。",
                                        }
                                    },
                                    "required": ["location"],
                                }
                            }
                        },
                        {
                            "type": "function",
                            "function": {
                                "name": "get_current_date",
                                "description": "当你想知道今天的日期时非常有用,可以通过该工具获取今天的日期",
                                "parameters": {
                                    "type": "object",
                                    "properties": {}
                                }
                            }
                        }
            ]        
        }
    ]

将会得到类似如下结果:

🌤️ 关于天气查询
针对您询问上海今天的天气如何”,我将调用以下工具
工具名称get_weather
参数
location: "上海市"
📅 关于日期查询
针对您询问今天是什么日期”,我将调用以下工具
工具名称get_current_date
参数该工具不需要额外参数