用 Rust 构建可控执行的编程智能体
M31A 将自主编程视为完整工程流程,涵盖任务理解、规划、执行、验证与故障恢复。文章围绕越权命令、并发文件修改和任务中断等问题,介绍执行控制与策略约束的设计思路。
AI 深度解读
AI 编程代理能改代码、跑命令之后,真正麻烦的问题才开始出现:两个代理同时改一个文件怎么办,任务跑到一半崩溃怎么办,模型提出的命令越权怎么办?M31A 作者给出的方向是:用 Rust 构建一个负责调度、权限和恢复的运行时,让模型负责推理与提议,让程序控制执行。
这篇值得读的地方,是它把编码代理放进了系统工程的讨论里。不过,原文展示的是架构设计和实现入口,没有性能测试、安全评估或任务成功率对照。读它适合拿走设计思路,判断成熟度还需要更多证据。
模型说完成了,为什么还不能结束任务
常见的代理工作方式,是把上下文交给 LLM,再反复调用工具,直到模型判断任务结束。短任务可以这样做,但长任务会遇到另一组约束:哪些步骤必须先完成,哪些步骤可以并行,执行状态存在哪里,何时等待人工批准,以及凭什么认定工作已经完成。
模型的结束判断,只是一次输出,不能单独充当验收证据。
M31A 将职责分成两层:模型推理并提出动作,运行时执行规则、维护系统状态。作者希望后者成为稳定的工程底座,而不是跟着模型每轮输出临时决定一切。
它使用带类型的任务和有向无环图(DAG)组织工作。可以把 DAG 理解为一张带依赖关系的任务表:前置任务完成,后续任务才能开始;没有依赖的任务,可以同时执行。
原文列出的机制包括:用 Kahn's algorithm 校验 DAG,按拓扑关系分批执行,检测写入路径冲突,并限制 worker 并发数。它还提到推测执行,但没有展开具体策略和适用条件。
这里有两个约束要分开看。依赖关系决定执行顺序,写入冲突决定任务能否安全并行。两个任务即使互不依赖,也可能同时修改同一文件。
任务领取由 WorkflowStore::claim_ready_task 处理,涉及依赖校验和 worker 租约。租约可以理解为有期限的任务执行资格;配合 worker fencing,可以阻止过期 worker 把旧结果写回,覆盖新的执行结果。相关实现位于 crates/m31a-storage/src/workflow_store.rs。
这套设计回答了谁能领取任务、何时执行、谁的结果仍然有效。至于测试结果、验收条件如何成为完成证据,原文没有给出足够细节。
把权限写进运行时,比多弹一次确认框多做了什么
代理拥有 shell 权限,就拥有了实际操作开发环境的能力。让用户点击同意,只能表达授权,不能替代权限边界的建立与执行。
M31A 为此设计了执行策略子系统,包括一个 11 阶段策略关卡,并处理策略优先级和默认拒绝的审批语义。后者的大意是:审批条件不成立时,不能把它当作已经获准。不过,原文没有列出这 11 个阶段,也没有完整展示决策类型,不能仅凭阶段数量判断覆盖程度。
策略还需要沙箱落实。在 Linux 上,M31A 支持 Bubblewrap 隔离和基于 Landlock 的文件系统限制。作者强调,如果策略要求的隔离无法建立,运行时应按失败规则处理,不能静默假定执行安全。
另一个具体设计,是区分 shell 命令与直接启动程序。原文用下面的类型表达这种区别:
pub enum SandboxCommand { Shell(String), Exec { program: String, args: Vec, }, }
要看的是 Shell(String) 和 Exec { program, args } 两个分支:前者交给 shell 解释,后者分别传递程序与参数。这样可以减少把所有调用都拼成字符串时出现的参数拆分和引号问题。
这段代码按原文保留,其中 args: Vec 缺少元素类型,不能直接作为完整 Rust 定义使用。它说明的是接口思路,也提醒我们别把文章里的片段直接当成可运行实现。
两种调用方式分开,并不自动保证命令安全。允许执行什么程序、能访问哪些路径,仍要由策略和沙箱共同约束。相关子系统分别位于 crates/m31a-execpolicy/ 和 crates/m31a-sandbox/。
崩溃后接着干,难在代码已经改了一半
作者举出的故障很实际:代理修改了三个文件,却在记录结果前崩溃。下一位 worker 接手后,是重做任务、跳过任务,还是先检查现场?
这个问题不能只靠保存聊天记录解决。聊天记录可以保留意图,却未必准确反映磁盘上的执行结果。
M31A 使用 SQLite 持久化工作流状态,并引入过期任务状态校正、乐观并发检查、持久化工作流事件,以及内容寻址产物。内容寻址是根据内容标识产物,便于准确引用执行结果。它们共同服务于恢复和审计:任务中断后,系统仍能找到状态、事件与产物,而不是把每次重启都当成新会话。
但恢复记录与恢复文件现场之间还有距离。原文没有解释文件修改和状态提交如何协调,也没有说明外部命令的副作用能否安全重试。它提供了处理这些问题的基础设施方向,没有证明所有中断都已得到完整处理。
执行引擎之外,M31A 还通过基于 JSON-RPC 2.0 的 App Server 提供接口,支持线程与会话管理、流式通知、工具生命周期事件、人工审批回调和检查点操作。目标是让不同客户端使用同一套运行时,避免每个界面各做一个代理引擎。
Rust 在这里承担的是系统实现角色。作者看重所有权与资源管理、类型边界、并发执行和可预测的失败处理,并用包含多个 crate 的 Cargo workspace 划分职责。工作流、策略、沙箱、存储和 App Server 分别放在独立子系统里。这些是选型理由,原文没有提供与其他语言的比较测试。
我的判断
我认同作者把注意力放在执行控制、故障恢复和可验证结果上。代理一旦开始直接操作仓库,代码生成质量就只是整个交付过程的一部分。边界、状态和验收都需要由程序明确表达。
但 DAG 也有适用前提。依赖清晰、步骤较稳定的工作容易建图;探索式调试则经常要根据新线索调整路径。原文没有说明任务图如何随执行结果变化,也没有对比 DAG 与常规代理循环的任务成功率。把 DAG 作为长任务基础是一种有理由的设计选择,是否更好,目前仍是经验判断。
对于只处理短任务、一次只运行一个代理的工具,简单循环可能更容易维护。持久化、租约和状态校正都会带来开发与排障成本;当任务需要跨进程恢复、多个 worker 协作时,这些成本才更容易体现价值。
安全和验收也应分别评估。11 阶段策略关卡说明作者重视权限,但缺少阶段细节和安全评估,不能据此确认防护强度。同样,保存事件和产物能帮助追溯,却不直接证明用户需求已经满足。
M31A 值得参考的是它提出并拆解的问题,完整效果仍需要实现审查和测试证据。
能拿来做什么
- 如果你正在给前端或 Node 项目接入编码代理,可以先把模型提出动作与程序批准执行分开,让文件访问、命令执行和人工审批都有明确入口。
- 如果代理经常把程序与参数拼成命令字符串,可以借鉴
SandboxCommand的区分方式:普通程序调用独立传参,需要 shell 语义的操作单独处理。 - 如果计划让多个 worker 协作,可以先明确任务依赖、写入路径和并发上限,再设计领取、租约与过期结果拒绝机制,避免只靠提示词协调共享文件。
- 如果任务需要长时间执行,可以记录关键状态、事件和产物,并单独定义完成证据,例如要求相关构建或测试通过,让恢复能力与验收能力都有可检查的结果。