教育 AI 平台为何总在第二年崩盘:成本熔断、存储自清理与一键 Terraform

2026/11/12
教育 AI 平台为何总在第二年崩盘:成本熔断、存储自清理与一键 Terraform

AI Summary (BLUF)

本文是Google Developer Expert系列教程的最终篇,详细讲解如何为教育机构构建生产级AI平台。核心围绕三大Day-2工程保障:细粒度FinOps与原子配额熔断、自清理存储生命周期与零孤儿级联清除、以及单命令可复现的Terraform基础设施即代码。文章提供了完整的生产代码和架构模式,确保平台在数十名讲师、数千名学生和严格预算治理下稳定运行。

核心洞察

这篇文章最实在的地方,是把“AI 进课堂”从 demo 层面拉到了运维层面。大多数教育 AI 项目死在第二年——预算超支、存储堆积、没人敢动生产环境。作者拿出的三根柱子(成本熔断、存储自清理、一键 Terraform)恰好对应这三个死法。我唯一存疑的是 24 小时同步一次计费目录够不够快,模型降价那天你的账面数字可能还停在昨天。


核心结论

  1. 教育 AI 平台真正死在第二年,主要原因是预算超支、存储堆积和没人敢动生产环境,而非 demo 效果不佳;作者用成本熔断、存储自清理、一键 Terraform 三项 Day-2 工程保障分别对应这三种死法。

  2. 成本控制上,系统每 24 小时通过 Google Cloud Billing Catalog API v1 同步一次官方 SKU 费率(覆盖 Gemini 4 Argon、Gemini 3.8 Flash、Gemini 3.5 Flash-Lite、Gemini 3.5 Transcribe 及香港 asia-east2 区域 Cloud Storage),AI 调用成本计算精确到小数点后六位(toFixed(6))。

  3. 存储管理上,系统按 9 类媒体资源(截图、视频、录播、音频、异常事件、ZIP 包、任务、报告、毕设会议)分别追踪 Cloud Storage 字节数;上传一旦超出班级配额,立即删除该 blob 并原子回退计数器,删除侧带非负漂移保护。

  4. 删除一个班级时,onClassDocDeleted 执行四阶段级联清除:13 个 Cloud Storage 前缀、11 个顶层 Firestore 集合、18 个嵌套子集合(均按 Firestore 单批次 500 次操作上限分块删除),最后从 teacherProfiles 和 studentProfiles 中移除 classId 引用,实现零孤儿清理。

  5. ./setup-new-project.sh 可用一条终端命令开通全新 Google Cloud 项目,自动启用 27 个 API、Firestore Native 数据库、Cloud Storage 桶和 Identity Platform(GCIP),并将配置同步到 7 个独立 Cloud Functions 代码库,由单元测试、安全规则测试和 Playwright 端到端测试把关。

1. 机构级 Day-2 生产就绪的三根柱子

花一个周末就能搭出一个惊艳的 AI 课堂演示;但要在一个拥有几十位讲师、几千名学生、预算审批严格的公立院校里,把 AI 平台真正跑起来,支撑教学、学习、学生表现评估和监考,需要三样 Day-2 工程保障:

  1. 细粒度 FinOps 与原子配额熔断:院系管理员要能看到精确到小数点后六位的美元支出,覆盖 Gemini 4 Argon、Gemini 3.8 Flash、Gemini 3.5 Flash-Lite 和 Gemini 3.5 Transcribe 各模型,同时实时追踪 9 类媒体资源的 Cloud Storage 字节数。自动熔断器要在某个班级花超预算之前就掐断。

  2. 自清理存储生命周期与零孤儿级联清除:一个学期下来,录播课、实操评分视频、毕业项目演示、监考证据,会产生成千上万个媒体文件。学生截图、实验视频、语音咨询、考试片段,都得在评分和申诉结束后按可配置的 TTL 策略自动过期。学年末删除一个班级时,必须清掉 100% 关联的 Cloud Storage blob 和 20 多个 Firestore 子集合,一条孤儿记录都不留。

  3. 一条命令可复现的 Terraform 基础设施即代码(./setup-new-project.sh)与持续验证:全世界任何大学、理工学院或 Google 开发者社区,都应该能用一条终端命令开出一个全新的 Google Cloud 项目,启用全部 27 个 API、Firestore Native 数据库、Cloud Storage 桶、Identity Platform(GCIP)和一个 24/7 演示沙盒,并由自动化单元测试、安全规则测试和 Playwright 端到端测试套件把关。

这是十部分 Google Developer Expert 系列的第十篇,也是收官篇。下面拆解实现这三项保障的生产代码和基础设施模式。

2. 图一:实时 AI FinOps 管道、云计费 SKU 实时同步与原子配额熔断

在 scheduledTasks.js、cost.js、jobLogger.js、useCloudPricing.js 和 aiCostAggregator.js 这几个文件里,每一次 AI 调用和存储事件都按 Google Cloud Billing Catalog 的实时 SKU 费率进行计量。

生产代码一:云计费目录 SKU 实时同步(syncGeminiPricing)与多阶段成本引擎(cost.js)

把云定价常量硬编码在前端代码里,迟早会漂移——新模型层级上线,或者区域存储 SKU 费率调整,你的数字就对不上了。syncGeminiPricing 每 24 小时查询一次官方 Google Cloud Billing Catalog API v1,分别拉取 Gemini / Cloud AI 服务(C7E2-9256-1C43)和香港区域标准 Cloud Storage(95FF-2EF5-5EA1,asia-east2)的费率,把权威价格表写进 system_config/pricing:

// Source: functions/scheduled_tasks/scheduledTasks.js
export const syncGeminiPricing = onSchedule({
  schedule: 'every 24 hours',
  memory: '256MB',
  region: FUNCTION_REGION,
}, async () => {
  const VERTEX_SERVICE_ID = 'C7E2-9256-1C43';
  const STORAGE_SERVICE_ID = '95FF-2EF5-5EA1';
  const pricingData = {
    'gemini-4-argon': { input: 2.00, output: 10.00 },
    'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
    'gemini-3.8-flash': { input: 0.75, output: 3.75 },
    'gemini-3.7-pro': { input: 3.00, output: 15.00 },
    'gemini-3.5-transcribe-preview': { input: 0.50, output: 2.50 },
    'cloud-storage': {
      unit: 'GiB/month',
      ratePerGibMonth: 0.023,
      region: 'asia-east2',
      currency: 'USD',
      description: 'Standard Storage Hong Kong (Baseline)',
    },
    lastSyncedAt: new Date().toISOString(),
    source: 'catalog_sync_or_baseline',
  };

  // Queries https://cloudbilling.googleapis.com/v1/services/{SERVICE_ID}/skus
  // Extracts live nanos/units tieredRates for asia-east2 & updates system_config/pricing
  await db.collection('system_config').doc('pricing').set(pricingData, { merge: true });
});

在 AI 执行侧,functions/ai_flows/cost.js 维护一份 system_config/pricing 的 12 小时热内存缓存,把 Genkit(inputTokens/outputTokens)、Google GenAI SDK(promptTokenCount/candidatesTokenCount)以及两阶段复合管道(gemini-3.5-transcribe-preview + gemini-4-argon)的 token 用量元数据统一归一化,算出精确到美分以下的美元成本(Number((inputCost + outputCost).toFixed(6)))。

每次文件在 Cloud Storage 桶里落定或删除,functions/storage_triggers/storageQuota.js 里的 updateStorageUsageOnUpload 和 updateStorageUsageOnDelete 就会跑起来。它们先调 getStorageCategoryAndClass(filePath) 把 blob 路径归到 9 个存储分类中的一个:

存储路径前缀 Firestore 计数器字段(classes/{id}/metadata/storage) 对应资产
screenshots/{classId}/... storageUsageScreenShots 周期性截图和摄像头 JPEG 原片,MP4 合成后自动清理
videos/{classId}/... storageUsageVideos 合成好的 1-FPS H.264 学生延时 MP4
recordings/{classId}/... 和 subtitles/{classId}/... storageUsageRecordings 教师讲课录像、归一化 M4A 音频、.vtt/.srt 字幕
audio/{classId}/... storageUsageAudio 学生异常声片段和课堂音频分片
irregularities/{classId}/... storageUsageIrregularities 高危监考事件快照
zips/{classId}/... storageUsageZips 批量证据打包 .zip,24 小时自动过期
classes/{classId}/tasks/...、tasks/...、submissions/... storageUsageTasks 实训任务参考视频和学生提交的切片 MP4
reports/{classId}/... storageUsageReports 可直接提交的 .docx 和 .csv 取证报告
projectMeetings/{classId}/... storageUsageProjectMeetings 毕设咨询语音、720p 手机拍摄的演示视频、0.1 秒精度的照片

上传的文件一旦把 storageUsage 推过班级配置的 storageQuota,updateStorageUsageOnUpload 会立刻把该 blob 从 Cloud Storage 删掉,同时用 FieldValue.increment(-fileSize) 原子地把计数器回退。删除侧则加了自愈式非负漂移保护,乱序的删除事件不会把任何分类计数打到 0 以下:

// Source: functions/storage_triggers/storageQuota.js
export function getStorageCategoryAndClass(filePath) {
  if (!filePath || typeof filePath !== 'string') return null;
  const parts = filePath.split('/');
  if (parts.length < 3) return null;
  if (parts[0] === 'classes') return { classId: parts[1], usageField: 'storageUsageTasks' };

这个映射表把路径前缀和 Firestore 字段一一对应起来。screenshots 归到 storageUsageScreenShots,videos 归到 storageUsageVideos,recordings 和 subtitles 共用 storageUsageRecordings,tasks 和 submissions 共用 storageUsageTasks。函数返回 classId 和对应的 usageField,找不到匹配就返回 null。

如果讲师或系统管理员想验证 Firestore 里的事件驱动字节计数和 Cloud Storage 桶里实际存储的字节是否一致(比如手动批量清理归档之后),点一下 🔄 Audit & Recalculate Storage,就会调用 functions/storage_triggers/storageQuota.js 里的 recalculateStorageUsage。它并行扫描每个物理前缀,然后打上 lastAuditedAt 时间戳:

// Source: functions/storage_triggers/storageQuota.js
export async function recalculateStorageUsageInternal(classId) {
  const bucket = adminStorage.bucket();
  const categories = [
    { prefix: `screenshots/${classId}/`, field: 'storageUsageScreenShots' },
    { prefix: `videos/${classId}/`, field: 'storageUsageVideos' },
    { prefix: `zips/${classId}/`, field: 'storageUsageZips' },
    { prefix: `audio/${classId}/`, field: 'storageUsageAudio' },
    { prefix: `recordings/${classId}/`, field: 'storageUsageRecordings' },
    { prefix: `subtitles/${classId}/`, field: 'storageUsageRecordings' },
    { prefix: `irregularities/${classId}/`, field: 'storageUsageIrregularities' },
    { prefix: `classes/${classId}/tasks/`, field: 'storageUsageTasks' },
    { prefix: `tasks/${classId}/`, field: 'storageUsageTasks' },
    { prefix: `submissions/${classId}/`, field: 'storageUsageTasks' },
    { prefix: `reports/${classId}/`, field: 'storageUsageReports' },
  ];

  const results = {
    storageUsageScreenShots: 0, storageUsageVideos: 0, storageUsageZips: 0,
    storageUsageAudio: 0, storageUsageRecordings: 0, storageUsageIrregularities: 0,
    storageUsageTasks: 0, storageUsageReports: 0, storageUsage: 0,
  };

  for (const cat of categories) {
    const [files] = await bucket.getFiles({ prefix: cat.prefix });
    let catTotal = 0;
    for (const file of files) {
      const sz = parseInt(file.metadata?.size, 10);
      if (!isNaN(sz) && sz > 0) catTotal += sz;
    }
    results[cat.field] = (results[cat.field] || 0) + catTotal;
    results.storageUsage += catTotal;
  }

  await db.doc(`classes/${classId}/metadata/storage`).set(
    { ...results, lastAuditedAt: FieldValue.serverTimestamp() },
    { merge: true }
  );
  await db.doc(`classes/${classId}`).set({ storageUsage: results.storageUsage }, { merge: true });
  return results;
}

生产代码 3:四阶段零孤儿级联班级清除(cleanupTriggers.js 里的 onClassDocDeleted)

在 Firestore 里删掉一个父文档(classes/{classId}),并不会自动删掉它的子集合、引用 classId 的顶层任务文档,也不会动 Cloud Storage 里的物理文件。

要保证老师学期末删班级时做到 100% 零孤儿清理,functions/storage_triggers/cleanupTriggers.js 里的 onClassDocDeleted 会执行一套完整的四阶段级联清除:

[图片:四阶段零孤儿级联班级清除(onClassDocDeleted)]

下面是 functions/storage_triggers/cleanupTriggers.js 的生产实现。它用 500 文档分块的批量提交(deleteCollectionByClassId 和 deleteSubcollectionByPath)来遵守 Firestore 单批次 500 次操作的上限:

先看清理触发器。这个函数监听 classes/{classId} 的删除事件,超时设到 540 秒,内存 512MiB。为什么要给这么长的超时?因为一个班级积累下来的数据可能横跨几十个集合,删起来不是几秒钟能收工的。

第一阶段清 Cloud Storage。13 个前缀全部列出来,从 screenshots/${classId}/ 到 projectMeetings/${classId}/,一个不落。每个前缀调 deleteFiles 时带上 force: true,后面挂一个空的 catch。这里空 catch 是有意为之:某个前缀下没有文件,或者已经被删过,不该让整个流程中断。

第二阶段处理顶层集合。screenshots、audio、videoJobs、zipJobs、videoAnalysisJobs、irregularities、progress、propertyUploadJobs、lectureMergeJobs、lectureSubtitleJobs、aiJobs,一共 11 个。每个都走 deleteCollectionByClassId,内部按 500 条一批来删。Firestore 单次批量删除有上限,分批是必须的。

第三阶段是嵌套子集合,数量更多。projectMeetings、projectGroups、projectStudentProfiles、activeMeetingCaptures、lectureRecordings、bingoSessions、status、lessons、bingoRecords、attendanceAdjustments、studentProperties、classProperties、passkeyBypassRequests、screenBroadcastViewers、messages、audio_audits、irregularities、questionBank,18 个。路径拼成 classes/${classId}/${subcol},同样按 500 条一批删。

最后一步,把 classId 从 teacherProfiles 和 studentProfiles 里摘掉。这两个集合里的文档不会随班级删除而消失,得手动把引用清理干净,否则老师或学生的个人资料里会挂着一条指向已删除班级的记录。

四个阶段串行执行,任何一个阶段失败都会中断后面的步骤。这个设计合理吗?看场景。如果 Storage 清理失败但 Firestore 清理成功,下次重试时会重复删一遍 Storage,但 force: true 和空 catch 让重复删除无害。反过来,如果 Firestore 清理到一半失败,已删的集合不会恢复,重试时从头发起,已删的部分自然跳过。整体是幂等的。


4. 图 3:一条命令零点击 Terraform 接入与多层自动化验证金字塔

任何大学、学校或 GDG 社区想部署自己的 Google AI Classroom 实例,不用在 Cloud Console 里点几十个向导页。setup-new-project.sh、terraform/ 和 deploy.sh 三个东西把整个开通和验证流程自动化了。

`./setup-new-project.sh` 怎么省掉手动点击

跑 ./setup-new-project.sh my-school-ai-2026 01ABCD-23EFGH-45IJKL,背后是一条端到端流水线:

  1. 建项目、绑账单:GCP 项目不存在就新建,然后把账单账户挂上去。
  2. Terraform 状态与 27 个 API:在 terraform/ 目录里跑 terraform init && terraform apply,启用 Compute、Cloud Run、Cloud Functions Gen 2、Cloud Build、Artifact Registry、Eventarc、Cloud Tasks、Cloud Scheduler、Secret Manager、Google Cloud AI / Firebase AI Logic(aiplatform.googleapis.com 和 firebasevertexai.googleapis.com)、Cloud Billing Catalog、Firestore Native、Firebase Storage、Identity Platform(GCIP)。
  3. 跨代码库配置同步(sync-config.sh):自动提取新生成的 Firebase Web App SDK 凭据,写进 web-app/.env 和 functions/config.js,然后跑 ./sync-config.sh 把 region、CORS、模型常量同步到 7 个独立的 Cloud Functions 代码库:ai_flows、media_processing、lecture_processing、scheduled_tasks、storage_triggers、ClassFunctions、user_management。
  4. 零冷启动 24/7 演示沙箱填充(seed-demo-data.mjs):预先把 IT114115-Demo 班级填满,包括学生遥测数据、预编译的 H.264 实验视频、双语 .vtt 讲座录音,以及 13 条默认 AI 系统提示词。部署完 60 秒内,老师就能把每个 Gemini 4 Argon 工作室都试一遍。

5. 十部分 Google AI Classroom 系列总结

在这个十部分的架构深入解析里,我们把 Google AI Classroom 平台的每一层都过了一遍:教学、学习、学生表现评估、监考测评。

把边缘和云端拼起来之后,实际拿到了什么

浏览器端用 WebGPU 跑 Gemma 4 E2B、MediaPipe 3D 人脸网格和 LiteRT Whisper,云端用 Gemini 4 Argon、3.8 Flash、3.5 Flash-Lite 做多模态推理。这两层接上以后,三件事同时成立了:

  • 40 多个座位的实时考场巡查。60 FPS 下抓 3 秒钟的瞬时异常,屏幕、摄像头、音频三路证据对齐。
  • 老师可以在所有场景里自由选模型。实时字幕、屏幕广播、Bingo、实操视频评分、毕设指导工作室,全都走 gemini-4-argon。
  • 闲置成本归零。端侧推理扛掉大部分负载,AI 配额用原子熔断器卡住,班级数据级联清理,没有空转的服务器账单,也没有孤儿存储。

开源仓库在哪

  • 45 分钟的 Google Cloud 技术演讲幻灯片(google-cloud-slides.marp.md)
  • 主仓库 README 和架构文档索引

代码都在里面,想看哪部分直接翻。

常见问题(FAQ)

教育机构AI平台怎么防止班级预算超支?

靠细粒度FinOps与原子配额熔断:按Google Cloud Billing Catalog实时SKU费率计量每次AI调用,精确到小数点后六位美元,并在班级花超预算前自动掐断。

学年末删除班级时怎么保证不留下孤儿数据?

采用四阶段零孤儿级联清除,删除班级时清掉100%关联的Cloud Storage blob和20多个Firestore子集合,一条孤儿记录都不留,媒体文件另按可配置TTL自动过期。

新学校怎么快速复现整套AI平台基础设施?

运行一条命令./setup-new-project.sh即可开出全新Google Cloud项目,启用全部27个API、Firestore Native、Cloud Storage、GCIP和24/7演示沙盒,并有单元、安全规则和Playwright端到端测试把关。

晓婷深圳
本文由 晓婷 审核,最后更新于 2026年11月12日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。