系列: 与 AI 一起学编程

在纸上勾勒 SDK 的文件夹结构

在任何代码存在之前画出的 SDK 骨架。

文件夹结构就是规格说明 先画出智能体必须写入的方框 /sdk /apps /modules /assets /docs /config arena_demo/ coach_demo/ hud_designer/ ...共8个演示 hud gesture gaze sync fx voice widgets audio gesture icons README guides API refs layouts roles 模块桩在实现存在之前就锁定了契约
结构就是纪律——智能体通过它们写入的方框继承这份纪律。

一个由智能体构建的代码库,要么在三个月内变成一团糟,要么能保持多年的连贯性——而这个差异在写下第一行代码之前就已经决定了。RakuAI 先画出方框,这样智能体就总能落在正确的位置上。

有一种设计工作,从外部看不见,但从内部看是承重的。代码库的文件夹结构就是这样的东西之一。它看起来像文件管理。它实际上是代码的组织架构图。早期做对了,接下来两年的每一个 PR 都会落在正确的位置。早期做错了,接下来两年的每一个 PR 都得为自己该归属在哪里而争论一番。

这个周六的工作就是尽早把它做对。

结构必须支持什么

在画方框之前,我先列出了这个结构必须满足的约束条件清单。

每个演示都有自己的归属。 我上个月规定的八个演示,每一个都需要一个文件夹,让它们的代码、素材和文档共同存放在一起。一个阅读某个演示的开发者,不应该需要跳转到 SDK 的四个不同部分才能理解它。

共享模块可以与演示分离。 任何跨演示复用的东西(HUD 渲染、手势输入、注视追踪、多人同步、语音控制、视觉特效)都存放在自己的位置,并通过一个公共界面被演示消费。

素材按类型组织,而非按演示组织。 一张血条 PNG 是一个 HUD 组件,而不是竞技场演示的素材。可复用的视觉素材存放在一个共享素材文件夹中,需要它们的演示则链接过去。

文档与其所描述的内容放在一起。 SDK 安装文档放在 SDK 旁边。演示指南放在演示旁边。HUD API 文档放在 HUD 模块旁边。文档的可发现性,很大程度上取决于文档的位置。

配置是自己独立的关注点。 HUD 配置文件、默认布局、多人游戏角色配置。这些既不是代码,也不是素材,也不是文档。它们有自己的归属。

结构本身

经过一个上午的白板讨论,布局看起来是这样的。

根文件夹就是 SDK 本身。在根目录之下:

/apps/ 是演示存放的地方。每个演示有一个子文件夹。arena_demo/ 对应单人竞技场。coach_demo/ 对应 AR 教练。hud_designer/ 对应 HUD 叠加层设计器。streamer_demo/ 对应主播模式。pos_demo/ 对应服务点叠加层。companion_demo/ 对应 AR 伴侣 HUD。aimlab_demo/ 对应 AR 瞄准训练器。hudsync_demo/ 对应多人 HUD 同步。每个演示文件夹都包含自己的源代码、自己的配置、自己特定的素材,以及自己的 README。

/modules/ 是共享代码存放的地方。一开始有六个模块。每个演示都要消费的 HUD 渲染器。大多数演示都要消费的手势输入检测。需要它的演示所使用的注视追踪。局域网多人演示所使用的多人同步。用于视觉特效的叠加层动画器。接受语音输入的演示所使用的语音控制界面。每个模块都暴露一个小而稳定的公共界面,供演示链接使用。

/assets/ 是共享素材库。HUD 组件(血条、弹药计数器、指南针叠加层)以 PNG 和 SVG 形式存在。用于命中、警报、旁白的音效。向用户展示某个手势已被检测到的手势图标。演示在这里链接素材,而不是重复存放它们。

/docs/ 是文档的归属地。一份介绍 SDK 并引导完成安装的 README。一份解释如何运行八个演示中每一个的演示指南。一份 HUD 模块的 API 参考文档。一份语音控制模块的 API 参考文档。这些文档被特意放在一起,这样一个想知道 HUD 如何工作的开发者,就能在同一个文件夹里同时读到源代码和文档。

/config/ 是配置。用作新项目起点的默认 HUD 布局。从 HUD 设计器导出的示例布局。多人游戏角色定义。把配置从代码中分离出来,能让演示保持干净,并让开发者可以在不重新编译的情况下改变行为。

模块桩

一个单独的文件夹结构本身只是一个空的文件系统。我今天草拟的另一件事,是一组代码桩,向智能体(当它们最终开始针对这个结构编写代码时)展示每个入口点上一个真正的实现应该是什么样子。

这些模块桩目前是 Python 伪代码。一旦引擎仓库开放,它们将被翻译成生产语言(运行时用 C++,并绑定到 Python 和其他生态系统)。Python 形式让我能够推理这些接口,而不会陷入实现细节之中。

一个示例模块桩,针对手势输入模块。大致是这样:

def detect_gesture(frame):
    """Detect gestures like swipe, point, raise, duck in a single sensor frame."""
    if is_swipe(frame):
        return "swipe"
    elif is_raise(frame):
        return "raise_arm"
    else:
        return None

这不是生产代码。它是契约。生产代码会把 is_swipeis_raise 替换成真正的计算机视觉流水线。接口保持不变。这个签名是演示要据以编码的东西。及早锁定签名,意味着我可以在计算机视觉流水线还没有做到万无一失之前就发布这些演示,因为演示不依赖于实现,它们依赖于接口。

我为 /modules/ 文件夹中的每一个模块都草拟了这种形式的模块桩。六个小型 Python 文件。它们都没有实现任何东西。它们全都定义了未来实现必须遵守的契约。

为什么智能体需要这个

这就是和几个周六之前的智能体名单之间的关联。

到了真正编写代码的时候,智能体不会是在发明架构。架构已经在纸面上了。文件夹结构告诉它们新代码该放在哪里。模块桩告诉它们该实现什么接口。素材组织告诉它们该去哪里找需要的东西。文档结构告诉它们该把自己写的文档放在哪里。

这就是一个由智能体构建的代码库在三个月后变成一团糟、和一个能保持多年连贯性的代码库之间的区别。结构就是纪律。智能体通过结构继承这份纪律。

没有这份前期工作,一个被要求”实现手势输入”的智能体会自己发明一个文件夹、发明一个 API、写出代码,产出一个孤立看来能运行的东西。这种做法乘以六个模块,就会产出六个互不兼容的迷你引擎。结构通过画出智能体必须写入的方框,防止了这种失败模式。

这件事的难点在哪里

几件坦诚的事情。

文件夹结构总想生长。 六个月后我会想加一个第七模块。这个结构必须允许这一点,同时又不能变成一个杂物抽屉。我给自己定的规则是:任何新的顶层文件夹都需要说明为什么它不该是现有文件夹的子文件夹。大多数新东西最终都会变成子文件夹。

模块桩需要维护。 随着真正的实现落地,模块桩会变得陈旧。把模块桩当作”契约”来阅读的智能体,会继承模块桩与真正实现之间的任何偏差。解决办法是把模块桩当作权威真相来源,并要求真正的实现在契约变化时更新它们。

其中一些文件夹是推测性的。 /config/ 文件夹是一个赌注,赌运行时配置文件会成为这个 SDK 中一个真实的产物类别。如果结果并非如此,这个文件夹就会缩小或并入别的东西。我对此没有意见。以后删除一个文件夹,比一年后约定已经漂移时再去发明一个要便宜得多。

接下来是什么

下一个周六是 API 界面设计。上面六个模块中的每一个都需要规定其公共界面。这意味着要决定哪些数据类型跨越边界、返回什么错误码、发出什么事件。文件夹结构做完了,API 设计就有地方落地了。

在那之后是逐个演示地规定每个演示消费哪些模块、以什么顺序消费。然后是运行时架构。然后,终于,引擎仓库才会开放。

这份专利资产已经等了十年。再多花几周把它设计对,不会是让我们错失窗口期的那件事。

文件夹结构完成了。代码库现在有了形状,尽管里面还没有任何代码。

一个不会在你脚下移位的 SDK

一个不会变的文件夹结构,一个在实现之前就锁定的 API 界面——基于一个为长期而设计的空间运行时进行构建。

← 所有文章