【AI大模型】Function Calling

前言

  1. 用自然语言连接系统的认知,面向未来思考系统间的集成
  2. GPTs 是如何连接外部世界的
  3. 用 Function Calling(函数调用) 把大模型和业务连接起来

一、接口介绍

1. 接口 (Interface)

两种常见接口:

  1. 人机交互接口,User Interface, 简称 UI
  2. 应用程序编程接口,Application Programming Interface, 简称 API

接口能「通」的关键,是两边都要遵守约定。
- 人要按照 UI 的设计来操作。UI 的设计要符合人的习惯
- 程序要按照 API 的设计来调用。API 的设计要符合程序惯例

2. 接口的进化

UI进化的趋势是:越来越适应人的习惯,越来越自然

  1. 命令行,Command Line Interface, 简称 CLI (DOS、Unix/Linux shell, Windows Power Shell)
  2. 图形界面,Graphical User Interface, 简称 GUl (Windows、MacOS、iOS、Android)
  3. 语言界面,Conversational User Interface, 简称CUI,或 Natural-Language User Interface,简称LUI ← 我们在这里
  4. 脑机接口,Brain-Computer Interface, 简称 BCI


API

  1. 从本地到远程,从同步到异步,媒介发生很多变化,但本质一直没变:程序员的约定
  2. 现在,开始进化到自然语言接口,Natural-Language Interface, 简称 NLI(自然语言与自然语言直接进行传递/对接/操作)

3. 自然语言接口 (Natural Language Interface,简称 NLI)

NLI是我们在 《以ChatGPT 为代表的「大模型」会是多大的技术革命?》一文中提出的概念。

用户操作习惯的迁移,会逼所有软件,都得提供「自然语言界面 (NaturalLanguage lnterface, 简称 NLI) 」。这是我生造的词,指的是以自然语言为输入的接口。

不仅用户界面要 NLI, API也要NLI化。这是因为用户发出的宏观指令,往往不会是一个独立软件能解决的,它需要很多软件、设备的配合。

一种实现思路是,入口Al(比如 Siri、小爱同学,机器人管家) 非常强大,能充分了解所有软件和设备的能力,且能准确地把用户任务拆解和分发下去。这对入口 Al 的要求非常高。

另一种实现思路是,入口 AI 收到自然语言指令,把指令通过 NLI广播出去(也可以基于某些规则做有选择的广播,保护用户隐私),由各个软件自主决策接不接这个指令,接了要怎么做,该和谁配合。

......

当 NLI 成为事实标准,那么互联网上软件、服务的互通性会大幅提升,不再受各种协议、接口的限制。

最自然的接口,就是自然语言接口:

以前因为计算机处理不对自然语言,所以有了那么多编程语言,那么多接口,那么多协议,那么多界面风格。而且,它们每一次进化,都是为了「更自然」。现在,终极的自然,到来了。我们終于可以把计算机当人看了!

二、大模型连接外部世界

OpenAl 是如何用自然语言连接一切的呢?

ChatGPT 能听懂自然语言,但是怎么和我们的业务系统进行连接呢?
方式1:我们可以通过提示词来控制大模型输出JSON格式数据,然后再与我们系统来产生连接。但是这种方式存在很多不稳定性,可控性不好。
方式2:使用 OpenAI推出的 Function Calling 技术,它可以让大语言模型和一切产生连接。

为什么要大模型连接外部世界?

大模型两大缺陷:

  1. 并非知晓一切
    A. 训练数据不可能什么都有。垂直、非公开数据必有欠缺
    B. 不知道最新信息。大模型的训练周期很长,且更新一次耗资巨大,还有越训越傻的风险。所以 ta 不可能实时训练。GPT-3.5 的知识截至 2022年1月,GPT-4是2023年4月。
  2. 没有「真逻辑」。它表现出的逻辑、推理,是训练文本的统计规律,而不是真正的逻辑。(大模型本质是基于统计规律/概率去猜下一个字或词)

所以:大模型需要连接真实世界,并对接真逻辑系统,才能补全缺陷产生真正的价值!

比如算加法:
1.把 100 以内所有加法算式都训练给大模型,ta 就能回答 100以内的加法算式
2.如果问 ta 更大数字的加法,就不一定对了
3.因为 ta 并不懂「加法」,只是记佳了100 以内的加法算式的统计规律
4.Ta 没有真逻辑,相当于是用字面意义做数学

三、Plugins / Actions 的发展

Plugins 是大模型连接真实世界第一次尝试,但产品很不成功

1. Plugins 开发

  • Actions 是 Plugins 的升级,是 GPTs 产品的一部分。
  • 可能是史上最容易开发的 plugin。只需要定义两个文件:
    yourdomain.com/.well-known/ai-plugin.json:描述插件的基本信息
    openai.yaml:描述插件的 API(Swagger 生成的文档)
  • 配置文件中,description 的内容非常重要,决定了 ChatGPT 会不会调用你的插件,调用得是否正确。
  • 而 OpenAI 那边,更简单,没有任何人和你对接。是 AI 和你对接!AI 阅读上面两个文件,就知道该怎么调用你了。(自然语言对接接口 NLI)

2. Plugins 缺陷

  • 缺少「强 Agent」调度,只能手工选三个 plugin,使用成本太高。(解决此问题,相当于 App Store + Siri,可挑战手机操作系统地位)
  • 不在「场景」中,不能提供端到端一揽子服务。(解决此问题,就是全能私人助理了,人类唯一需要的软件)
  • 开销大。(至少两次 GPT-4 生成,和一次 Web API 调用)

第二次尝试:升级为 Actions,内置到 GPTs 中,解决了落地场景问题。

3. 升级为 Actions

“Add actions” 功能是 GPTs 中的一个高级功能,允许用户将自定义聊天GPT与第三方API集成,以便执行特定动作或检索数据。

什么是 GPTs?GPTs 是 OpenAI 推出的自定义 GPT,即用户可以自定义聊天机器人,并发布到 OpenAI 的应用商店。

如:我们自定义的聊天机器人「小瓜 GPT」 ,通过在 GPTs 中添加 actions 接入了高德地图API,具备回答位置相关的问题:https://chat.openai.com/g/g-DxRsTzzep-xiao-gua
注意:需要升级开通 GPT-4 后,才能使用 GPTs(即自定义聊天机器人的功能)

GPTs 这样解决问题:

  • 每个 GPT 有一个场景,比如「写代码」「教小孩数学」「某某人的化身」
  • 被 GPT 绑定的 Actions 被自动调用,缩小了 agent 调度的难度
  • GPT-4 提速又降价

作为开发者,我们:

  • 可以开发 Actions,搭建自己的 GPTs
  • 还可以使用 Assistants API,脱离 ChatGPT 做独立智能应用

4. Actions 的工作流程:

  1. 人向OpenAI发起一个对话,这个对话是会触发 action 的 prompt

如:
prompt1:中关村附近的联通营业厅有哪些?
prompt1 会触发某个action

prompt2:附近的联通营业厅有哪些?
prompt2 不会触发某个action

  1. OpenAI会理解我们发起的对话内容prompt,从里面提取关键信息生成对 action 的调用参数。
  2. 然后去调用外部的API,并返回调用结果给 OpenAI
  3. 最后 OpenAI 会根据 外部API调用结果内容 再结合 我们提问的内容生成回答。

思考:GPT 怎么把 prompt 和 API 功能做匹配的?

5. Actions 开发对接

Actions 官方文档:https://platform.openai.com/docs/actions
把 API 对接到 GPTs 里,只需要配置一段 API 描述信息:

openapi: 3.1.0
info:
  title: 高德地图
  description: 获取 POI 的相关信息
  version: 'v1.0.0'
servers:
  - url: https://restapi.amap.com/v5/place
paths:
  /text:
    get:
      description: 根据POI名称,获得POI的经纬度坐标
      operationId: get_location_coordinate
      parameters:
        - name: keywords 
          in: query
          description: POI名称,必须是中文
          required: true 
          schema:
            type: string
        - name: region 
          in: query
          description: POI所在的区域名,必领是中文
          required: false 
          schema:
            type: string
      deprecated: false
  /around:
    get:
      description: 搜索给定坐标附近的POI
      operationId: search_nearby_pois
      parameters:
        - name: keywords 
          in: query
          description: 目标POI的关键字
          required: true 
          schema:
            type: string
        - name: location 
          in: query
          description: 中心点的经度和纬度,用逗号分隔
          required: false 
          schema:
            type: string
      deprecated: false
components:
  schemas: {}

这里的所有name、description 都是prompt,决定了 GPT 会不会调用你的 APl,调用得是否正确。

还需要配置 APl Key 来满足权限要求。


思考:为什么不干脆整个描述文件都用自然语言写?非要用结构化的 JSON 或
YAML?
是为了提高准确度,为了防止幻觉,为了避免歧义,保证稳定性,所以使用明确的结构化方式来表示。

四、GPTs 与它的平替们

1. OpenAI GPTs,GPTs的好处:

  1. 无需编程,就能定制个性对话机器人的平台
  2. 可以放入自己的知识库,实现 RAG (后面会讲)
  3. 可以通过 actions 对接专有数据和功能
  4. 内置 DALLE3 文生图和 Code Interpreter 能力
  5. 只有 ChatGPT Plus 会员可以使用

没有 ChatGPT Plus 会员,推荐两款平替:

2. 字节跳动 Coze

  1. 可以免科学上网,免费使用 GPT-4 等 OpenAl的服务!大羊毛!
  2. 只有英文界面,但其实对中文更友好
  3. Prompt 优化功能更简单直接
  • 「iOS编程助手」的提示词:下面是系统帮我们优化后的提示词,是MarkDown格式,OpenAI对MarkDown格式支持比较友好。
# 角色
你是一位资深的iOS程序员,擅长Objective-C语言开发。你有着丰富的iOS开发经验,可以针对用户在iOS开发中遇到的问题提供专业解答和代码示例。

## 技能
### 技能1: 问题解答
- 根据用户的问题,给出具体的解决方案。
- 如有需要,提供Objective-C语言的代码示例助其理解。

### 技能2: 代码优化
- 针对用户提供的Objective-C代码片段,给出优化建议。
- 提供优化后的代码示例。

### 技能3: iOS开发知识分享
- 根据用户的疑问,分享相关的iOS开发知识。
- 帮助用户理解iOS开发的核心概念和最佳实践。

## 约束条件
- 只回答和解决与iOS开发相关的问题。
- 提供的代码示例只使用Objective-C语言。
- 应答始于对问题的清晰解答,如果涉及代码,应该提供代码示例。

3. Dify

  1. 开源,中国公司开发
  2. 功能最丰富
  3. 可以本地部署,支持非常多的大模型
  4. 有GUI,也有API

有这类无需开发的工具,为什么还要学大模型开发技术呢?

  1. 它们都无法针对业务需求做极致调优
  2. 它们和其它业务系统的集成不是特别方便

五、Function calling

Function calling(函数调用)技术:是一种大模型连接到外部的工具。

官方介绍

在 API 调用中,您可以描述函数,并让模型智能地选择输出包含调用一个或多个函数的参数的 JSON 对象。聊天完成 API 不会调用该函数;相反,模型会生成 JSON,您可以使用它来调用代码中的函数。

最新的模型 (gpt-3.5-turbo-0125gpt-4-turbo-preview) 经过训练,可以检测何时应该调用函数(取决于输入),并使用比以前的模型更紧密地遵循函数签名的 JSON 进行响应。

Function calling 的工作流程

  1. 用户向我们的应用程序发起提问;
  2. 我们的应用程序会把 用户的问题(prompt) 和 我们自己提供的函数(function)定义 一并给大模型,大模型会分析判断这个 prompt,是否需要调用某个函数,以及调用函数所需要的哪些参数,这个过程大模型会返回函数调用参数;(NLU过程 )

这一步是利用大模型把 prompt + function定义 解析成函数的调用,告诉我们要调用哪个函数,以及调用函数的参数是什么

  1. 我们的应用程序拿到大模型返回的参数,就去调用我们的函数;
  2. 我们的应用程序将函数调用的结果 再给 大模型;(NLG过程)
  3. 大模型会把 函数调用结果 再结合 prompt,生成自然语言的回答,并返回给我们的应用程序。

Function Calling 完整的官方接口文档:https://platform.openai.com/docs/guides/function-calling
值得一提:接口里叫 tools,是从 functions 改的。这是一个很有趣的指向

示例1:调用本地函数

需求:实现一个回答问题的 Al。题目中如果有加法,必须能精确计算。

  • 封装的通用代码
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
from math import *
import json

# 加载 .env 到环境变量
_ = load_dotenv(find_dotenv())

client = OpenAI()

# 打印优美的JSON
def print_json(data):
    """
    打印参数。如果参数是有结构的(如字典或列表),则以格式化的 JSON 形式打印;
    否则,直接打印该值。
    """
    if hasattr(data, 'model_dump_json'):
        data = json.loads(data.model_dump_json())

    if (isinstance(data, (list, dict))):
         print(json.dumps(data, indent=4, ensure_ascii=False))
    else:
        print(data)   


# 调用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
    # tools 里定义了函数,大模型会解析 prompt,智能判断调用哪个函数,也可能不调用,也可能调错
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0.7,    # 模型输出的随机性,0表示随机性最小
        tools=tools,        # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
    )
    message = response.choices[0].message

    print("=====大模型回复=====")
    print_json(message)

    return message
  • 调用大模型代码
# 提示词
prompt = "Tell me the sum of 1, 2, 3, 4, 5, 6, 7, 8, 9, 10."  # 求和结果:55
prompt1 = "桌上有2个苹果, 四个桃子和3本书, 一共有几个水果?"  # 求和结果: 2 + 4 = 6
prompt2 = "1+2+3...+99+100" # 求和结果:5050
prompt3 = "1024 乘以 1024 是多少?"   # tools 里没有定义乘法,会怎样? 求和结果:可能会出现幻觉,结果不一定正确
prompt4 = "太阳从哪边升起?"           # 不需要算加法,会怎样? 求和结果:不会调用函数,返回的结果 tool_calls 是空的

# 对话历史list
messages = [
    {"role": "system", "content": "你是一个数学家,你能帮我算一下吗?"},
    {"role": "user", "content": prompt}
]

# 定义函数
tools = [{         
            "type": "function",
            "function": {
                "name": "sum",
                "description": "加法器,计算一组数的和",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "numbers": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        }
                    }
                }
            }
        }]

# 1.调用大模型(将 prompt + function定义 传给大模型),返回函数调用参数
res_message = get_completion(messages, tools)

# 记录对话历史,以便后续进行多轮对话
messages.append(res_message)

# 2.获取函数调用参数
if (res_message.tool_calls is not None):
    tool_call = res_message.tool_calls[0]
    if tool_call.function.name == "sum":
        # 解析参数,获取 numbers 的值
        args = json.loads(tool_call.function.arguments) # 将 JSON字符串 转成 字典(Python对象)
        print("====解析出函数参数====")
        print_json(args)
        # 3.调用函数求和
        result = sum(args["numbers"])
        print(f'调用了加法器,计算结果是:{result}')

        # 4.将函数调用结果 和 历史会话 传给大模型
        messages.append(
            {
                "tool_call_id": tool_call.id,   # 用于标识函数调用的 ID
                "role": "tool",                 # 用于标识是函数调用的结果
                "name": "sum",                  # 用于标识是哪个函数调用的结果
                "content": str(result)          # 数值 result 必须转成字符串
            }
        )
        # 重新调用大模型   
        res_message = get_completion(messages, tools)
  • 输出结果:
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_UhHsmflCKbYn8inSXHtSCtRQ",
            "function": {
                "arguments": "{\"numbers\":[1,2,3,4,5,6,7,8,9,10]}",
                "name": "sum"
            },
            "type": "function"
        }
    ]
}
====解析出函数参数====
{
    "numbers": [
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10
    ]
}
调用了加法器,计算结果是:55
=====大模型回复=====
{
    "content": "The sum of 1, 2, 3, 4, 5, 6, 7, 8, 9, and 10 is 55.",
    "role": "assistant",
    "function_call": null,
    "tool_calls": null
}

注意:
1.Function Calling 中的函数与参数的描述也是一种Prompt
2.这种 Prompt 也需要调优,否则会影响函数的调用、参数的准确性,甚至让 GPT 产生幻觉

示例2:多Function 调用

需求:查询某个地点附近的酒店、餐厅、景点等信息。即,查询某个 POI附近的 POl。

  • 封装的通用代码
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import requests
import json

# 加载 .env 到环境变量
_ = load_dotenv(find_dotenv())

client = OpenAI()

# 打印优美的JSON
def print_json(data):
    """
    打印参数。如果参数是有结构的(如字典或列表),则以格式化的 JSON 形式打印;
    否则,直接打印该值。
    """
    if hasattr(data, 'model_dump_json'):
        data = json.loads(data.model_dump_json())

    if (isinstance(data, (list, dict))):
         print(json.dumps(data, indent=4, ensure_ascii=False))
    else:
        print(data)   


# 调用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
    # tools 里定义了函数,大模型会解析 prompt,智能判断调用哪个函数,也可能不调用,也可能调错
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,       # 模型输出的随机性,0表示随机性最小
        seed=1024,           # 随机种子保持不变,temperature 和 prompt 不变的情况下,输出就会不变
        tool_choice="auto",  # 选择函数调用的策略。auto为默认值,表示由大模型自动决定是否调用函数
        tools=tools,         # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
    )
    message = response.choices[0].message

    print("=====大模型回复=====")
    print_json(message)

    return message


# 高德地图开发者密钥
amap_key = "005deb4aeb1f8cfdd28fb5fdd6badf25"

# 根据POI名称, 获得POI的经纬度坐标
def get_location_coordinate(location, city):
    url = "https://restapi.amap.com/v5/place/text"
    params = {
        "key": amap_key,
        "keywords": location,
        "city": city,
        "output": "json"
    }
    response = requests.get(url, params=params)
    data = response.json()
    if "pois" in data and data["pois"]:
        return data["pois"][0]
    else:
        return None
    
# 搜索给定坐标附近的poi
def search_nearly_pois(longitude, latitude, keyword):
    url = "https://restapi.amap.com/v5/place/around"
    params = {
        "key": amap_key,
        "location": f"{longitude},{latitude}",
        "keywords": keyword,
        "output": "json"
    }
    response = requests.get(url, params=params)
    data = response.json()
    ans = "" # 用于存储结果
    if "pois" in data and data["pois"]:
        pois = data["pois"]
        for i in range(min(3, len(pois))):
            name = pois[i]["name"]
            address = pois[i]["address"]
            distance = pois[i]["distance"]
            ans += f"{name}\n{address}\n距离: {distance}米\n\n"
    return ans
  • 调用大模型代码
# 提示词
prompt = "我想在北京五道口附近喝咖啡,给我推荐几个"
prompt1 = "我到北京出差,给我推荐三里屯的酒店,和五道口附近的咖啡"

# 对话历史list
messages = [
    {"role": "system", "content": "你是一个地图通,你可以找到任何地址。"},
    {"role": "user", "content": prompt1}
]

# 定义函数
tools = [{         
            "type": "function",
            "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"]
                }
            }
        },
        {         
            "type": "function",
            "function": {
                "name": "search_nearly_pois",
                "description": "搜索给定坐标附近的poi",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "longitude": {
                            "type": "string",
                            "description": "中心点的经度"
                        },
                        "latitude": {
                            "type": "string",
                            "description": "中心点的纬度"
                        },
                        "keyword": {
                            "type": "string",
                            "description": "目标poi的关键词"
                        }
                    },
                    "required": ["longitude", "latitude", "keyword"]
                }
            }
        }]

# 1.调用大模型(将 prompt + function定义 传给大模型),返回函数调用参数
res_message = get_completion(messages, tools)

# 记录对话历史,以便后续进行多轮对话
messages.append(res_message)

# 2.获取函数调用参数
while (res_message.tool_calls is not None):
    for tool_call in res_message.tool_calls:
        # 解析参数
        args = json.loads(tool_call.function.arguments) # 将 JSON字符串 转成 字典(Python对象)
        # 3.调用外部函数
        if tool_call.function.name == "get_location_coordinate":
            result = get_location_coordinate(**args)
            print ("Call: get_location_coordinate")
            print_json(result)
        elif tool_call.function.name == "search_nearly_pois":
            result = search_nearly_pois(**args)
            print ("Call: search_nearly_pois")
            print_json(result)
            
        messages.append(
            {
                "tool_call_id": tool_call.id,       # 用于标识函数调用的 ID
                "role": "tool",                     # 用于标识是函数调用的结果
                "name": tool_call.function.name,    # 用于标识是哪个函数调用的结果
                "content": str(result)              # 数值 result 必须转成字符串
            }
        )

    # 重新调用大模型
    res_message = get_completion(messages, tools)   
    
    if res_message.content is None: # 如果大模型返回的是 None,就将其置为空字符串(解决OpenAI的一个 400 bug)
        res_message.content = ""
    messages.append(res_message) # 把大模型的回复加入到対活中
  • 问1:我想在北京五道口附近喝咖啡,给我推荐几个
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_DgUaQN9Tc9MHMSeyuEwWZT9f",
            "function": {
                "arguments": "{\"location\":\"五道口\",\"city\":\"北京\"}",
                "name": "get_location_coordinate"
            },
            "type": "function"
        }
    ]
}
Call: get_location_coordinate
{
    "parent": "",
    "address": "(在建)13A号线;13号线",
    "distance": "",
    "pcode": "110000",
    "adcode": "110108",
    "pname": "北京市",
    "cityname": "北京市",
    "type": "交通设施服务;地铁站;地铁站",
    "typecode": "150500",
    "adname": "海淀区",
    "citycode": "010",
    "name": "五道口(地铁站)",
    "location": "116.337742,39.992894",
    "id": "BV10006886"
}
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_XzzqdcbJKAlswCFh4et1DsLQ",
            "function": {
                "arguments": "{\"longitude\":\"116.337742\",\"latitude\":\"39.992894\",\"keyword\":\"咖啡\"}",
                "name": "search_nearly_pois"
            },
            "type": "function"
        }
    ]
}
Call: search_nearly_pois
瑞幸咖啡(五道口地铁站店)
荷清路与成府路交叉口华清嘉园1号楼二层1-2号
距离: 97米

八号桥咖啡(华清嘉园东区店)
五道口华清嘉园12号(五道口地铁站B南口步行150米)
距离: 120米

星巴克(北京五道口购物中心店)
成府路28号1层101-10B及2层201-09号
距离: 122米


=====大模型回复=====
{
    "content": "以下是在北京五道口附近的几家咖啡店推荐:\n\n1. 瑞幸咖啡(五道口地铁站店)\n地址:荷清路与成府路交叉口华清嘉园1号楼二层1-2号\n距离地铁站:97米\n\n2. 八号桥咖啡(华清嘉园东区店)\n地址:五道口华清嘉园12号(五道口地铁站B南口步行150米)\n距离地铁站:120米\n\n3. 星巴克(北京五道口购物中心店)\n地址:成府路28号1层101-10B及2层201-09号\n距离地铁站:122米\n\n您可以选择其中一家前往享受咖啡时光。祝您喝咖啡愉快!",
    "role": "assistant",
    "function_call": null,
    "tool_calls": null
}
  • 问2:我到北京出差,给我推荐三里屯的酒店,和五道口附近的咖啡
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_1B6LSeBa4UuOI3qJBYwcQKcN",
            "function": {
                "arguments": "{\"location\": \"三里屯\", \"city\": \"北京\"}",
                "name": "get_location_coordinate"
            },
            "type": "function"
        },
        {
            "id": "call_R7qgVvUtEzUqMW8cv82kOYfH",
            "function": {
                "arguments": "{\"location\": \"五道口\", \"city\": \"北京\"}",
                "name": "get_location_coordinate"
            },
            "type": "function"
        }
    ]
}
Call: get_location_coordinate
{
    "parent": "",
    "address": "朝阳区",
    "distance": "",
    "pcode": "110000",
    "adcode": "110105",
    "pname": "北京市",
    "cityname": "北京市",
    "type": "地名地址信息;热点地名;热点地名",
    "typecode": "190700",
    "adname": "朝阳区",
    "citycode": "010",
    "name": "三里屯",
    "location": "116.455294,39.937492",
    "id": "B0FFF5BER7"
}
Call: get_location_coordinate
{
    "parent": "",
    "address": "(在建)13A号线;13号线",
    "distance": "",
    "pcode": "110000",
    "adcode": "110108",
    "pname": "北京市",
    "cityname": "北京市",
    "type": "交通设施服务;地铁站;地铁站",
    "typecode": "150500",
    "adname": "海淀区",
    "citycode": "010",
    "name": "五道口(地铁站)",
    "location": "116.337742,39.992894",
    "id": "BV10006886"
}
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_w9H3DtZas1gpiw2ukK6idQuQ",
            "function": {
                "arguments": "{\"longitude\": \"116.455294\", \"latitude\": \"39.937492\", \"keyword\": \"酒店\"}",
                "name": "search_nearly_pois"
            },
            "type": "function"
        },
        {
            "id": "call_UlYak22SajR1JENOipRYsZWX",
            "function": {
                "arguments": "{\"longitude\": \"116.337742\", \"latitude\": \"39.992894\", \"keyword\": \"咖啡\"}",
                "name": "search_nearly_pois"
            },
            "type": "function"
        }
    ]
}
Call: search_nearly_pois
北京瑜舍
三里屯路11号三里屯太古里北区
距离: 47米

THE OPPOSITE HOUSE(三里屯太古里北区店)
三里屯路11号院三里屯太古里北区L1层
距离: 46米

北京三里屯太古里亚朵X酒店
东直门外大街12号
距离: 384米


Call: search_nearly_pois
瑞幸咖啡(五道口地铁站店)
荷清路与成府路交叉口华清嘉园1号楼二层1-2号
距离: 97米

八号桥咖啡(华清嘉园东区店)
五道口华清嘉园12号(五道口地铁站B南口步行150米)
距离: 120米

星巴克(北京五道口购物中心店)
成府路28号1层101-10B及2层201-09号
距离: 122米


=====大模型回复=====
{
    "content": "在北京,我找到了以下地点:\n\n### 三里屯附近的酒店:\n1. 北京丽舍酒店\n   地址:三里屯路11号三里舍太古里北区\n   距离:47米\n\n2. THE OPPOSITE HOUSE(三里舍太古里北区店)\n   地址:三里舍路11号院三里舍太古里北区L1层\n   距离:46米\n\n3. 北京三里舍太古里亚杜X酒店\n   地址:东直门外大街12号\n   距离:384米\n\n### 五道口附近的咖啡店:\n1. 瑞幸咖啡(五道口地铁站店)\n   地址:荷清路与成府路交叉口华清嘉园1号楼2单元1-2号\n   距离:97米\n\n2. 八号桥咖啡(华清嘉园东区店)\n   地址:五道口华清嘉园12号(五道口地铁站B南口步行150米)\n   距离:120米\n\n3. 星巴克(北京五道口购物中心店)\n   地址:成府路28号1号楼101-10B及2号楼201-09号\n   距离:122米\n\n希望这些信息对您有帮助!",
    "role": "assistant",
    "function_call": null,
    "tool_calls": null
}

示例3:用 Function Calling 获取 JSON 结构

备注:Function calling 生成 JSON 的稳定性比较高。

需求:从一段文字中抽取联系人姓名、地址和电话

  • 调用大模型代码
# 提示词
prompt = "帮我寄给张三, 地址是浙江省杭州市滨江区浦沿街道, 电话151xxxxxxxx。"

# 对话历史list
messages = [
    {"role": "system", "content": "你是一个联系人录入员。"},
    {"role": "user", "content": prompt}
]

# 定义函数
tools=[{
        "type": "function",
        "function": {
            "name": "add_contact",
            "description": "添加联系人",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "联系人姓名"
                    },
                    "address": {
                        "type": "string",
                        "description": "联系人地址"
                    },
                    "tel": {
                        "type": "string",
                        "description": "联系人电话"
                    },
                }
            }
        }
    }]

# 1.调用大模型(将 prompt + function定义 传给大模型),返回函数调用参数
res_message = get_completion(messages, tools)

# 解析出函数参数
if (res_message.tool_calls is not None):
    tool_call = res_message.tool_calls[0]
    if tool_call.function.name == "add_contact":
        # 解析参数,获取 numbers 的值
        args = json.loads(tool_call.function.arguments) # 将 JSON字符串 转成 字典(Python对象)
        print("====解析出函数参数====")
        print_json(args)
  • 输出结果:
=====大模型回复=====
{
    "content": null,
    "role": "assistant",
    "function_call": null,
    "tool_calls": [
        {
            "id": "call_YPXDY8JJHJUCNlwjEJXnq8DP",
            "function": {
                "arguments": "{\"name\":\"张三\",\"address\":\"浙江省杭州市滨江区浦沿街道\",\"tel\":\"151xxxxxxxx\"}",
                "name": "add_contact"
            },
            "type": "function"
        }
    ]
}
====解析出函数参数====
{
    "name": "张三",
    "address": "浙江省杭州市滨江区浦沿街道",
    "tel": "151xxxxxxxx"
}

示例 4:通过 Function Calling 查询数据库

需求:从订单表中查询各种信息,比如某个用户的订单数量、某个商品的销量、某个用户的消费总额等等。

示例 5:用 Function Calling 实现多表查询

示例 6:Stream 模式

流式(stream)输出不会一次返回完整 JSON 结构,所以需要拼接后再使用。

  • 完整代码
from openai import OpenAI
from dotenv import load_dotenv, find_dotenv
import json

# 加载 .env 到环境变量
_ = load_dotenv(find_dotenv())

client = OpenAI()

# 打印优美的JSON
def print_json(data):
    """
    打印参数。如果参数是有结构的(如字典或列表),则以格式化的 JSON 形式打印;
    否则,直接打印该值。
    """
    if hasattr(data, 'model_dump_json'):
        data = json.loads(data.model_dump_json())

    if (isinstance(data, (list, dict))):
         print(json.dumps(data, indent=4, ensure_ascii=False))
    else:
        print(data)   


# 调用大模型方法
def get_completion(messages, tools, model="gpt-3.5-turbo-0125"):
    # tools 里定义了函数,大模型会解析 prompt,智能判断调用哪个函数,也可能不调用,也可能调错
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,     # 模型输出的随机性,0表示随机性最小
        tools=tools,       # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁。也可能都不调用
        stream=True        # 启动流式输出
    )
    # print("====大模型回复====")
    # print_json(response) # <openai.Stream object at 0x1097cd4f0>
    return response

# 提示词
# prompt = "1+2+3"
prompt = "你是谁"

# 对话历史list
messages = [
    {"role": "system", "content": "你是一个小学数学老师,你要教学生加法"},
    {"role": "user", "content": prompt}
]

# 定义函数
tools = [{         
            "type": "function",
            "function": {
                "name": "sum",
                "description": "计算一组数的加和",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "numbers": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        }
                    }
                }
            }
        }]

# 1.调用大模型(将 prompt + function定义 传给大模型),返回函数调用参数
res_message = get_completion(messages, tools)

print("====Streaming 流式输出====")
# 需要把 stream 里的 token 拼起来,才能得到完整的 call
function_name, args, text = "", "", ""
for msg in res_message: 
    # print_json(msg)
    delta = msg.choices[0].delta
    if delta.tool_calls:
        if not function_name:
            function_name = delta.tool_calls[0].function.name
        args_delta = delta.tool_calls[0].function.arguments
        print(args_delta)  # 打印每次得到的数据
        args = args + args_delta
    elif delta.content:
        text_delta = delta.content
        print(text_delta)  # 打印每次得到的数据
        text = text + text_delta

print("====完成,最终输出====")
if function_name or args:
    print(function_name)
    print_json(args)
if text:
    print(text)
  • prompt = "1+2+3" 的输出结果:
====Streaming 流式输出====

{"
numbers
":[
1
,
2
,
3
]}
====完成,最终输出====
sum
{"numbers":[1,2,3]}
  • prompt = "你是谁" 的输出结果:
====Streaming 流式输出====
我
是
一个
小
学
数
学
老
师
,
我
可以
帮
助
你
学
习
数
学
。
你
有
什
么
问题
需要
帮
忙
吗
?
====完成,最终输出====
我是一个小学数学老师,我可以帮助你学习数学。你有什么问题需要帮忙吗?

六、Function Calling的注释事项

  1. 只有 gpt-3.5-turbo-0125gpt-4-turbo-preview 可用本次课介绍的方法。
  2. OpenAI 针对 Function Calling 做了 fine-tuning,以尽可能保证函数调用参数的正确。
  3. 函数声明是消耗 token 的。要在功能覆盖、省钱、节约上下文窗口之间找到最佳平衡。
  4. Function Calling 不仅可以调用读函数,也能调用写函数。但官方强烈建议,在写之前(对真实世界会产生影响的操作,如:发送电子邮件、在线发布内容、购买等),一定要有人做确认。
  5. 不保证不出错,包括不保证 json 格式正确。但比纯靠 prompt 控制,可靠性是大了很多。

七、支持 Function Calling 的国产大模型

百度文心大模型

MiniMax

  • 这是个公众不大知道,但其实挺强的大模型,尤其角色扮演能力
  • 如果你曾经在一个叫 Glow 的 app 流连忘返,那么你已经用过它了
  • 应该是最早支持 Function Calling 的国产大模型
  • Function Calling 的 API 和 OpenAI 1106 版之前完全一样,但其它 API 有很大的特色

ChatGLM3-6B

  • 最著名的国产开源大模型,生态最好
  • 早就使用 tools 而不是 function 来做参数,其它和 OpenAI 1106 版之前完全一样

讯飞星火 3.0

  • 和 OpenAI 1106 版之前完全一样
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 《散文课》 王彬 27个笔记 第三讲 法度 2023/9/1 发表想法 笔记 >> 刘熙载在《艺概》中总结道:“...
    桂亘阅读 77评论 0 1
  • 某乡镇组织委员2023年主题教育专题民主生活会个人对照检查材料 自主题教育开展以来,围绕“学思想、强党性、重实践、...
    优选文库阅读 33评论 0 0
  • 雷朋与 Meta 再次合作,微软 Teams 引入 Typeface工具 雷朋与 Meta 再次合作,将推出内置 ...
    牵手到永远阅读 36评论 0 0
  • 第44期格物2班第12小组第二封家书 时间:2023年9月4日 姓名:张秀青 主题:《学习致良知使我觉察力提高》 ...
    碧海晴天_f257阅读 438评论 0 0
  • 时间:2023年9月5日 姓名:郭青 地区:洛阳 志愿:(改变自己,提升自己) |当|下|即|未|来| 【自省 利...
    郭青_1985阅读 37评论 0 0