第 13 章:模块系统——文件的命名与导入

难度:入门
你 clone 了仓库,打开
src/agentscope/看到一堆_开头的文件——_agent_base.py、_react_agent.py、_model_base.py……为什么有些文件有下划线前缀?导入路径又是怎么组织的?
上一章:旅程复盘
知识补全:Python 模块与包
Python 用文件和目录组织代码:
- 一个
.py文件就是一个模块(module) - 一个包含
__init__.py的目录就是一个包(package) from agentscope.agent import ReActAgent的意思是:从agentscope包的agent子包中导入ReActAgent
__init__.py 决定了 from package import X 时能导入什么。
命名规则:下划线前缀
打开 src/agentscope/ 的任意子目录:1
2
3
4
5
6
7
8
9src/agentscope/agent/
├── __init__.py
├── _agent_base.py # 下划线前缀 = 内部实现
├── _react_agent_base.py
├── _react_agent.py
├── _user_agent.py
├── _user_input.py
├── _a2a_agent.py
└── _realtime_agent.py
规则:以 _ 开头的文件是内部实现,不应被外部直接导入。公共 API 通过 __init__.py 暴露。
为什么这样做?
1 | # 不推荐:直接导入内部文件 |
__init__.py 充当”门面”,控制哪些类/函数对外可见:1
2
3
4# agent/__init__.py:3-6
from ._agent_base import AgentBase
from ._react_agent_base import ReActAgentBase
from ._react_agent import ReActAgent
这意味着:
- 内部文件可以自由重构——只要
__init__.py的导出不变,外部代码不受影响 - IDE 自动补全更干净——只显示公共 API,不显示内部实现
- 文档更有条理——公共 API 就是
__init__.py中列出的那些
设计一瞥:下划线前缀是 Python 社区的惯例,不是语言强制的。
Python 没有真正的”私有”概念。_前缀只是一种约定,告诉其他开发者”这是内部实现,请勿直接使用”。
但在框架设计中,这个约定非常重要——它定义了”公共 API 边界”,让框架作者可以在内部自由修改而不破坏用户代码。
导入路径的三层结构
AgentScope 的导入路径是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22agentscope # 顶层包
├── .agent # 子包
│ ├── ReActAgent # 公共类
│ ├── UserAgent
│ └── AgentBase
├── .model # 子包
│ ├── OpenAIChatModel
│ ├── AnthropicChatModel
│ └── ChatModelBase
├── .formatter
│ └── OpenAIChatFormatter
├── .tool
│ ├── Toolkit
│ └── ToolResponse
├── .memory
│ ├── InMemoryMemory
│ └── MemoryBase
├── .message
│ ├── Msg
│ └── TextBlock, ToolUseBlock, ...
└── .module
└── StateModule
顶层 __init__.py
src/agentscope/__init__.py 做了两件事:
1. 导入子模块(第 43-59 行):1
2
3
4
5
6from . import agent
from . import model
from . import formatter
from . import tool
from . import memory
# ... 等等
这让 agentscope.agent 这样的路径可用。
2. 定义 __all__(第 159 行开始):列出 from agentscope import * 时导出的名称。
子包的 __init__.py
每个子包的 __init__.py 从内部文件导入公共类:1
2
3
4
5
6
7
8
9
10
11
12# agent/__init__.py
from ._agent_base import AgentBase
from ._react_agent_base import ReActAgentBase
from ._react_agent import ReActAgent
from ._user_agent import UserAgent
from ._a2a_agent import A2AAgent
from ._realtime_agent import RealtimeAgent
__all__ = [
"AgentBase", "ReActAgentBase", "ReActAgent",
"UserAgent", "A2AAgent", "RealtimeAgent",
]
特殊模块
_run_config.py:全局配置
以 _ 开头但不在子包中,直接在 src/agentscope/ 下。它定义了 _ConfigCls——全局配置类,通过 ContextVar 实现异步安全。
_logging.py:日志配置
另一个顶层内部模块,被 init() 调用来设置日志。
_version.py:版本号
存储 __version__,被顶层 __init__.py 导入。
_utils/:工具函数
1 | src/agentscope/_utils/ |
框架内部使用的工具函数,不对外暴露。
试一试:追踪一个 import 语句
AgentScope 官方文档的 Basic Concepts 页面展示了 Msg、Agent、Model 等公共 API 的使用方法,而本章解释了这些公共 API 是如何从 _ 前缀的内部文件中暴露出来的。
AgentScope 1.0 论文对模块组织的设计说明是:
“we abstract foundational components essential for agentic applications and provide unified interfaces and extensible modules”
— AgentScope 1.0: A Comprehensive Framework for Building Agentic Applications, arXiv:2508.16279, Section 2
四大基础模块(Message、Model、Memory、Tool)各有统一的接口和可扩展的实现——这正是 _base.py 定义抽象类、__init__.py 暴露公共 API 的组织逻辑。
agentscope.init() 的模块发现机制
在 ch03 我们调用了 agentscope.init(),但没有展开它做了什么。现在我们知道了模块系统的组织方式,可以理解 init() 的模块发现逻辑了。
打开 src/agentscope/__init__.py,init() 函数(大约第 80 行)做了这些事:1
2
3
4
5
6
7
8
9def init(project, ...):
# 1. 设置全局配置
_ConfigCls(...)
# 2. 导入所有子模块(触发子模块的 __init__.py)
from . import agent, model, formatter, tool, memory, ...
# 3. 设置日志
_logging.setup()
第 2 步是关键——from . import agent 会执行 agent/__init__.py,后者又导入了 _agent_base.py、_react_agent.py 等所有内部文件。这意味着:
- 子模块的公共 API 在
init()时就被注册了 - 用户不需要手动 import 每个子模块
- 新增的子模块只需要在顶层
__init__.py中加一行from . import new_module
__all__ 的作用
__all__ 定义了 from agentscope import * 时导出的名称。它的实际用途:1
2# agentscope/__init__.py
__all__ = ["init", "agent", "model", ...]
- IDE 提示:
__all__中的名称会被 IDE 列为自动补全候选项 - 文档生成:Sphinx 等文档工具读取
__all__来确定要文档化的对象 - import * 控制:防止
from agentscope import *导入不需要的内部对象
检查每个子包暴露了哪些公共类:1
2grep "from \." src/agentscope/agent/__init__.py
# 输出所有公共 API 的导入行
目标:理解 from agentscope.agent import ReActAgent 背后发生了什么。
步骤:
- 打开
src/agentscope/agent/__init__.py,找到ReActAgent的导入行 - 追踪到
src/agentscope/agent/_react_agent.py,看第 98 行的类定义 - 再看
ReActAgent继承自ReActAgentBase,在_react_agent_base.py中 ReActAgentBase继承自AgentBase,在_agent_base.py中
这条继承链跨越了 3 个文件。但用户只需要写 from agentscope.agent import ReActAgent。
- 进阶:搜索
__init__.py中的__all__列表,看看每个子包暴露了多少个公共类:
1 | grep -c "^ \"" src/agentscope/agent/__init__.py src/agentscope/model/__init__.py src/agentscope/tool/__init__.py |
调试实践:验证公共 API 边界
目标:确认 _ 前缀的内部文件确实不能被直接导入。
步骤:
- 尝试直接导入内部文件(会成功但会有警告):
1 | # 这可以工作但不推荐 |
- 对比推荐方式:
1 | from agentscope.agent import ReActAgent |
- 检查两者是否完全相同:
1 | from agentscope.agent._react_agent import ReActAgent as R1 |
- 进阶:用
grep统计每个子包的公共 API 数量 vs 内部文件数量:
1 | echo "=== 公共 API ===" |
检查点
你现在理解了:
_前缀文件是内部实现,公共 API 通过__init__.py暴露- 导入路径是
agentscope.子包.公共类,不是直接导入_文件 - 顶层
__init__.py负责导入子模块和定义__all__ - 这种命名约定定义了框架的公共 API 边界
自检练习:
- 如果你添加了一个新的 Agent 类型
_my_agent.py,需要修改哪个文件才能让用户用from agentscope.agent import MyAgent导入它? _utils/下的工具函数为什么不对外暴露?
下一章预告
下一章我们打开继承体系,从 StateModule → AgentBase → ReActAgentBase → ReActAgent 的四层继承链,看每一层提供了什么能力。
下一章:第 14 章 继承体系