Appearance
工厂函数与Python动态导入
工厂函数(Factory Function)是一种创建对象的函数。这种函数会返回一个对象,而不是直接创建一个对象的实例,工厂函数通常用于需要创建不同类型对象的场景。它可以根据输入参数来决定创建哪种类型的对象,从而在一定程度上实现了对象创建的封装和灵活性。
例如:
python
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def animal_factory(animal_type):
if animal_type == 'dog':
return Dog()
elif animal_type == 'cat':
return Cat()
else:
raise ValueError("Unknown animal type")
# 使用工厂函数创建对象
animal = animal_factory('dog')
print(animal.speak()) # 输出: Woof!工厂函数 和 动态创建插件 的思路非常接近,例如我们想创建一个函数,名字叫 get_provider_plugin(),这个函数传递两个参数 provider_name 和 plugin_name,通过这个函数即可快速创建不同 提供商对应的工具。
例如在前面课时中,我们想获取 google_serper 工具,必须导入对应的类,然后实例化才可以获取:
python
from langchain_community.tools import GoogleSerperRun
google_serper = GoogleSerperRun(
name="google_serper",
description="一个低成本的谷歌搜索API。当你需要回答有关时事的问题时,可以调用该工具。该工具的输入是搜索查询语句。",
args_schema=GoogleSerperArgsSchema,
api_wrapper=GoogleSerperAPIWrapper(),
)
print(google_serper.invoke("2024年北京半程马拉松前3名成绩是多少?"))如果有了 工厂函数 之后,我们只需传递提供商名字 google 和工具名字 google_serper,工厂函数会自动返回该工具,伪代码如下:
python
google_serper = get_provider_plugin("google", "google_serper")
print(google_serper.invoke("2024年北京半程马拉松前3名成绩是多少?"))使用 工厂函数 这种设计模式之后,我们可以单独存储 provider_name + plugin_name,而不需要预先导入特定的类即可快速实例化。
那么大家思考下,在这个工厂函数的底层是如何实现的呢?最简单的思路是不是在工厂函数内部预先实例化所有要使用的工具,然后存储到字典中,需要使用的时候将对应的值返回,例如下方伪代码:
python
def get_provider_plugin(provider_name: str, plugin_name: str) -> BaseTool:
provider_plugin_map = {
"google": {
"google_serper": GoogleSerperRun(...),
"google_news": GoogleSerperNews(...),
},
"gaode": {
"gaode_ip": GaodeIp(...),
"gaode_weather": GaodeWeather(...),
},
}
return provider_plugin_map[provider_name][plugin_name]这种方式虽然可以实现,但是略显笨拙,和我们手动去导入实现看着差异不大,其实在 Python 中支持 动态导入,利用 动态导入 可以优化这个逻辑,让这个过程实现根据配置自动化。
首先来介绍下 动态导入,其实指的是在运行时加载模块或者包,而不是在程序启动时就静态导入。
在 Python 中可以标准库提供的 importlib 模块来实现动态导入,使用示例如下:
python
import importlib
# 动态导入模块
module = importlib.import_module("math")
# 使用导入的模块
print(module.sqrt(16)) # 输出: 4.0这个写法和静态导入的结果是一模一样的:
python
import math
# 使用导入的模块
print(math.sqrt(16)) # 输出: 4.0这样就可以考虑将 动态导入 和 工厂函数 结合,一般来说有两种形式:
- 提供一个特定格式的配置文件,在 工厂函数 内部先读取这个配置文件,找到所有 工具 的路径,然后循环遍历进行 动态导入 所有工具,从而完成工具映射,这样新增工具时,只需要修改 配置文件 并在特定的位置存放模块即可。
- 提供一个特定的路径,在 工厂函数 内部先遍历该 路径,并默认该路径下所有的
*.py文件都是 工具,将遍历的所有文件进行动态导入,从而完成工具映射,这样新增工具时,只需要在特定路径下新增,即可被工厂函数识别。
这两种方式各有优缺点,配置文件 的方式更灵活,对存储位置也没有要求,并且工厂函数除了根据 provider_name 和 plugin_name 加载对应工具,还可以加载一些额外信息,例如:工具顺序、图标地址、工具的使用权限等,不过 配置文件 没有 遍历文件夹 这么便捷。
YAML+工厂函数实现添加插件流程
在 LLMOps 项目中,我们考虑使用 YAML配置文件 + 工厂函数 的形式来实现这个需求,运行流程非常简单:
- 在项目中我们通过工厂函数读取
providers.yaml文件,先找到所有 服务提供者 对应的路径,例如provider="google"; - 循环遍历所有 服务提供者 文件夹下的
positions.yaml文件,该文件存储了 服务提供者 所有插件的顺序+名字,例如plugins=["google_serper", "google_news"]; - 根据得到的 插件名字 找到对应的 插件名字.py 与 插件名字.yaml文件,并动态导入该工具与插件的配置;
- 最后根据得到的信息创建 提供商-插件名-插件 映射关系,让工厂函数可以根据
provider_name和plugin_name动态加载工具;
简化后的运行流程如下:
经过这一步操作后,后期如果想为 LLMOps 项目添加 内置插件,步骤也非常简单:
- 在
providers.yaml中添加 内置插件 的服务提供商名字; - 在特定的文件夹下创建 服务提供商名字 文件夹,并在底下配置
positions.yaml记录各个工具的顺序,配置 工具名.yaml 代表特定工具的详细信息,_asset文件夹代表该工具的 图标资源 路径等;
这样无需修改其他代码,工厂函数 就可以通过 provider_name + plugin_name 来获取新创建的 内置工具。
总结
- 工厂函数核心价值是封装对象创建逻辑,可根据参数动态返回不同类型的对象实例;
- Python 动态导入通过
importlib模块实现运行时加载模块,可替代静态导入方式; - LLMOps 项目中结合 YAML 配置文件 + 工厂函数 + 动态导入,可实现插件的自动化加载和管理,新增插件仅需修改配置和新增文件,无需改动核心代码。