第 31 章:上帝类 vs 模块拆分

个人公众号

难度:中等

src/agentscope/tool/_toolkit.py 有 1684 行。注册、调用、中间件、分组、异步任务、Schema 管理——全在一个文件里。这是上帝类,还是合理的聚合?

上一章:第 30 章 为什么不用装饰器注册工具

决策回顾

打开 _toolkit.py 看看它包含了多少职责:

职责方法大致行数
注册工具register_tool_function274-450
调用工具call_tool_function852-920
中间件_apply_middlewares, register_middleware57-115, 1441-1540
工具分组create_tool_group, set_active_group200-270
Schema 管理get_json_schemas, set_extended_model450-540
异步任务call_tool_function_async, view_task750-850, 1541+
MCP 对接register_mcp_server550-700
Agent Skillskill 相关方法700-750
序列化state_dict, load_state_dict1200-1400

9 种职责,1684 行。


被否方案:按职责拆分

方案:每个职责一个类,Toolkit 作为外观(Facade):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 拆分方案
class ToolRegistry: # 注册工具
class ToolExecutor: # 调用工具
class MiddlewareChain: # 中间件管理
class ToolGroupManager: # 分组管理
class SchemaBuilder: # Schema 生成

class Toolkit: # Facade
def __init__(self):
self.registry = ToolRegistry()
self.executor = ToolExecutor(self.registry)
self.middlewares = MiddlewareChain()
self.groups = ToolGroupManager()

好处

  • 每个类职责单一
  • 可以独立测试每个组件
  • 文件更短,更容易理解

问题

  1. 调用链变长toolkit.toolstoolkit.registry.tools
  2. 状态共享复杂ToolExecutor 需要访问 ToolRegistryMiddlewareChain
  3. 序列化变复杂state_dict 需要协调多个子类的序列化
  4. 过度工程:对于大多数开发者,只用到注册和调用两个功能

AgentScope 的选择:大文件聚合

Toolkit 是一个聚合根——所有工具相关的操作都通过它。它的”大”不是偶然的,而是有意为之:

  1. 内聚性高:所有方法都围绕”工具”这个概念
  2. 调用简单toolkit.register_tool_function(), toolkit.call_tool_function() —— 不需要知道内部结构
  3. 状态一致:注册、调用、分组、中间件共享同一个 tools 字典

判断标准

“上帝类”是一个贬义词,但不是所有大类都是上帝类。关键区别:

上帝类(坏)大聚合类(可接受)
职责不相关职责围绕同一概念
修改一个功能影响其他功能修改被 _apply_middlewares 等封装隔离
难以独立测试每个方法可以独立测试
类之间紧耦合类对外接口简洁

Toolkit 属于右侧——大,但内聚。

实际的拆分边界

虽然没有拆分类,但 Toolkit 的内部已经有清晰的边界:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 注册相关
toolkit.register_tool_function(...)
toolkit.register_mcp_server(...)

# 调用相关
toolkit.call_tool_function(...)
toolkit.call_tool_function_async(...)

# 配置相关
toolkit.register_middleware(...)
toolkit.create_tool_group(...)
toolkit.set_extended_model(...)

# 查询相关
toolkit.get_json_schemas(...)
toolkit.view_task(...)

这些方法组之间互不干扰。开发者通常只用其中 2-3 个方法组。


后果分析

好处

  1. 使用简单:一个类搞定所有工具操作
  2. 状态一致:不需要跨类协调
  3. 序列化简单:一个 state_dict 搞定

麻烦

  1. 代码审查困难:PR 改了哪个职责需要仔细看
  2. 认知负担:新开发者看到 1684 行会畏惧
  3. 合并冲突:多人修改同一文件容易冲突
  4. 导入开销:即使只用注册功能,也会加载中间件、MCP 等代码

横向对比

框架工具模块组织文件大小
AgentScope单文件 Toolkit1684 行
LangChainTool 基类 + 多个工具子类每个工具一个文件
AutoGen函数列表 + FunctionTool 包装分散
CrewAITool 基类 + 装饰器中等

AgentScope 官方文档的 Building Blocks > Tool Capabilities 页面按功能分组展示 Toolkit 的方法:Custom Tools(注册和调用)、Tool Groups(动态激活/停用)、Middleware(中间件链)和 Agent Skills(技能系统)。这些功能被组织在同一个 Toolkit 类中。

关于”大类”何时需要拆分,Refactoring Guru 的代码味道分类指出:

“Bloaters are code, methods and classes that have increased to such gargantuan proportions that they’re hard to work with. Usually these smells don’t crop up right away, rather they accumulate over time as the program evolves.”

对于 Large Class,推荐的处理方式包括:Extract Class(将部分行为抽取到独立组件)、Extract Subclass(将部分行为实现为不同变体)、Extract Interface(提取公共操作列表)。

Toolkit 目前 1684 行,如果继续增长,可以考虑将 Middleware 管理和 Skill 管理各自抽取为独立模块。



验证性实验:测量 Toolkit 的职责边界

目标:定量分析 Toolkit 单文件的职责分布。

步骤

  1. 运行 grep -c " def \| async def " src/agentscope/tool/_toolkit.py 统计 Toolkit 的方法数。

  2. wc -l src/agentscope/tool/_toolkit.py 获取总行数。

  3. 对你认为的 9 种职责(注册、调用、Schema、中间件、分组、序列化、Agent Skill、MCP 集成、工具响应)各分配一个方法计数。

  4. 挑战:尝试设计一个提取方案——把”中间件”职责拆到独立文件 _toolkit_middleware.py 需要改哪些 import?


你的判断

  1. 如果你是 AgentScope 的维护者,会把 Toolkit 拆分吗?如果会,按什么边界?
  2. 1684 行是”太大了”还是”刚好够用”?阈值在哪里?

横向对比:Toolkit vs ReActAgent

Toolkit 不是 AgentScope 中唯一的”大文件”。_react_agent.py 有约 1100 行,包含 reply_reasoning_acting_summarizing_compress_memory_if_needed 等方法。

对比两者:

维度Toolkit (1684 行)ReActAgent (~1100 行)
职责数量9 种5-6 种
内聚性围绕”工具”围绕”ReAct 循环”
扩展点中间件、MCP、SkillHook、自定义记忆
拆分可行性中等(Middleware 可抽离)低(循环步骤紧密耦合)

ReActAgent 的 _reasoning_acting_summarizing 形成紧密的循环依赖,拆分会增加状态传递复杂度。Toolkit 的中间件和 MCP 对接相对独立,是更自然的拆分边界。

模拟重构:提取 MiddlewareChain

如果将来 Toolkit 继续增长,最可能的重构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 当前:Toolkit 自己管理中间件
class Toolkit:
def register_middleware(self, middleware): ...
def _apply_middlewares(self, func, args): ...

# 重构后:中间件管理委托给专门类
class MiddlewareChain:
def __init__(self):
self._middlewares: list = []
def register(self, middleware): ...
async def apply(self, func, *args, **kwargs): ...

class Toolkit:
def __init__(self):
self.middlewares = MiddlewareChain()

好处:中间件逻辑可以独立测试,Toolkit 的构造函数更简洁。
代价:toolkit.register_middleware(...)toolkit.middlewares.register(...),调用链变长。

Refactoring Guru 的 Extract Class 原则建议:当一部分方法使用独立的数据子集时,适合提取。Toolkit 的中间件方法使用 _middlewares 列表,与 tools 字典无直接关系——满足提取条件。


下一章预告

Toolkit 的”大”是空间维度的问题。接下来我们看时间维度的设计选择——Hook 为什么在类定义时注入(编译期),而不是在调用时添加(运行时)?

下一章:第 32 章 编译期 Hook vs 运行时 Hook