赞
踩
使用Python时候,我们常常并不添加函数参数类型,阅读代码时候,常常需要根据上下文才能得知具体的类型。比如上文get_current_weather
函数,通常我们直接写作:
def get_current_weather(date, location, unit):
pass
当然,有时候我们还会采用注释来说明函数功能,参数类型,如下所示。
def get_current_weather(date, location, unit):
"""Get the current weather in a given location
Args:
date: the date of weather
loction: the location of weather
unit: the unit of temperature, should be one of Celsius and Fahrenheit
Returns:
Returned a dict contain the current weather of given location and date like
{'date': '2024-01-02',
'location': '南京',
'unit': 'Celsius',
'temeprature': 23}
虽然注释已经说明了函数的功能、输入和输出,但是并没有一个统一的标准,只能作为阅读文档参考。所以Python自3.5引入了typing来支持类型提示,让我们简单过一遍typing的使用方法,本文并不打算对着typing文档翻译,而是选择我们常用的typing进行解决,更深入的仍然需要读者自行探索。此外,在[PEP-484]仍然备注到:
Python 仍然是一种动态类型语言,并且作者不希望强制使用类型提示,即使按照惯例。
def greeting(name: str, age: int) -> str:
return f'Hello {name}, you are {age} years old'
使用参数名:类型以设置参数类型,通过->
来设置预期返回值的类型。除了这些通用类型,对于dict、tuple和list,也同样适用标注。
from typing import List, Tuple, Dict
x: List[Union[int, float]] = [2, 3, 4.1]
dim: Tuple[int, int, int] = (11, 22, 33)
rectangle: Dict[str, int] = {
'height': 340,
'width': 500,
}
然而,我们经常一个参数多个类型,我们可以使用Union来表示参数可选类型。
def greeting(name: str, age: int, height: Union[int, float]) -> str:
return f'Hello {name}, you are {age} years old and {height} cm'
对于同时支持输入除了自身类型,还可以为None
,除了使用Union表示Union[str, None]
,还可以使用更简洁的Optional
。
def greeting(name: Optional[str], age: int, height: Union[int, float]) -> str:
return f'Hello {name}, you are {age} years old and {height} cm'
有时候我们会使用enum来限定输入参数的值选项,在类型提示中也有用于限定输入参数的字面量可选值Literal
,比如打开文件的模式选择,
type Mode = Literal['r', 'rb', 'w', 'wb']
def open_helper(file: str, mode: Mode) -> str:
...
以上文中自定义的天气类型为例,我们是用TypedDict这一特殊构造函数来添加类型提示到字典dict中。有时候,我们经常使用dict来传递参数,然而dict中有哪些键值是不明确的。个人觉得这并不是是一个好的工程方式,当然Python本身是支持这种动态特性的。使用TypedDict可以用来限定的dict的key和类型,如下所示,如果没有提供定义的key,也只是在类型提示时候会报错,运行时并不会出问题。
from typing import TypedDict
class Weather(TypedDict):
location: str
date: str
unit: str
temperature: int
a: Weather = {'location': 'nj', 'date': '2024-01-01', 'unit': 'Celsius', 'temperature': 23} # OK
b: Weather = {'label': 'nj'} # Fails type check
默认TypedDict
是需要填入所有参数才不会提示报错。但如果为了实现key是可选的,即部分key强制包含,其他可选,可以配合total
使用Required
。
class Weather2(TypedDict, total=False):
location: Required[str]
date: str
unit: str
temperature: int
c: Weather2 = {'location': 'nj'} # OK
注意:Required只在3.11才提供,之前版本需要使用
from typing_extensions import Required
。
对于函数,我们使用Callable标注,使用类似于Callable[[int], str]
来注解,语法包含两个参数,前者是参数类型列表,后者表示返回类型,返回类型必须是单一类型。
from typing import Callable
def concat(x: str, y: str) -> str:
return x + y
x: Callable[..., str] = concat
使用Annotated
给注解加入元数据,方法是使用形如Annoated[T, x]
将元数据x添加到给定类型T。有点抽象,举个例子,就是在使用LLM的工具调用时候,参数也需要类型和注释,Annotated
就能实现。
def get_current_weather(date: Annotated[str, "the date"], location: Annotated[str, "the location"],
unit: Annotated[str, "the unit of temperature"]) -> Annotated[
Weather, "A dictionary representing a weather with location, date and unit of temperature"]:
"""Get the current weather in a given location"""
return {
"location": location,
"unit": unit,
"date": date,
"temperature": 23
}
通过使用Annoated
我们给参数date
、location
、unit
甚至是返回值自定义类型都加上了类型和参数说明。
类型变量,构造类型变量的推荐方式是使用泛型函数、泛型类和泛型类型别名的专门语法。
typing.TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False)
其中name,比如说定义泛型类型T,就是这个T。bound用来将泛型类型绑定到特定类型,这样在静态类型检查器就可以知道他们到底接受的是什么类型,它可以作为泛型类型、泛型函数和类型别名定义形参。
T = TypeVar('T') # Can be anything S = TypeVar('S', bound=str) # Can be any subtype of str A = TypeVar('A', str, bytes) # Must be exactly str or bytes def repeat[T](x: T, n: int) -> Sequence[T]: """Return a list containing n references to x.""" return [x]*n def print_capitalized[S: str](x: S) -> S: """Print x capitalized, and return x.""" print(x.capitalize()) return x def concatenate[A: (str, bytes)](x: A, y: A) -> A: """Add two strings or bytes objects together.""" return x + y
前面已经说到Pydantic是世界上最广泛使用的Python的validation库,它基于typing包进行数据验证和序列化,采用Rust编写最快的内核验证代码,可以轻易生成JSON Schema,支持常见数据类型转换(比如str解析为int、字符串转换为时间),还兼容dataclass和TypedDict等。
pip install pydantic
基本使用方法,是定义类型,并继承自Pydantic的BaseMode。
from datetime import datetime class User(BaseModel): id: int name: str = "X2046" birthday: Optional[datetime] = None age: Annotated[int, Field(description="the age of user", alias="age", gt=18)] address: Annotated[str, Field(default='nj', description="the default location of user", alias="city")] user = User(id='123', birthday='2024-06-01') assert user.id == 123 print(user.name) > X2046 print(isinstance(user.id, int)) > True print(user.model_dump()) > {'id': 123, 'name': 'X2046', 'birthday': datetime.datetime(2024, 6, 1, 0, 0), 'age': 20, 'address': 'nj'} print(user.model_dump_json()) > {"id":123,"name":"X2046","birthday":"2024-06-01T00:00:00","age":20,"address":"nj"} print(user.model_dump_json(by_alias=True)) > {"id":123,"name":"X2046","birthday":"2024-06-01T00:00:00","age":20,"city":"nj"}
在实例化User时,会执行所有的解析和验证,可以看到
NotRequired
,而id是Required
,因此在validation阶段如果id没填就会报错。使用BaseModel
的model_dump
方法可以将类型转换为dict
,也可以使用dict(user)
转换,但前者支持对嵌套类型转换,而后者不会递归只能转换第一层。model_dump_json()
支持将类型转换为JSON字符串。之前就有说到Pydantic完全兼容Python的typing,因此我们也能够使用Annotated标注注解,并且结合Pydantic的Field
设定一些一些额外的元数据,比如描述、默认值、序列化的标签以及一些简单的限制。你可以看到我们对age设定了参数描述,并且要求必须大于等于18,否则validation就会报错。而再address中,我们要求alias为city,所以在序列化为JSON时候并且指定by_alias时,即可输出定义的别名。
上文我们使用ConversableAgent
的方法register_for_llm
将函数注册到Agent
上,并且提供了函数的描述。为啥需要单独提供函数的说明呢?因为typing
也没有提供描述函数功能的API,所以还是需要单独描述一下。
assistant.register_for_llm(name="get_current_weather", description="Get the current weather in a given location")(
get_current_weather)
那么register_for_llm
内部是如何实现的呢?如下代码显示,F
是用来定义函数类型,并且将其绑定到一个Callable[..., Any]
,然后返回的注解是Callable[[F], F]
,其内部是实现一个装饰器。这里注意那个api_style
它使用了字面量进行注解,其值可以是function
和tool
,并且默认是tool
,这是因为OpenAI API自v1.1.0弃用了function
转而采用tool
。
F = TypeVar("F", bound=Callable[..., Any])
def register_for_llm(
self,
*,
name: Optional[str] = None,
description: Optional[str] = None,
api_style: Literal["function", "tool"] = "tool",
) -> Callable[[F], F]:
def _decorator(func: F) -> F:
...
return func
return _decorator
_decorator
接收一个函数参数,如下代码所示,代码有所省略。
def _decorator(func: F) -> F:
...
# get JSON schema for the function
f = get_function_schema(func, name=func._name, description=func._description)
...
elif api_style == "tool":
self.update_tool_signature(f, is_remove=False)
else:
raise ValueError(f"Unsupported API style: {api_style}")
return func
从这个代码来看,它将函数的注释、名称保存到func上,并且最后通过update_tool_signature
将f更新到llm_config的tools上。其中get_function_schema
获取function的JSON Schema。
def get_function_schema(f: Callable[..., Any], *, name: Optional[str] = None, description: str) -> Dict[str, Any]: typed_signature = get_typed_signature(f) required = get_required_params(typed_signature) default_values = get_default_values(typed_signature) param_annotations = get_param_annotations(typed_signature) return_annotation = get_typed_return_annotation(f) missing, unannotated_with_default = get_missing_annotations(typed_signature, required) ... fname = name if name else f.__name__ parameters = get_parameters(required, param_annotations, default_values=default_values) function = ToolFunction( function=Function( description=description, name=fname, parameters=parameters, ) ) return model_dump(function)
这个方法通过获取函数的签名信息,然后将函数转换为Pydantic的子类,从而就可以通过Pydantic的model_dump转换为tools的dict行驶。该函数中最主要的是get_typed_signature
,它采用python的inspect
包实现对函数的解析,也被称为现场检查对象,在其他语言中差不多就是反射。这里我们只看signature
,它获取方法和函数签名,一般来讲函数签名通常包括函数名、参数类型列表、返回类型等。
signature = inspect.signature(call)
具体如何反射就不多说了,大家可以搜索相关教程或者参考官方文档,它将返回如下信息。
之后就是从返回的signature中获取参数要求,它会通过parameters中获取到想要的一切信息,比如param_annotations
,生成一个dict。
def get_param_annotations(typed_signature: inspect.Signature) -> Dict[str, Union[Annotated[Type[Any], str], Type[Any]]]:
return {
k: v.annotation for k, v in typed_signature.parameters.items() if v.annotation is not inspect.Signature.empty
}
然后实例化ToolFunction
并将其导出为dict,其中ToolFunction
是AutoGen自定义的类,它继承自Pydantic的BaseModel,其内部字段function也是继承自Pydantic,如下代码所示。
class ToolFunction(BaseModel):
"""A function under tool as defined by the OpenAI API."""
type: Literal["function"] = "function"
function: Annotated[Function, Field(description="Function under tool")]
class Function(BaseModel):
"""A function as defined by the OpenAI API"""
description: Annotated[str, Field(description="Description of the function")]
name: Annotated[str, Field(description="Name of the function")]
parameters: Annotated[Parameters, Field(description="Parameters of the function")]
最后,通过Pydantic的model_dump将继承自Pydantic BaseModel的ToolFunction转换成字典,也就是Open AI API所需的tools参数。
def model_dump(model: BaseModel) -> Dict[str, Any]:
return model.dict()
{'type': 'function', 'function': {'description': 'Get the current weather in a given location', 'name': 'get_current_weather', 'parameters': {'type': 'object', 'properties': {'date': {'type': 'string', 'description': 'the date'}, 'location': {'type': 'string', 'description': 'the location'}, 'unit': {'type': 'string', 'description': 'the unit of temperature'}}, 'required': ['date', 'location', 'unit']}}}
回到装饰器中,此时已经获得了函数schema f
,然后通过update_tool_signature
将其赋值到self.llm_config["tools"] = [tool_sig]
。
本文通过简单介绍Python的类型提示系统typing和Python中广受开发者爱好的数据验证器Pydantic,并深入分析AutoGen中工具调用的流程分析,从而为我们如何更好的进行工具调用打下了坚实的基础。下一篇我们开始分析如何使用AutoGen进行规划分析。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。