一文學會 OpenAI 的函數調用功能 Function Calling

函數調用 (Function Calling) 提供了一種將 GPT 的能力與外部工具和 API 相連接的新方法。

在這個文章中,我想向您展示 OpenAI 模型的函數調用 (Function Calling) 能力,並向您展示如何將此新功能與 Langchain 集成。

我將通過以下代碼詳細介紹這個工作原理,開始吧!

安裝包

!pip install --upgrade langchain
!pip install python-dotenv
!pip install openai

注意:langchain 的版本不低於 0.0.200, 之前的版本尚不支持函數調用 (Function Calling)

可以調用下面的print_version函數(Function Calling) 看看自己目前的 langchain 的版本是否大於 0.0.200。

import pkg_resources


def print_version(package_name):
    try:
        version = pkg_resources.get_distribution(package_name).version
        print(f"The version of the {package_name} library is {version}.")
    except pkg_resources.DistributionNotFound:
        print(f"The {package_name} library is not installed.")


print_version("langchain")
The version of the langchain library is 0.0.205.

連接 OpenAI

我們加載 API 密鑰,並將 OpenAI 模塊的 API, 密鑰屬性設置爲環境變量的值,這樣我們就可以連接到 OpenAI 了。

from dotenv import load_dotenv
import os
import openai
import json

load_dotenv()
openai.api_key = os.environ.get("OPENAI_API_KEY")

首先,爲了使函數 (Function Calling) 工作,我們假設有一個函數 get_pizza_info可以獲取披薩信息,傳入一個字符串參數,它是披薩的名稱 pizza_name,例如 Salami 披薩或其他任何披薩,然後您會得到一個固定的價格,本例中始終爲 10.99,最後返回字符串 JSON。

定義一個 Function Calling

def get_pizza_info(pizza_name: str):
    pizza_info = {
        "name": pizza_name,
        "price": "10.99",
    }
    return json.dumps(pizza_info)

現在開始切入主題,我們來提供這個 get_pizza_info 函數的描述。

functions = [
    {
        "name": "get_pizza_info",
        "description": "Get name and price of a pizza of the restaurant",
        "parameters": {
            "type": "object",
            "properties": {
                "pizza_name": {
                    "type": "string",
                    "description": "The name of the pizza, e.g. Salami",
                },
            },
            "required": ["pizza_name"],
        },
    }
]

爲什麼必須提供一個函數 (Function Calling) 描述?

必須提供一個描述,這對於 llm 非常重要, llm 用函數描述來識別函數 (Function Calling) 是否適合回答用戶的請求。

在參數字典中,我們必須提供多個信息,例如在屬性字典中,有關披薩名稱的信息,並且還必須提供類型和描述。

這對於 llm 獲取關於這個函數 (Function Calling) 的信息非常重要。

我們還必須將pizza_name參數設置爲 required,因爲我們這裏沒有默認值,所以這是 llm 瞭解如何處理函數 (Function Calling) 的信息。

運行 OpenAI 的 Function Calling

在提供了這種信息之後,我們必須使用它,我這裏定義了一個名爲 chat 的小助手函數 (Function Calling) 。

def chat(query):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[{"role": "user", "content": query}],
        functions=functions,
    )
    message = response["choices"][0]["message"]
    return message

chat 的小助手函數 (Function Calling) 只接受一個名爲 query 的參數,這將是一個字符串,這將是用戶的請求,首先我們必須定義 API Call,我們通過 ChatCompletion類從 OpenAI 進行調用,我們在這裏調用 create 函數 。

注意: 使用最新的模型非常重要,所有以 0613 結尾的模型,例如 gpt-3.5-turbo-0613, 如果是 gpt4 也是需要 0613 結尾的模型。

然後,我們必須提供消息messages,這將是一個字典,其中包含角色role,角色當前爲 user,並且我們提供內容content,該內容將被傳遞給此處的 chat 函數,然後作爲附加參數,您必須提供這些函數的參數functions=functions, (functions 變量是我們上一步定義的函數 描述列表,這個列表中我們暫時只定義了一個函數描述)。

通過調用這個 chat 函數 ,我們將獲得一個響應,響應是一個更復雜的對象,我們可以通過調用響應來獲取實際的消息,然後有一個名爲 choices 的字典,這是一個我們嘗試檢索第一個元素的列表,這將是另一個字典,我們將檢索消息,所以有很多東西,比如核心令牌等。

現在我們可以問模型法國的首都是什麼,如果您運行,我們會得到一個答案。

chat("What is the capital of france?")
<OpenAIObject at 0x166d47a42f0> JSON: {
  "role": "assistant",
  "content": "The capital of France is Paris."
}

這將給我們一個返回的 openai 對象,其中包含content,答案在這裏,法國的首都是巴黎,角色是助手,系統始終是 AI 的角色,用戶始終是發出請求的用戶 user 的角色,這非常好,因爲 API 已經提供了一個 JSON 對象。

那麼當我們問披薩的價格是多少時會發生什麼,這顯然與比薩有關了。我們希望 LLM 回答我們的是關於披薩的信息。

query = "How much does pizza salami cost?"
message = chat(query)
message

我們還向 LLM 提供了應該使用 get_pizza_info 函數 (Function Calling) 的信息,這裏的描述與比薩有關。如果您現在運行,它應該看起來不同,所以現在輸出看起來非常不同。

運行代碼後你會大喫一驚,內容中沒有任何東西,只是 null。

<OpenAIObject at 0x166d47a43b0> JSON: {
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_pizza_info",
    "arguments": "{\n\"pizza_name\": \"Salami\"\n}"
  }
}

但是我們有此附加的 function_call 對象,它是一個字典,有參數 "{\n\"pizza_name\": \"Salami\"\n}",所以這是函數 (Function Calling) 的參數,這是我們要傳遞給我們的函數 (Function Calling) 的值。

我們要傳遞給它的函數 (Function Calling) 的名稱是 get_pizza_info 函數。

所以我們現在要使用這個信息,我們可以首先檢查是否實際上我們有這個函數調用 (Function Calling) 對象在這裏。

如果是的話,我們知道 llm 要求我們調用函數 (Function Calling) ,並從這裏提取函數 (Function Calling) 名稱。

if message.get("function_call"):
    # 解析第一次調用的時候返回的 pizza 信息
    function_name = message["function_call"]["name"]
    pizza_name = json.loads(message["function_call"] ["arguments"]).get("pizza_name")
    print(pizza_name)
    # 這裏將 chat 小助手函數的響應結果提取後,傳遞 function_response
    function_response = get_pizza_info(
        pizza_name=pizza_name 
    )

    second_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {"role": "user", "content": query},
            message,
            {
                "role": "function",
                "name": function_name,
                "content": function_response, # function calling 的 content 是 get_pizza_info 函數 
            },
        ],
    )

second_response

就像這樣我們有這個函數調用 (Function Calling) 對象,然後我們想要檢索名稱,所以這將是我們要調用的函數 (Function Calling) 名稱,我們還希望得到 pizza 名稱臘腸,這將是我們要傳遞給我們的函數 (Function Calling) 的參數。

我們得到函數調用 (Function Calling) 和參數,然後我們有這個對象,我們使用 JSON.loads() 將其從 JSON 對象轉換爲字典,然後我們提取 pizza_name 鍵,這是我們要傳遞給我們的函數 (Function Calling) 的名稱,然後我們可以通常進行 API Call 並檢索信息,然後這 是函數 (Function Calling) 的響應,這也是一個 JSON 對象。

我們從這個函數 (Function Calling) 中檢索到它,通過這個結果,我們想再次調用 API,我們在這裏使用最新的模型,並且在之前我們還傳遞了消息,我們還傳遞了一個對象,其中有一個名爲 message["function_call"]["name"] ,並在這裏傳遞了函數 (Function Calling) 名稱.

第一個函數 (Function Calling) 響應是具有名稱和價格的披薩信息,我們在這裏傳遞它給第二個函數 second_response,然後我們進行第二個響應 。

如果我們打印這個:

<OpenAIObject chat.completion id=chatcmpl-7Y9045lCV15L1psS5SNYclk4SGcDU at 0x166c574fa10> JSON: {
  "id": "chatcmpl-7Y9045lCV15L1psS5SNYclk4SGcDU",
  "object": "chat.completion",
  "created": 1688372104,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The cost of a pizza salami is $10.99."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 58,
    "completion_tokens": 13,
    "total_tokens": 71
  }
}

我們得到一個複雜的對象,我們可以在這裏找到內容,臘腸的成本是 10.99,所以函數 (Function Calling) 響應將用於從我們的 API 中使用信息(也就是第一個函數 (Function Calling) )創建了類似於人的答案。

這就是函數調用 (Function Calling) 的全部內容,讓 llm 決定是否要使用其他信息或外部信息。

只需讓 LLM 自己回答問題,這是最基本的用法。

如何與 LangChain 一起使用?

我們來看看如何與 LangChain 一起使用。

首先導入 ChatOpenAI 類和 HumanMessage、AIMessage,還有 ChatMessage 類,這些類可以幫助我們創建這種功能,包括用戶角色等。 我們可以不必要像之前那樣,定義角色等,只需要傳遞 content。其他的都交給了 Langchain.

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, AIMessage, ChatMessage

我們只需要提供內容 content,可以看到我們有這個 HumanMessageAIMessage

然後在這裏創建模型 LLM,通過實例化 ChatOpenAI,我們還在這裏傳入最新的模型 gpt-3.5-turbo-0613,然後運行預測消息函數 predict_messages,在這裏可以提供一個額外的關鍵字參數,稱爲 functions,我們將提供我們之前定義的函數列表 functions=[...]

llm = ChatOpenAI(model="gpt-3.5-turbo-0613")
message = llm.predict_messages(
    [HumanMessage(content="What is the capital of france?")], functions=functions
)

運行後,我們可以看到:

AIMessage(content='The capital of France is Paris.', additional_kwargs={}, example=False)

AIMessagecontent, 法國的首都是巴黎。

使用 Langchain 後,過程變得很標準化。

現在我們再次運行查詢披薩莎拉米在餐廳裏的價格。

llm = ChatOpenAI(model="gpt-3.5-turbo-0613")
message_pizza = llm.predict_messages(
    [HumanMessage(content="How much does pizza salami cost?")], functions=functions
)
message
AIMessage(content='', additional_kwargs={'function_call': {'name': 'get_pizza_info', 'arguments': '{\n"pizza_name": "Salami"\n}'}}, example=False)

現在我們預期我們不會得到內容 content='',也不會得到任何東西,但是我們得到了 message.additional_kwargs

message.additional_kwargs 中包括了函數調用 (Function Calling) 字典,名稱是披薩信息,因此我們可以看到 LLM 建議我們首先調用披薩信息函數 (Function Calling) ,這裏的參數是 Pizza_namesalami,這看起來與我們之前做的非常相似,但是沒有使用 LangChain。

現在如果我們看一下第一個響應中的 message.additional_kwargs,它們是空的,這只是一個空字典。

如果我們得到一個函數調用 (Function Calling) 建議,我們可以在這裏得到它, additional_kwargs 中有我們的函數調用 (Function Calling),名稱和參數。

因此我們現在可以使用這種功能來定義我們是否要進行額外的調用。

要獲得披薩名稱,我們與之前一樣,只需提取額外的參數 message.additional_kwargs,並將其轉換爲具有json.loads的字典,並通過調用字典的 get 方法獲得披薩名稱,然後我們得到返回的披薩名稱,即莎拉米.

然後我們再次調用這個函數 (Function Calling) ,就像這樣再次調用函數 (Function Calling) ,我們得到名稱莎拉米和價格爲 10.99。

import json

# 打印結果是 'Salami'
pizza_name = json.loads(message_pizza.additional_kwargs["function_call"]["arguments"]).get("pizza_name")
# 將'Salami'傳參給 get_pizza_info 函數
pizza_api_response = get_pizza_info(pizza_name=pizza_name)

返回的結果是:

'{"name": "Salami", "price": "10.99"}'

現在我們可以使用這個 API 響應並創建我們的新 API Call,使用預測消息函數 llm.predict_messages,在這裏我們只有 HumanMessage,我們將提供我們的查詢,而 AIMessage 我們將只提供額外的關鍵字參數 additional_kwargs 的字符串,而不是提供這個函數 ,我們將只使用 ChatMessage,在這裏提供角色是 role="function

additional_kwargs 我們將提供名稱 name,這個是上一次調用的 API 返回結果 message_pizza.additional_kwargs["function_call"]["name"]

因此,如果我們再次運行這個,我們應該得到與之前類似的響應,AIMessage 內容是在餐廳裏的披薩價格爲 10.99,因此非常容易使用.

second_response = llm.predict_messages(
    [
        HumanMessage(content=query), # query = "How much does pizza salami cost?"
        AIMessage(content=str(message_pizza.additional_kwargs)),
        ChatMessage(
            role="function",
            additional_kwargs={
                "name": message_pizza.additional_kwargs["function_call"]["name"]
            },
            # pizza_api_response = get_pizza_info(pizza_name=pizza_name)
            content=pizza_api_response
        ),
    ],
    functions=functions,
)
# second_response

運行結果:

<OpenAIObject at 0x166d47a43b0> JSON: {
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_pizza_info",
    "arguments": "{\n\"pizza_name\": \"Salami\"\n}"
  }
}
Salami
<OpenAIObject chat.completion id=chatcmpl-7Y9045lCV15L1psS5SNYclk4SGcDU at 0x166c574fa10> JSON: {
  "id": "chatcmpl-7Y9045lCV15L1psS5SNYclk4SGcDU",
  "object": "chat.completion",
  "created": 1688372104,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The cost of a pizza salami is $10.99."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 58,
    "completion_tokens": 13,
    "total_tokens": 71
  }
}
AIMessage(content='The capital of France is Paris.', additional_kwargs={}, example=False)
AIMessage(content='', additional_kwargs={'function_call': {'name': 'get_pizza_info', 'arguments': '{\n"pizza_name": "Salami"\n}'}}, example=False)
'Salami'
'{"name": "Salami", "price": "10.99"}'
AIMessage(content='The pizza Salami costs $10.99.', additional_kwargs={}, example=False)

使用 LangChain 的 tools

但是使用這種 message.additional_kwargs 工作仍然感覺有點複雜。

LangChain 已經提供了與外部世界交互的另一種標準化方法,以進行請求或其他操作,這些稱爲工具 tools,工具 tools 是由 Chain 提供的類,您也可以創建自己的工具,我將向您展示如何做到這一點。

from typing import Optional
from langchain.tools import BaseTool
from langchain.callbacks.manager import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)


class StupidJokeTool(BaseTool):
    name = "StupidJokeTool"
    description = "Tool to explain jokes about chickens"

    def _run(
        self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None
    ) -> str:
        return "It is funny, because AI..."

    async def _arun(
        self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("joke tool does not support async")

首先,我們必須導入基類,您可以通過繼承基類工具來創建自定義類或自定義工具,然後必須提供工具的名稱和描述,這是一個非常簡單和非功能性的工具,只是爲了向您提供一些語法,您必須定義一個下劃線 _run 函數和一個下劃線 _arun 函數,這提供了異步支持和同步支持。

這裏函數 (Function Calling) 返回的東西是沒有實際功能。

當然您可以在這裏查詢數據庫或者做任何您想做的事情,但是我只是向您展示您可以做什麼。

如果您有了自己的工具與類一起使用,您可以輕鬆將自己的類轉換爲格式化的工具: format_tool_to_openai_function,我還在這裏導入了 MoveFileTool 工具,它允許您在計算機上移動文件。

from langchain.tools import format_tool_to_openai_function, MoveFileTool


tools = [StupidJokeTool(), MoveFileTool()]
# 將自己的 tools 轉換爲格式化的 function
functions = [format_tool_to_openai_function(t) for t in tools]
# functions 是之前定義的一個變量:一個函數列表
query = "Why does the chicken cross the road? To get to the other side"
output = llm.predict_messages([HumanMessage(content=query)], functions=functions)
# output

我們看看 output 運行結果:

AIMessage(content='', additional_kwargs={'function_call': {'name': 'StupidJokeTool', 'arguments': '{\n"__arg1": "To get to the other side"\n}'}}, example=False)

我們現在將 output 的結果傳遞進 second_response (注意 additional_kwargs)。

我在這裏創建了一個工具列表,其中實例化了我的自定義類和 MoveFile 工具類,因此我有兩個工具。

如果我們看一下它們,它們有一個名稱 name,如愚蠢的笑話工具 StupidJokeTool,它們有一個描述,這是直接從這些類參數 arguments 中獲取的,arguments裏的屬性是 __arg1

現在我們可以再次像這樣使用它,例如爲什麼雞過馬路?

query = "Why does the chicken cross the road? To get to the other side"

second_response = llm.predict_messages(
    [
        HumanMessage(content=query),
        AIMessage(content=str(output.additional_kwargs)),
        ChatMessage(
            role="function",
            additional_kwargs={
                "name": output.additional_kwargs["function_call"]["name"]
            },
            content="""
                {tool_response}
            """,
        ),
    ],
    functions=functions,
)
# second_response

現在我可以運行預測消息函數,並在這裏提供這兩個函數 (Function Calling) ,並讓 LLM 決定是否使用工具,因此,正如我們所看到的,我們不會得到任何內容。

因此,AI 希望我們調用一個函數,函數調用 (Function Calling) 是我們的 StupidJokeTool工具類或我們將類轉換爲的函數 (Function Calling) 。

因此現在我們實際上沒有函數 (原生 openai 案例,我們是定義了函數的描述) ,我們只是將工具本身轉換爲函數 (Function Calling) 定義,但不是函數 (Function Calling) 本身。

運行後返回的:

AIMessage(content='', additional_kwargs={'function_call': {'name': 'StupidJokeTool', 'arguments': '{\n  "__arg1": "To get to the other side"\n}'}}, example=False)

我們必須提取 additional_kwargs,然後我們將它傳遞給我們的函數 (Function Calling) ,然後我們得到我們的工具響應。

現在我們可以再次使用這兩個響應來進行其他請求,仍然是與之前相同的模式,我們通過傳入一個聊天消息,這裏的角色是函數 (role="function") ,內容是完全自動的響應,因此這可能甚至不會像預期的那樣工作。

它不像預期的那樣工作,因爲我們的工具 StupidJokeTool 沒有提供任何功能,但是我認爲您會看到模式,進行初始調用,如果 LLM 要求您調用函數 (Function Calling) ,則調用函數 (Function Calling) 並提供函數 (Function Calling) 的輸出到另一個 LLM 調用。

這就是目前與普通 LLM 鏈的工作方式。

老實說,對於代理,這個功能已經實現得更好。

Langchain Agent 如何實現 Function Calling ?

我將向您展示它是如何工作的,首先我們導入一些鏈 Chain,例如 LLMMathChain,還有一個 chat_models,聊天模型在這裏使用 ChatOpenAI 創建我們的 LLM。

from langchain import LLMMathChain
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI


llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0613")
llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)
tools = [
    Tool(
        ,
        func=llm_math_chain.run,
        description="useful for when you need to answer questions about math"
    ),
]

此代理能夠回答普通問題並進行一些計算,因此例如我們像這樣使用它,我們使用初始化代理函數 (Function Calling) ,現在我們在這裏使用它與我們的工具一起。

agent = initialize_agent(tools, llm, agent=AgentType.OPENAI_FUNCTIONS, verbose=True)

現在我們有了新的代理類型 OPENAI_FUNCTIONS,它屬於 openai 函數類型,所以如果運行這個代理,我們不需要傳遞任何關鍵字參數或額外的參數,只需要像這樣使用它。

現在我們運行 “法國的首都是什麼”,我們得到的結果是法國的首都:巴黎。

agent.run("What is the capital of france?")

我們可以得到:

> Entering new  chain...
The capital of France is Paris.

> Finished chain.
'The capital of France is Paris.'

如果我們想知道 100 除以 25 等於多少,這時候計算器被調用,我們得到最終答案 100 除以 25 等於 4。

agent.run("100 除以 25 等於多少?")

我們可以得到:

> Entering new  chain...

Invoking: `Calculator` with `100 / 25`




> Entering new  chain...
100 / 25```text
100 / 25
...numexpr.evaluate("100 / 25")...
Answer: 4.0
 Finished chain. Answer: 4.0100 除以 25 等於 4。 ```

所以對於代理 Langchain Agent 來說,它的工作非常流暢,我相信很快它也會與其他的 llm 鏈一起工作。

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://zhuanlan.zhihu.com/p/641239259?utm_id=0