返回 FEED
AGENT2026-05-12

Runway 开源 confingy:用 Python 替代 YAML 配置 ML 系统

每个 ML 代码库的相同进化路径

工业级机器学习已在生产环境中运行数十年,但配置这些复杂系统仍然是整个行业的主要挑战。工具已经成熟,但配置问题没有。

Runway 的 Ethan Rosenthal 分享了每个公司都会构建的相同系统:

阶段 1:全能脚本

一个 Python 脚本,硬编码字符串值、到处用魔法数字、读起来像《在路上》的意识流代码等价物。用户假设它只运行一次,所以谁在乎多丑。

阶段 2:CLI

利益相关者想要改进。需要试验不同数据、prompt、模型和指标。最简单的方法是在用户和脚本之间塞一个 CLI。

阶段 3:YAML 配置

更多灵活性需求,于是用上 YAML 配置文件。每次配置结构变化都要更新脚本,产生痛苦的反馈循环。

阶段 4:DAG + YAML 标签

不同数据源、复杂 prompt 构造……直到意识到脚本应该是一个 DAG,每个节点是遵循高级抽象的类或函数。

这个模式与 YAML 配置冲突——不清楚如何从 YAML 指定和实例化类。于是人们用上 YAML 标签,实现基于字符串的动态类实例化。

这就是每个公司最终构建的系统。

YAML 陷阱

所有这些 ML 代码库最终都淹没在复杂性和糟糕的开发者体验中。

通过把太多逻辑推入配置,人们最终把 YAML 变成了图灵完备的 DSL。Runway 依赖 OmegaConf 扩展控制需求:

  • 配置通过任意深度的字段联合继承自其他配置
  • 单个配置可以继承自数十个文件的 web
  • 支持全局变量,也可以通过继承系统覆盖
  • 添加标签支持字符串 Python 代码的内联执行

单个训练配置最终变成数千行 YAML,继承自几十个文件。

开发者体验同样糟糕:

  • Cmd+点击跳转到函数定义对 YAML 中定义为字符串的类不 work
  • 类构造函数的参数是 YAML 字典时,失去现代类型提示和验证
  • 重构类不可能——无法轻易看到哪些类在生产中被使用,因为它们分散在配置中

更糟的是,这种方式实际上伤害了代码结构。如果所有类都从 YAML 动态实例化,依赖注入变得烦人,因为所有类现在必须支持动态实例化。结果:要么选择继承而非组合,要么创建构造函数标志不断增长的 God Class。

直接写代码?

初看似乎应该直接写代码。用 dataclass 或 pydantic model,把类设为字段。

但遇到两个问题:

1. 如何追踪类构造函数的参数?

实验工作流中的代码可复现性很重要——尤其是 AI 领域, billion dollar 公司把它作为服务。如何追踪特定作业运行的开始和结束日期?

2. 如果类实例化太"昂贵"?

也许类在实例化时连接数据库。也许它为 LLM 分配万亿参数的内存。也许配置在本地定义但要在远程机器上运行。所有场景都需要现在定义对象但延迟实例化。

有人通过为每个类关联配置类来解决。但这很 drag,会鼓励继承而非组合——谁想为新类写那么多样板代码?

四个核心需求

一切结晶为任何真正解决方案必须满足的四个需求:

  1. 一切都应该是 Python
  2. 追踪构造函数参数
  3. 延迟实例化
  4. 不要让我重构整个代码库

confingy 登场

confingy 是一个支持上述需求的 Python 库。Runway 内部已把所有 YAML 配置迁移到 confingy。你可能会好奇怎么完成这种壮举——答案是当人们足够恨现有系统时,重构很容易。

库有四个主要能力:序列化/反序列化、延迟加载、验证和转译。全部自然地从单个 @track 类装饰器流出。

@track 装饰器

from confingy import track

@track
class SQLQuerier:
    def __init__(self, query: str, start: str, end: str):
        self.query = query
        self.start = start
        self.end = end

    def execute(self): ...

querier = SQLQuerier("SELECT * FROM table", "2026-01-01", "2026-02-01")

当 tracked 类实例化时,confingy 在对象的私有 _tracked_info 属性中存储构造函数参数和其他类信息。

序列化和反序列化

confingy 可以把任何 tracked 对象序列化为 JSON,甚至追踪类代码的哈希:

from confingy import serialize_fingy

print(serialize_fingy(querier))
{
    "_confingy_class": "SQLQuerier",
    "_confingy_module": "my_script",
    "_confingy_init": {
        "query": "SELECT * FROM table",
        "start": "2026-01-01",
        "end": "2026-02-01",
    },
    "_confingy_class_hash": "3aa6871...",
}

Serialized "fingys" 可以反序列化回 Python:

from confingy import deserialize_fingy
print(deserialize_fingy(serialize_fingy(querier)))
# <my_script.SQLQuerier object at 0x7c670313c0d0>

如果构造函数参数也是 tracked,它会漂亮地序列化,免费获得依赖注入。嵌套的 Matryoshka 风格类可以打包成单个 dataclass 字段,clean 序列化为嵌套 JSON。

延迟加载

任何 tracked 类获得 .lazy() 类方法。传递构造函数参数会创建类的延迟版本:

from confingy import Lazy, track

@track
class DBConnector:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

lazy_db = DBConnector.lazy("postgresql://user:pass@host:port/db")
print(lazy_db)
# Lazy<DBConnector>(config={'connection_string': 'postgresql://user:pass@host:port/db'})

db = lazy_db.instantiate()

Lazy 类甚至与类型良好配合,支持 mypy(通过 confingy mypy_plugin)。

验证

延迟实例化好,延迟失败坏。没有理由等到集群启动才发现字符串被传给了 float 参数。

confingy 不仅追踪构造函数参数,还解析构造函数参数名和类型提示。因此对延迟和非延迟类都会引发验证错误:

lazy_db = DBConnector.lazy(99.1)
# confingy.exceptions.ValidationError: Validation failed for DBConnector:
#   • Field 'connection_string': Input should be a valid string (got 99)

转译

当 confingy 序列化的对象反序列化回 Python 时,用户(或计算机或 agent)很难理解对象内部是什么。虽然可以在 REPL 中打开对象并 tab-complete 查看属性,但这不理想。

理想情况下,应该能看到产生序列化对象的原始 Python 代码。通过 confingy.transpile_fingy,序列化对象可以被转译回产生它的 Python。这段 Python 可以签入版本控制、在 IDE 中检查等。

开源与未来

confingy 已开源,可通过 PyPI 安装。功能已足够完整以替代旧的 YAML 设置,但总有改进空间:

  • 序列化支持仍有局限(如尚不支持 Pydantic)
  • 仍在探索代码即配置这个新世界的最佳实践:如何 diff 变更?如何处理可变性和不可变性?

配置显然没有被"解决",而且可能永远不会被。但现在(谢天谢地)花在试图解决它上的时间少多了。

核心洞察

20 年前 Netflix Prize,更久之前 MapReduce。工业级 ML 已在生产运行数十年,但配置问题不断被以相同方式重新发明。

confingy 的启示是:配置不应该是一种不同的语言。当 YAML 变成图灵完备的 DSL 时,它就是拙劣的 Python。与其在配置语言和编程语言之间来回翻译,不如直接用编程语言——只要解决追踪、延迟加载和验证的问题。

Runway 的实践证明了这条路可行:全部 YAML 已迁移,开发者体验显著改善,而且不需要重构整个代码库。