Skip to content

工厂函数与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

这样就可以考虑将 动态导入 和 工厂函数 结合,一般来说有两种形式:

  1. 提供一个特定格式的配置文件,在 工厂函数 内部先读取这个配置文件,找到所有 工具 的路径,然后循环遍历进行 动态导入 所有工具,从而完成工具映射,这样新增工具时,只需要修改 配置文件 并在特定的位置存放模块即可。
  2. 提供一个特定的路径,在 工厂函数 内部先遍历该 路径,并默认该路径下所有的 *.py 文件都是 工具,将遍历的所有文件进行动态导入,从而完成工具映射,这样新增工具时,只需要在特定路径下新增,即可被工厂函数识别。

这两种方式各有优缺点,配置文件 的方式更灵活,对存储位置也没有要求,并且工厂函数除了根据 provider_name 和 plugin_name 加载对应工具,还可以加载一些额外信息,例如:工具顺序、图标地址、工具的使用权限等,不过 配置文件 没有 遍历文件夹 这么便捷。

YAML+工厂函数实现添加插件流程 ​

在 LLMOps 项目中,我们考虑使用 YAML配置文件 + 工厂函数 的形式来实现这个需求,运行流程非常简单:

  1. 在项目中我们通过工厂函数读取 providers.yaml 文件,先找到所有 服务提供者 对应的路径,例如 provider="google";
  2. 循环遍历所有 服务提供者 文件夹下的 positions.yaml 文件,该文件存储了 服务提供者 所有插件的顺序+名字,例如 plugins=["google_serper", "google_news"];
  3. 根据得到的 插件名字 找到对应的 插件名字.py 与 插件名字.yaml文件,并动态导入该工具与插件的配置;
  4. 最后根据得到的信息创建 提供商-插件名-插件 映射关系,让工厂函数可以根据 provider_name 和 plugin_name 动态加载工具;

简化后的运行流程如下:

经过这一步操作后,后期如果想为 LLMOps 项目添加 内置插件,步骤也非常简单:

  1. 在 providers.yaml 中添加 内置插件 的服务提供商名字;
  2. 在特定的文件夹下创建 服务提供商名字 文件夹,并在底下配置 positions.yaml 记录各个工具的顺序,配置 工具名.yaml 代表特定工具的详细信息,_asset 文件夹代表该工具的 图标资源 路径等;

这样无需修改其他代码,工厂函数 就可以通过 provider_name + plugin_name 来获取新创建的 内置工具。

总结 ​

  1. 工厂函数核心价值是封装对象创建逻辑,可根据参数动态返回不同类型的对象实例;
  2. Python 动态导入通过 importlib 模块实现运行时加载模块,可替代静态导入方式;
  3. LLMOps 项目中结合 YAML 配置文件 + 工厂函数 + 动态导入,可实现插件的自动化加载和管理,新增插件仅需修改配置和新增文件,无需改动核心代码。

Talk is cheap, show me the code!