【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 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
    )
    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 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
    )
    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 描述函數??梢远x多個。由大模型決定調用誰。也可能都不調用
        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 版之前完全一樣
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 《散文課》 王彬 27個筆記 第三講 法度 2023/9/1 發表想法 筆記 >> 劉熙載在《藝概》中總結道:“...
    桂亙閱讀 98評論 0 1
  • 某鄉鎮組織委員2023年主題教育專題民主生活會個人對照檢查材料 自主題教育開展以來,圍繞“學思想、強黨性、重實踐、...
    優選文庫閱讀 48評論 0 0
  • 雷朋與 Meta 再次合作,微軟 Teams 引入 Typeface工具 雷朋與 Meta 再次合作,將推出內置 ...
    牽手到永遠閱讀 41評論 0 0
  • 第44期格物2班第12小組第二封家書 時間:2023年9月4日 姓名:張秀青 主題:《學習致良知使我覺察力提高》 ...
    碧海晴天_f257閱讀 502評論 0 0
  • 時間:2023年9月5日 姓名:郭青 地區:洛陽 志愿:(改變自己,提升自己) |當|下|即|未|來| 【自省 利...
    郭青_1985閱讀 53評論 0 0