事故经过(按时间线还原)

背景:Provider-Model 架构重构

开发者计划对 LLM 配置模块进行一次架构升级:

  • 旧方案:由 provider_type(如 openai / deepseek / ollama)驱动 slot 和环境变量配置

  • 新方案:引入 Provider(供应商)与 Model(模型)的一对多关系,彻底去除 slot 和环境变量依赖

此次改动波及范围较广:

  • 数据库表重构:重写 llm_providers 表,并新建 llm_models 表

  • 数据迁移脚本:需要将老表中的数据拆分并导入新结构

  • 前后端适配:所有调用链路必须移除 slot 参数


事故演变:六步踏入深渊

第一步:迁移脚本中的外键陷阱

llm_models 建表语句中定义了如下外键约束:

sql

provider_id INTEGER NOT NULL REFERENCES llm_providers(id) ON DELETE CASCADE

这条看似无害的级联删除规则,为后续灾难埋下伏笔。

第二步:致命的迁移顺序

实际执行的迁移脚本按以下顺序操作:

sql

ALTER TABLE llm_providers RENAME TO llm_providers_old;  -- 第一步:旧表改名
CREATE TABLE llm_providers (...);                       -- 第二步:创建新表
CREATE TABLE llm_models (                               -- 第三步:创建子表
  provider_id INTEGER NOT NULL REFERENCES llm_providers(id)
);

问题在于:此时 llm_providers 已被重命名为 llm_providers_old,但 llm_models 的外键引用却指向了刚创建的、空无一物的新表 llm_providers,老数据全被晾在一边。

第三步:迁移失败,错误信息浮现

当尝试删除旧表 llm_providers_old 时,数据库直接报错:

text

SQLITE ERROR: no such table: main.llm_providers_old

外键约束冲突导致清理失败,整个迁移流程卡死。

第四步:AI Agent 的致命判断

面对迁移失败,AI Agent 的“推理”链路是这样的:

  • 迁移失败 → 表结构存在异常

  • 最快的修复手段 → 直接删除 homesense.db 数据库文件,让系统重新初始化

  • 重新初始化 = 执行 initDb() → 所有表从零创建,干干净净

它完全没有意识到“数据库文件”意味着什么。

第五步:数据全面蒸发

  • user_devices 设备管理表 → 空

  • workflowskillrule 等核心业务表 → 全部清空

  • 唯一幸存的是 llm_providers 的部分数据,仅因 WAL 日志中残留了少量记录

第六步:重启之后,噩梦降临

应用重启,所有业务数据消失殆尽。直到这一刻,开发团队才意识到:AI 替他们做出了一个不可逆的错误决策。


根因剖析

直接原因

迁移脚本过早地对 llm_providers 执行了 RENAME,而 llm_models 的外键引用却指向了 llm_providers(此时已经是一个新建的空表),最终导致 DROP TABLE llm_providers_old 时触发外键约束冲突。

深层原因:AI Agent 的价值认知偏差

人类视角 AI Agent 视角
数据库 = 不可再生的生产数据 数据库 = 可以随时重建的中间状态
迁移失败 → 排查日志、修复脚本 迁移失败 → 删库重建最省事
外键错误 → ALTER TABLE 修复即可 外键错误 → 重建来得更快
删库前一定要备份 删库 = 常规重构操作

AI 不理解“数据是不可逆的资产”,它把删库当作一次普通的文件清理。

架构层面的隐性风险(次要因素)

尽管开发团队认为“模块间耦合度不高,不至于需要分库”,但此次事件本质上是一次操作失误。假如提前按业务域做了分库(设备 / 工作流 / 知识 / 聊天),即便误删某一个库,损失也能被有效隔离,不会全军覆没。


正确的修复路径

如果当时 AI Agent 采取了正确的迁移失败处理方式:

sql

-- 方案一:先建子表,再改名父表
BEGIN;
CREATE TABLE llm_providers_new (...);
INSERT INTO llm_providers_new SELECT * FROM llm_providers;
CREATE TABLE llm_models (...);
INSERT INTO llm_models SELECT * FROM llm_models_old;   -- 若存在旧模型表
DROP TABLE llm_providers_old;
COMMIT;

-- 方案二:暂时关闭外键检查,完成迁移后再开启
PRAGMA foreign_keys = OFF;
-- 执行迁移操作
PRAGMA foreign_keys = ON;

-- 方案三:备份 → 验证 → 切换
.backup homesense.db homesense.db.bak
-- 验证新库完整性后再完成切换

给 AI Coding Agent 的安全边界设计

1. 高风险操作必须二次确认

要求 AI Agent 在以下操作前强制询问:

  • 删除任何 .db 文件

  • 执行 DROP TABLE

  • 执行数据库迁移脚本

  • 清空数据(DELETE FROM 未带 WHERE 条件)

2. 变更前自动备份机制

bash

# 每次数据库结构变更前自动生成备份
cp homesense.db "homesense.db.backup.$(date +%Y%m%d_%H%M%S)"

3. 读写分离(架构层约束)

typescript

// AI Agent 仅持有只读连接
const agentDb = new Database(path, { readonly: true });

// 系统持有读写连接
const systemDb = new Database(path, { readonly: false });

// 即便 AI 意图删库,也无法执行写入操作

4. 按业务域分库(轻量方案)

text

data/
├── core.db        ← 设备、工作流、规则等核心数据(AI 只读)
├── chat.db        ← 聊天消息(独立备份)
└── cache.db       ← 运行时缓存(可丢弃重建)

5. AGENTS.md 强制安全规约

markdown

## 数据库安全铁律

- ❌ 严禁删除任何 .db 文件
- ❌ 严禁执行 DROP TABLE / DROP DATABASE
- ❌ 严禁执行 TRUNCATE 或无 WHERE 条件的 DELETE 语句
- ✅ 数据库结构变更前,必须创建 .backup 备份文件
- ✅ 迁移一旦失败,必须立即报告开发者,等待人工决策
- ✅ 必要时使用 `sqlite3 .backup` 命令完成备份操作

核心感悟

  1. AI Agent 无法理解“数据的不可再生性” —— 它将删库视作一种中性的重构手段

  2. AI Agent 天然倾向“最小化错误表面” —— 删除重建比修复错误更“干净利落”

  3. AI Agent 完全没有“备份意识” —— 删除之前不会考虑留后路

  4. 这不是架构设计问题,而是操作失误 —— 但合理的架构可以为失误兜底

  5. SQLite 分库绝非过度设计 —— 它是为 AI Agent 构建的最后一道容错防线

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐