FunctionCalling:当大模型开始理解这个世界

267 阅读25分钟

FucntionCalling,一个现在经常被提起的东西,那它到底是个什么东西,有什么作用,原理又是怎样的,下面我们来逐步的进行解释

FunctionCalling基本概念

首先是FunctionCalling是个啥,一句话

FunctionCalling 为模型提供了一种调用函数的能力

这就完了?没错,就这么简单,FunctionCall嘛,函数调用,那模型凭什么就可以调用函数呢?这要求模型具备几个基础能力

FunctionCalling成立的基础

问题理解和行动规划

首先是要求模型必须能理解自己接收到的问题,然后是模型能知道这个问题需要分几步解决。

我们来看一个例子我们向qwen-turbo提出这个问题:我需要你帮我查询怎么从北京南站去天安门的路线,为了完成这个查询任务,你会做哪些事情 下面是回答:

为了回答“如何从北京南站到天安门”的路线查询任务,我会按照以下步骤进行思考和验证,确保信息的准确性和实用性:

---

### **1. 确认起点和终点的具体位置**
   - **起点**:北京南站(通常指北京南站地铁站或火车站,需确认是否包含具体出口或公交站位置)。
   - **终点**:天安门广场或天安门周边区域(需明确是天安门东站、天安门西站,还是天安门广场的入口)。
   - **注意事项**:北京南站附近有多个地铁和公交站点,需确保路线描述的准确性(例如地铁4号线北京南站站的具体位置)。

---

### **2. 收集交通方式的实时或基础信息**
   - **地铁**     - 查询北京地铁4号线和1号线的运营时间、换乘站(如西单站)的准确性。
     - 确认天安门东站/西站是否为最近的地铁站,以及出站后的步行路线(例如从地铁口到天安门广场的具体方向)。
   - **公交**     - 检查公交线路(如特8路、旅游公交等)的运营时间、路线是否直达,以及是否需要换乘。
     - 验证公交站名和站点位置(例如北京南站附近的公交站是否为“陶然桥南”或“北京南站枢纽站”)。
   - **打车/网约车**     - 估算距离(约15公里)和预估时间(通常20-30分钟,但需考虑早晚高峰拥堵)。
     - 确认天安门周边是否有临时交通管制或停车限制。
   - **骑行/步行**     - 验证骑行路线是否安全(如需骑行需注意天安门周边禁止自行车进入)。
     - 步行路线是否可行(全程约3公里,但需绕行部分区域)。

---

### **3. 验证信息的时效性和可靠性**
   - **地铁施工或临时调整**:检查是否有地铁线路的临时关闭或换乘站调整(例如疫情期间或节假日的临时措施)。
   - **公交线路变更**:确认公交线路是否因道路施工或政策调整而改道。
   - **天安门周边管制**:了解天安门广场的开放时间和安检要求(例如节假日或重大活动期间可能需要提前预约)。
   - **官方数据来源**:参考北京地铁官网、高德/百度地图、北京公交集团等权威渠道的信息。

---

### **4. 整合信息并优化路线推荐**
   - **优先级排序**     - **地铁**:最推荐,因北京南站直达地铁,且天安门地铁站步行即达。
     - **公交**:次选,但需注意线路是否直达或需换乘。
     - **打车**:方便但费用较高,适合行李多或时间紧张的用户。
   - **细节补充**     - 提示地铁换乘时需注意方向(如4号线换乘1号线需选择正确车厢)。
     - 提醒公交可能因路况拥堵,需预留更多时间。
     - 警示天安门周边禁止社会车辆进入,需步行或换乘其他交通方式。

---

### **5. 检查用户潜在需求**
   - **是否携带大件行李**:地铁可能更方便,但需注意电梯和通道宽度。
   - **是否对费用敏感**:地铁费用最低(单程约5元),公交约2元,打车约50-80元。
   - **是否赶时间**:地铁和打车速度较快,公交可能受堵车影响。
   - **是否需要游览周边景点**:推荐结合地铁路线规划,如经过西单、王府井等商圈。

---

### **6. 最终输出结构化路线**
   - 分步骤描述每种交通方式的具体路线、耗时、费用和注意事项。
   - 使用清晰的标题和分点,便于用户快速获取关键信息。
   - 补充温馨提示(如地铁运营时间、天安门安检要求等)。

---

### **示例输出(简化版)**

### **从北京南站到天安门的路线方案**

#### **1. 地铁(推荐,约35分钟)**
- **路线**:北京南站站(4号线大兴线) → 西单站换乘1号线 → 天安门东站/西站。
- **费用**:5元,支持交通卡或手机支付。
- **提示**:地铁1号线天安门站需安检,出站后步行2分钟即到广场。

#### **2. 公交(约40-60分钟)**
- **路线**:北京南站乘特8路 → 天安门广场东站下车。
- **费用**:2元,支持现金或卡支付。
- **提示**:早晚高峰可能堵车,建议提前查实时到站信息。

#### **3. 打车/网约车(约20-30分钟)**
- **路线**:导航至“天安门广场”(周边禁止停车,需步行1公里内)。
- **费用**:约50-80元(含高速费)。
- **提示**:天安门周边禁止社会车辆进入,需下车后步行或换乘公交/地铁。

#### **注意事项**
- **地铁/公交运营时间**:地铁早5:50-23:30,公交早5:30-23:00。
- **天安门安检**:广场入口需安检,节假日建议提前预约。


通过以上步骤,确保提供的路线信息全面、准确且实用,同时覆盖用户可能未明确提到的需求。

可以看到qwen-turbo理解了我们要做什么并且对问题进行了拆解。

然后是知道这个问题和可调用工具的关系 我们继续向qwen-turbo提问

我需要你帮我查询怎么从北京南站去天安门的路线。现在有几个个工具可以用来完成这个任务,分别是:计算器,天气查询,经纬度获取,交通路线查询。请说明要完成这个路线查询任务需要做的事情和使用的工具,以表格形式输出

得到的输出: 以下是完成从北京南站到天安门路线查询任务所需步骤和工具的表格:

步骤需要做的事情使用的工具
1查询北京当天的天气情况,判断是否需要调整出行方式(如雨天避免骑行)。天气查询
2获取北京南站的经纬度坐标。经纬度获取
3获取天安门的经纬度坐标。经纬度获取
4将两地的经纬度输入交通路线查询工具,生成具体路线(如公交、地铁、驾车等)。交通路线查询
5(可选)若需计算不同交通方式的费用或时间差异,使用计算器辅助对比。计算器

可以看到qwen-turbo是知道自己要做什么并且需要调用哪些工具的

结构化输出

我们都知道函数的签名由函数名,入参,返回值三个部分构成,如果我们想调用一个函数,至少我们需要知道函数名和入参什么,这就要求模型要能进行结构化的输出,否则的话,参数都传不对又该如何调用呢?

让我们继续用qwen-turbo举例,我们提出下面的问题:

现在天气查询工具接收两个参数,日期(date string类型),城市名称(city string类型) 当前日期2025年6月23日,城市北京,请输出json结构数据

输出

{
  "date": "2025-06-23",
  "city": "北京"
}

可以看到,模型完成了数据的结构化

In-Context-Learn(上下文学习)

前面模型已经理解了问题,选择了正确的工具,也能够根据入参的要求格式化数据,但是如果,在我们调用某个函数之后,把返回值给到模型,模型把前面的内容忘了,不知道我们拿返回值做什么,岂不是白折腾?所以最后一个基础就是要求模型有进行上下文学习的能力,说白了就是整合前面所有的信息形成最终输出。

这个测试方式比较简单,找一个几百字的新闻发给模型,然后提问就可以了。

FunctionCalling的作用

好,即使前面的都能做到,又有什么用呢?比如,模型在训练好之后,一般是只有已经训练过的知识和能力的,是单机版,问额外的知识或执行新的任务是做不到的,FunctionCalling使模型不再局限于资深的知识和推理,可以和外部世界联动,完成更复杂的任务,比如:

  • 查询检索:补充额外信息,如:RAG,搜索
  • 理解用户输入向外部系统写入信息,如:表单填写
  • 调用外部系统能力,完成实际动作,如:下订单
  • 。。。。

现在我们知道了FunctionCall成立的基础,以及有什么用,那FunctionCall是如何工作的呢?

FunctionCalling是如何工作的

首先我们来看官方版本

官方版本

function-calling-diagram-steps.png

大体过程是这样的

执行步骤

  1. 给出问题及可用工具(function)
  2. 模型返回调用哪个工具(function)
  3. 本地执行模型给出的工具(function)
  4. 将上文所有消息和工具(function)执行结果给到模型
  5. 模型整合所有信息输出

上面是从整体的执行流程角度来讲的,下面我们来看一下从这个过程中不同的角色的角度下又是什么样子的

从角色角度看

function_calling.drawio.png

  1. 工具提供方(绿色)
    1. 需要给我们定义好的工具(function)写注释
    2. 这里的注释类似于元数据,用于告知模型函数的名字,作用,参数都是什么
    3. function需要提供输出
    4. 组合工具输出和诉求方的prompt,向模型发起请求
  2. 诉求方(黄色)
    1. 提供prompt用于告知系统,要做什么
  3. 模型(蓝色+白色)
    1. 将需要要调用的工具和调用方式(函数名称,参数)告知给工具提供方
    2. 给出最终输出

这个过程简单来说就是,有一天你从你老板那接到了一个任务,然后你评估并拆解了一下这个任务,发现你有人来帮你做某件事,恰好是隔壁部门的工作范围,于是你找到隔壁部门,告诉他们你需要他们做某个事情,然后现在有一二三四这几个条件,隔壁部门完成这个任务后把结果给到你,你拿着隔壁部门的产出,结合你老板的要求,再加上你自己的产出整合成最后的输出找你老板交差,大概就是这么个过程。

所以,这里面决定性的一步是什么?隔壁部门资源?不是,你不能平白无故给隔壁部门派活儿吧,你需要隔壁部门的协助,来源于你拆解并评估了任务,所以FunctionCalling最关键的一步,也是第一步就是模型对任务的理解和选择

模型对任务的理解和选择

calling是最终的动作,更关键的是模型的理解和选择,俗话说的好,当任务走到执行哪一步的时候其实已经完成了一半了

也就是一开始和模型交互的时候,除了用户的prompt,我们还需要把可以用的工具信息给到模型,这一步是为了解决以下几个问题:

  1. 让模型能理解prompt中想要的东西和我们提供的工具之间有什么关系
  2. 模型能够对需要做的动作,也就是需要使用的工具进行选择(问题1中已经获取了关系),并且能判断是要执行这个工具
  3. 如果判定要执行工具需要给出执行工具所需要的参数

总结下来其实就是三个词:理解、选择、行动

我们知道大语言模型是概率模型,既然是概率就有不准的时候,那是什么在影响准不准呢? 首先是理解,这个更多是模型的能力问题,这篇文章不涉及,而且很多时候不是应用层开发者可以解决的,我们把问题聚焦在能解决以及效果更好的部分。

选择

影响选择的有以下几个因素:

  1. 诉求的清晰度(prompt),也就是prompt里面有没有写清楚做什么,有哪些工具可以用,这些工具怎么用。好比你去肯德基吃饭,和前台小姐姐说给我来份肯德基。。。
  2. 工具的定义和描述,也就是我们有没有写明白函数的作用,入参是否足够清晰明了,想象一下你拿到了一分都是func_a(a,b,c) func_b(b,c,d)这样的文档,没有任何的注释或说明,别说大模型,神仙也不知道这是干嘛用的
  3. 模型的推理能力,你同事的聪明程度决定了你的沟通成本和他的任务执行效果。。。

作为应用层开发者,我们很大程度上能够影响的是前两个东西,诉求清晰度和工具的定义和描述:

  1. 函数名称是否足够表意以及入参的信息,也就是函数签名的含义要足够清晰明了
  2. prompt对于诉求和行动有没有清晰的定义,比如:使用xxx工具查询xxx事情

行动

影响行动的有两个非常关键的因素,模型的结构化输出能力以及我们对入参的描述是否足够清晰

至于怎么提高模型的结构化输出能力,那是模型层的问题,这里不做展开。而入参描述的问题,其实就是怎么写好注释的问题,也不用细说了吧。

最终输出是可选项

不管是官方给出的流程图,还是我们从角色角度拆分,都有一个最终输出的存在,但其实,这个不是必须的。

如果是对话场景,那当然需要把最终输出给到用户,但是如果是执行完工具后还有其他业务流程呢?比如拿到返回值后去下订单,或者大模型给到结构化输出的参数后,但是后续流程执行的时候需要鉴权,我们需要补充一些参数再去执行对用的工具等等许多场景,就没有必要给出最终输出。

FunctionCalling的实际使用

好的,到这里理论就掰扯完了,我们来上点代码感受一下

第一步:工具决策和调用信息生成

首先我们定义好,需要调什么工具,怎么调用告诉给大模型

import os
import dotenv
dotenv.load_dotenv('.env')

from openai import OpenAI
print(os.environ.get("OPENAI_API_KEY"))
print(os.environ.get("OPENAI_API_BASE"))
client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY"),
    base_url=os.environ.get("OPENAI_API_BASE"),
)

# 给出工具定义
tools = [
    # 每一个列表元素项,就是一个工具定义
    {
        # 类型标注(固定格式)
        "type": "function",
        # 函数定义
        "function": {
            # 函数名称(帮助我们去查找本地的函数在哪里,函数映射ID)
            "name": "get_weather",
            # 函数描述(帮助模型去理解函数的作用,适用场景,可以理解为Prompt的一部分)
            "description": "Get current temperature for provided coordinates in celsius.",
            # 函数依赖参数的定义(帮助模型去理解如果要做参数生成,应该怎么生成)
            "parameters": {
                # 参数形式
                "type": "object", # 对应输出JSON String
                # 参数结构
                "properties": {
                    # 参数名,参数类型
                    "latitude": {"type": "number"},
                    # 参数名,参数类型
                    "longitude": {"type": "number"}
                },
                # 必须保证生成的参数列表(每个元素对应上面properties的参数名)
                # 如果没有这一项,可能会导致原型没有在prompt中获得足量信息从而
                # 没有生成需要的参数
                "required": ["latitude", "longitude"],
                # 是否允许模型自由发挥加入额外参数
                "additionalProperties": True
            },
            # 格式是否严格(默认为True)
            "strict": True
        }
    }
]

# 给出诉求表达
messages = [{"role": "user", "content": "今天上海天气怎么样?"}]


# 发起请求
completion = client.chat.completions.create(
    model="qwen-turbo",
    messages=messages,
    # 把工具定义提交给模型,就已经默认启用了Function Calling
    tools=tools,
)
# print(completion.choices[0].message)
# print(completion.choices[0].message.tool_calls[0].function)
if completion.choices[0].message.tool_calls:
    # funcs = completion.choices[0].message.tool_calls[0].function
    print(completion.choices[0].message.tool_calls[0].function)
    # print(funcs.arguments)
    # print(funcs.name)
else:
    print("No function is called.")

看下输出:

Function(arguments='{"latitude": 31.2304, "longitude": 121.4737}', name='get_weather')

不对啊?你这上面get_weather方法也没定义啊。没错,确实没有定义,其实这里大模型不关心你本地有没有某个方法,你只需要让它相信你有就可以了。。

如果我提一些无关的问题会怎样:

messages = [{"role": "user", "content": "你今天咋样?"}]

输出:

No function is called.

第二步:实际调用

下面我们来真实调用一个方法来看一下效果

import json
function_calling_message = completion.choices[0].message
function_calling = completion.choices[0].message.tool_calls[0]

print("Call Function Name:", function_calling.function.name)
print("Call Function Arguments:", function_calling.function.arguments)


def get_weather(*, latitude:float, longitude:float):
    return {
        "temperature": 23,
        "weather": "Sunny",
        "wind_direction": "South",
        "windy": 2,
    }

functions = {
    "get_weather": get_weather
}

function_result = functions[function_calling.function.name](**json.loads(function_calling.function.arguments))
print(function_result)

输出:

Call Function Name: get_weather
Call Function Arguments: {"latitude":31.2304,"longitude":121.4737}
{'temperature': 23, 'weather': 'Sunny', 'wind_direction': 'South', 'windy': 2}

第三步:获取结果

现在我们已经拿到了prompt和天气查询工具返回的数据,下面让我们生成最终结果

# 必须:让模型知道自己之前给了一个什么指令(包含tool_call_id)
messages.append(function_calling_message)
# 包含了tool_call_id的结果加入消息列
messages.append({
    "role": "tool",
    "tool_call_id": function_calling.id,
    "content": str(function_result),
})
print(messages)

final_result = client.chat.completions.create(
    model="qwen-turbo",
    messages=messages,
    tools=tools,
)
print(final_result.choices[0].message.content)

输出:

[{'role': 'user', 'content': '今天上海天气怎么样?'}, ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_NfWH5ABWprjoGskMpIp0dZ3V', function=Function(arguments='{"latitude":31.2304,"longitude":121.4737}', name='get_weather'), type='function')]), {'role': 'tool', 'tool_call_id': 'call_NfWH5ABWprjoGskMpIp0dZ3V', 'content': "{'temperature': 23, 'weather': 'Sunny', 'wind_direction': 'South', 'windy': 2}"}]

今天上海的天气是晴天,气温为23摄氏度,风向为南,风力为2级。

还有一个问题,如果我函数签名变了,或者模型生成的参数不对怎么办?下面我们来测试一下

error_messages = messages[:1]
print(error_messages)
error_messages.append(function_calling_message)
error_messages.append({
    "role": "tool",
    "tool_call_id": function_calling.id,
    "content": str(TypeError("Key 'latitude' can not be supported any more, please use 'lat' instead.")),
})
print(error_messages)

final_result = client.chat.completions.create(
    model="gpt-4o",
    messages=error_messages,
    tools=tools,
)
print(final_result.choices[0])

输出:

[{'role': 'user', 'content': '今天上海天气怎么样?'}]
[{'role': 'user', 'content': '今天上海天气怎么样?'}, ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_NfWH5ABWprjoGskMpIp0dZ3V', function=Function(arguments='{"latitude":31.2304,"longitude":121.4737}', name='get_weather'), type='function')]), {'role': 'tool', 'tool_call_id': 'call_NfWH5ABWprjoGskMpIp0dZ3V', 'content': "Key 'latitude' can not be supported any more, please use 'lat' instead."}]
Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_OLPgcqlFG7FqU6istBeN3VD7', function=Function(arguments='{"lat":31.2304,"longitude":121.4737}', name='get_weather'), type='function')]))

可以看到它是会自动重试的,但不多。。

封装

上面就是一个完整的FunctionCalling的流程,但是还是存在一些问题,整个流程还是比较长的,而且还存在失败重试的问题,总不能写一次就把代码copy一次吧,下面我们进行简单的封装


import os
import dotenv
dotenv.load_dotenv(dotenv.find_dotenv())

import json
from typing import TypedDict
from openai import OpenAI

class FunctionCallingResult(TypedDict):
    name: str
    arguments: str

class ModelRequestWithFunctionCalling:
    def __init__(self):
        self._client = OpenAI(
            api_key=os.environ.get("OPENAI_API_KEY"),
            base_url=os.environ.get("OPENAI_API_BASE"),
        )
        self._function_infos = {}
        self._function_mappings = {}
        self._messages = []
    
    def register_function(self, *, name, description, parameters, function, **kwargs):
        self._function_infos.update({
            name: {
                "type": "function",
                "function": {
                    "name": name,
                    "description": description,
                    "parameters": parameters,
                    **kwargs
                }
            }
        })
        self._function_mappings.update({ name: function })
        return self

    def reset_messages(self):
        self._messages = []
        return self
    
    def append_message(self, role, content, **kwargs):
        self._messages.append({ "role": role, "content": content, **kwargs })
        print("[Processing Messages]:", self._messages[-1])
        return self
    
    def _call(self, function_calling_result:FunctionCallingResult):
        function = self._function_mappings[function_calling_result.name]
        arguments = json.loads(function_calling_result.arguments)
        return function(**arguments)

    def request(self, *, role="user", content=None,retry_counts=0):
        if role and content:
            self._messages.append({ "role": role, "content": content })
        result = self._client.chat.completions.create(
            model="qwen-turbo",
            messages=self._messages,
            tools=self._function_infos.values(),
        )
        self.append_message(**dict(result.choices[0].message))
        if result.choices[0].message.tool_calls and retry_counts<5:
            for tool_call in result.choices[0].message.tool_calls:
                call_result = self._call(tool_call.function)
                self.append_message("tool", str(call_result), tool_call_id=tool_call.id)
            retry_counts+=1
            return self.request(retry_counts=retry_counts)
        else:
            self.append_message("assistant", result.choices[0].message.content)
            return result.choices[0].message.content

在request方法中我们使用了递归调用,这里有两个原因,一是实际场景中,一个prompt未必只调用一次function,二是处理失败重试问题,并限制重试次数

下面我们使用封装好的类完成联网搜索和数据库查询两个场景

联网搜索

import requests
import os
import json

amap_key = os.getenv("AMAP_KEY")
amap_base_url = os.getenv("AMAP_URL") # 默认是 https://restapi.amap.com/v5


def get_location_coordinate(location, city):
    url = f"{amap_base_url}/place/text?key={amap_key}&keywords={location}&region={city}"
    r = requests.get(url)
    result = r.json()
    if "pois" in result and result["pois"]:
        return result["pois"][0]
    return None


def search_nearby_pois(longitude, latitude, keyword):
    url = f"{amap_base_url}/place/around?key={amap_key}&keywords={keyword}&location={longitude},{latitude}"
    r = requests.get(url)
    result = r.json()
    ans = ""
    if "pois" in result and result["pois"]:
        for i in range(min(3, len(result["pois"]))):
            name = result["pois"][i]["name"]
            address = result["pois"][i]["address"]
            distance = result["pois"][i]["distance"]
            ans += f"{name}\n{address}\n距离:{distance}米\n\n"
    return ans

function_calling_request = ModelRequestWithFunctionCalling()

(
    function_calling_request
        .register_function(
            name="get_location_coordinate",
            description="根据POI名称,获得POI的经纬度坐标",
            parameters={
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "POI名称,必须是中文",
                    },
                    "city": {
                        "type": "string",
                        "description": "POI所在的城市名,必须是中文",
                    }
                },
                "required": ["location", "city"],
            },
            function=get_location_coordinate,
        )
        .register_function(
            name="search_nearby_pois",
            description="搜索给定坐标附近的poi",
            parameters={
                "type": "object",
                "properties": {
                    "longitude": {
                        "type": "string",
                        "description": "中心点的经度",
                    },
                    "latitude": {
                        "type": "string",
                        "description": "中心点的纬度",
                    },
                    "keyword": {
                        "type": "string",
                        "description": "目标poi的关键字",
                    }
                },
                "required": ["longitude", "latitude", "keyword"],
            },
            function=search_nearby_pois,
        )
)
result = function_calling_request.request(content="朝阳的大悦城附近的展览")
print("----------------------\n\n", result)

输出:

[Processing Messages]: {'role': 'assistant', 'content': '', 'refusal': None, 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': [ChatCompletionMessageToolCall(id='call_dba254bc2f984402848217', function=Function(arguments='{"location": "朝阳大悦城", "city": "北京"}', name='get_location_coordinate'), type='function', index=0)]}
[Processing Messages]: {'role': 'tool', 'content': "{'parent': '', 'address': '朝阳北路101号', 'distance': '', 'pcode': '110000', 'adcode': '110105', 'pname': '北京市', 'cityname': '北京市', 'type': '购物服务;商场;购物中心', 'typecode': '060101', 'adname': '朝阳区', 'citycode': '010', 'name': '朝阳大悦城', 'location': '116.519009,39.924544', 'id': 'B000A856LJ'}", 'tool_call_id': 'call_dba254bc2f984402848217'}
[Processing Messages]: {'role': 'assistant', 'content': '', 'refusal': None, 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': [ChatCompletionMessageToolCall(id='call_6b41b135f9644ea0abe8f7', function=Function(arguments='{"keyword": "展览", "latitude": "39.924544", "longitude": "116.519009"}', name='search_nearby_pois'), type='function', index=0)]}
[Processing Messages]: {'role': 'tool', 'content': '朝阳大悦城-朝阳大悦城当季展览\n朝阳北路101号朝阳大悦城(青年路地铁站B东北口旁)\n距离:9米\n\n北京四玖城儿展览有限公司\n国际创展中心2805室\n距离:854米\n\n北京神雕展览展示有限公司\n建外街道建国路29号兴隆家园2号楼2层商201\n距离:1036米\n\n', 'tool_call_id': 'call_6b41b135f9644ea0abe8f7'}
[Processing Messages]: {'role': 'assistant', 'content': '朝阳大悦城附近有以下展览相关信息:\n\n1. **朝阳大悦城-朝阳大悦城当季展览**\n   - 地址:朝阳北路101号朝阳大悦城(青年路地铁站B东北口旁)\n   - 距离:9米\n\n2. **北京四玖城儿展览有限公司**\n   - 地址:国际创展中心2805室\n   - 距离:854米\n\n3. **北京神雕展览展示有限公司**\n   - 地址:建外街道建国路29号兴隆家园2号楼2层商201\n   - 距离:1036米\n\n希望这些信息对您有所帮助!如果需要更多详情,请随时告诉我。', 'refusal': None, 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None}
[Processing Messages]: {'role': 'assistant', 'content': '朝阳大悦城附近有以下展览相关信息:\n\n1. **朝阳大悦城-朝阳大悦城当季展览**\n   - 地址:朝阳北路101号朝阳大悦城(青年路地铁站B东北口旁)\n   - 距离:9米\n\n2. **北京四玖城儿展览有限公司**\n   - 地址:国际创展中心2805室\n   - 距离:854米\n\n3. **北京神雕展览展示有限公司**\n   - 地址:建外街道建国路29号兴隆家园2号楼2层商201\n   - 距离:1036米\n\n希望这些信息对您有所帮助!如果需要更多详情,请随时告诉我。'}
----------------------

 朝阳大悦城附近有以下展览相关信息:

1. **朝阳大悦城-朝阳大悦城当季展览**
   - 地址:朝阳北路101号朝阳大悦城(青年路地铁站B东北口旁)
   - 距离:92. **北京四玖城儿展览有限公司**
   - 地址:国际创展中心2805室
   - 距离:8543. **北京神雕展览展示有限公司**
   - 地址:建外街道建国路29号兴隆家园2号楼2层商201
   - 距离:1036米

希望这些信息对您有所帮助!如果需要更多详情,请随时告诉我。

数据库查询

import sqlite3

database_schema_string = """
CREATE TABLE orders (
    id INT PRIMARY KEY NOT NULL, -- 主键,不允许为空
    customer_id INT NOT NULL, -- 客户ID,不允许为空
    product_id STR NOT NULL, -- 产品ID,不允许为空
    price DECIMAL(10,2) NOT NULL, -- 价格,不允许为空
    status INT NOT NULL, -- 订单状态,整数类型,不允许为空。0代表待支付,1代表已支付,2代表已退款
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间,默认为当前时间
    pay_time TIMESTAMP -- 支付时间,可以为空
);
"""

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

cursor.execute(database_schema_string)

mock_data = [
    (1, 1001, 'TSHIRT_1', 50.00, 0, '2023-09-12 10:00:00', None),
    (2, 1001, 'TSHIRT_2', 75.50, 1, '2023-09-16 11:00:00', '2023-08-16 12:00:00'),
    (3, 1002, 'SHOES_X2', 25.25, 2, '2023-10-17 12:30:00', '2023-08-17 13:00:00'),
    (4, 1003, 'SHOES_X2', 25.25, 1, '2023-10-17 12:30:00', '2023-08-17 13:00:00'),
    (5, 1003, 'HAT_Z112', 60.75, 1, '2023-10-20 14:00:00', '2023-08-20 15:00:00'),
    (6, 1002, 'WATCH_X001', 90.00, 0, '2023-10-28 16:00:00', None)
]

for record in mock_data:
    cursor.execute('''
    INSERT INTO orders (id, customer_id, product_id, price, status, create_time, pay_time)
    VALUES (?, ?, ?, ?, ?, ?, ?)
    ''', record)

conn.commit()

def query_db(query):
    cursor.execute(query)
    return cursor.fetchall()
    
function_calling_request = ModelRequestWithFunctionCalling()

(
    function_calling_request
        .register_function(
            name="query_db",
            description="使用此函数查询业务数据库获取结果,输出的SQL需要能够在Python的sqlite3中执行",
            parameters={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": f"""
                        SQL query extracting info to answer the user's question.
                        The query should be returned in plain text, not in JSON.
                        The query should only contain grammars supported by SQLite.
                        """,
                    }
                },
                "required": ["query"],
            },
            function=query_db,
        )
)

question = "2023年10月总共成交了几笔订单?"

result = function_calling_request.request(
    content=f"""
    问题:{ question },
    数据库元数据信息:{ database_schema_string },
"""
)
print("----------------------\n\n", result)

输出:

[Processing Messages]: {'role': 'assistant', 'content': '', 'refusal': None, 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': [ChatCompletionMessageToolCall(id='call_b85b454123f04a288cf240', function=Function(arguments='{"query": "SELECT COUNT(*) FROM orders WHERE strftime('%Y-%m', create_time) = '2023-10';"}', name='query_db'), type='function', index=0)]}
[Processing Messages]: {'role': 'tool', 'content': '[(4,)]', 'tool_call_id': 'call_b85b454123f04a288cf240'}
[Processing Messages]: {'role': 'assistant', 'content': '2023年10月总共成交了4笔订单。', 'refusal': None, 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None}
[Processing Messages]: {'role': 'assistant', 'content': '2023年10月总共成交了4笔订单。'}
----------------------

 202310月总共成交了4笔订单。

FunctionCalling还可以进行跨模型协作,比如,我们需要另一个具有特殊能力的模型帮忙完成某项任务,这里用使用文心一言4.0版本举例,因为这个版本以上的文心一言默认集成了联网搜索能力

跨模型协作

安装文心SDK

pip install erniebot

封装工具

import erniebot

erniebot.api_type = "aistudio"
erniebot.access_token = os.environ.get("AISTUDIO_ACCESS_TOKEN")

def nl_search(question:str):
    prompt = f"""
基于联网搜索结果回答此问题:{ question }
其他输出要求:答案中的关键信息必须标注精确到内容页面的来源链接
你的回答:
"""
    response = erniebot.ChatCompletion.create(
    model="ernie-4.0",
    messages=[{
        "role": "user",
        "content": prompt,
    }])
    return response.get_result()

调用执行

function_calling_request = ModelRequestWithFunctionCalling()

(
    function_calling_request
        .register_function(
            name="nl_search",
            description="使用此工具,可以用自然语言输入,获得基于网络搜索的事实性结果总结",
            parameters={
                "type": "object",
                "properties": {
                    "question": {
                        "type": "string",
                        "description": "使用自然语言总结用户关注的关键问题",
                    }
                },
                "required": ["question"],
            },
            function=nl_search,
        )
)

question = "今年英伟达GTC大会主要讲了哪些关键信息?"

result = function_calling_request.request(
    content=question,
)
print(result)

总结

Function Calling在大模型应用场景中带来的“质变”

  • 知识层面:从模型自身知识(来源于训练语料)扩展到真实世界知识
  • 行为层面:从“思考模拟器”、“问题应答”扩展到“理解问题-选择行动-发起请求-理解结果-给出回应”
  • 架构层面:让模型不再是一个孤立模块,而是可以融入现有信息系统之中

给软件开发思想带来的冲击:

  • 不是基于“规则”而是基于“世界理解”的调用
  • 接纳没有明确的处理过程带来的输出不确定性(如数据查询)
  • 不走极端:“全盘拒绝”和“全盘接受”都不可取