模块化
本系统中包含各种具体技术实现内容,为了规范这些技术的具体实现,我们称系统中所有使用到的技术为模块,并且它们都继承与一个统一的接口 BasicModule
,通过 OOP
中继承和多态的思想,不仅可以规范和约束模块实现,也能提供模块特定的接口。
属性
在先前的模块实现中,只能通过模块名描述子模块,但每个具体的实现类型其需要的子模块有所不同,且需要的子模块实现类型也不同,因此缺乏配置的灵活性。
在当前配置框架下,我们扩充了子模块的描述方式为 name:cond
,其中前部分为模块名,后半部分则是模块的条件,系统在加载模块和切换模块时就按照按照设置的条件进行加载,从而保障更安全的运行条件。
*
: 支持所有实现类型(实际配置时可以直接忽略:*
)kind
: 仅支持实现类型kind
[kind1,kind1,...]
: 支持实现类型kind1
和kind2
等![kind1,kind2,...]
: 不支持实现类型kind1
和kind2
等
注意:当经过条件筛选后如果存在多个实现类型,则优先会使用对应模块默认的实现类型,如果默认实现类型不再支持的列表或在不支持的列表中,则会按照顺位加载第一个实现类型;当然如果只存在一种实现类型,则会使用之;如果不存在实现类型,则会产生错误。
1. 基础属性
名称
name
必须
别名
alias
必须
模块对应的中文名
路径
path
模块代码所在路径, module
目录的相对路径
子模块列表
sub
[]
包含的子模块名
嵌套深度
depth
程序生成
记录子模块的嵌套深度
父模块
sup
程序生成
父模块名
实现类型列表
kinds
[]
存储支持的实现类型
默认实现类型
default
basic
默认实现类型
非空性
not_null
true
记录该模块是否能被加载为空
注意:模块信息中不会描述当前的实现类型,因为这个属性属于运行时的属性,会存储在模块管理单元
ModuleManageCell
中。
2. 模块嵌套
在本系统中,模块是允许被嵌套的,一个模块允许拥有多个子模块,最终的结果就是是构建了一颗模块树,本系统则是这颗模块依赖树的根节点。
对于叶节点模块模块来说,他们必须需要提供功能,除非它的实现类型为 null
,否则不实现具体功能的模块是无意义的,所以我们称之为功能性模块(functional module
)。而非叶子节点则属于组织者模块(organizer module
),它通过依靠子模块来实现复杂的逻辑处理。
比如说在本系统中,bot
需要使用 searcher
检索专业知识数据,并结合提示词供词,最终通过 caller
调用大语言模型,以生成问题对应的答案。
模块嵌套为系统开发提供了两个好处,首先是其实现了逻辑和实现解耦,子模块只需要关心具体的技术实现细节,父模块则关注如何组织这些子模块的逻辑即可。其次,模块嵌套也建立了一套父子模块的依赖链,这为后续模块的级联控制打下了结构基础。
可控模块
本系统提供更细粒度控制模块逻辑, 我们称 booter
的一级子模块为可控模块(当然其本身也是)。 一方面为了更细粒度地控制不同模块的启停, 避免系统切换开销过大。 另一方面,则是并不是所有模块都需要控制, 而且单独启动也并没有意义(除了程序调试)。
当然,系统通过引入null
实现类型,来补充对模块的控制能力。 当模块实现被设置为null
时,系统在启动时就不会加载该模块, 自然不会启动该模块。 一个模块都否被设置为空实现由not_null
属性决定。
目前仅在 Webui 控制器对其进行限制。
3. 实现类型
每个模块可以存在多个具体的实现类型, 但是本系统保留了 2 个实现类型名称 basic
和 null
。
basic
:模块的的基础实现类型,对于一些组织者模块,它一般来说只有一种实现类型,那么就可以使用basic
称呼。除此之外,如果你不知道这个模块实现需要起什么名字,那你就可以使用basic
了。在大多数情况下,其并没有提供任何额外的语义信息,但是在系统配置和动态导入时,其会提供不少的便利性。null
:空实现,其在动态导入过程时会直接返回None
。模块是否能设置未空,也由其非空性决定。引入空实现的主要原因是用于关闭一些非核心的模块。当然,在进行父模块功能开发时也需要注意这些空模块的调用问题。
在描述模块依赖时,可以配置子模块列表设置需要的子模块信息,我们称其为基础子模块信息。而对于具体的实现类型中,也可以针对性地单独设置子模块信息。
值得注意的是,这种单独设置只是用于覆盖和修正,目前暂不支持对基础子模块信息的删除操作,所以需要注意选择哪些模块做我基础子模块。
生命周期
模块的生命周期包括两个阶段,模块的加载阶段和运行阶段。前者负责动态导入对应代码中的类,并进行实例化的过程,而后者则是将覆盖模块停止和运行之间的来回切换的过程。
值得注意的是,由于控制反转(详情请见 模块管理器 部分)的机制,因此对于模块生命周期的控制权并不在模块本身,而是在模块管理器上,但实际的逻辑仍需要模块中提供的信息和代码逻辑进行处理和控制。
1. 加载阶段
加载阶段是导入具体模块实现类型的过程,其主要根据模块属性进行吗拼接组合得到具体的模块代码路径。通过这样约定的模块代码存放逻辑,系统就可以方便和简洁地导入具体模块的实现类型,减少了代码和实际模块之间的耦合度。
导入过程分为模块路径和类名称,主要使用的是以下 3 个模块属性。模块路径为为 path.name.kind
其中 path
是以项目文件夹 module
为基础的相对路径。类的名称使用大驼峰命名法,格式为KindName
。值得注意的是,当 kind
为 basic
时,kind
就不需要显式地标明,为 null
时,将不会进行加载。
path
: 模块所在代码路径name
: 模块的名字kind
: 模块的实现类型
以上的模块导入逻辑可以转换为如下代码。
注意: 大驼峰命名法意味着无论配置文件中
kind
为什么, 最终只会将开头字母大写。
2. 运行阶段
模块在运行阶段实际上是模块停止和运行之间的切换,其流程图如下所示,主要分为 4 中状态。
未加载状态:当模块实现类型为
null
或加载失败时,则会处于这个阶段主要状态:相对固定的两个状态,表示模块正常运行或停止,
Stopped
和Started
中间状态:表示运行和停止之间切换的过程
异常状态:当模块状态在切换过程中,发生异常时会产生的状态
TODO: 运行阶段的状态运行仍然在优化和完善中...
a. 钩子函数
由于控制反转机制,模块的控制权实际上在管理器中。而模块的实际的启动逻辑则是封装在模块的钩子函数中,管理器在接收到对应的控制指令时,会调用模块中的钩子函数,以执行模块中自定义的处理逻辑。 目前提供的钩子函数如下。
“启动子模块”前:
before_starting_submodules
自定义启动逻辑:
handing_starting
自定义停止逻辑:
handing_stopping
“停止子模块”后:
after_stopping_submodules
b. 多线程
某些模块在启动之后,需要持续的运行处理, 因此需要使用多线程的技术。 本系统为了对多线程的创建和回收进行统一的管理, 因此封装了 make_thread
用于创建和运行线程。 该函数创建的线程对象会存储在模块内部, 开发者需要自行选择合适的钩子函数作为时机,创建和运行线程。
在线程内部,通常使用死循环的方式进行循环操作,以保证可持续运行。 但是在实际实现上,需要通过标志位来控制线程终止, 以进行优雅地停机。 目前本系统提供 is_running
属性, 当模块处于 Started
和 Starting
时, 该属性为 true
。
最后更新于