Codex + Spec-Kit 开发 LeRobot Agent 的实践与反思

“现在问题解决了,但请你重新 Review 一下今天的几轮修改,看看是不是补丁叠补丁式的修改,如果是的话,重构成最优解”,这是我在这个项目里对 AI 说得最多的一句话。 你也可以说 “review the code, think from greenfield” 或者 “/samplify”,但不管怎么说,这句话的核心意思是:当 AI 在一个已经快速迭代过的代码库里继续修改时,它很容易陷入“补丁叠补丁”的陷阱,而不是停下来重新思考最优解。

过去一周,我做了个基于 LeRobot 的 SO-101 机械臂 PoC。最初只是一个能跑起来的遥操作脚本,后来长成具备配置驱动、基础测试、服务接口和调试界面的 MVP。这个项目并不是我一个人独立完成的。我主要搭起了大约 60% 的骨架和核心功能,包括命令行入口、配置体系、API 分层、Web 服务、调试面板(dashboard),以及大部分基础测试。到了后期,团队里的同事也逐步参与进来,补上了模型推理、训练以及一些更贴近业务场景的能力。

现在的结构:

  • 命令行入口:teleoperaterecordreplayserve
  • Python API 分层:src/api
  • FastAPI 服务:src/webapi
  • React 调试面板:dashboard/
  • 单元测试:tests/unit
  • 配置文件:config/agent.toml

开发环境与机械臂

一句话概括:用 Spec-Kit 管"要做什么",用 Codex 管"怎么更快做出来"

但这不是一个"AI 加 Spec-Kit 就一路顺风"的故事。中间有很多反复、修正和取舍。Spec-Kit 帮了我不少,也带来过混乱;Codex 让我省了很多重复劳动,也把我前端、协作和问题表达上的短板照得很清楚。

这篇文章讲的就是这些真实经验。

一、为什么一开始没有直接写代码

表面上看这是个"硬件 + Python"工具,但做起来才发现是一整条链路的问题:

  1. 读取 SO-101 的 follower、leader、camera 配置
  2. 跑通 teleoperate(遥操作)
  3. 支持 record(数据采集)和 replay(回放)
  4. 把能力暴露成 HTTP / WebSocket 服务,方便外部调用
  5. 做调试面板,把串口、相机、控制状态、任务状态可视化
  6. 后续接入推理和 LLM,而不是推翻重来

直接写代码很容易在第 3 步以后失控。配置解析、硬件连接、任务生命周期、Web 接口、前端调试、测试替身——这些边界一旦不清,后面每加一个功能都牵一发而动全身。

所以我一开始没急着写功能(实际情况是拿到机械臂那一刻我不知道如何开始),而是先走 Spec-Kit 的标准流程,把问题拆成几个可以连续交付的阶段。回头看这个决定是对的,它很早就把项目骨架定下来了。

二、怎么用 Spec-Kit 定义这个项目

这不是我第一次用 Spec-Kit。我的理解是把它当成一个"收敛模糊需求"的约束器,而不是一套文档模板。

最开始不是先写实现方案,而是先写清楚系统必须具备什么能力:

  • 用统一配置描述 follower、leader、cameras
  • 支持 teleoperaterecordreplay 三个核心动作
  • 代码要有 lint/format 校验,单元测试,git commit hook
  • 可以在不碰硬件的前提下做单元测试
  • 需要有 Web 层,方便外部系统和前端接入

然后再写约束:

  • 底层依赖 LeRobot
  • Python 版本固定在 3.12
  • uv 管理环境和依赖
  • 配置用 TOML
  • 测试用 pytest
  • 尽量走真实的 LeRobot 路径,而不是再造一层假的 runtime

具体执行上,把不同功能依次推进到 specify -> plan -> tasks 这条链路。先形成规格,再形成计划,最后拆成任务。最终阶段大致是:

  • 第一阶段:命令行入口跑通 teleoperaterecordreplay
  • 第二阶段:把配置、校验、控制会话抽成稳定的 API 层
  • 第三阶段:暴露 FastAPI 接口,补齐状态查询和流式能力
  • 第四阶段:做调试面板,验证整条链路是否真的可操作
  • 第五阶段:接入 LLM / 推理,让这个 PoC 具备更明确的 agent 雏形

这套做法最大的价值不是"文档更完整",而是每轮功能落地之后,项目都处在一个能跑、能验的最小闭环状态。

三、怎么用 Spec-Kit 的标准流程驱动 Codex

前面那套拆分思路落地时,我不是手工写一堆文档再扔给 Codex,而是沿着 github/spec-kit 的官方工作流推进。

Spec-Kit 的核心流程:

  1. specify init 初始化项目结构
  2. /speckit.specify 把功能想法变成结构化规格
  3. /speckit.plan 从规格生成实现计划,以及 research、data model、contracts、quickstart 等设计产物
  4. /speckit.tasks 把计划拆成可执行的任务列表
  5. /speckit.implement 或让 Codex 基于这些产物执行实现

GitHub 在 spec-driven.md 里写得很明确:/speckit.specify 负责"做什么",/speckit.plan 负责"怎么做",/speckit.tasks 负责"具体任务",/speckit.implement 负责"执行"。

我在项目里基本遵循了这个顺序,但中间也做了不少调整。

关于 /speckit.specify

这个命令用来把模糊想法变成结构化规格。我在项目早期用得最多,特别是定义配置模型、CLI 接口、API 契约的时候。

实际用下来发现,specify 适合处理"边界清晰、输入输出明确"的功能。比如定义配置结构、CLI 参数、API 路由——这些场景的 specify 产物可以直接进入 plan 阶段。

但对于"需要探索"的问题,specify 容易过度承诺。我早期在前端调试面板的 specify 阶段花了不少时间定义组件结构和状态流转,但后来实际实现时发现,很多假设在真实数据流面前并不成立。specify 阶段产出的规格成了需要修正的约束,而不是可以直接执行的蓝图。

关于 /speckit.plan

plan 阶段从 specify 产物生成实现计划。我主要用它做几件事:

  • 把规格拆成可执行的步骤
  • 识别依赖关系和阻塞点
  • 生成 data model、contracts、quickstart 等设计产物

plan 的价值在于它强迫你在动手之前想清楚"先做哪块,后做哪块"。我在项目里用 plan 阶段明确了几个关键顺序:必须先稳定配置层,才能做 API 层;必须先有 API 层的 mock,才能写单元测试;必须先有 webapi 的基础路由,才能做调试面板。

这些顺序看起来理所当然,但在实际开发中很容易因为"这块看起来简单,先做了吧"而打乱。plan 阶段的作用就是提前暴露这些依赖。

但 plan 也有局限。它假设 specify 阶段的规格是稳定的,而实际上规格经常需要调整。我在项目中期遇到过几次 plan 产物和实际实现脱节的情况,原因是指定阶段对 LeRobot 的某些行为假设不准确,导致 plan 阶段拆出来的任务无法直接执行。

关于 /speckit.tasks

tasks 阶段把 plan 产物拆成具体任务。这是我最常用的阶段,因为它直接产生可以丢给 Codex 执行的任务列表。

我在项目里形成的习惯是:每个 task 只聚焦一个具体目标;任务描述里包含前置条件和验收标准;对于复杂任务,先让 Codex 生成伪代码或接口草案,确认方向后再完整实现。

tasks 阶段的一个教训是:不要把"探索性工作"和"实现性工作"混在一起。我早期有几个任务描述是"调研 X 并实现 Y",结果 Codex 在调研部分花了大量时间,实现部分却很粗糙。后来我把这类任务拆成两个:先调研,再实现。

关于 /speckit.implement

implement 阶段是实际编码。我通常不会直接用 /speckit.implement 命令,而是把 tasks 产物丢给 Codex,让它基于上下文执行。

这里的关键是:Codex 的执行质量严重依赖于前面的 specify 和 plan 质量。如果 specify 阶段的规格有模糊之处,Codex 会按自己的理解补全,而这些补全不一定符合预期。如果 plan 阶段的步骤有依赖错误,Codex 会在执行时卡住或产生循环依赖。

我在项目里养成的一个习惯是:在让 Codex 执行之前,先人工过一遍 tasks 列表,确认每个任务的输入输出是清晰的。这个步骤花不了多少时间,但能避免很多返工。

四、Spec-Kit 带来过哪些混乱

Spec-Kit 不是银弹。我在项目里遇到过几次明显的混乱:

1. 过时的计划、任务、设计过程没有及时清理

Spec-Kit 的 workflow 会产生大量中间产物:specifications、plans、tasks、design docs。这些产物在早期很有用,但一旦实现偏离了原始规格,它们就变成了噪音。

我在项目中期发现,Codex 有时会引用过时的设计文档来做决策。比如某个 API 接口在 specify 阶段定义了字段 X,但实际实现时去掉了 X,只保留了 Y。但 Codex 在后续修改时,偶尔会基于旧的 specify 文档建议"补充 X 字段的校验逻辑"。

问题根源是:我没有及时清理已经过时的 spec 产物。Spec-Kit 的 workflow 假设规格是稳定的,但 PoC 项目经常需要快速调整。当调整发生时,旧的 spec 产物如果没有被清理,就会成为上下文的污染源。

2. 不是所有问题都适合走 Spec-Kit 流水线

Spec-Kit 适合处理"边界清晰、可以结构化描述"的问题。但对于"需要试错"或"依赖直觉"的问题,Spec-Kit 的 overhead 不值得。

我在前端调试面板的开发上体会很深。调试面板的 UI 布局、交互流程、状态展示方式——这些很难在 specify 阶段就定义清楚,因为它们依赖"用起来顺不顺手"这种主观感受。我早期花了太多时间在前端的 specify 和 plan 上,结果很多规格在真实实现后都被推翻了。

后来我的调整是:前端和交互问题,先快速搭一个能跑的版本,用起来了再迭代。Spec-Kit 的 workflow 留给后端和 API 层,前端用更轻量的方式推进。

3. 多人协作时的 spec 冲突

项目后期变成团队协作后,Spec-Kit 的产物管理变得更复杂。不同人可能对同一个模块有不同的 specify 理解,而这些理解如果没有及时对齐,就会产生冲突。

我们遇到的一个典型情况是:A 同事基于某个 specify 文档实现了功能 X,B 同事基于另一个版本的 specify 文档实现了功能 Y,两个功能在集成时发现接口不兼容。

解决方式不是禁止并行开发,而是明确"哪个 specify 是当前主分支的权威版本"。我们在项目后期约定:spec 产物也走 git 管理,任何 specify 的更新都要提 PR,确保所有人看到的是同一个版本。

五、Codex 真正替我做了什么

Codex 真正提高效率的地方主要有四个:

1. 把 Spec-Kit 流程产出的规格和任务快速翻译成第一版工程骨架

从配置模型、请求对象、命令行参数、API 路由到测试样例,Codex 能基于已经成型的 Spec-Kit 产物、计划和任务,很快给出一版结构正确的初稿。这样我不用把时间花在重复劳动上,能把精力放在真正的设计决策上。

2. 适合做结构化重构

把逻辑从命令行入口挪到 api 层,把通用状态收敛到会话对象里,把 Web 层改成统一的 routes/schemas/state 组织方式——这种工作如果纯手工做,非常琐碎;但只要 Spec-Kit 流程已经把边界和任务拆清楚,Codex 执行起来就很快。

3. 适合补测试和文档

这个项目里的 README、docs/API.md 以及一系列单测,本质上都属于"高价值但重复度高"的工作。这类任务很适合让 Codex 跟着已有实现补全。

4. 能持续保持上下文

项目从 teleoperate 扩展到 record / replay,再扩展到 webapi 和调试面板,如果每次都换一个完全没有上下文的助手,沟通成本会很高。Codex 的优势在于它能沿着既有代码结构继续往下做,而不是每次都从头理解一遍项目。

六、Codex 照出了我哪些短板

Codex 不是万能的。它很强,但会把我的短板放大。

1. 前端和交互是我的盲区

我在后端和系统架构上经验多一些,但前端和交互设计是明显的短板。Codex 能帮我写出"功能正确"的前端代码,但写不出"体验好"的交互。

调试面板的早期版本就是个例子。Codex 按我的描述实现了状态展示、按钮控制、日志输出——功能都对,但用起来很别扭。布局拥挤、信息层级不清、操作反馈不明确。这些问题不是 Codex 的错,是我自己没有想清楚"用户真正需要看到什么"。

我后来调整的方式是:前端任务不再直接丢给 Codex,而是先自己画个草图,明确每一屏的核心信息和操作路径,再把草图描述给 Codex 实现。这个步骤省不了,因为 Codex 只能执行"描述清楚"的需求,不能补全"描述模糊"的意图。

2. 问题定义比解决方案更重要

Codex 会把你的问题定义当成真理。如果你问的是"怎么实现 X",它会认真给你实现 X 的方案;但如果 X 本身不是最优解,Codex 不会提醒你。

我在项目早期遇到过典型的 XY problem。我想要一个"能实时显示机械臂状态的调试面板",于是让 Codex 实现了一个 WebSocket 实时推送状态的前端。实现完后发现,真正的需求不是"实时",而是"在关键操作节点能看到状态快照"。实时推送带来了不必要的复杂度和性能开销。

问题出在我一开始的问题定义上。我描述的是"我想要的方案",而不是"我真正想解决的问题"。Codex 忠实地执行了方案,但方案本身错了。

后来我给自己加了一条规则,特别是在前端和交互问题上:先描述真正目标,再描述我以为可行的实现方式,明确告诉 Codex——如果实现方式不对,优先纠正我的问题定义。

这其实是在主动对抗 XY problem。对 AI 来说,最有价值的不是我先给出一个貌似具体的方案,而是先把真正目标说清楚。

3. 多人都用 AI 之后,协作冲突比以前更容易放大

我也在尝试通过 Harness Engineering 解决这个问题,以后有机会分享这部分内容

项目后期变成团队协作。以前主要是"我和同事协作",现在很多时候变成了"我和我的 AI、同事和他的 AI,再加上我们彼此协作"。

表面上看大家都更快了,但实际冲突反而更容易变大。

以前两个人写代码,冲突通常发生在实现细节层;现在两个人各自带着 AI 写代码,冲突经常在更早的阶段就出现了,因为 AI 会把每个人当前上下文里的默认假设继续往前推。

典型情况:你让 AI 按会话模型继续扩展,同事让 AI 按推理任务模型继续扩展,两边都各自"看起来合理",合到一起时才发现状态流、接口语义、模块职责已经互相侵入了。

我们后来摸索出的一个更有效的协作方式是:先按模块严格确认边界,再各自实现,最后统一重构。

“确认边界"不是口头分工,而是要明确到足够可执行:谁负责 api 的哪一层,谁负责 webapi 的哪些路由和状态,谁负责模型训练和推理链路,哪些对象是稳定接口不能随意变更,哪些地方允许先局部实现最后再统一整理。

这样做有两个直接的好处:每个人都可以在自己的边界内放心使用 AI 提速,系统级冲突被推迟到可控的整合和重构阶段。

在 AI 时代里,模块边界比以前更重要。以前边界不清主要伤的是开发效率,现在边界不清很容易让多个人各自带着 AI 把系统一起推偏。

七、如果让我再从头做一次

回头看,这个 PoC 能比较顺地从一个 CLI 工具长成现在这个带服务层、前端层、测试和文档的 MVP,靠的不是我一开始就把所有设计都想对了,而是采用了一种可以持续修正的工作方式:

  • 先用 Spec-Kit 的标准流程把功能收敛成 Spec-Kit 产物、计划和任务
  • 用 Codex 快速生成初版实现
  • 用测试和真实联调不断收紧接口
  • 再把稳定下来的能力继续往下一层扩展
  • 在多人协作阶段先锁模块边界,再各自借助 AI 实现,最后做统一重构

但如果让我重新来一次,我会比当时更明确地做三件事:

  • 更早清理已经过时的计划、任务和设计过程,避免上下文污染
  • 更早区分"架构问题"和"微调问题”,不要让所有任务都走 Spec-Kit 流水线
  • 在前端和交互问题上,先讲清楚真正目标,再讲我以为的方案,降低 XY problem 的概率

八、总结

  • Spec-Kit 适合搭骨架,不适合覆盖所有微调
  • Codex 很强,但会被错误上下文和错误问题定义带偏
  • 多人都用 AI 之后,模块边界比以前更重要

对这个项目来说,AI 不是用来替代工程方法的,它只会把原有的方法放大。方法对了,推进会很快;方法错了,跑偏也会很快。

这也是我现在更相信的一种开发范式:不是让模型代替工程方法,而是让模型在工程方法之内做执行;同时在必要的时候,主动清理上下文、修正问题定义、重新划清协作边界。

参考链接