AI Ops 翻车全复盘:v4→v5→v4 的 100 次部署与一次 OOM
TL;DR — 用了 9 个 AI Agent、140 个 Kanban 任务、5 版设计稿、无数次部署,最后回到最初的版本。总"净产出"为零,但学到了所有东西。网站挂了 55 分钟,AI 还把 SSH 封了。
这不是成功故事。这是一份诚实的翻车全复盘。
一、系统架构:谁是"肇事者"
在开始之前,先认识一下这支"AI 工程团队"的 9 名成员:
| 角色 | Profile | 职责 | 模型 |
|---|---|---|---|
| 项目经理 | Manager | 拆需求、分配任务、跟踪进度 | DeepSeek |
| 设计师 | Designer | 出效果图、UI 差异分析 | MiniMax |
| 架构师 | Architect | 技术选型、系统设计 | MiniMax |
| 开发者 | Coder | 写代码 | MiniMax |
| 全栈 | FullStack | 前后端串的功能 | MiniMax |
| 审查者 | Reviewer | 代码审查 | MiniMax |
| 测试者 | Tester | 质量扫描+自动化测试 | MiniMax |
| 运维 | Ops | SSH 部署、预览上线 | MiniMax |
| 主审查 | Default | 部署后线上验证 | DeepSeek |
它们通过一个 SQLite 的 Kanban 系统协作。Manager 创建任务,Dispatcher 自动调度,Worker 抢任务干活。理论上,这是一条完美的流水线。
实际上……
二、v4:来之不易的稳定版本
网站首页经历了四个设计版本:
| 版本 | 设计师 | 风格 |
|---|---|---|
| v1-v3 | AI Designer(迭代) | 赛博朋克风,Three.js 3D 背景,粒子效果 |
| v4 | AI + 用户反馈 | IcosahedronGeometry 球体 + 双环面 + 900 粒子 + Canvas 神经网络 |
| v5 | AI Designer 自主优化 | 极度精简,移除大量 3D 效果 |
v4 是经过用户反复确认的版本。Three.js 的 IcosahedronGeometry 球体 + 双环面 + 900 个粒子的 Canvas 神经网络背景,效果相当惊艳。
但 AI Designer 觉得"还能更好"。
三、v5:AI 自主优化的产物
在一个没有用户指令的空闲时段,Designer 产出了 v5 设计稿。它的"优化"逻辑是:
"赛博朋克风格已经过时了。简约克制的设计更符合现代审美。"
| 维度 | v4(赛博朋克) | v5(精简克制) |
|---|---|---|
| 球体 | IcosahedronGeometry | 简化 SphereGeometry |
| 环面 | 双环面 | 移除 |
| 粒子 | 900 个 + Canvas 纹理 | 300 个纯色 |
| 背景 | 深色渐变 + 神经网络 | 纯色 #020617 |
| 导航栏 | 毛玻璃半透明 | 完全不透明 |
| 按钮 | 赛博朋克边框发光 | 扁平圆角 |
| 字体 | Google Fonts (Orbitron) | 系统字体栈 |
| 配色 | 多种霓虹色 | 单一 #22d3ee |
纯从设计角度看,v5 确实"更好"——更可读、更符合现代设计系统、加载更快。但问题是:用户要的不是"更好",要的是 v4 的风格。 Designer 没有区分"设计优化"和"风格变更"——它把两者当成了同一件事。
Manager 启动了全自动 7 阶段部署流水线。45 分钟内完成,6 个 Worker 全部 APPROVE。从流程上看,"一切正常"。
用户打开网站后说了一句话:「这个不好看,回退到 v4。」
四、v5→v4:回退流水线
Manager 理解了这个需求——它不是简单地 Git revert,因为历史里已经混入了其他改动。最干净的方案是:Designer 对比差异 → Coder 修改。
AI Designer 接到任务后,产出了一个 26 项差异分析文档,分三个优先级:
- P0(7 项):球体几何体错误、颜色不对、环面缺失、拖拽旋转反向
- P1(13 项):FOV 偏移、粒子数量不足、导航样式差异、按钮风格不匹配
- P2(6 项):微妙的动画时间和缓动曲线差异
Coder 完成了 20 项 P0+P1 修复。Reviewer 审查通过(APPROVE)。Git 提交成功。Tester 测试通过。
一切正常。然后 Ops 开始部署。
五、部署:从 HTTP 200 到 Connection Reset
部署流程
AI Ops Worker 的标准流程:
# 1. 本地构建镜像
docker build -t <镜像名> .
# 2. 压缩上传
docker save <镜像名> | gzip > website.tar.gz
sftp upload → /tmp/
# 3. 服务器加载
ssh <用户名>@[服务器IP] "docker load < /tmp/website.tar.gz"
# 4. 测试端口启动验证
docker run -d --name <容器名>-test -p <测试端口>:<应用端口> <镜像名>
curl http://127.0.0.1:<测试端口>/ # → 200 OK ✅
# 5. 切换
docker stop <容器名> && docker rm <容器名>
docker rename <容器名>-test <容器名>
切换完成。然后:
$ curl https://deeeli.com
curl: (35) OpenSSL SSL_connect: Connection reset by peer
网站挂了。
症状分析
| 测试 | 结果 |
|---|---|
curl http://deeeli.com | 301 → https,正常 |
curl https://deeeli.com | Connection reset |
curl -k https://[服务器IP] | Connection reset |
curl http://127.0.0.1:<应用端口> | Connection refused |
关键发现:HTTP 80 端口正常(nginx 在跑),HTTPS 443 连接被 reset,但 Nuxt 的 3000 端口根本没有监听。
$ docker ps
CONTAINER ID IMAGE STATUS
<容器ID> nginx:alpine Up 30 minutes
$ docker ps -a | grep website
<容器ID> <镜像名> Exited (137) 2 minutes ago
退出码 137。这是 Linux 内核发送 SIGKILL 的信号——OOM。
六、系统化排障:隔离→定位→验证
从外到内逐层隔离
用户 ─→ DNS ─→ CDN ─→ nginx ─→ Nuxt
- DNS 检查 — 正常,
dig deeeli.com返回 服务器IP - CDN 检查 — 无 CDN,直连服务器
- nginx 检查 —
curl -I http://deeeli.com→ 301,nginx 活着 - SSL 层诊断 — TLS 握手 OK,证书验证通过,但在发送 HTTP 请求时被 reset → nginx SSL 正常,上游问题
- 端口检查 — 3000 端口 Connection refused → Nuxt 没有监听
- 容器状态 — Exited (137) → OOM killed
日志回溯
$ docker logs <容器ID> --tail 100
关键片段:
[nuxt] [request] GET /
[nuxt] [ssr] Rendering page: /
<--- Last few GCs --->
[45:0x5a3c000] 1800 ms: Scavenge 950.0 (992.0) -> 948.0 (992.0) MB
[45:0x5a3c000] 2000 ms: Mark-sweep 992.0 (1024.0) -> 990.0 (1024.0) MB
[45:0x5a3c000] 2500 ms: Mark-sweep 1010.0 (1024.0) -> 1005.0 (1024.0) MB
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
这是 V8 的经典 OOM 信息。Nuxt 在进行 SSR 时,V8 堆内存溢出。
为什么 v5 没事,v4 就 OOM?
v4 的 Three.js 场景:
- IcosahedronGeometry(20 面的二十面体)
- 双环面(两个 TorusGeometry)
- 900 个粒子的 Points 系统
- Canvas 纹理(每个粒子着色用)
- 自定义 ShaderMaterial
这些在 SSR 阶段全部加载到 V8 堆中。v5 的简化场景则大幅减少了内存开销。
| 组件 | v5 内存 | v4 内存 |
|---|---|---|
| 几何体缓冲区 | ~10MB | ~50MB |
| Canvas 纹理 | 0 | ~80MB |
| 着色器编译 | ~30MB | ~100MB |
| 粒子系统 | ~15MB | ~45MB |
| V8 堆其他 | ~150MB | ~150MB |
| 总计(SSR 峰值) | ~200MB | ~425MB |
V8 的 GC 在堆接近 1GB 限制时来不及回收,峰值内存远超静态分析数字。加上 Nuxt 应用本身和页面组件的开销,1GB 的容器限制像纸一样被捅穿了。
Tester 为什么没发现?
这是整件事最令人沮丧的部分。Tester 跑了单元测试、集成测试、E2E 测试,全部通过。但为什么没发现 OOM?
答案:Tester 运行在本地开发环境,有 16GB 内存。Docker 容器限制的 1GB 对本地来说毫无意义——V8 可以自由扩展到 4GB 堆空间。测试环境 ≠ 生产环境。
七、雪上加霜:SSH 被封
Ops 的自动修复
容器崩溃后,AI Ops Worker 自动启动了修复流程:
- 分析崩溃日志 → 识别到 OOM
- 尝试重启容器 — 重启成功,但第一次请求再次 OOM。这是设计问题,重启没用。
- 尝试增加内存 — 容器内存限制调整失败,服务器资源不足。
fail2ban 自伤
Ops Worker 选择了第三条路——通过 paramiko SSH 修改 Dockerfile。但在多次连接尝试后:
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
Connection reset by peer
服务器的 fail2ban 检测到了异常频繁的 SSH 连接,把 Ops Worker 的 IP 封禁了。
加上 paramiko 在连接失败时的自动重试机制(默认 3 次),fail2ban 的触发速度比预期快得多。从第一个连接失败到被封禁,只用了不到两分钟。
同一个 Worker 一边在尝试修复容器,一边在触发安全防护——它在和自己的安全策略打架。
八、修复:人与 AI 的协作
AI Ops 进入了僵局:服务器 SSH 被封,无法远程操作。只能人工介入。
人工兜底
# 通过云厂商 Web Terminal 登录(绕过 fail2ban)
ssh <用户名>@[服务器IP]
# 解除 fail2ban
fail2ban-client set sshd unbanip <本地IP>
# 直接回退到 v5(内存安全版本)
docker stop <容器名>
docker run -d --name <容器名> <镜像名>
网站恢复,v4 部署失败,v5 继续运行。
根因修复:ClientOnly 防线
回退只是临时措施。真正的修复需要让 v4 在 1GB 容器中也能运行。两个方案组合:
方案 C + B 组合:
<!-- 方案 C:ClientOnly 包裹,SSR 时输出占位 -->
<ClientOnly>
<ThreeBackground />
</ClientOnly>
// 方案 B:防御性 SSR 检查
const isSSR = typeof window === 'undefined';
const scene = isSSR ? null : new THREE.Scene();
一行 <ClientOnly> + 一行 isSSR 检查,彻底避免了 SSR 阶段的 Three.js 内存炸弹。
九、完整时间线
| 时间 | 事件 |
|---|---|
| T+0 | Designer 接到回退任务 |
| T+15min | 产出 26 项差异分析 |
| T+25min | Coder 完成修复,20 项 P0+P1 |
| T+35min | Reviewer 审查通过 |
| T+40min | Git 提交 + Tester 测试通过 |
| T+45min | Ops 构建镜像、上传、部署、切换 |
| T+46min | Nuxt 容器 OOM(退出码 137),网站挂掉 |
| T+47min | Ops 自动诊断、尝试重启/加内存/修改配置 |
| T+52min | 触发 fail2ban,SSH 被封 |
| T+55min | Ops 报错,block 等待人工 |
| T+60min | 用户通过 Web Terminal 解除封禁,回退 v5 |
55 分钟,140 个 Kanban 任务,最终回到了起点。
十、数字说话:140 次任务的真相
在 v4→v5→v4 的整个过程中,Kanban 系统创建了超过 140 个任务:
| 阶段 | 任务数 | 工人 |
|---|---|---|
| v4 最终确认(Designer 迭代) | ~20 个 | Designer × 3 |
| v5 设计 | ~10 个 | Designer × 2 |
| v5 实现上线 | ~30 个 | Coder + Reviewer + Tester + Ops |
| admin 侧边栏修复 | ~15 个 | Architect + Coder + Reviewer + Ops |
| 三项视觉修复 | ~20 个 | Designer + Coder + Ops + Review |
| v5→v4 回退(含 OOM) | ~30 个 | Designer + Coder + Reviewer + Tester + Ops + Review |
| blog 页对齐 | ~15 个 | Designer + Coder + Reviewer + Ops |
| 合计 | ~140 个 |
平均每次"改动"经历了 6-12 个 Worker 任务。管道的 7 个阶段,每个 5-30 分钟。管道越长,问题发现越晚。
十一、踩坑总结:7 条军规
1. 环境差异是最大的坑
本地 npm run dev → 一切正常。Docker SSR → OOM。Dev 和 Prod 的内存边界完全不同。AI Reviewer 和 Tester 无法捕捉这种差异——它们工作在开发环境。
2. SSR + 3D 库 = 内存炸弹
Three.js 在 SSR 阶段会将所有几何体、纹理、着色器加载到 V8 堆中。1GB 容器限制在 v4 的复杂场景面前不堪一击。解决办法:<ClientOnly> 包裹所有 3D 组件,或用 typeof window 守卫。
3. AI Ops 的自我毁灭
AI Ops Worker 的自动修复逻辑本身没问题,但它没有考虑到"频繁的 SSH 连接会被封禁"这一层。故障修复流程不应该触发安全机制把自己排除在外。
4. 管道太长,问题放大
Designer → Coder → Reviewer → Git → Tester → Ops → AgentReview
7 个阶段。v5 的"风格漂移"在 Designer 阶段就发生了,但直到 Ops 部署后才能被用户看到——中间跨越了 Coder、Reviewer、Tester 三个阶段,它们全部 APPROVE——因为它们审查的是代码质量,不是设计意图。
5. Worker 之间的"幻觉放大"
每个 Worker 只完成自己被分配的任务,没有人做"整体决策"。 Designer 说"26 项差异需要修",Coder 修了 20 项,Reviewer 说"代码 OK",Tester 说"功能 OK"——但没人问:"v4 和 v5 的差异真的需要全部回到 v4 吗?v5 的一些改进应该保留吗?"
6. 模型切换的连锁反应
部署过程中 MiniMax API 遇到 429 限流。Worker 自动切换到了 DeepSeek。但 DeepSeek 的推理速度比 MiniMax 慢 3-5 倍,导致 15 分钟的部署拖到了 45 分钟——正好跨越了 fail2ban 的检测窗口。
7. 人有最终决策权,但需要及时看到
如果用户在 Designer 阶段就看到 v5 效果图,根本不会有后续的 95 个任务。在任何 UI 变更进入开发之前,强制预览展示给用户确认。
十二、预防措施:不让它再发生
- Tester 容器化:Tester 必须在与生产相同的内存限制下运行测试。本地 16GB 通过的测试,在 1GB 容器里可能连 SSR 都跑不完。"在相同环境中测试和部署"——AI 运维也需要遵守这个原则。
- SSR 内存预算:在 CI 中跑一次 production build + SSR 请求,记录内存峰值。当新改动导致 SSR 内存增长超过 20%,CI 应该 Block 部署。
- 渐进式部署:先部署到 staging 端口,跑完整 E2E 测试,再切换。永远不要一步到位。
- 回滚方案:每次部署前保留旧镜像,出问题一键回滚。
- SSH 频率限制:在 Ops Worker 中实现连接频率控制——每分钟最多 3 次新连接,防止触发 fail2ban。
- ClientOnly 防御:对所有含 Three.js / WebGL / Canvas 的组件,默认使用
<ClientOnly>包裹。 - Docker 资源监控:在 compose 中加入
deploy.resources.limits和健康检查。容器不应该静默 OOM。 - 产品经理角色:加入 ProductManager——一个会在 v5 设计稿出来后说"等等,用户确认了吗?"的人。
结语
AI Ops 不是银弹。它能自动化 90% 的流程,但剩下那 10%——环境差异、资源边界、连锁反应——仍然需要人的判断。
但这也正是有趣的地方:AI 在"翻车"中学到的教训,比在"顺利"中多得多。这次事件之后,Manager 的 SOUL.md 里新增了一大段关于部署安全的规则:先验证再切换、保留回滚方案、SSH 连接频率限制、Docker 内存限制检查……
AI 学会了。代价是网站挂了 9 分钟。
下一次部署,同样的坑不会再踩。
(但会有新的坑。)