本文内容
AI 编程智能体的多文件原子修改:Semaprax 开发教训
当 AI 编程智能体修改多个相互依赖的文件时,安全替换每个文件仍然不够。项目还需要一个统一的发布边界。否则,构建进程、语言服务器、测试 watcher 或另一个智能体都可能读到一半旧程序与一半新程序。
我们在开发实验性智能体原生系统语言 Semaprax 时遇到了这个问题。这个教训并不限于语言设计:先准备完整且不可变的世代,完成验证,再切换一个很小的活动指针。本文解释该模式、适用边界,以及技术采购者在相信任何“原子修改”承诺前应该提出的问题。
什么是多文件原子修改?
多文件原子修改只向约定的读取者展示完整旧状态或完整新状态,不展示二者混合的中间状态。只有系统明确说明读取者、文件范围、提交点和故障模型后,“原子”这个词才有完整含义。
假设一次修改需要重命名导出函数,并在另一个文件中更新调用者。如果先改定义,watcher 可能看到旧调用与新定义;如果先改调用者,则会看到相反的不一致。两个文件各自都可能是合法文本,但组合程序在那个时刻并不合法。
核心教训:发布完整世代,而不是一串写入
Semaprax 需要在多个源文件之间发布经过验证的修改。它的架构决策否定了顺序替换,因为读取者可能观察到混合世代。最终设计保留完整且不可变的源码世代,并通过一个 ACTIVE 记录选择其中一个。固定版本的 Semaprax 架构决策记录了最终方案与被否决的替代方案。
.semaprax-workspace/
ACTIVE
generations/
<old-workspace-revision>/
<candidate-workspace-revision>/写入路径分为五个阶段:
- 把提案绑定到基础修订版。如果当前 workspace 已经变化,则拒绝提案。
- 预检每个变更文件。在写入候选状态之前,针对同一个经过认证的基础状态解析全部操作。
- 单独构建完整候选世代。其中也包含未修改文件,因此候选对象是完整世代,而不是一袋 patch。
- 验证完整候选世代。检查格式、身份、限制、digest,以及协议真正承诺的不变量。
- 切换一个指针。最终复检完成后,只替换
ACTIVE。遵守协议的读取者解析该指针,并读取一个不可变世代。
为什么单个文件的原子 rename 还不够?
先写临时文件,再 rename 到目标路径,是很有价值的单文件模式。Rust 将 std::fs::rename 定义为一次重命名操作,同时列出平台差异与跨挂载点失败。该 API 并没有为任意多个路径提供可移植事务。可参考 Rust 标准库的 rename 契约。
对五个文件重复这个操作,会产生五个发布时刻。如果读取者在第二次和第三次之间运行,整体程序仍然可能呈现混合状态。文件级原子性不会自动组合为项目级原子性。
Git 不是已经让多文件修改具备原子性了吗?
Git commit 标识一个完整 tree,但它不会让编辑器、watcher 或其他进程眼中的每次活动 working tree 变化都具备原子性。发布仓库历史与修改运行中工具正在读取的文件,是两个不同边界。
Git 自身也说明了精确定义边界的重要性。git update-ref 可以验证预期的旧 object ID,并把多个 ref 修改放入事务。其文档同时提醒,即使单个 ref 的更新具备原子性,并发读取者仍可能看到多个 ref 修改的一个子集。Git 官方 update-ref 文档很好地展示了预期版本检查与显式事务状态。
分支、worktree 和 commit 仍然适合协作、审查与恢复。只有当运行中的消费者必须在修改期间看到一致的应用快照时,才需要额外加入受管发布层。
为什么不可变世代是实用模式?
不可变世代把大部分风险移到提交点之前。候选世代可以在不改变当前选中状态的情况下构建、检查和拒绝。最终操作很小,因为它只改变指针,而不是所有载荷文件。
这种模式并非智能体工具独有。Nix 对原子升级的解释很相似:软件包不会被就地覆盖,profile 会转向新世代,因此不会出现新旧文件混合的时间窗口。Nix 官方架构指南提供了一个成熟的世代模式示例。
智能体场景还需要证据。流畅的模型回答不应拥有提交权限。系统应保留基础修订版、提议操作、候选 digest、验证结果和最终指针切换结果,让另一个组件能够复核发生了什么。
Semaprax 实际实现了什么,又没有实现什么?
在审计的 commit 上,Semaprax 为 2 到 16 个受管 .spx 文件定义了有限事务。写入者取得独占锁,构建或认证完整候选世代,执行最终检查并替换 ACTIVE。遵守协议的读取者取得共享锁,并解析被选中的不可变世代。固定版本的 workspace 事务规范定义了 wire 格式、限制、诊断、证据与非声明事项。
边界比标题更重要。该协议不会让原始源码路径、Git、编辑器或不遵守协议的读取者获得原子视图。它不承诺网络文件系统行为、断电持久性、自动回滚、通用仓库语义或任意多文件修复。Semaprax 仍是 pre-alpha 研究,本文也不会把有限证据扩大为生产就绪声明。
自建还是采购:向智能体工具供应商提出八个问题
- 哪些读取者受到保护?保证只覆盖工具 API,还是也覆盖 working tree、语言服务器、构建进程与外部进程?
- 基础修订版是什么?每个提案都需要预期版本和明确的过期拒绝路径。
- 提交点在哪里?“我们使用临时文件”并没有回答多文件问题。
- 是否验证完整候选状态?逐文件语法检查会遗漏跨文件损坏。
- 谁拥有写入权限?模型输出、审查证据和批准 token 不应自动变成可复用的提交权力。
- 切换前后分别会发生什么?切换前拒绝与切换后歧义需要不同的恢复流程。
- 测试了哪一种持久性声明?进程崩溃、操作系统崩溃、断电和网络存储是不同故障模型。
- 证据能否复现?要求查看敌意测试、固定 fixture、限制与精确版本,而不只是演示。
什么时候需要这种架构?
如果智能体只提出 patch,然后停下来等待人工审查普通 Git diff,你很可能不需要受管世代协议。当自主任务修改多个耦合文件,同时构建、服务或其他智能体持续读取 workspace,而且混合状态可能触发部署、代码生成、迁移或不可逆操作时,才应认真考虑它。
如果智能体还无法可靠识别正确的程序实体,应从更早一层开始。我们的 Semaprax 语义身份工程笔记解释了稳定声明 ID 与修订版绑定 patch。AI 智能体 Harness 指南则把发布放回上下文、策略、工具、验证与可观测性组成的完整系统。
常见问题
Git commit 具备原子性吗?
一个 commit 标识完整仓库 tree。这不等于保证读取变化中工作目录的进程永远看不到中间文件状态。
原子写入等于可以回滚吗?
不等于。原子发布定义切换时什么会变得可见。崩溃后的回滚、清理与恢复是独立契约,需要独立证据。
不可变世代能避免 merge conflict 吗?
不能。它控制遵守协议的读取者所见的发布状态。分支协调、语义冲突与人工审查仍是独立问题。
生产级 AI 支持
正在构建 AI 产品,却担心推理成本、架构或生产可用性?Wavect 帮助创始人把 AI 原型变成可靠的生产系统。
查看相关服务:
编辑说明:OpenAI Codex 协助了调研、起草与翻译。Wavect 于 2026 年 9 月 6 日对照 Semaprax commit 942ed70 与文中链接的第一方文档检查了技术声明。本文不会从模型输出推导性能或生产就绪结论。
最终思考
原子修改承诺中最重要的词不是原子,而是范围。安全设计会明确谁在读取、提案绑定哪个版本、验证哪个状态,以及发布究竟发生在哪里。
Semaprax 让我们认识到,多次安全的文件写入不等于一次安全的程序修改。先构建完整世代,完成验证,再切换一个指针。对非声明事项的描述,应当与正常路径同样明确。
