在我之前写的两篇博客中:OpenAI的函数调用,LangChain的表达式语言(LCEL)中介绍了如何利用openai的api来实现函数调用功能,以及在langchain中如何实现openai的函数调用功能,在这两篇博客中,我们都需要手动去创建一个结构比较复杂的函数描述变量,如下图所示:
由于我们手动创建这样的函数描述变量会非常的费时,且容易出错, 那么今天我们再介绍一种更加轻松的方式在langchain中实现openai的函数调用功能。在介绍今天的主要内容之前先让我们做一些初始化的工作,如设置opai的api_key,这里我们需要说明一下,在我们项目的文件夹里会存放一个 .env的配置文件,我们将api_key放置在该文件中,我们在程序中会使用dotenv包来读取api_key,这样可以避免将api_key直接暴露在程序中:
#pip install -U python-dotenv
import os
import openai
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']
一、Pydantic 语法
今天的介绍的内容中,我们会用到Pydantic 语法,Pydantic是一个Python库,用于数据类型验证和解析。它使用类型注释来控制模式验证和序列化。Pydantic的核心验证逻辑是用Rust编写的,因此它是Python中最快的数据验证库之一,Pydantic提供了一种简洁的方法来定义数据结构,同时确保数据遵守指定的类型和约束。下面我们来演示一个例子,在这个例子中我们会创建一个简单的python类:
class User:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
在这个User类中有三个成员分别是name,age,email,其中它们的类型分别定义为str,int,str。下面我们创建两个类的实例foo1和foo2:
foo1 = User(name="Joe",age=32, email="joe@gmail.com")
foo2 = User(name="Joe",age="bar", email="joe@gmail.com")
print(foo1.age)
print(foo2.age)
这里我们看到User的age定义的类型是int, 然而我们却给User的实例foo2的age赋了str的值“bar”,但是结果任然不受影响,也就是说python中的变量的类型不受定义的约束,这种不严格的类型定义方式有时候会导致程序的崩溃和不可预料的后果,下面我们看看pydantic的是怎么来解决这个问题的:
from typing import List
from pydantic import BaseModel, Field
#定义类pUser
class pUser(BaseModel):
name: str
age: int
email: str
#创建类的实例
foo_p = pUser(name="Jane", age=32, email="jane@gmail.com")
print(f'name:{foo_p.name}')
print(f'age:{foo_p.age}')
print(f'email:{foo_p.email}')
下面我们创建一个新的pUser实例,并且给age赋一个str值看看会怎么:
foo_p = pUser(name="Jane", age="bar", email="jane@gmail.com")
这里我们看到pUser类是从pyPantic的子类BaseModel继承而来,因此pUser也具备了pyPantic提供的数据类型验证机制,当我们给变量赋了一个错误的类型值时就会发生异常,并告知类型错误。下面我们来创建一个具有List变量的类:
class Class(BaseModel):
students: List[pUser]
obj = Class(
students=[pUser(name="Jane", age=32, email="jane@gmail.com")]
)
obj
这里我们创建了一个班级类(Class),并且包含了一个students的List成员 ,List中的元素类型为pUser。
二、使用pydantic创建Openai的函数描述对象
下面我们使用pyPantic创建一个函数描述对象类:
class WeatherSearch(BaseModel):
"""Call this with an airport code to get the weather at that airport"""
airport_code: str = Field(description="airport code to get weather for")
这里我们创建了一个WeatherSearch类,它继承自pyPantic的BaseModel子类,因此WeatherSearch类的所有成员都被具备了数据类型校验机制,该类有一个str类型的成员airport_code它表示机场代码,并且它有一个描述信息description,用来说明airport_code的作用,在airport_code的上方也有一段文本描述信息:"""Call this with an airport code to get the weather at that airport""" 这段文本信息是对类WeatherSearch的说明,意思是通过机场代码可以查询天气情况,接下来我们要使用langchain将这个WeatherSearch类转换成openai的函数描述对象:
from langchain.utils.openai_functions import convert_pydantic_to_openai_function
weather_function = convert_pydantic_to_openai_function(WeatherSearch)
weather_function
这里我们使用了langchain的convert_pydantic_to_openai_function方法将pydantic类转换成了openai的函数描述对象。需要的注意的是在定义pydantic类时文本描述信息不可缺少,如缺少文本描述信息会导致转换时出错,下面我们定义一个pydantic类WeatherSearch1:
class WeatherSearch1(BaseModel):
airport_code: str = Field(description="airport code to get weather for")
convert_pydantic_to_openai_function(WeatherSearch1)
这里我们看到,由于我们没有在 WeatherSearch1中加入对WeatherSearch1本身的描述信息,导致在转换时报错,虽然我们加了类成员airport_code的描述信息description,但是缺少对类本身的描述信息,所以最终导致在转换时出错,这说明在定义pydantic类时,类本身的描述信息是必须要有的。下面我们再看一个例子:
class WeatherSearch2(BaseModel):
"""Call this with an airport code to get the weather at that airport"""
airport_code: str
weatherSearch2=convert_pydantic_to_openai_function(WeatherSearch2)
weatherSearch2
这里我们在定义WeatherSearch2时添加了类本身的描述信息,但是对于类成员airport_code我们只定义了类型却没有添加描述信息,但在转换时却没有报错,这可能是因为llm可以从类的描述信息中推断出类成员的含义和作用,因此有时候定义类成员的时候不添加描述信息也是可以的。下面我们是在langchain中的invoke方法增加一个functions参数来绑定函数描述对象看看会得到什么样的结果:
from langchain.chat_models import ChatOpenAI
#创建llm
model = ChatOpenAI()
#执行函数调用
response = model.invoke("what is the weather in SF today?",
functions=[weather_function])
response
这里我们看到当我们向llm询问机场天气情况时,llm返回了函数调用参数airport_code,这说明llm认为回答用户的这个问题需要调用外部函数,并将调用外部函数的参数返回给了我们,然后我们就可以拿着函数的参数去实际调用外部函数了。除了在invoke方法中增加一个functions参数来绑定函数描述对象以外我们还可以在执行invoke之前使用bind方法来绑定函数描述对象,这样也会达到同样的效果:
#创建llm
model = ChatOpenAI()
#绑定函数描述对象
model_with_function = model.bind(functions=[weather_function])
#执行函数调用
response = model_with_function.invoke("what is the weather in sf?")
response
下面我们测试一下,当我们只和llm打招呼时,它会返回什么结果:
response = model_with_function.invoke("hi!")
response
这里我们可以看到当我们只和llm打招呼时("hi!"), llm并没有激活函数调用,也就是说llm意识到当前用户只是在做礼节性的打招呼,因此无需激活函数调用,所以它没有返回函数调用的信息。
三、强制执行函数调用
在之前第一篇博客OpenAI的函数调用中,我们介绍了让llm强制激活函数调用功能,这里我们也同样可以强制llm激活函数调用,只要我们在bind时增加一个function_call参数就可以了,无论用户提什么问题都会返回函数参数信息:
#指定调用的函数名称
model_with_forced_function = model.bind(functions=[weather_function],
function_call={"name":"WeatherSearch"})
response = model_with_forced_function.invoke("what is the weather in sf?")
response
如果用户的问题和天气无关时,llm也同样会返回调用函数的参数信息:
model_with_forced_function.invoke("hi!")
这里我们看到,当我们和LLM打招呼时,它也返回了函数调用参数airport_code,只是它的值时随机的。
四、使用chain来实现函数调用
在一般情况下我们会使用chain来实现整个问答的流程,接下来我们通过创建chain来实现函数调用功能:
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
#通过prompt模板创建prompt
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant"),
("user", "{input}")
])
#创建llm
model = ChatOpenAI()
#绑定函数描述对象
model_with_function = model.bind(functions=[weather_function])
#创建chain
chain = prompt | model_with_function
#测试函数调用功能
response = chain.invoke({"input": "what is the weather in sf?"})
response
这里使用了chain的invoke方法来实现对llm的问答,llm根据问题的自行判断是否需要激活函数调用。如何需要函数调用则返回函数调用参数。
下面我们来提取arguments参数:
arguments=response.additional_kwargs['function_call']['arguments']
arguments = eval(arguments)
arguments
这里我们使用了eval函数将原先的字符串变量转换成了字典对象,这样便于我们从中提取我们需要的数据。
五、使用多个函数
前面我们只是通过pydantic创建了一个函数描述对象,但在很多应用场景中,我们可能需要传递一组函数,让 LLM 根据问题上下文决定使用哪个函数。下面我们再创建一个函数描述对象ProductSearch,用来查询商品信息,这样再加上之前的天气查询函数,我们就有了两个函数描述对象了,我们可以让llm自己根据用户的问题来自行判断调用哪个函数:
#创建天气查询函数描述对象
class WeatherSearch(BaseModel):
"""Call this with an airport code to get the weather at that airport"""
airport_code: str = Field(description="airport code to get weather for")
#创建商品查询函数描述对象
class ProductPriceSearch(BaseModel):
"""Call this with product name to get the price of product """
product_name: str = Field(description="name of product to look up")
#创建函数列表
functions = [
convert_pydantic_to_openai_function(WeatherSearch),
convert_pydantic_to_openai_function(ProductPriceSearch),
]
#绑定函数列表
model_with_functions = model.bind(functions=functions)
#用户提问
model_with_functions.invoke("what is the weather in sf?")
这里我们向llm询问了天气情况,llm正确返回了天气函数的调用参数,下面我们再询问一个商品的问题:
model_with_functions.invoke("what are the price of iphone 14 pro ")
这里我们提出了一个关于手机的问题,llm返回了商品查询函数的参数,下面我们和llm打给招呼,看看会返回什么:
model_with_functions.invoke("hi!")
这里我们看到,当我们和llm打招呼时,llm没有返回任何函数的参数, 也就是说llm意识到了用户的问题和预先设定的两个函数没有任何关系,所以无需返回函数调用参数。
六、总结
今天我们学习了pydantic的基础语法,以及如何利用langchain将pydantic定义的类转换成openai的函数描述对象,通过pydantic我们可以轻松定义函数描述对象的类,然后使用langchain的convert_pydantic_to_openai_function方法将其转换成openai所需要的格式,如果不使用pydantic我们必须手动创建openai的函数描述对象,这将是非常低效且繁琐的工作。
七、参考资料
DLAI - Learning Platform Beta
Welcome to Pydantic - Pydantic
Introduction | 🦜️🔗 Langchain