news 2026/10/20 4:08:17

搞懂 Python __init__.py:包管理、导入机制与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
搞懂 Python __init__.py:包管理、导入机制与工程实践

1. 从一次导入失败说起:init.py 到底在解决什么问题

如果你写过 Python 超过三个月,大概率遇到过这种场景:自己精心组织了一个项目目录,把功能拆分到不同文件里,然后想在另一个脚本里 import 进来,结果迎面一个 ModuleNotFoundError。于是你开始百度,答案千篇一律地说“加一个init.py 文件”,你加了,问题确实解决了,但心里始终不明白这个东西是个啥。

我在带新人的时候经常说一句话:init.py 不是一张贴在目录上的“包身份证”,它是一个真实存在的 Python 模块,而且每个包在被 import 的那一瞬间,都会先执行它。你完全可以把这个文件当成包的门面、入口、甚至“构造函数”。搞懂它,才算真正理解了 Python 的模块与包管理机制。

这篇文章面向的读者,是从刚学会 def 和 import 的新手,到已经在写多文件项目、却经常被相对导入和循环依赖折磨的进阶用户。我不会只告诉你“加个文件就完事”,而是把它拆开,讲清楚文件的作用机制、设计逻辑、实战用法和那些只有踩过坑才写得出来的经验。整个内容围绕 Python 的init.py 展开,从入门用法讲到进阶模式,最后给你一份常见报错排查表。

先抛一个最基础的问题:Python 里“包”到底是什么?很多人以为“包 = 文件夹”,这是错的。准确地说,包是一个包含模块的命名空间,它在文件系统上的体现通常是目录,但目录要成为包,历史上必须满足一个条件——目录里有init.py 文件。在 Python 3.3 之前,这叫“常规包”,没有这个文件的目录即使放在路径里,Python 也不认它是包。Python 3.3 之后引入了“命名空间包”,情况有了变化,这个后面我会单独讲,但你先记住:常规包靠init.py 标识,它负责初始化包、暴露接口、控制导入行为。

2. 入门用法:把init.py 用对,你的项目结构瞬间清爽

2.1 最朴素的用法:空文件标识目录为包

先看最简单的场景。假设你的项目长这样:

my_project/ ├── utils/ │ ├── __init__.py │ ├── string_ops.py │ └── file_ops.py └── main.py

utils 目录下的init.py 一开始是空的,它存在的唯一意义就是告诉 Python:这是一个包,你可以用from utils.string_ops import clean_text这种方式导入。没有这个文件,Python 3.3 之前直接报错;Python 3.3 之后虽然能作为命名空间包导入,但很多工具链(比如 setuptools 的 find_packages、某些IDE的代码补全、pytest 的测试收集)对命名空间包的支持并不完美,所以常规做法还是老老实实放一个空文件。

我知道有人嫌麻烦,心想“现在 Python 不是支持不带init.py 的命名空间包了吗?我不放行不行?”行,但我不建议你在业务项目里这么干。原因有三:第一,不同工具对命名空间包的处理方式有差异,你依赖的第三方库可能不认;第二,没有init.py 的目录在解释器里显示的__name__是namespace,区别于常规包的package,调试时容易产生困惑;第三,规范统一比省一个空文件重要得多,团队协作时每个包都有init.py,心智负担最低。

2.2 控制导入范围:all的妙用

空文件虽然能用,但你很快就会遇到一个尴尬:包里的模块太多了,每次都要写from utils.string_ops import xxx,路径很长很啰嗦。这时候init.py 的第一个进阶价值就出来了——它可以在包这层做一次“再导出”。

我在 utils/init.py 里写成这样:

# utils/__init__.py from .string_ops import clean_text, split_words from .file_ops import read_json, write_json __all__ = ["clean_text", "split_words", "read_json", "write_json"]

这样一来,外部调用方可以直接写:

from utils import clean_text, read_json

而不是钻进深层路径里找模块。这就是把init.py 变成“包的门面”:外部使用者只需要了解包暴露出来的 API,内部模块怎么拆分、怎么命名,都是可以实现细节。你以后要重构 string_ops 内部实现,只要保持 clean_text 这个函数签名不变,调用方一行代码都不用改。

这里要注意all的作用场景。它的准确含义有两个:一个是约束from package import *的行为,只有all列出的名字会被导入;另一个是在 IDE 和静态分析工具中作为约定,提示这个模块对外公开的接口有哪些。这两个作用是完全不同的,前者是运行时行为,后者是开发期约定。写项目时我建议始终维护all,哪怕你暂时不打算用星号导入——它是一份免费的 API 文档,你把鼠标悬停在包名上时,IDE 会直接给你列出all里的东西。

2.3 包级变量与初始化逻辑:init.py 是包的“构造函数”

Python 的 import 和函数调用很像,一个包第一次被导入时,解释器会完整执行这个包对应init.py 里的全部代码,而且只执行一次(后续导入走 sys.modules 缓存)。所以你可以把init.py 理解为包的构造函数或者入口文件。

我见过很多项目在init.py 里干这些事:

  • 定义包级常量或版本号:VERSION = "1.2.0"
  • 初始化日志配置:logging.getLogger(__name__).addHandler(...)
  • 检查依赖环境,缺了就抛异常给出友好提示
  • 设置全局默认值,比如pandas.set_option("display.max_columns", 100)
  • 加载包内的静态资源、读取配置文件

这里有一道经典的面试题:import package和import package.module有何区别?答案是:前者只执行了包的init.py,并不会自动导入包内的子模块;后者同样会先执行包的init.py,然后再导入指定子模块。也就是说,init.py 已经执行完毕,你才可能访问到包里的任何子模块。知道这一点,你就明白为什么不能指望“import 了包,子模块就自动可用了”——除非你在init.py 里主动显式导入它们。

举个我实际处理过的项目。当时做一个数据采集框架,目录结构是这样的:

collector/ ├── __init__.py ├── sources.py ├── processors.py └── exporters.py

业务方希望外部只通过一句from collector import run就能用,不需要关心 sources、processors、exporters 这些内部模块。于是我在init.py 里做了一层组合:

# collector/__init__.py from .sources import get_source from .processors import process from .exporters import export def run(config): data = get_source(config["source"]) result = process(data, config.get("rules", [])) export(result, config["target"]) return result __all__ = ["run", "get_source", "process", "export"]

这就是把init.py 当作包级“编排层”的典型模式。内部模块各司其职,组合逻辑放在包入口处,外部使用体验非常清爽。我特别推荐小团队项目采用这种做法,因为它天然形成了分层:使用方只看包暴露的接口,开发方在内部分解模块,互不干扰。

2.4 手动整理包结构:控制包的“外部形状”

说到包的门面效应,还有个更极致的玩法:包的外部形态可以完全不同于内部文件结构。假设你有一个包叫 analysis,内部模块是 data_loader.py、eda.py、modeling.py,但你希望外部用户能用analysis.load、analysis.describe、analysis.fit这种简洁而统一的命名,甚至想把不同子模块里的函数合并到一个命名空间下。你完全可以在init.py 里做这种“重新映射”。

# analysis/__init__.py from .data_loader import load_data as load from .eda import describe from .modeling import LinearModel # 业务内部还保留了原来的模块路径,外部感知不到这些细节

这种做法的价值体现在两个地方:一是隐藏内部实现细节,包怎么拆模块都可以灵活调整,外部 API 稳定不变;二是可以“扁平化”深层嵌套的包结构,避免使用者写出from analysis.subpackage.module import func这种又臭又长的导入。我在后端接口层使用过这种模式,效果很好,团队里新同学上手几乎没有学习成本,因为所有功能都能从一个“总入口”摸到。

3. 进阶模式:把init.py 变成包的智能门面

3.1 批量导出与延迟导入:在性能与体验之间找平衡

如果你把包内所有子模块都在init.py 里直接 import,代码写起来确实方便,但会带来一个隐患——启动时间变长。尤其是当一个包有几十个子模块,每个子模块又依赖重量级第三方库(比如 pandas、numpy、matplotlib)时,一次导入可能要多花好几秒。

我在一个数据分析库的设计中尝试过“延迟导入”方案,核心思路是利用 Python 3.7 引入的 PEP 562:模块可以定义__getattr__函数,当访问模块中不存在的属性时,解释器会调用这个函数。这就给了我们一个机会——把精确导入放到首次访问的那一刻。

先看一个简化版的例子,这是 data_tools/init.py 的代码:

# data_tools/__init__.py __all__ = ["load", "transform", "plot"] def __getattr__(name): if name == "load": from .loader import load return load if name == "transform": from .transformer import transform return transform if name == "plot": from .visualizer import plot return plot raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

这样写的好处是,外部调用from data_tools import load时,loader 模块才会真正导入;如果你只是想用from data_tools import plot,那 transformer 的依赖(比如一个很重的库)就不会被加载。对于很多数据工具类包,这种方式能把冷启动时间缩短一半以上。

但要注意,延迟导入也带来一个代价:访问不存在的属性时,IDE 的自动补全一般不会提示,因为__getattr__是动态执行的,静态分析器很难识别。我的建议是,你自己维护一个init.py 的 API 文档注释,或者依赖all让 IDE 至少给出列表提示,别让使用者对着一个“空包”无从下手。

3.2 版本信息与包元数据:init.py 里的信息管理

几乎所有主流 Python 库都习惯在init.py 里维护version、author、license等元数据。为什么是这里?因为它是包被导入时最先执行的文件,任何代码都能通过package.__version__拿到版本号,而不需要深入子模块。

我在发布自己的开源包时,通常会在init.py 里建立唯一的版本权威来源:

# mypackage/__init__.py __version__ = "2.1.0" __author__ = "Your Name" __license__ = "MIT"

然后在 setup.py 或 pyproject.toml 里读取这个变量,保证打包发布和运行时版本完全一致,避免“代码里版本号改了,发布配置忘了改”这种低级事故。使用 setuptools 时,可以通过配置让打包过程只读init.py 里的版本:

[project] name = "mypackage" dynamic = ["version"] [tool.setuptools.dynamic] version = {attr = "mypackage.__version__"}

这是个细节,但很重要。版本号同步问题我至少见过三四个项目踩坑:代码里已经大改版了,PyPI 上还是旧版本,用户 pip install 拉下来的根本不是最新代码。把版本号收口到init.py 之后,这个家族问题就根治了。

3.3 条件导入与平台适配:一份代码覆盖多种环境

init.py 里还可以做平台判断和依赖适配。因为它在 import 的第一时间执行,所以非常适合做“根据当前环境决定暴露什么 API”的场景。举个例子,如果你的包同时支持不同操作系统上的不同底层实现,你可以在init.py 里这样写:

# platform_adapter/__init__.py import sys if sys.platform == "win32": from ._windows_impl import run elif sys.platform == "linux": from ._linux_impl import run else: from ._posix_impl import run __all__ = ["run"]

这个模式在工业级库里非常常见,它把平台差异彻底隔离在包入口的适配层里,外部调用者看到的是一个统一的 run 函数。同样道理,你也可以根据 Python 版本来做特性适配,比如 Python 版本太低时自动切换到一个兼容实现。这种“适配器模式”的实现位置,选在init.py 是语义上最合理的——它就是包面向调用方的第一个接触点。

3.4 命名空间包:没有init.py 的包怎么玩

我在前面提到过 Python 3.3 引入的命名空间包。它允许一个包被拆分到多个目录中,每个目录都没有init.py,但它们在逻辑上属于同一个包。这是怎么做到的呢?机制是 sys.path 中每个目录都被扫描,所有匹配包名的命名空间分段被合并成一个命名空间包。

举例来说,你的项目包含两部分:

part_a/ └── plugin_system/ ├── core.py part_b/ └── plugin_system/ ├── extra.py

如果把 part_a 和 part_b 都加入 sys.path,那么import plugin_system得到的将是一个组合后的命名空间包,plugin_system.core和plugin_system.extra都能访问。这种机制特别适合插件系统:主程序提供核心目录,第三方插件在完全独立的位置注册扩展,两者不冲突。但现实中使用命名空间包要注意两个问题:一是部分打包工具(比如旧版 setuptools)对它的支持需要显式配置namespace_packages参数,处理不当会发布失败;二是调试时它没有唯一的init.py 执行点,所以很难理解“初始化发生在哪里”。因此,除非你做插件体系,否则我还是建议你使用常规包。

4. 避坑指南:init.py 里最容易踩的五个坑

4.1 循环导入:init.py 是重灾区

循环导入是 Python 开发中最经典、最恶心的错误之一,而它发生的频率在init.py 里尤其高。典型场景是你在init.py 里导入了子模块 A,A 的代码反过来想要访问包级定义的某个函数或变量,结果要么是 ImportError: cannot import name,要么是诡异的部分初始化状态。

我举个真实例子。假设你的包结构是:

app/ ├── __init__.py ├── config.py └── settings.py

app/init.py 里写了:

from .config import load_config

而在 config.py 内部又做了from app import APP_NAME,如果 APP_NAME 是定义在init.py 里的变量,此时就是典型的循环依赖:init.py 执行到 from .config 时,config 模块又被触发,config 反过来要 import app 包,但 app 还没初始化完成,APP_NAME 尚未定义,于是报错。

解决这个问题有几种思路,我的优先级是:

  • 把共享的常量定义下沉到一个不依赖任何模块的独立文件里,比如 constants.py,让其他模块都从它那里引用,断开循环。
  • 把init.py 里的“编排性导入”放到文件末尾,等包级基础变量定义完,再做子模块导入。
  • 在子模块里把 from app import xxx 改成 from .constants import xxx,用相对导入避免触发整个包。

最糟糕的做法是把正确的“修复”理解成“调整导入顺序就好”——循环依赖本质上是你把模块职责分错了,换个顺序只是治标,重构依赖关系才是根治。

4.2 相对导入的边界:在init.py 里使用相对导入要注意层级

相对导入是init.py 中常用的导入方式,比如from .module_a import func。相对导入以点开头,一个点代表当前包,两个点代表上层包,这在包内部非常好使,因为它不依赖你安装到哪个站点目录,也不依赖绝对路径。但相对导入有一个致命边界——只有在一个包内被导入时才有效,如果你把某个子模块当作顶层脚本执行(python app/module_a.py),那么from .xxx import yyy必然报错:ImportError: attempted relative import with no known parent package。

这个问题在使用命令行运行测试、Jupyter Notebook 调试时特别常见。我遇到过同事在 Notebook 里直接写%run module_a.py,然后报错,他以为是 Python 环境坏了,折腾半天才发现是相对导入不支持顶层脚本执行。规避方式:保证任何模块的“入口”都在包外,所有包内文件都不应直接执行;或者入口文件主动修改 sys.path,并采用绝对导入。我在企业内部讲课总说一句话:包内的模块是“公民”,不是“主角”,不要轻易拿脚本方式去运行它们。

4.3 不要在init.py 里做重活

init.py 虽然承担初始化任务,但绝对不适合做重量级计算或者启动耗时操作,比如连接数据库、下载大文件、启动线程池。原因是它会在 import 时执行,而 import 在很多框架里是“冷启动路径”——加载慢,整体响应时间就会恶化。尤其当你的包被其他库复用时,import 路径会被无限叠加,一个init.py 多花 0.5 秒,依赖链上就有可能出现肉眼可见的延迟。

我记得排查过一个内部工具库,导入它总是要等三秒以上。翻代码后发现它在init.py 里做了 subprocess 调用检查外部程序版本。后来我把那段检查挪到真正用到的模块里,并且做懒加载缓存,导入时间立刻降到了两百毫秒以内。这是很典型的问题:初始化代码写得“爽”,使用的人就要买单。所以init.py 里应只做轻量、必要且不依赖外部服务的初始化。

4.4all只约束星号导入,不要误解它

不少人以为写了all就“只能导入列出的名字”,其实这是对all作用范围的误解。from package import *确实只会导入all列出的名字。但是如果你用import package然后访问package.some_attr,或者直接from package import some_internal_name,它依然能访问到all之外的任何已定义属性。all更像公共接口的“白名单”,而不是一个“防火墙”。如果你想彻底隐藏一个模块,让它无法被外部直接导入,靠all是做不到的——你需要使用单下划线命名约定(比如_internal.py),同时不把它暴露在init.py 里,再配合静态检查工具的配置。

这个误解在团队协作中经常引发问题:有人给包定义好了all,以为内部的私有函数不会被外部引用,结果别人照样 import 了私有模块,后面一重构,代码全炸。我的经验是,包的“公开”与“私有”边界,一定要通过命名、文档、代码审查三方面共同保证,不要指望__all__一个人扛。

4.5 不要重复执行:注意 sys.modules 缓存机制

再强调一次:一个包在整个 Python 进程生命周期内,init.py 只会执行一次,之后 import 直接从 sys.modules 拿缓存。这意味着如果你在包内做“重导入”(比如导入后修改代码再 import),变动不会生效,必须 reload。很多新手在交互式环境下调试包代码时特别容易踩这个坑,在init.py 里改完东西重新 import 没反应,以为自己改错了,其实是解释器缓存了旧版本。解决办法是用importlib.reload(package),或者干脆重启内核。同时也说明,init.py 里如果有“全局可变状态”,它会在进程内共享——这在并发程序里会造成隐性的状态污染,写代码时要格外注意。

5. 实战排查:常见init.py 相关报错与解决实录

5.1 ModuleNotFoundError / ImportError 排查思路

这是最高频的报错,现象是ModuleNotFoundError: No module named 'xxx'。排查次序我建议这么走:

  • 确认模块实际存在,文件名、包名拼写有没有错误(包括大小写)。
  • 确认它所在的目录是否被 sys.path 覆盖。可以临时打印sys.path查看,或者在脚本开头sys.path.insert(0, "/path/to/project")试一下。
  • 确认包目录是否包含init.py。如果是旧工具链或者 IDE 的解析器,缺失init.py 时常常识别不了。
  • 检查是否有重名干扰:有没有一个本地的 xxx.py 文件和 site-packages 里的第三方包同名,把官方包“遮蔽”掉了?这种情况我见得太多了,尤其是命名特别通用的模块,比如 utils.py 或 models.py。它会导致你 import 的其实是自己的文件,而非第三方库。

5.2 真实案例:torch/cuda/init.py 的 UserWarning 和 ctypes 加载报错

我在热搜词里看到有人提到torch/cuda/__init__.py:180: userwarning。这个我很熟悉:在 Windows 上跑 PyTorch 相关项目时,经常能看到__init__.py里的警告信息,第 180 行附近一般是在检测 CUDA 是否可用,如果没装显卡驱动或版本不匹配,它就在包初始化时抛出 warning。这不是程序致命错误,但它揭示了init.py 在导入时真实执行了探测逻辑。如果这个警告让你焦虑,可以用 warnings 模块控制:

import warnings warnings.filterwarnings("ignore", category=UserWarning)

但请只在确定不需要该信息时才屏蔽它,否则后续排查会失去线索。同理,热搜里提到的ctypes\__init__.py, line 351, in __init__这类 traceback,本质是 ctypes 这个标准库包的init.py 在运行时抛出了异常,通常是 Windows 下加载某些 DLL 失败。看这个报错时,不要被路径里的init.py 迷惑,真正的根因往往是依赖的动态库缺失或位数不匹配。记住一个原则:init.py 只是把你的代码“掐”在这里抛错,根因往往在它导入的更底层依赖里。

5.3 报错速查表

下面是我整理的init.py 相关报错速查表,适合贴到团队文档里:

报错特征常见原因首选解决方式
ModuleNotFoundError: No module named 'xx'模块不存在、路径未加入 sys.path、拼写错误检查 sys.path,确认模块文件存在
ImportError: attempted relative import with no known parent package子模块被当作顶层脚本运行从包外入口导入,或修改 sys.path
ImportError: cannot import name 'xx'循环导入、属性在导入时刻尚未定义重构依赖关系,下沉共享常量
包导入非常慢init.py 里做了重量级初始化迁移到子模块或做懒加载
修改包代码后 import 无效sys.modules 缓存了旧模块使用 importlib.reload 或重启进程
第三方库在init.py 输出警告依赖探测失败或版本不匹配定位根因,必要时用 warnings 过滤

5.4 一段实际排查对话(简化版)

有一次同事给我发了段代码,说在本地跑很正常,一上服务器就报ModuleNotFoundError。我让他先打印 sys.path,发现服务器上的项目根目录没有被自动加入,因为入口脚本是通过 cron 触发的,工作目录和项目目录不一致。解决方案很简单,在入口脚本最前面加了一段:

import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

然后再 import 他那个包,问题立刻解决。很多人觉得 sys.path 是玄学,实际它就是 Python 找模块的顺序清单。理解了这个顺序,大部分模块缺失问题都能自己排查掉,不需要瞎碰。

6. 把init.py 变成你的开发利器

说了这么多,最后聊一点我个人的经验。我见过很多项目,init.py 永远是一个空白文件,这没错,但也是浪费。真正把 Python 包用得好的团队,会把init.py 当作包最重要的 API 设计文件来对待:每个init.py 里都清楚写明了对外暴露哪些函数、内部结构如何组织、版本号是多少、依赖关系是什么方向。新成员接手项目时,第一件事就是阅读每个包的init.py,这比看任何架构文档都快。

如果你今天只想带走一个操作建议,那就是:从下一个新包开始,不要只放空文件。先在init.py 里定义version,再用相对导入把最核心的接口 re-export 出去,顺手维护一份all。这三步做完,你的包就已经具备了一个专业 Python 包的雏形,后续不管是做测试、写文档、发布到包管理平台,都会顺畅很多。

从入门到进阶,init.py 的本质其实就是一句话:它是包在 Python 解释器里的“出生程序”,控制着包如何被首次唤醒、暴露什么外表、隐藏什么细节、如何调度内部模块。把这个文件用好了,你的模块与包管理就能从“能跑”升级到“好维护、好扩展”,而这恰恰是 Python 项目从玩具走向工程的分水岭。

一点小补充:如果你正在用from package import *,我建议逐步改成显式导入。显式导入能让你看到依赖来源,也让 IDE 的跳转和重构准确得多。这不是什么教条,而是我在大型项目里见过太多星号导入导致命名冲突后的切身体会。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:56:49

HED边缘检测实战:从VGG16到多任务学习的深度学习流水线

简介:HED_edgeDetect 是一份面向计算机视觉初学者与深度学习实践者的边缘检测资源包,围绕 HED(Hypercolumns for Edge Detection)这一基于卷积神经网络的端到端边缘检测方法展开,可用于理解多尺度特征融合、预训练与微…

作者头像 李华
网站建设 2026/10/16 0:32:08

Cursor高效配置四步法:用规则驱动代码减量

1. 项目概述:这不是在教你怎么点开设置,而是在重建你和代码的协作关系“Cursor怎么配置才好用?这套规则让我少写一半代码”——这句话我第一次看到时,手停在键盘上三秒。不是因为夸张,而是太真实。过去两年&#xff0c…

作者头像 李华
网站建设 2026/10/14 9:23:17

Linux压缩解压缩:从tar归档到zstd流式处理的工程实践

1. 项目概述:为什么“Linux 压缩与解压缩”不是一句命令,而是一套生存技能?在某高校实验室部署一批边缘计算节点时,我遇到过一个典型场景:运维同事发来一条消息:“打包失败,tar: Cannot write t…

作者头像 李华
网站建设 2026/10/15 14:36:38

AI漫剧量产80集:模块化管线与角色一致性实战

1. 从“Seedance已死”说起:AI漫剧量产的真实门槛在哪里“Seedance已死”这句话最近在AI漫剧圈子里传得很凶,乍一听像是某个工具彻底凉了,但实际拆开看,它说的根本不是某个具体产品挂了,而是一种旧的生产方式被淘汰了。…

作者头像 李华
网站建设 2026/10/14 11:21:45

claude-mem记忆系统实战:分层架构、写入压缩与召回优化

1. 从零理解 claude-mem:它到底在解决什么问题第一次看到 claude-mem 这个名字,很多人会以为它只是某个记忆插件的小玩具。但真正在长会话场景里折腾过的人都知道,上下文窗口再大,也扛不住几十轮对话之后模型开始“失忆”——前面…

作者头像 李华
网站建设 2026/10/14 13:18:35

多智能体协作框架agency-agents:架构设计与实战踩坑指南

1. 从"agency-agents"这个名字说起:它到底想解决什么问题第一次看到agency-agents这个命名,我的直觉是:这大概率是一个围绕"代理"或"智能体"概念构建的模块化系统,而且agency这个词暗示了它不是一个…

作者头像 李华