ADK Feature Flags 机制全解析:用ADK_ENABLE_*/ADK_DISABLE_*掌控 adk-python 的实验性功能
【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python
ADK(Agent Development Kit,即本仓库 adk-python)将尚未稳定、API 可能变化的特性统一收口在特性注册表(Feature Registry)中,通过ADK_ENABLE_<NAME>/ADK_DISABLE_<NAME>环境变量或 Python API 按需开关。本文以官方指南 docs/guides/features/feature_registry/index.md 为主线,结合 src/google/adk/features 源码与 tests/unittests/features 测试,完整讲解特性开关的三级解析顺序、三种生命周期阶段、UserWarning与RuntimeError的触发原理,并给出测试隔离与部署可复现的实战方案。
为什么需要特性注册表:一次警告与一次报错
先看两个最典型的“遇见”场景。当你构造某个尚不稳定的对象(比如GCSToolset)时,可能会收到一条UserWarning:
[EXPERIMENTAL] feature GCS_TOOLSET is enabled.当你主动关闭了某个特性、又去构造对应的类时,则会抛出RuntimeError:
RuntimeError: Feature GCS_TOOLSET is not enabled.两者都来自特性注册表,但含义完全不同:
UserWarning只是提示:你正在使用一个可用但 API 可能变化的特性,一切正常,没有任何东西出错;RuntimeError表示:守卫该类别的开关是关的,类拒绝被构造,失败是立即且彻底的,不会留下半成品对象。
注册表存在的根本目的,是让 ADK 能把新特性交付给想要它的用户,同时不改变其他人的默认行为。每个 flag 都携带一个默认值和一个生命周期阶段,见下文。
三种生命周期阶段(FeatureStage)
源码 src/google/adk/features/_feature_registry.py 中用FeatureStage枚举定义了三个阶段,与官方文档的划分一致:
| 阶段 | 枚举值 | 行为 |
|---|---|---|
| Stable(稳定) | FeatureStage.STABLE | 默认开启、静默运行,无任何警告 |
| Experimental(实验性) | FeatureStage.EXPERIMENTAL | 按成熟度默认开或关,运行时每个进程警告一次 |
| Work in progress(开发中) | FeatureStage.WIP | 默认关闭,仅供 ADK 内部开发使用 |
每个 flag 在注册表中对应一个FeatureConfig(见 _feature_registry.py),由stage和default_on两个字段组成。中央注册表_FEATURE_REGISTRY(_feature_registry.py)集中定义了当前版本全部 flag 的阶段与默认值。以本仓库当前版本为例:
- Stable 且默认开:
BIG_QUERY_TOOLSET、BIG_QUERY_TOOL_CONFIG、DATA_AGENT_TOOLSET、DATA_AGENT_TOOL_CONFIG、SKILL_TOOLSET; - Experimental 且默认开(占大多数):
GCS_TOOLSET、SPANNER_TOOLSET、COMPUTER_USE、JSON_SCHEMA_FOR_FUNC_DECL、FALLBACK_MODEL、MCP_AGENT_SERVER、PLUGGABLE_AUTH等; - Experimental 且默认关:
DYNAMIC_INSTRUCTION_ROUTING、SNAKE_CASE_SKILL_NAME; - WIP 且默认关:
IN_MEMORY_SESSION_SERVICE_LIGHT_COPY。
此外还有一个带下划线前缀的私有成员_MCP_GRACEFUL_ERROR_HANDLING(_feature_registry.py),源码注释明确说明它不属于公共 API 表面,只是一个临时的内部 kill-switch,通过ADK_ENABLE_MCP_GRACEFUL_ERROR_HANDLING=1由内部开启,不应对其产生向后兼容义务。
注意:flag 集合会随版本变化——特性毕业(转 Stable)时会被移除,新特性会加入。务必以你实际安装版本中
list(FeatureName)的输出为准,下文会展开。
快速上手:环境变量与 Python API 两种开关方式
方式一:环境变量(作用于整个进程)
在启动进程之前设置环境变量即可。开启一个特性:
export ADK_ENABLE_SNAKE_CASE_SKILL_NAME=1关闭一个特性:
export ADK_DISABLE_JSON_SCHEMA_FOR_FUNC_DECL=1变量名的规则是ADK_ENABLE_或ADK_DISABLE_前缀加上 flag 的名字,而 flag 名字就是FeatureName成员本身拼写的原样(如SNAKE_CASE_SKILL_NAME、JSON_SCHEMA_FOR_FUNC_DECL)。
取值判定是个易错点:只有1和true(大小写不敏感)算“被设置”,其余任何值——包括yes、on、0——都视为未设置。这一点由底层工具函数 is_env_enabled 保证,其实现为os.environ.get(env_var_name, default).lower() in ['true', '1']。也就是说ADK_ENABLE_X=0并不会关闭 X,而是让解析逻辑“穿透”到下一级规则(即注册表默认值);要真正关闭一个默认开启的特性,必须用ADK_DISABLE_X=1。
方式二:Python API(适用于 Notebook、测试等场景)
在环境变量不方便的地方(如 Jupyter Notebook 或单元测试中),可以在构造任何读取该 flag 的对象之前,用 Python 代码设置:
from google.adk.features import FeatureName from google.adk.features import is_feature_enabled from google.adk.features import override_feature_enabled override_feature_enabled(FeatureName.SNAKE_CASE_SKILL_NAME, True) assert is_feature_enabled(FeatureName.SNAKE_CASE_SKILL_NAME)FeatureName是一个str枚举(class FeatureName(str, Enum),见 _feature_registry.py),因此可以直接枚举当前版本的全部 flag:
from google.adk.features import FeatureName print(list(FeatureName))注意FeatureName的成员集合在不同版本间会增删,请以你安装的版本为准。
工作原理:is_feature_enabled的三级解析顺序
is_feature_enabled(_feature_registry.py)按以下优先级解析一个 flag,命中即返回:
- 程序化覆盖(最高优先级):如果对该 flag 调用过
override_feature_enabled,其值直接胜出; - 环境变量:先查
ADK_ENABLE_<NAME>,值为1或true时返回True;再查ADK_DISABLE_<NAME>,同值返回False。两者同时设置时,Enable 胜出; - 注册表默认值:每个 flag 的
FeatureConfig中记录的阶段与默认值即为最终答案。
这一顺序带来两个重要推论,官方文档与源码实现(_feature_registry.py)一致:
ADK_DISABLE_X=1无法关掉已被程序化覆盖打开的 flag。因此一个库如果调用了override_feature_enabled,就等于把决策权从部署者手中拿走;- 覆盖一旦设置就无法通过公共 API 撤销,只能翻转为另一值。所以在测试里调用
override_feature_enabled会“泄漏”到同进程中后续的所有测试——这是下文“把 flag 限定在单个测试内”一节的动机。
警告只会发一次
当某个非 Stable 阶段的 flag 解析结果为“启用”时,is_feature_enabled内部会调用_emit_non_stable_warning_once(_feature_registry.py),发出UserWarning,文本以[EXPERIMENTAL] feature或[WIP] feature开头并点名该 flag。实现上用_WARNED_FEATURES集合记录已警告过的 flag(_feature_registry.py),保证每个 flag 每进程只警告一次——即使它被反复读取。警告纯粹是信息性的,看到它不代表任何失败;如果觉得它是日志噪音,可用标准的warnings过滤器屏蔽,例如:
import warnings warnings.filterwarnings("ignore", message=r"\[EXPERIMENTAL\] feature .* is enabled.")无缓存:每次都实时读取
解析过程没有任何缓存:每次调用都会重新读取覆盖字典和os.environ(源码中直接查询_FEATURE_OVERRIDES与is_env_enabled(enable_var))。因此进程中途修改环境变量是立即生效的。但“生效与否”取决于该 flag 何时被读取——不同特性读取时机不同:有的在每次调用时读取,有的只在对象构造时读取一次(详见下文“一个 flag 到底管住什么”)。
一个 flag 到底管住什么:两种执行风格
源码中 flag 的落地有两种风格,症状完全不同。
风格一:受守护的单元(gated unit)。一些类和函数带有@experimental、@working_in_progress或@stable装饰器(定义见 src/google/adk/features/_feature_decorator.py)。这些装饰器由_make_feature_decorator统一生成:装饰类时包装__init__,装饰函数时包装调用本身,在真正执行前调用is_feature_enabled;若 flag 关闭,则抛出RuntimeError: Feature <name> is not enabled.(_feature_decorator.py)。官方文档点名的GCSToolset、SpannerToolset、ComputerUseTool以及 agent-config 加载器都属于这一类。在仓库中可以看到大量实际用例,例如:
@experimental(FeatureName.AGENT_CONFIG)用于 src/google/adk/agents/agent_config.py、src/google/adk/agents/base_agent_config.py 等 agent 配置类;@experimental(FeatureName.AGENT_STATE)用于 src/google/adk/agents/base_agent.py、src/google/adk/agents/loop_agent.py;@experimental(FeatureName.PLUGGABLE_AUTH)用于 src/google/adk/auth/auth_provider_registry.py 与 src/google/adk/auth/base_auth_provider.py;@experimental(FeatureName.GCS_ADMIN_TOOLSET)用于 src/google/adk/integrations/gcs/admin_toolset.py;@experimental(FeatureName.DAYTONA_ENVIRONMENT)/@experimental(FeatureName.E2B_ENVIRONMENT)分别用于 src/google/adk/integrations/daytona/_daytona_environment.py 与 src/google/adk/integrations/e2b/_e2b_environment.py。
对象不是“半构建”的——失败是立即且彻底的。就目前发布的版本而言,这类被装饰器守护的 flag 默认都是开启的,所以只有在你主动禁用某个 flag 之后,才会碰到这个RuntimeError。
风格二:受守护的代码路径(gated code path)。另一种情况下,is_feature_enabled的检查位于一个已经可用的功能内部,用于在旧行为与新行为之间做选择。这种风格不会抛异常,flag 只是改变行为走向,唯一获知方式就是发布说明或源码。仓库中一个现成的例子是SNAKE_CASE_SKILL_NAME:在 src/google/adk/skills/models.py 的 skill 名称校验器中,flag 开启时允许snake_case与kebab-case两种命名并拒绝混用,关闭时则只允许kebab-case——对应技能指南中的说明(见 docs/guides/skills/skill/index.md:“Skill names are kebab-case by default. Snake_case requires enabling theSNAKE_CASE_SKILL_NAMEfeature”)。JSON_SCHEMA_FOR_FUNC_DECL也属于此类:源码注释(_feature_registry.py)展示了它如何在“用 JSON Schema 描述参数”的新行为与“用 Schema 对象描述”的旧行为之间切换。
函数与类型速查
整个系统只需记住三个名字:一个命名 flag、一个读取 flag、一个设置 flag。三者均由 src/google/adk/features/init.py 从google.adk.features导出。
| 符号 | 签名 | 说明 |
|---|---|---|
FeatureName | str枚举 | 全部 flag 的集合。成员随版本增删。 |
is_feature_enabled | (feature_name: FeatureName) -> bool | 按上述优先级立即解析一个 flag。 |
override_feature_enabled | (feature_name: FeatureName, enabled: bool) -> None | 设置进程级最高优先级的覆盖。 |
is_feature_enabled的边界行为:对不在注册表中的名字抛出ValueError(Feature <name> is not registered.)。由于FeatureName成员总是已注册的,这只会发生在你传入裸字符串时。对应测试见 tests/unittests/features/test_feature_registry.py。
override_feature_enabled的边界行为:对未注册的名字抛出同样的ValueError;设置后对整个进程的剩余生命周期生效,且无法通过公共 API 撤销(_feature_registry.py)。
顺带一提:源码内部还提供了一个上下文管理器temporary_feature_override(_feature_registry.py),进入上下文时临时覆盖 flag、退出时恢复原状,但它并未从google.adk.features的__all__中导出(见init.py),属于内部实现细节,公共 API 只承诺上文三个符号,使用内部符号需自行承担兼容性风险。
高级应用:两种需要多想一步的场景
场景一:把 flag 限定在单个测试内
由于override_feature_enabled无法撤销,测试里翻转 flag 会污染同进程内后续所有测试。官方推荐的正确做法是改用 pytest 的monkeypatch设置环境变量——monkeypatch会在 teardown 时自动恢复原值:
def test_snake_case_skill_name(monkeypatch): monkeypatch.setenv("ADK_ENABLE_SNAKE_CASE_SKILL_NAME", "1") # 被测代码通过 is_feature_enabled 读取该 flag。这套做法成立的前提有二:一是环境变量每次调用都实时读取、从不缓存;二是该测试没有设置过程序化覆盖(那会压过环境变量)。tests/unittests/features/test_feature_registry.py中大量使用monkeypatch.setenv验证环境变量优先级、警告只发一次等行为(例如 test_feature_registry.py),可作为参照。
场景二:让部署结果可复现
风险在于:后续 ADK 版本可能翻转某个 flag 的默认值,导致你下次部署时 agent 行为悄然改变,而自己的代码一行未动。解决方案是在部署环境中显式钉住(pin)你依赖的 flag,而不是依赖注册表默认值。两个方向都值得钉:
ADK_ENABLE_<NAME>=1:钉住你依赖的、默认关闭或未来可能变化的特性;ADK_DISABLE_<NAME>=1:钉住你暂时不想接受的、默认开启的实验特性。
配合 CI/CD 的同一份环境配置,即可保证升级前后行为一致。
已知限制(Limitations)
以下限制与官方文档一致,均可在源码中找到对应实现依据:
- 覆盖无法清除:公共 API 只能把覆盖设为
True或False,无法移除,因此一旦覆盖过某个 flag,进程就无法回到“环境变量/默认值”解析模式; ADK_ENABLE_X=0不会禁用:只有1和true视为已设置,0等价于“未设置”,解析会穿透到注册表默认值;真正关闭要用ADK_DISABLE_X=1(实现见 env_utils.py);- flag 集合跨版本不稳定:成员随特性毕业而增删;指向已不存在 flag 的环境变量会被静默忽略,无警告、无报错(因为
is_feature_enabled只在被实际调用时才对未注册名抛ValueError,而环境变量本身不触发调用); - flag 读取时机因特性而异:有的单元在构造时读取,有的在每次调用时读取;在相关对象已存在之后再设环境变量可能不生效;
- 警告没有 per-flag 开关:屏蔽实验性警告需要
warnings过滤器,若不按消息文本精确匹配,也会一并屏蔽其他UserWarning。
关联指南
- Skill 指南(docs/guides/skills/skill/index.md):讲解了一个具体 flag——
SNAKE_CASE_SKILL_NAME——及其改变的 skill 命名行为(默认kebab-case,开启后允许snake_case); - 想从源码层面继续深挖,可阅读 src/google/adk/features/_feature_registry.py(注册表与解析逻辑)、src/google/adk/features/_feature_decorator.py(三类装饰器)以及 tests/unittests/features/test_feature_registry.py(优先级、警告、覆盖行为的完整测试矩阵)。
【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考