目录
入门例子
openai调用function calling
LangChain调用function calling
OpenAI Assistant API支持function calling
LangChain Assistant API支持function calling
网页Assistant支持function calling
函数调用(Function Calling)是OpenAI
在今年6月13日对外发布的新能力。根据OpenAI官方博客描述,函数调用能力可以让大模型输出一个请求调用函数的消息,其中包含所需调用的函数信息、以及调用函数时所携带的参数信息。这是一种将大模型
(LLM)能力与外部工具/API
连接起来的新方式。
比如用户输入:
What’s the weather like in Tokyo?
使用function calling,可实现函数执行get_current_weather(location: string)
,从而获取函数输出,即得到对应地理位置的天气情况。这其中,location
这个参数及其取值是借助大模型能力从用户输入中抽取出来的,同时,大模型判断得到调用的函数为get_current_weather
。
开发人员可以使用大模型的function calling能力实现:
• 在进行自然语言交流时,通过调用外部工具回答问题(类似于ChatGPT插件);
• 将自然语言转换为调用API调用,或数据库查询语句;
• 从文本中抽取结构化数据
• 其它
那么,在OpenAI发布的模型中,是如何实现function calling的呢?
本文中,使用的第三方模块信息如下:
openai==1.3.2
langchain==0.0.339
我们以函数get_weather_info
为例,其实现逻辑(模拟实现世界中的API调用,获取对应城市的天气状况)如下:
def get_weather_info(city: str):
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
该函数只有一个参数:字符串变量city,即城市名称。为了实现function calling功能,需配置函数描述(类似JSON化的API描述),代码如下:
functions = [
{
"name": "get_weather_info",
"description": "Get the weather information of a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Shanghai",
},
},
"required": ["city"],
}
}
]
对于一般的用户输入(query),大模型回复结果如下:
import json
from openai import OpenAI
client = OpenAI(api_key="sk-xxx")
query = "What is the capital of france?"
response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=[{"role": "user", "content": query}],
functions=functions
)
message = response.dict()["choices"][0]["message"]
print(message)
>>> {'content': 'The capital of France is Paris.', 'role': 'assistant', 'function_call': None, 'tool_calls': None}
此时function_call
为None,即大模型判断不需要function calling.
对于查询天气的query,大模型输出结果如下:
import json
from openai import OpenAI
client = OpenAI(api_key="sk-xxx")
query = "What is the weather like in Beijing?"
response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=[{"role": "user", "content": query}],
functions=functions
)
message = response.dict()["choices"][0]["message"]
print(message)
>>> {'content': None, 'role': 'assistant', 'function_call': {'arguments': '{\n "city": "Beijing"\n}', 'name': 'get_weather_info'}, 'tool_calls': None}
此时我们看到了令人吃惊的输出,大模型的输出内容为空,而判断需要function calling, 函数名称为get_weather_info
,参数为{'arguments': '{\n "city": "Beijing"\n}
。
下一步,我们可以调用该函数,传入参数,得到函数输出,并再次调用大模型得到答案回复。
func_name = message["function_call"]["name"]
func_args = json.loads(message["function_call"]["arguments"])
print("func name and args: ", func_name, func_args)
func_response = get_weather_info(**func_args)
final_response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": query},
{"role": "assistant", "content": None, "function_call": message["function_call"]},
{
"role": "function",
"name": func_name,
"content": func_response
},
],
)
print("answer: ", final_response.dict()["choices"][0]["message"]["content"])
输出结果如下:
func name and args: get_weather_info {'city': 'Beijing'}
answer: The weather in Beijing is currently snowy.
以上仅是function calling的简单示例,采用一步一步的详细过程来演示大模型中function calling如何使用。
在实际场景中,我们还需要实现中间过程的函数执行过程。
以下将介绍在OpenAI, LangChain中如何实现function calling。后面我们将使用的3个函数(这些函数仅用于测试,实际场景中可替换为具体的工具或API)如下:
def get_pizza_info(pizza_name: str):
# get pizza info by pizza name
pizza_info = {
"name": pizza_name,
"price": "10.99"
}
return json.dumps(pizza_info)
def get_weather_info(city: str):
# get city weather info with mock result
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
def get_rectangle_area(width: float, length: float):
# calculate the rectangle with given width and length
return f"The area of this rectangle is {width * length}."
在OpenAI的官方模块openai
中实现function calling
的代码如下:
# -*- coding: utf-8 -*-
from openai import OpenAI
import json
client = OpenAI(api_key="sk-xxx")
def get_pizza_info(pizza_name: str):
pizza_info = {
"name": pizza_name,
"price": "10.99"
}
return json.dumps(pizza_info)
def get_weather_info(city: str):
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
def get_rectangle_area(width: float, length: float):
return f"The area of this rectangle is {width * length}."
function_mapping = {"get_pizza_info": get_pizza_info,
"get_weather_info": get_weather_info,
"get_rectangle_area": get_rectangle_area}
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"],
}
},
{
"name": "get_weather_info",
"description": "Get the weather information of a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Shanghai",
},
},
"required": ["city"],
}
},
{
"name": "get_rectangle_area",
"description": "Get the area of a rectangle with given width and length",
"parameters": {
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "The width of a rectangle",
},
"length": {
"type": "number",
"description": "The length of a rectangle",
}
},
"required": ["width", "length"],
}
}
]
def chat(query):
response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=[{"role": "user", "content": query}],
functions=functions
)
message = response.dict()["choices"][0]["message"]
print('message: ', message)
function_call_info = message.get("function_call")
if not function_call_info:
return message
else:
function_name = function_call_info["name"]
arg_name = json.loads(function_call_info["arguments"])
print(f"function name and arg name: ", function_name, arg_name)
if function_name in function_mapping:
function_response = function_mapping[function_name](**arg_name)
final_response = client.chat.completions.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": query},
{"role": "assistant", "content": None, "function_call": function_call_info},
{
"role": "function",
"name": function_name,
"content": function_response
},
],
)
return final_response.dict()["choices"][0]["message"]
else:
return "wrong function name with function call"
query1 = "What is the capital of france?"
print("answer: ", chat(query1), end="\n\n")
query2 = "How much does pizza Domino cost?"
print("answer: ", chat(query2), end="\n\n")
query3 = "What is the weather like in Beijing?"
print("answer: ", chat(query3), end="\n\n")
query4 = "calculate the rectangle area with width 3 and length 5."
print("answer: ", chat(query4), end="\n\n")
输出结果如下:
query: What is the capital of france?
message: {'content': 'The capital of France is Paris.', 'role': 'assistant', 'function_call': None, 'tool_calls': None}
answer: {'content': 'The capital of France is Paris.', 'role': 'assistant', 'function_call': None, 'tool_calls': None}
query: How much does pizza Domino cost?
message: {'content': None, 'role': 'assistant', 'function_call': {'arguments': '{\n "pizza_name": "Domino"\n}', 'name': 'get_pizza_info'}, 'tool_calls': None}
function name and arg name: get_pizza_info {'pizza_name': 'Domino'}
answer: The cost of a Domino pizza is $10.99.
query: What is the weather like in Beijing?
message: {'content': None, 'role': 'assistant', 'function_call': {'arguments': '{\n "city": "Beijing"\n}', 'name': 'get_weather_info'}, 'tool_calls': None}
function name and arg name: get_weather_info {'city': 'Beijing'}
answer: The weather in Beijing is currently experiencing snow.
query: calculate the rectangle area with width 3 and length 5.
message: {'content': None, 'role': 'assistant', 'function_call': {'arguments': '{\n "width": 3,\n "length": 5\n}', 'name': 'get_rectangle_area'}, 'tool_calls': None}
function name and arg name: get_rectangle_area {'width': 3, 'length': 5}
answer: The area of a rectangle can be calculated by multiplying its width by its length. In this case, the width is 3 and the length is 5. Therefore, the area of the rectangle is 3 * 5 = 15 square units.
在langchain
中实现function calling
的代码相对简洁写,function calling
的结果在Message中的additional_kwargs变量中,实现代码如下:
# -*- coding: utf-8 -*-
import os
import json
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, AIMessage, ChatMessage
os.environ["OPENAI_API_KEY"] = "sk-xxx"
def get_pizza_info(pizza_name: str):
pizza_info = {
"name": pizza_name,
"price": "10.99"
}
return json.dumps(pizza_info)
def get_weather_info(city: str):
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
def get_rectangle_area(width: float, length: float):
return f"The area of this rectangle is {width * length}."
function_mapping = {"get_pizza_info": get_pizza_info,
"get_weather_info": get_weather_info,
"get_rectangle_area": get_rectangle_area}
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"],
}
},
{
"name": "get_weather_info",
"description": "Get the weather information of a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Shanghai",
},
},
"required": ["city"],
}
},
{
"name": "get_rectangle_area",
"description": "Get the area of a rectangle with given width and length",
"parameters": {
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "The width of a rectangle",
},
"length": {
"type": "number",
"description": "The length of a rectangle",
}
},
"required": ["width", "length"],
}
}
]
def chat(query):
llm = ChatOpenAI(model="gpt-3.5-turbo-0613")
message = llm.predict_messages(
[HumanMessage(content=query)], functions=functions
)
print('message: ', message, type(message))
function_call_info = message.additional_kwargs.get("function_call", None)
if not function_call_info:
return message
else:
function_name = function_call_info["name"]
arg_name = json.loads(function_call_info["arguments"])
print(f"function name and arg name: ", function_name, arg_name)
if function_name in function_mapping:
function_response = function_mapping[function_name](**arg_name)
final_response = llm.predict_messages(
[
HumanMessage(content=query),
AIMessage(content=str(message.additional_kwargs)),
ChatMessage(
role="function",
additional_kwargs={
"name": function_name
},
content=function_response
),
]
)
return final_response.content
else:
return "wrong function name with function call"
query1 = "What is the capital of Japan?"
print("answer:", chat(query1), end='\n\n')
query2 = "How much does pizza Domino cost?"
print("answer:", chat(query2), end='\n\n')
query3 = "What is the weather like in Paris?"
print("answer: ", chat(query3), end="\n\n")
query4 = "calculate the rectangle area with width 3 and length 10"
print("answer: ", chat(query4), end="\n\n")
Assistant API
是OpenAI在今年OpenAI开发者大会中提出的创新功能。Assistants API
允许用户在自己的应用程序中构建AI助手。助手有指令,可以利用模型、工具和知识来响应用户查询。Assistants API
目前支持三种类型的工具:代码解释器(Code Interpreter)、检索(Retrieval)和函数调用(Function Calling)。
我们来看看,在openai中的Assistant API如何支持function calling。
import time
import json
from openai import OpenAI
def get_pizza_info(pizza_name: str):
pizza_info = {
"name": pizza_name,
"price": "10.99"
}
return json.dumps(pizza_info)
def get_weather_info(city: str):
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
def get_rectangle_area(width: float, length: float):
return f"The area of this rectangle is {width * length}."
function_mapping = {"get_pizza_info": get_pizza_info,
"get_weather_info": get_weather_info,
"get_rectangle_area": get_rectangle_area}
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"],
}
},
{
"name": "get_weather_info",
"description": "Get the weather information of a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Shanghai",
},
},
"required": ["city"],
}
},
{
"name": "get_rectangle_area",
"description": "Get the area of a rectangle with given width and length",
"parameters": {
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "The width of a rectangle",
},
"length": {
"type": "number",
"description": "The length of a rectangle",
}
},
"required": ["width", "length"],
}
}
]
client = OpenAI(api_key="sk-xxx")
assistant = client.beta.assistants.create(
name="assistant test",
instructions="You are a helpful assistant, ready to answer user's questions.",
model="gpt-3.5-turbo-0613",
)
MATH_ASSISTANT_ID = assistant.id
print("assistant id: ", MATH_ASSISTANT_ID)
assistant = client.beta.assistants.update(
MATH_ASSISTANT_ID,
tools=[
{"type": "function", "function": function} for function in functions
],
)
print("assistant id: ", assistant.id)
def submit_message(assistant_id, thread, user_message):
client.beta.threads.messages.create(
thread_id=thread.id, role="user", content=user_message
)
return client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant_id,
)
def create_thread_and_run(user_input):
thread = client.beta.threads.create()
run = submit_message(MATH_ASSISTANT_ID, thread, user_input)
return thread, run
def wait_on_run(run, thread):
while run.status == "queued" or run.status == "in_progress":
run = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id,
)
time.sleep(0.5)
return run
def get_response(thread):
return client.beta.threads.messages.list(thread_id=thread.id, order="asc")
# Pretty printing helper
def pretty_print(messages):
print("# Messages")
for m in messages:
print(f"{m.role}: {m.content[0].text.value}")
print()
query = "How much does pizza Domino cost?"
thread, run = create_thread_and_run(query)
run = wait_on_run(run, thread)
# Extract single tool call
tool_call = run.required_action.submit_tool_outputs.tool_calls[0]
name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
my_response = function_mapping[name](**arguments)
print("function response: ", my_response)
# use function response for rerun
final_run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=run.id,
tool_outputs=[
{
"tool_call_id": tool_call.id,
"output": json.dumps(my_response),
}
],
)
final_run = wait_on_run(final_run, thread)
pretty_print(get_response(thread))
输出结果如下:
assistant id: asst_ElovRUJRLqBeYk2Gu2CUIiU9
assistant id: asst_ElovRUJRLqBeYk2Gu2CUIiU9
function response: {"name": "Domino", "price": "10.99"}
# Messages
user: How much does pizza Domino cost?
assistant: The pizza Domino from the restaurant costs $10.99.
可以看到在openai模块中,在Assistant API中实现function calling,较为麻烦。而新版的langchain(0.0.339)中已经添加对Assistant API的支持,我们来看看在langchain中如何支持function calling。
实现代码如下:
# -*- coding: utf-8 -*-
# @place: Pudong, Shanghai
# @contact: [email protected]
# @file: assistant_api_with_functions.py
# @time: 2023/11/23 11:00
import os
import json
from langchain.agents.openai_assistant import OpenAIAssistantRunnable
from langchain.schema.agent import AgentFinish
from langchain.agents import Tool
from langchain.tools import StructuredTool
os.environ["OPENAI_API_KEY"] = "sk-xxx"
def get_pizza_info(pizza_name: str):
pizza_info = {
"name": pizza_name,
"price": "10.99"
}
return json.dumps(pizza_info)
def get_weather_info(city: str):
weather_info = {"Shanghai": "Rainy", "Beijing": "Snow"}
return weather_info.get(city, "Sunny")
def get_rectangle_area(width: float = 1.0, length: float = 1.0):
return f"The area of this rectangle is {width * length}."
function_mapping = {"get_pizza_info": get_pizza_info,
"get_weather_info": get_weather_info,
"get_rectangle_area": get_rectangle_area}
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"],
}
},
{
"name": "get_weather_info",
"description": "Get the weather information of a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Shanghai",
},
},
"required": ["city"],
}
},
{
"name": "get_rectangle_area",
"description": "Get the area of a rectangle with given width and length",
"parameters": {
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "The width of a rectangle",
},
"length": {
"type": "number",
"description": "The length of a rectangle",
}
},
"required": ["width", "length"],
}
}
]
tools = [Tool(name=func["name"],
func=function_mapping[func["name"]],
description=func["description"]) for func in functions[:-1]]
tools.append(StructuredTool.from_function(get_rectangle_area, name="get_rectangle_area", description="Get the area of a rectangle with given width and length"))
agent = OpenAIAssistantRunnable.create_assistant(
name="langchain assistant",
instructions="You are a helpful assistant, ready to answer user's questions.",
tools=tools,
model="gpt-3.5-turbo-0613",
as_agent=True,
)
def execute_agent(agent, tools, input):
tool_map = {tool.name: tool for tool in tools}
response = agent.invoke(input)
while not isinstance(response, AgentFinish):
tool_outputs = []
for action in response:
print(action.tool, action.tool_input)
tool_output = tool_map[action.tool].invoke(action.tool_input)
print(action.tool, action.tool_input, tool_output, end="\n\n")
tool_outputs.append(
{"output": tool_output, "tool_call_id": action.tool_call_id}
)
response = agent.invoke(
{
"tool_outputs": tool_outputs,
"run_id": action.run_id,
"thread_id": action.thread_id,
}
)
return response
query = "How much does pizza Domino cost?"
query = "What is the capital of france?"
query = "What is the weather like in Beijing?"
query = "calculate the rectangle area with width 3 and length 5."
response = execute_agent(agent, tools, {"content": query})
print(response.return_values["output"])
注意,函数get_rectangle_area
为多参数输入,因此需使用StructuredTool.
在OpenAI中的官网中,Assistant已经支持function calling.