我让 AI 参与维护 9 台 NixOS(上):为什么声明式系统适合人机协作

发布于

本文不是一篇“让 AI 帮我写了几个配置文件”的体验文,而是一份持续协作记录:我让 AI 参与维护一套真实运行的 NixOS 基础设施,经历需求讨论、代码审查、静态检查、测试、金丝雀部署、全节点发布和线上告警处理。它确实提高了效率,也确实犯过一些只有人类知道为什么不能犯的错误。

本文涉及的公开配置与实现:[LokiSharp/nix-config](https://github.com/LokiSharp/nix-config)。真实凭据保存在独立私有仓库中,不包含在公开配置里。

### 写在前面

先说明两点。

第一,本文中的“AI”不是一个被接入生产环境后完全自主行动的机器人。它运行在我的工作区中,能读取仓库、修改文件和执行检查;只有在我明确授权后,才会提交、连接节点或部署。敏感值由 SOPS 管理,我不会因为调试方便就把明文 secret 交给它。

第二,这篇文章本身也由 AI 参与整理。事实材料来自真实仓库、提交历史、部署输出、监控邮件和我们之间的连续对话;章节结构和初稿由 AI 生成,我负责提供语境、纠正错误和决定哪些经验值得公开。换句话说,文章的产生方式就是文章主题的一部分。

如果不想读完整连载,可以先记住下面五点:

1. NixOS 最适合 AI 的地方,不是 Nix 语法容易生成,而是系统状态能被求值、构建、测试、比较和回滚;
2. AI 最有价值的工作不是一次写出完整配置,而是持续做仓库搜索、约束翻译、机械重构、故障调查和文档同步;
3. `nix flake check` 通过只代表验证链的一层,真实部署仍然需要 Test 金丝雀、目标节点探针和一段时间的监控;
4. AI 会删除有意保留的模板、添加恒真测试、误解历史地址、把告警安静误当成故障解决,所以权限边界和停止条件必须写清楚;
5. 最可靠的合作方式是让人负责目标与授权,让 AI 负责搜索和执行,让 Nix、Git、测试、Test 节点与监控分别提供不同层次的证据。

本文不会证明“NixOS 是唯一正确的发行版”,也不会证明“AI 已经能替代运维工程师”。我想讨论的是一个更具体的问题:当系统本身可以被声明、检查和回滚时,我们是否能用一种比“复制 AI 给出的命令并祈祷”更成熟的方式与它合作?

### 一、先说结论

过去一段时间,我一直在尝试让 AI 深度参与自己的 NixOS 配置仓库。

这里的“深度参与”不是让它生成一段 `configuration.nix`,然后由我复制粘贴;而是让它进入仓库,阅读现有模块,理解主机模型,修改代码,运行格式化和静态检查,补测试,按照提交规范拆分 commit,先部署到 Test 节点,验证无误后再推向其他节点,最后继续读取 systemd、Prometheus 和 Alertmanager 的反馈。

目前这套仓库管理:

- 9 台 NixOS 节点;
- 1 台 nix-darwin 设备;
- 裸机服务器、虚拟机、测试节点以及多个不同供应商的 VPS;
- 261 个 Nix 文件;
- 30 组 x86_64-linux 求值测试;
- Home Manager、Colmena、Disko、impermanence、sops-nix;
- BIRD、DN42、ZeroTier、Tailscale、sing-box、Caddy、PostgreSQL、Gitea、Grafana、VictoriaMetrics、Alertmanager 等服务;
- Btrfs 快照、scrub、SMART、mdraid、coredump 和部署后健康检查。

我的结论是:

**NixOS 并不会让 AI 自动变得可靠,但它会把 AI 的大量错误提前变成可观察、可比较、可拒绝的结果。**

这两者的组合真正有价值的地方,不是“AI 会写 Nix”,而是 NixOS 把系统状态变成了代码,又把代码变成了一套可以求值、构建、比较、回滚和部署的闭环。

如果使用普通发行版,AI 可能会告诉你执行十几条命令、修改五个路径下的配置、重启三个服务。命令执行完以后,机器究竟处于什么状态,需要靠人记忆。

在 NixOS 中,更理想的合作方式是:

1. 人描述目标、约束和不可接受的风险;
2. AI 阅读仓库中的现有事实;
3. AI 修改声明式配置;
4. Nix 对配置进行求值;
5. 测试检查跨节点约束;
6. Test 节点承担真实运行验证;
7. 监控系统提供部署后的长期反馈;
8. 失败时回到上一个 generation 或上一个 commit。

它仍然不是“按一下按钮全自动运维”,但已经很接近一种可审计的人机协作工程。

### 二、为什么偏偏是 NixOS

#### 1. AI 最擅长操作文本,而 NixOS 恰好把系统变成文本

AI 对当前机器里“曾经运行过哪些命令”没有天然记忆,也很难仅凭 `/etc`、数据库和服务状态还原管理员过去几年的意图。

但它很擅长:

- 搜索代码;
- 比较相似模块;
- 找出重复结构;
- 根据类型和约束补全字段;
- 把手工流程整理成函数;
- 根据错误信息做局部修正;
- 为已经明确的规则生成测试。

NixOS 把软件包、用户、systemd unit、内核参数、防火墙、文件系统、服务配置和部署元数据都放进同一个表达式系统。这意味着 AI 不必先猜“这台机器可能被手动改过什么”,而可以从仓库中得到一个相对完整的意图模型。

Nix 官方文档把 Nix 的典型使用场景概括为可复现开发环境和 Linux 机器的声明式定义;Flake 又提供了统一入口、输入锁定和标准化输出。对 AI 来说,这些恰好意味着更稳定的上下文:依赖版本、主机输出和测试入口都在仓库里,而不是散落在聊天记录中。

#### 2. Nix 的失败通常发生得比较早

传统运维脚本的典型风险是:前八步成功,第九步失败,机器停在一种很难描述的中间状态。

Nix 当然也可能在 activation 阶段失败,但大量问题会更早暴露:

- 语法错误在解析阶段暴露;
- option 类型错误在模块求值阶段暴露;
- 不存在的属性在 evaluation 阶段暴露;
- package、unit、secret 声明可以在构建阶段暴露;
- 跨节点重复地址可以通过纯求值测试暴露;
- Prometheus 规则可以通过 Promtool 暴露;
- Nushell 的错误数据形状可以通过类型签名和测试暴露。

这对 AI 特别重要。AI 最大的问题通常不是完全不会,而是“看起来很像对的”。越早让机器检查它,越不需要依赖人类逐字阅读几千行差异。

#### 3. 旧 generation 给试错留下了空间

NixOS 的代际模型让一次系统切换不会直接覆盖所有旧状态。只要启动链、磁盘和远程入口还在,很多错误都可以切回上一代。

这并不意味着可以让 AI 随便部署。错误的防火墙、磁盘布局、SSH 配置和 secret 仍然可能把人锁在门外。但是相较于不可追踪的命令历史,generation 至少让“刚才那次系统变更”有一个清楚边界。

#### 4. Flake 既是入口,也是边界

我的仓库使用 Flake 管理输入和输出。AI 可以从 `flake.nix` 开始理解:

- 使用哪些 nixpkgs 分支;
- 哪些输入是公开依赖;
- 哪个输入来自私有 secrets 仓库;
- 有哪些 NixOS、Darwin、package、check 和 devShell 输出;
- CI 和本地执行的求值是否一致。

这里也有一个很实际的坑:Git 仓库中的 Flake 默认只看到已跟踪或已暂存的文件。

我曾经让 AI 新增一组测试。它运行 `just test`,所有测试都通过了,但报告里没有新测试。原因不是测试写得好,而是新文件还没有进入 Git,Flake 根本没看见它。

后来流程被修正成:

1. 新增测试文件;
2. 确认 Git 能看到它;
3. 重新运行求值;
4. 必须在测试报告中明确看到新测试名称;
5. 才能说“新增测试通过”。

这是一个非常典型的例子:AI 会把“命令退出码为 0”理解为成功,而工程系统必须继续追问“我们想测的东西真的参与测试了吗?”

### 三、我的仓库不是从一开始就这么整齐

这套配置最早也有大量常见问题:

- 主机元数据散落在不同文件;
- index、地址和部署标签之间没有统一约束;
- 一些测试只是把实现重新抄一遍;
- 部署依靠单节点命令,没有强制 Test 金丝雀;
- 健康检查默认使用高权限账户;
- CI 只能求值,无法发现部分真实构建问题;
- 静态检查加入后,预留模板被误判为无用代码;
- 网络、日志和监控告警有不少历史噪声;
- 文档只覆盖局部目录,新接手者很难得到全局图。

AI 真正带来的改变不是一次“大重构”,而是把这些模糊的不舒服逐步变成具体问题,然后一项一项处理。

这个过程持续了很多轮对话。很多时候,我只会问:

> 接下来还有什么可以优化?

AI 会先读代码,列出若干候选;我再问每一项的意义、代价和风险。确认以后,只改其中一项,测试、提交、部署,再讨论下一项。

这种节奏比让 AI 一次生成一个“完美架构”可靠得多。

### 四、案例一:从“这个节点该用几号”到全局地址约束

我的多节点网络里,主机 index 不只是一个展示字段,它还参与生成多个内部网络地址、部署标签和 DNS 记录。

一次讨论从一个很小的问题开始:某个节点应该使用哪个编号。

AI 先分析现有地址分配逻辑,又解释 `/26` 的地址范围、主机位和可用地址。我们讨论过是否把整个序号换掉,最后给 OVH 节点选择了 7。

如果只是修改一个数字,这件事没有太大意义。真正有价值的是随后补出的约束:

- 每个主机 index 全局唯一;
- 由 index 派生的各网络地址不能重复;
- 启用某网络时,相关字段必须完整;
- ZeroTier node ID 不能在两个主机上复用;
- DNS 记录必须覆盖所有应该出现的节点;
- 已退休节点不能继续残留在输出中。

最后,主机元数据从“方便写配置的 attrset”变成了一种小型数据模型。

AI 在这里的优势很明显:它很适合沿着字段依赖关系搜索所有消费者,然后生成跨节点测试。

但它也暴露了一个危险倾向:喜欢加“看起来更保险”的判断。

例如它曾经加入:

```nix
benchmarkIPv4RoutesAbsent =
lib.all (route: !lib.hasPrefix "198.19." route.target) routes;
```

问题是,我的 ZeroTier 正常地址使用的是 `198.18.<index>.0/24`,而 `198.19.*` 已经不再使用。这条检查虽然会通过,却没有保护任何当前需求。它只是检查一个已经不存在的历史路径。

我追问:

> 为什么要加这个判断?这个是我 ZeroTier 的地址。

继续讨论以后,我们删掉了这类冗余测试。

这件事让我形成了一个很重要的判断标准:

**测试不能只证明“某个字符串不存在”,它必须对应一个仍然存在的风险。**

AI 很容易生成大量断言,但断言数量不等于保障强度。

### 五、案例二:SSH 不能只检查“服务开着”

我最不能接受的部署事故之一,是远程修改后把自己锁在服务器外面。

仓库原来有一个类似 `public-ssh-port.nftablesEnabled` 的检查。我没有同意删除它,反而要求:

> 这个还是检查吧,我不想被关在外面。是不是要再扩展一下,从各个角度保证 SSH 端口开放?

于是 SSH 保障不再只是检查一个布尔值,而是从多个层面验证:

- OpenSSH 服务启用;
- 实际监听端口与主机元数据一致;
- 防火墙允许同一个端口;
- 公开暴露面期望值与配置一致;
- 部署健康元数据携带正确端口;
- Colmena 的目标端口没有走另一条遗留配置;
- 所有公网节点遵循同一约束。

这正是 NixOS 与 AI 配合得很舒服的地方。

如果用普通脚本,AI 可能给出:

```bash
systemctl status sshd
ss -lntp
iptables -L
```

这些命令只能描述当前机器的一瞬间。

在 NixOS 中,我们可以把“SSH 服务、端口、防火墙、部署目标和测试期望必须一致”写成仓库长期成立的不变量。以后改端口时,只要遗漏任何一个消费者,求值测试就会失败。

当然,这仍然不能完全替代外部探测。内部网络访问成功,不等于公网防火墙路径正确。因此仓库还保留了从不受信网络执行 public exposure 检查的入口。

声明式测试和真实网络探测并不是二选一:前者检查意图一致性,后者检查现实世界。

### 六、案例三:让 Test 节点成为真正的发布闸门

最初的部署方式是 `just <HostName>`:选一个节点,交给 Colmena。

这种方式对人来说很直接,对 AI 来说却太自由。只要命令构造错一个目标,就可能直接修改生产节点。

后来我们把发布流程改成固定顺序:

1. 完整求值 Flake;
2. 执行部署流程自身的可执行测试;
3. 只部署 `Test-NixOS`;
4. 检查 Test 的 SSH、主机名、systemd、内核、audit、ZeroTier、BIRD、DNS 和日志;
5. Test 全部通过后,才部署剩余节点;
6. 最后对所有节点执行健康检查。

现在完整发布入口是:

```console
just deploy-all
```

这里最关键的并不是命令变短,而是部署顺序从“聊天中的约定”变成了代码。

为了验证这个顺序,我们还给部署编排本身写了测试:

- Test 部署失败时,其他节点不能开始;
- Test 健康检查失败时,其他节点不能开始;
- 中途某节点部署失败时,不能伪装成全量成功;
- 最终健康检查失败时,命令必须返回失败;
- SSH multiplexing 失效时允许重建连接;
- 认证失败和真实网络错误不能被错误地当作 multiplexing 问题重试。

有一次真实发布中,所有节点都成功 activation,但最终健康检查发现 `MoeDove-TPE -> Test-NixOS` 的 SLK IPv6 在十个包里丢了两个,超过 10% 阈值。

发布命令因此返回失败。

AI 没有把它说成“部署失败”,也没有立刻重新部署全部节点,而是区分:

- 配置是否已经成功激活;
- 服务是否正常;
- 是哪一条健康检查失败;
- 同一节点的 IPv4 和另一条 IPv6 overlay 是否正常;
- 单独复测后丢包是否回落。

复测时,SLK IPv6 丢包降到 10%,按策略记录 warning;Loki-Net IPv6 无丢包,节点健康检查通过。

这类细节非常重要。一个好的自动化系统不应该只有“成功/失败”两个词,而应该保留失败发生在哪一层。

### 上篇小结

这一篇先回答了“为什么是 NixOS”:AI 擅长处理文本和结构,NixOS 则把系统意图变成可以求值、构建、测试和回滚的文本。主机编号、SSH 端口和部署顺序不再只是管理员脑中的约定,而可以成为仓库中的长期约束。

但让 AI 能修改配置,并不等于应该给它无限权限。下一篇会继续写健康检查账户、secrets 预留模板、Nushell 类型、Btrfs 快照、磁盘监控、Alertmanager 告警和测试反例,并进一步讨论怎样给 AI 下达运维任务、如何判断它提供的证据是否足够。

> 系列下一篇:《我让 AI 参与维护 9 台 NixOS (中):权限、故障案例与实战方法》

### 本篇参考

- [nix.dev:Nix 官方文档](https://nix.dev/)
- [nix.dev:Flakes 概念](https://nix.dev/concepts/flakes.html)
- [NixOS Wiki:NixOS 概览与 generations](https://wiki.nixos.org/wiki/Overview_of_the_NixOS_Linux_distribution)

---

原文链接:[点击查看](https://www.v2ex.com/t/1231205)

评论

暂无评论。

0.095395s