先看结果:AI 一下午写完 1,842 行工具,第一次更新却让 312 条客户请求全部消失
独立运营顾问林序请 AI 做一个客户请求分流工具:上传表格文件,按客户、主题和期限归类,再生成待办。首版有 1,842 行 TypeScript 代码,在她的电脑上演示成功,她便把文件复制到一台小服务器。两周后,她让 AI “加一个负责人筛选器,顺便升级依赖”。页面仍能打开,但数据库路径已经从项目目录变成临时目录。服务器重启后,312 条请求从界面中全部消失;旧数据库其实还在磁盘上,林序却没有任何恢复步骤。
这时再让 AI 猜着修,很可能把仍可恢复的数据真正破坏掉。林序先停止写入,再把工具重建成一个可维护项目。最终发布的 v0.3.0 固定了运行环境和依赖版本,增加配置规则、迁移编号、48 项自动测试、预发布环境、发布回执、备份与回滚手册。后来 v0.3.1 在预发布中漏过一个重试缺陷,生产环境出现重复任务;监控在第 6 分钟发现异常,林序按预演步骤用 7 分钟切回 v0.3.0,并保留已经发生的写入事件供对账。
| 首版 | 重建后 | 验收证据 |
|---|---|---|
| 本机目录,无版本 | 版本库与发布标签 | 提交与标签可定位 |
| 依赖取“最新” | 固定运行时与锁文件 | 干净安装的内容摘要一致 |
| 密钥写在代码 | 环境注入与权限隔离 | 密钥扫描与轮换回执 |
| 数据库路径隐式 | 数据目录与配置字段 | 启动前校验 |
| 0 项测试 | 28 项单元 + 12 项集成 + 8 项端到端 = 48 | 持续集成报告 |
| 直接复制生产 | 预发布→人工发布门→生产 | 部署回执 |
| “有备份”未恢复 | 备份 + 恢复演练 | 恢复结果 |
| 出错后现场修改 | 不可变构建产物与回滚 | v0.3.0 构建产物哈希值 |
本文是虚构教学案例。它不承诺任何技术栈天然安全,也不替代专业开发、安全、隐私或运维审阅;一旦工具处理付款、身份、医疗、人员决策、关键基础设施或大量客户数据,单人自行上线的边界会明显收紧。
可维护不是“代码好看”,而是未来改动可理解、可验证、可发布、可撤回
一个人维护工具最怕四种未知:不知道现在运行哪一版;不知道改动影响什么;不知道生产是否正常;不知道出错怎样回到已知状态。可维护性的工作定义因此不是抽象评分,而是八个问题:
| 维度 | 必须回答 |
|---|---|
| 重建 | 能否从空目录按说明得到相同可运行产物 |
| 理解 | 入口、数据流、外部依赖和关键约束在哪里 |
| 变更 | 一项需求对应哪些文件、测试和迁移 |
| 验证 | 正常、边界、失败和权限行为怎样自动检查 |
| 发布 | 谁把哪个构建产物放到哪个环境 |
| 观察 | 错误、延迟、数据变化与外部动作怎样被发现 |
| 恢复 | 代码、配置和数据分别怎样回退/恢复 |
| 交接 | 明天换工具或找专家时,证据能否交接 |
“AI 能重新生成”不能替代这些能力。重新生成的是另一个候选实现,不会自动保留旧数据、兼容接口或复现当时依赖。
先写工具合同:用户任务、输入输出、禁止动作和失败行为
林序先把模糊需求“帮我管理客户请求”改成一组可以验收的行为:导入 UTF-8 编码的 CSV 文件;以请求编号保证重复导入不会重复写入;允许人工修正分类;只生成草稿任务,不向客户发送。遇到字段、数据库或外部服务异常,必须保留原输入并停止整个批次。
| 合同字段 | 已填写内容 |
|---|---|
| 用户 | 林序本人;只读协作者 1 人 |
| 输入 | CSV 字段结构 v2,最多 1,000 行/批 |
| 输出 | 请求记录、分类候选、草稿任务 |
| 事实来源 | 本地受控数据库,不是导出 CSV |
| 外部行动 | 默认关闭;不自动发邮件/建第三方任务 |
| 重复规则 | 请求编号与来源组合唯一,重复时返回原回执 |
| 失败 | 整批回滚,保存错误回执 |
| 数据边界 | 不导入密码、支付卡、无关附件 |
| 可用性 | 工作时段工具,不承诺 24/7 |
| 负责人 | 林序对业务决定负责;高风险技术问题求助专家 |
这份合同随后成为验收测试的来源。AI 不能为了让演示通过,给缺少请求编号的数据行私自补号;也不能在网络失败时悄悄跳过没有写入的记录。
建项目地图:入口、模块、数据、外部系统与生成边界要一眼可见
app/
web/ # 页面与表单
domain/ # 分类、状态、幂等规则
adapters/ # CSV、数据库、可选外部任务系统
migrations/ # 001...不可改写的数据迁移
tests/ # unit/integration/e2e/fixtures
config/schema.ts # 允许的环境配置
docs/ # 架构、运行、发布、恢复
scripts/ # 可重复构建/检查/备份/恢复
每个目录都有明确职责:业务规则模块不直接读取环境变量或发起网络请求,连接外部系统的适配层也不决定业务分类。如果生成代码越过边界,例如让页面按钮直接写数据库并调用外部接口,就先拆开职责,再继续增加功能。
| 流程 | 输入 | 输出 | 失败边界 |
|---|---|---|---|
| CSV 解析 | 字节 + 字段结构 v2 | 已验证行 | 整批拒绝+错误行 |
| 导入 | 已验证行 | 幂等记录 | 事务回滚 |
| 分类 | 记录快照 | 建议 + 原因 | 弃权,不改原文 |
| 人工审批 | 建议 + 用户 | 分类事件 | 权限/版本冲突阻断 |
| 草稿任务 | 已批准记录 | 本地草稿 | 不触发外部动作 |
项目地图还要分别标出 AI 可以生成的区域,以及必须由人维护的不变量。后者包括数据唯一键、权限、事务、发送边界和恢复规则,任何模型修改都必须显式审阅。
林序又为五条关键不变量指定“证明位置”,避免规则只存在于说明书里。请求编号唯一,由数据库约束和集成测试共同证明;整批导入要么全部成功、要么全部回滚,由事务测试证明;人工修正必须保留前后值,由只追加不覆盖的事件记录证明;只读协作者不能批准,由权限测试证明;外部发送默认关闭,由配置默认值和端到端网络拦截证明。如果一条不变量找不到执行位置和测试位置,它就还只是愿望。
她还画出故障传播方向:CSV 解析失败不应触及数据库;分类服务失败,只能让分类进入待定;外部任务系统失败,只能把待发送内容留在发件箱,不能把已经批准的客户请求退回“未导入”;日志系统失败时,更不能把客户原文降级写到控制台。边界图的价值,是让非程序员能问出“这一层失败会改坏哪一层”,而不是逼自己读懂每一行代码。
冻结运行环境和依赖:能在今天安装,不代表下月还能重建
首版说明文档只有“安装依赖并运行”一句话,软件包声明使用宽泛版本,服务器运行时也没有记录。重建后,项目保存运行时的主次版本、包管理器版本、锁文件、系统库、构建命令和构建产物哈希值。
| 资产 | 规则 | 验收 |
|---|---|---|
| 运行时 | 声明版本,并在持续集成中使用同一版本 | 版本检查 |
| 依赖清单 | 每个直接依赖都有用途与负责人 | 复核差异 |
| 锁文件 | 与依赖清单一起提交 | 冻结安装 |
| 构建 | 无网络外输入或明确固定 | 重复构建 |
| 产物 | 一次构建,多环境晋级 | SHA-256 一致 |
| 基础镜像/操作系统 | 固定可支持版本 | 镜像摘要/补丁计划 |
| 许可证与软件物料清单 | 记录依赖与许可证候选 | 发布附件 |
固定版本不是永不升级,而是把升级变成一项独立变更:先阅读发布说明和安全信息,只更新一个可以解释的范围,跑完全部测试,再决定是否发布。AI 不能把“顺便升级所有依赖”夹在功能修改里。
语义化版本只有在公开接口已经定义后,主版本、次版本和修订版本才有明确含义。这个内部工具也要把 CSV 数据结构、网络接口路由、导出格式和配置键定义为公共契约;版本号表达的是兼容承诺,不是营销进度。
密钥移出代码后,配置仍要有规则、权限和轮换
首版把接口令牌和数据库路径直接写在源文件中。把它们移出去只是第一步,还要区分公共配置、机密、特定环境的值和运行状态,并在启动前完成验证。
| 配置 | 类型 | 默认 | 校验失败时的行为 |
|---|---|---|---|
| 运行环境(APP_ENV) | 枚举 | 无 | 缺失时阻断启动 |
| 数据目录(DATA_DIR) | 绝对路径 | 无 | 不存在或不可写时阻断 |
| 单批导入上限(MAX_IMPORT_ROWS) | 1—1000 的整数 | 500 | 超出范围时阻断 |
| 外部任务开关(EXTERNAL_TASKS) | 布尔值 | 关闭 | 未明确开启时不外发 |
| 任务接口令牌(TASK_API_TOKEN) | 机密 | 无 | 外发关闭时不需要 |
| 日志级别(LOG_LEVEL) | 枚举 | 信息 | 值不合法时阻断 |
机密不能写入日志、测试样本或前端软件包,也不能随备份以明文复制。首版已经泄露的令牌,不能只从代码中删除,还要立即轮换并检查版本历史与访问日志。是否清理版本历史需要专业评估,既不能破坏审计,也不能误以为改写历史后风险就自动消失。
权限按最小动作拆分:应用只能读写指定数据库目录;备份任务只能读取快照并写入备份库;部署流程在通过发布门之前拿不到生产机密。GitHub 当前的环境文档说明,环境可以配置保护规则、分支或标签限制和密钥,但部分能力受套餐或仓库可见性影响。使用前要核对自己的方案,不能把文档示例当成普遍可用的默认能力。
启动时生成一份不含秘密值的配置回执,记录配置规则版本、必填键是否存在、数据目录的规范化路径、外部任务开关、运行时和配置指纹。林序把开发、预发布、生产三份回执并排比较:具体值可以不同,键集合和类型却不能悄悄漂移。生产环境如果误用开发目录、相对路径,或任何用户都可写的路径,应用必须在接收第一条请求前退出;“先跑起来再修配置”只会把可诊断的错误变成数据事故。
用可执行例子定义需求:正常路径、边界、失败与禁止行为都要有样本
| 案例 | 输入 | 期望 |
|---|---|---|
| 成功 | 2 条合法记录,请求编号不同 | 写入 2 条,返回 2 份回执 |
| 重复重试 | 同一批再次导入 | 写入 0 条,返回原记录编号 |
| 部分无效 | 第 2 行缺少客户 | 整批写入 0 条,并指出第 2 行 |
| 过大 | 1,001 行 | 解析前阻断 |
| 编码 | 非 UTF-8 | 不猜字符,返回格式错误 |
| 模型不可用 | 分类服务超时 | 保留记录,分类待定 |
| 未授权 | 只读用户尝试批准 | 返回 403,状态不变 |
| 禁止字段 | 数据中包含密码字段 | 拒绝并提示移除,不保存该值 |
需求样本要先冻结旧行为基线,再让 AI 修改代码。如果模型同时修改期望结果,测试虽然会“通过”,却可能掩盖行为漂移;期望结果只能由业务负责人批准。
建 48 项分层测试:函数正确、组件协作和用户结果分别验证
28 个单元测试覆盖解析、验证、状态转换、幂等键和分类边界;12 个集成测试使用临时数据库检查事务、迁移、权限和适配器;8 个端到端测试从上传走到人工批准,检查页面、回执和禁止外发。
| 层级 | 快 | 能发现 | 不能单独证明 |
|---|---|---|---|
| 单元 | 是 | 规则/边界与纯函数回归 | 数据库和配置真实协作 |
| 集成 | 中 | 字段结构、事务、适配器、权限 | 浏览器完整任务 |
| 端到端 | 慢 | 用户路径和关键系统连接 | 所有输入组合 |
| 恢复演练 | 更慢 | 备份真的可恢复 | 新版本业务正确 |
| 安全检查 | 不同 | 机密、依赖、权限候选问题 | 没有漏洞 |
固定测试样本要记录结构版本和来源,不能复制真实客户数据。测试失败后,AI 不能直接删除测试或放宽断言;如果需求确实改变,应先批准契约变更,再更新测试与迁移。
48 项不是为了凑数字。28 个单元测试中,11 个覆盖字段与编码,7 个覆盖状态转换,6 个覆盖幂等键,4 个覆盖分类弃权;12 个集成测试中,4 个检查事务,3 个检查 001→003 迁移,3 个检查角色权限,2 个检查服务商连接和出站队列;8 个端到端测试中,4 个覆盖主任务,2 个覆盖中途失败后的恢复,1 个确认键盘无障碍路径,1 个确认外部发送确实关闭。测试名和清单一一对应,新增任何合同行为都要说明加入哪一层,不能用一个巨大的端到端测试替代可定位的证据。
林序还做了反向破坏抽查:临时把“缺少客户必须拒绝”改成允许,移除唯一约束,再让只读用户通过批准,确认相关测试都会失败,然后撤销这些故意破坏。如果关键实现被删掉后测试仍然全绿,即使覆盖率数字很好看,也说明断言没有守住合同。
审查 AI 改了什么,不看“最终文件似乎不错”
每次请求只解决一个变更合同,写清目的、允许修改的文件、禁止区域、验收测试和回滚影响。AI 返回变更文件清单、原因、假设、测试、迁移和风险;人工逐项查看实际差异。
| 差异信号 | 问题 | 动作 |
|---|---|---|
| 无关文件 | 为什么被修改 | 拆分变更或回退无关项 |
| 依赖已添加 | 现有代码能否完成 | 审版本、维护、许可、风险 |
| 测试被删除 | 行为是否被掩盖 | 阻断,要求负责人说明 |
| 通用错误 | 是否吞错/丢数据 | 要具体错误与回执 |
| 认证绕过 | 为了演示而关闭权限 | 绝不发布 |
| 数据结构变化 | 是否有迁移与回滚 | 阻断直接修改生产 |
| 网络副作用 | 是否默认自动执行 | 改为草稿并经过人工发布门 |
改动范围很大时,可以先让模型按模块解释,再用工具生成机械统计;但解释不是证据,测试和人工检查才是发布依据。
她把“新增负责人筛选”限制在页面组件、查询参数解析、数据查询和相应测试四个区域。AI 第一稿同时格式化了 37 个无关文件,升级 9 个软件包,还重命名状态枚举,净改动 1,126 行;林序拒绝了这一稿。第二稿净改动 146 行,没有改变依赖和数据结构,在 42 项基线测试上新增 6 项测试,共 48 项全部通过。小改动仍可能藏着严重问题,但审查范围缩小后,假设、影响和回滚路径更容易核对。
审查回执不能只写“看过了”,而要留下可复核结论:筛选条件为空时返回全部;负责人未知时返回空集而不是报错;查询参数不会改变存储记录;查询使用已有索引;关闭该功能只需回退一次提交。如果审阅者无法解释一条重要改动,就应暂停并找技术人员,不能用模型自信的说明代替真正理解。
数据结构必须靠迁移演进:改代码、改表和搬旧数据是三件不同的事
312 条请求并没有真的被删除。新版只是把数据目录指向临时位置,应用又在空目录中创建了一份新数据库。林序先停止写入,对旧文件制作只读副本并计算校验值,再用专门脚本盘点 312 条请求、287 条分类和 94 条草稿任务;她没有把文件来回拖动,直到界面“看起来恢复”为止。
重建后的迁移只追加、不改写:001 建立基础表,002 新增负责人字段和筛选索引,003 建立任务发件箱和幂等键。应用启动时读取数据结构版本;版本落后就拒绝服务并提示执行已审核迁移,版本超前也同样阻断,避免旧代码误读新结构。
| 迁移检查 | 001→003 | 003 空库 | 失败注入 |
|---|---|---|---|
| 数据结构到达目标版 | 是 | 是 | 中断后仍可识别状态 |
| 记录数不丢 | 312→312 | 0 | 事务回滚 |
| 唯一键有效 | 重复被拒 | 是 | 返回确定错误 |
| 旧版兼容窗口 | v0.3.0 可读 | 不适用 | 超窗禁止回滚代码 |
删除旧字段时采用“扩展—迁移—收缩”:先增加新结构,让新旧代码都能读取;再在后台搬运并核对数据;最后在后续版本中移除旧结构。如果所谓“回滚迁移”会抹掉新数据,就宁可向前修复。代码回滚和数据恢复必须分别设计。
依赖与供应链检查不能只问“有没有高危告警”
依赖审查先问是否需要,再查直接/间接来源、维护状态、许可证、安装脚本、已知漏洞和升级影响。自动扫描是发现线索,不等于安全证明;一个没有公开漏洞编号的包,也可能因无人维护、名称仿冒或过宽权限而不适合使用。
| 变更 | v0.3.0 决定 | 理由/证据 |
|---|---|---|
| CSV 解析器 | 保留既有版本 | 固定测试样本全过,没有功能需求推动升级 |
| UI 日期库 | 移除 | 原生格式化已满足,减少依赖 |
| 数据库驱动 | 单独升级 | 有支持期需求,迁移与恢复另测 |
| AI 开发工具包 | 隔离在适配层之后 | 业务层不绑定服务商响应 |
| 新筛选组件 | 不引入 | 42 行内部组件即可,不增加供应链面 |
发布时保存锁文件、构建清单和软件物料清单。发现漏洞后,要按漏洞是否真实可达、数据暴露范围、外部输入和补丁副作用排序;不能为了让扫描面板变绿,就在未经测试的情况下强制升级生产依赖。
每个允许进入生产的依赖还要有退出方案:AI 开发工具包改变接口时,可以替换适配层而不改变业务测试样本;数据库驱动停止维护时,数据可以导出为稳定结构;界面组件出问题时,核心导入和恢复命令仍不依赖浏览器。退出方案不要求提前写好第二套系统,却要求数据格式、边界和测试不被单一厂商的私有对象锁死。
错误必须可定位,监控必须对应用户结果,而不是只看服务器还活着
统一错误记录包含错误编号、时间、操作、安全错误码、是否可重试和追踪编号。日志只记录请求编号的不可逆摘要和状态,不记录客户原文、令牌或完整 CSV。页面向用户显示错误编号和下一步,内部日志保留因果链,两者都不能泄露机密。
| 信号 | 正常基线 | 告警条件 | 负责人动作 |
|---|---|---|---|
| 导入已接受 | 每批回执一致 | 写入数不等于合法唯一行数 | 停止批次,核对事务 |
| 重复已阻止 | 重试可见但不增加记录 | 同一键出现两条记录 | 关闭写入路径 |
| 待定分类 | 通常 0—8 条 | 连续 15 分钟超过 20 条 | 检查服务商和队列 |
| 任务出站队列 | 默认不外发 | 未批准却出现“已发送” | 紧急关闭外部任务开关 |
| 数据库路径 | 固定绝对路径 | 路径或存储设备改变 | 阻断启动 |
| 磁盘/备份 | 有空间且最近成功 | 低于阈值/备份超时 | 释放容量并验证备份 |
健康检查分为进程存活、服务就绪和业务探测。返回 200 只说明进程还会响应;只有一条合成请求能够完成验证、事务写入、读取和清理,才说明关键内部路径仍然可用。合成数据还必须与真实记录明确隔离。
告警也要有去重和升级路线。单次服务商超时先记录,并按合同把分类转为待定;连续超过阈值才通知林序。唯一键被突破、未经批准向外发送,或数据库路径发生变化,则第一次就立即告警并自动关闭写入。每条告警都要链接到对应运行手册、最近一次部署和追踪记录,而不是只发一句“系统异常”。月度复盘还要记录告警总数、真实事故、误报和漏报;长期被忽略的告警,等于没有监控。
持续集成提供发布证据,不代表自动上生产
每个候选提交都在干净环境中执行格式和类型检查、28 项单元测试、12 项集成测试、8 项端到端测试、迁移演练、密钥扫描、依赖审查和构建。构建只做一次,得到带哈希值的不可变产物;预发布和生产使用同一份产物,不能到服务器现场再安装“最新依赖”。
变更合同
→ 审查实际差异
→ 格式与类型检查
→ 48 项自动测试
→ 迁移演练、密钥与依赖检查
→ 构建产物、哈希值与软件物料清单
→ 预发布证据
→ 人工批准进入生产
| 发布门失败 | 能否忽略 | 正确处理 |
|---|---|---|
| 不稳定的端到端测试 | 否 | 记录并修测试/竞态,不能反复点到绿 |
| 扫描发现机密 | 否 | 阻断、确认暴露范围,必要时轮换 |
| 依赖警告 | 需判断 | 记录可达性与时限,不静默放行 |
| 迁移数量不匹配 | 否 | 保护数据,调查脚本与固定测试样本 |
| 预发布业务探测失败 | 否 | 不把产物推进到生产 |
持续集成也要执行最小权限:普通测试拿不到生产凭证,来自不可信分支的代码不能读取部署机密。平台的发布门只是控制手段,最终责任仍在批准人。
为了避免“持续集成和本机执行的不是同一套流程”,说明文档只调用仓库内的统一任务入口,持续集成也执行同一个入口。脚本先验证运行时和包管理器版本,不满足就明确失败。测试报告保存测试套件数量、通过与失败数、耗时、提交编号和构建产物哈希值。林序做过一次干净环境重建:从空目录取得固定提交,安装用时 43 秒,48 项测试用时 19 秒,构建用时 27 秒;第二次构建产生相同的构建产物哈希值。这些数字只是该次环境的回执,不是速度承诺。
预发布要复现真实发布方式和失败条件
预发布使用与生产相同的构建产物、运行时、启动命令和迁移机制,但使用独立机密与合成数据。林序准备了 80 条固定测试样本:50 条正常、8 条重复、6 条缺字段、4 条超长、4 条特殊字符、4 条模型超时和 4 条权限失败,总计 80 条。预期结果是成功写入 50 条,8 条重复记录返回既有回执,其余 22 条按合同失败,而且不能留下半批数据。
| 预发布验收 | 证据 | 停止条件 |
|---|---|---|
| 干净安装 | 锁文件已冻结 | 解析漂移 |
| 迁移复制 | 312 条匿名化结构样本 | 数量/约束不一致 |
| 合同套件 | 80 测试数据 | 状态或副作用偏离 |
| 浏览器任务 | 导入→修正→草稿 | 任一步不可完成 |
| 外部适配器 | 沙箱 +20 条批准样本 | 重复/越权外发 |
| 恢复演练 | 在新环境中恢复 | 文件哈希值或计数不符 |
不能为了让测试“更真实”,就把整库生产数据复制到预发布环境。如果必须验证罕见数据结构,应先最小化、去标识,并确认授权与保留周期;涉及高敏感数据时,还要引入隐私和安全专业审阅。
首轮预发布刻意演练了迁移中途终止、磁盘低于阈值、分类服务返回格式错误,以及外部系统明确失败。前两种故障要在真实写入前阻断或完整回滚,第三种要保留记录并把分类标为待定,第四种则把待发送内容留在发件箱。但这轮测试没有演练“远端已经创建成功,响应却在返回途中丢失”,所以 v0.3.1 的重试缺陷漏过了。事故后,这一情形才成为第五种强制故障注入。演练必须记录“系统实际做了什么”,不能因为按设计应该安全就直接勾选通过。
版本和发布说明要把兼容性、数据变化与操作者动作写清楚
林序为内部公共契约定义版本规则:破坏 CSV、接口或配置兼容性时提升主版本;增加向后兼容功能时提升次版本;兼容性修复则提升修订版本。语义化版本不会自行判断兼容性,只有先声明公开接口才有意义;数据库迁移、功能开关和外部适配器的版本也要写进发布说明。
| 发布说明字段 | v0.3.0 |
|---|---|
| 产物 | app-v0.3.0,记录 SHA-256 |
| 提交记录 | 从 v0.2.0 到候选标签的固定范围 |
| 行为 | 新增负责人筛选,不改变导入合同 |
| 迁移 | 002、003;先备份再执行 |
| 配置 | 新增外部任务开关,默认关闭 |
| 证据 | 48 项测试、80 条样本和恢复演练 |
| 回滚窗口 | 收缩式迁移前可回到 v0.2.0 |
| 操作者动作 | 核对数据目录,批准后再开外部任务开关 |
标签只能指向已经审查的构建产物。如果重建内容发生变化,即使版本号文字相同,也不能覆盖旧产物。发布回执要记录批准人、时间、环境、构建产物哈希值、数据结构版本、配置指纹和监控窗口。
部署先暗后明:迁移、启动、健康检查和试点有明确顺序
生产发布前先进入短暂的停止写入窗口,创建一致性快照并验证可读。执行向后兼容迁移后,启动新实例,但保持外部任务开关关闭。服务就绪检查、5 条合成路径和 10 条只读抽查全部通过,才允许内部流量进入;外部任务适配器只对 20 条人工选定的记录开放。
| 阶段 | 允许动作 | 观察窗 | 失败动作 |
|---|---|---|---|
| 预检 | 备份、容量、版本核对 | 即时 | 不开始 |
| 暗启动 | 无真实外发 | 5 分钟 | 停新实例 |
| 内部 | 导入/分类/本地草稿 | 10 分钟 | 切回旧实例 |
| 试点开放 | 20 条已批准的外部任务 | 15 分钟 | 关闭开关并对账 |
| 完整 | 仍需人工批准 | 30 分钟 | 回滚/热修复 |
功能开关不是永久分叉。每个开关都要有负责人、默认值、开放条件和删除日期;紧急开关必须事先证明能够阻断副作用,不能等到事故发生时才第一次尝试。
正式发布前,两个人一起暂停并逐项核对。林序作为业务负责人读出版本、数据目录、数据结构、备份回执和停止阈值,技术协作者只核对证据,不替她承担业务决定。发布期间禁止夹带修改配置、临时安装软件包或直接编辑数据库;如果出现运行手册没有覆盖的错误,先中止并保留现场。即使这是一家长期只有一位操作者的 OPC,也可以在高影响发布时购买一次独立技术复核,不能把“单人经营”理解成“所有风险都由一个人自行批准”。
备份、恢复和回滚是三套动作,必须在无压力时演练
备份是保存数据副本,恢复是把副本还原成可读系统,回滚则是把代码或配置切回旧状态。看到备份文件并不能证明它真的可以恢复;回退应用版本,也不会自动撤销新的数据结构或已经发送到外部的任务。
林序每小时创建一次数据库一致性快照,每天复制到独立加密位置,保留 14 个每日恢复点和 8 个每周恢复点。她每月在新目录中恢复一次,验证文件哈希值、数据结构版本、312 条基线记录及抽样关系,再运行只读查询。保留数量和频率只是本案例的选择,真实项目要按数据变化速度、可接受损失、成本和法规重新确定。
| 运行手册步骤 | v0.3.1 事件实际耗时 |
|---|---|
| 确认告警并关闭试点 | 1 分钟 |
| 记录产物、配置和数据结构 | 1 分钟 |
| 路由切回 v0.3.0 版本产物 | 2 分钟 |
| 服务就绪与业务探测 | 2 分钟 |
| 保留 v0.3.1 出站队列供对账 | 1 分钟 |
| 总计 | 7 分钟 |
运行手册要写清命令输入、预期输出、停止条件、所需权限和求助联系人,但不能把机密写进去。如果演练必须依靠作者临场回忆才能成功,手册仍然不合格。
恢复验收不能只比较各张表的总数。312 条请求要按状态、负责人和来源批次分组核对;287 条分类要确认每一条都引用了真实存在的请求;94 条草稿任务要确认没有孤立记录或重复的幂等键;最后抽取 12 条跨表轨迹,从原始 CSV 导入回执一直走到当前状态。总数相同,关系仍可能错连,所以关系检查和关键约束必须同时通过。恢复环境在验收后要安全销毁,销毁动作也写入回执。
v0.3.1 重复写入事故:回滚止住新增错误,对账才处理已发生副作用
v0.3.1 给外部任务适配器增加了超时重试。开发者只测试了“请求失败后重试成功”,却没有模拟“远端已经创建,响应在返回途中丢失”。重试时又生成了新的操作编号,20 条试点任务中有 3 条被重复创建。进程和网络接口指标都正常,重复率这一业务告警却在第 6 分钟触发。
林序先关闭外部任务开关,再按 7 分钟流程切回 v0.3.0。她没有通过删库制造“恢复干净”的假象,因为外部系统里的副作用已经发生。对账时,她用本地发件箱、远端任务编号、请求编号和时间窗口建立连接:20 条中有 17 条一一对应,另外 3 条各有两个远端编号。人工确认后,她关闭 3 个重复任务,保留审计事件,并通知受影响的内部协作者。
| 根本原因层 | 结论 | 改进 |
|---|---|---|
| 需求 | 未定义响应丢失时如何处理 | 外部创建必须接受稳定幂等键 |
| 测试 | 缺少“远端成功、本地超时” | 新增集成故障注入 |
| 适配器 | 重试生成新操作编号 | 同一业务动作复用稳定键 |
| 预发布 | 沙盒未模拟网络断点 | 通过代理注入响应丢失 |
| 监控 | 业务告警有效 | 保留告警并缩短试点评审周期 |
事故不是“AI 写错了”一句话。要找出为什么合同、测试和发布门没有挡住它,并把改进落到可重复控制。
维护日历和交接包决定工具三个月后是否仍可接手
| 周期 | 维护动作 | 完成证据 |
|---|---|---|
| 每周 | 看错误趋势、失败导入、磁盘与备份 | 运维说明 |
| 每月 | 依赖与支持期审查、恢复演练、开关清理 | 评审与恢复回执 |
| 每季度 | 权限复核、机密轮换演练、数据保留清理 | 访问与留存记录 |
| 每次变更 | 合同、差异、48 测试、预发布、放行 | 部署回执 |
| 事故后 | 时间线、影响、对账、控制改进 | 事件评审 |
交接包包含一页架构图、工具合同、数据字典、依赖与许可证记录、环境清单、配置规则、构建、发布、回滚和恢复手册、监控入口、最近两次发布证据、未解决风险和专业联系人。接手者应能在空环境中完成恢复并运行合成任务;只会在原作者电脑上打开工具,不算完成交接。
单人负责人也要设定求助阈值。发现疑似数据泄露、权限绕过、不可逆迁移、付款或身份副作用、恢复失败,或者无法解释的数据差异时,应停止发布并找有资质的开发、安全、隐私或法律人员,不能继续让模型在生产环境里试修。
用固定验收集判断工具是否变好,并保存本次结论与来源
最终验收冻结 36 个变更场景:10 个功能、8 个依赖、6 个迁移、5 个配置或机密、4 个回滚和 3 个权限场景。每个场景都有输入、预期状态、允许副作用、证据位置和失败后的停止规则;新版本必须在同一集合上比较,不能只挑容易成功的场景演示。
| 最终问题 | v0.3.0 证据 | 结论 |
|---|---|---|
| 能否重建 | 干净安装 + 构建产物哈希值 | 通过 |
| 数据能否演进 | 001→003 与 312 条计数 | 通过 |
| 改动能否验证 | 48 项测试与 80 条样本 | 通过 |
| 发布能否约束 | 预演、人工发布门和试点开关 | 通过 |
| 异常能否发现 | v0.3.1 第 6 分钟告警 | 发现能力通过、测试待补 |
| 能否撤回并对账 | 7 分钟回滚与 3 条重复对账 | 通过且形成改进项 |
| 能否交接 | 新目录恢复与合成任务 | 通过 |
本案例参考了几类公开框架:NIST《安全软件开发框架(SSDF)v1.1》关于把安全实践融入软件开发生命周期、降低未发现缺陷的影响并处理根因的原则;CISA 与 FBI 的《安全软件部署》关于从开发早期规划分阶段部署与测试;GitHub 当前部署与环境文档中有关环境保护规则、审批、分支或标签限制和密钥的能力边界;以及语义化版本 2.0.0 关于先声明公开接口,再解释版本变化的规则。这些资料提供的是通用开发、部署和版本表达参考,不会自动构成某个工具的合规、安全认证或可用性保证。
- NIST SSDF v1.1(最终版)
- NIST SSDF publications(v1.2 当前为 draft)
- CISA/FBI Safe Software Deployment
- GitHub Deployments and environments
- Semantic Versioning 2.0.0
这套方法的重点不是让非程序员假装成为资深工程师,而是把“不知道”显性化:哪些行为已定义,哪些风险已测试,哪个版本在运行,什么证据允许发布,发生什么立即停止,以及谁能接手。AI 可以加快生成、解释和补测试;可维护性来自版本化资产、独立验证、受控发布和练过的恢复路径。