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

AI Summary (BLUF)
本文是Google Developer Expert系列教程的最终篇,详细讲解如何为教育机构构建生产级AI平台。核心围绕三大Day-2工程保障:细粒度FinOps与原子配额熔断、自清理存储生命周期与零孤儿级联清除、以及单命令可复现的Terraform基础设施即代码。文章提供了完整的生产代码和架构模式,确保平台在数十名讲师、数千名学生和严格预算治理下稳定运行。
核心洞察
这篇文章最实在的地方,是把“AI 进课堂”从 demo 层面拉到了运维层面。大多数教育 AI 项目死在第二年——预算超支、存储堆积、没人敢动生产环境。作者拿出的三根柱子(成本熔断、存储自清理、一键 Terraform一种开源的基础设施即代码(IaC)工具,允许用声明式配置文件定义和部署云资源,实现可重复、版本控制的 infrastructure 部署。)恰好对应这三个死法。我唯一存疑的是 24 小时同步一次计费目录够不够快,模型降价那天你的账面数字可能还停在昨天。
核心结论
教育 AI 平台真正死在第二年,主要原因是预算超支、存储堆积和没人敢动生产环境,而非 demo 效果不佳;作者用成本熔断、存储自清理、一键 Terraform 三项 Day-2 工程保障分别对应这三种死法。
成本控制上,系统每 24 小时通过 Google Cloud Billing Catalog APIGoogle Cloud提供的API,用于查询所有云服务的SKU和定价信息,支持程序化获取实时费率。 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))。存储管理上,系统按 9 类媒体资源(截图、视频、录播、音频、异常事件、ZIP 包、任务、报告、毕设会议)分别追踪 Cloud Storage 字节数;上传一旦超出班级配额,立即删除该 blob 并原子回退计数器,删除侧带非负漂移保护。
删除一个班级时,
onClassDocDeleted执行四阶段级联清除:13 个 Cloud Storage 前缀、11 个顶层 Firestore 集合、18 个嵌套子集合(均按 Firestore 单批次 500 次操作上限分块删除),最后从teacherProfiles和studentProfiles中移除 classId 引用,实现零孤儿清理。./setup-new-project.sh可用一条终端命令开通全新 Google Cloud 项目,自动启用 27 个 API、Firestore Native 数据库、Cloud Storage 桶和 Identity Platform(GCIP),并将配置同步到 7 个独立 Cloud Functions 代码库,由单元测试、安全规则测试和 Playwright一个脚本层面的浏览器驱动库,与Browser-Use互补,适合精细控制浏览器流程,在组合使用时提供底层驱动支持。 端到端测试把关。
1. 机构级 Day-2 生产就绪的三根柱子
花一个周末就能搭出一个惊艳的 AI 课堂演示;但要在一个拥有几十位讲师、几千名学生、预算审批严格的公立院校里,把 AI 平台真正跑起来,支撑教学、学习、学生表现评估和监考,需要三样 Day-2 工程保障:
细粒度 FinOps云财务运营(Financial Operations),一种将财务问责制引入云支出的实践,通过实时监控、预算和优化来最大化云投资价值。 与原子配额熔断:院系管理员要能看到精确到小数点后六位的美元支出,覆盖 Gemini 4 Argon、Gemini 3.8 Flash、Gemini 3.5 Flash-Lite 和 Gemini 3.5 Transcribe 各模型,同时实时追踪 9 类媒体资源的 Cloud Storage 字节数。自动熔断器要在某个班级花超预算之前就掐断。
自清理存储生命周期与零孤儿级联清除删除父级资源(如班级)时,自动递归删除所有关联的子资源(如存储文件、数据库记录),确保不留任何孤立数据。:一个学期下来,录播课、实操评分视频、毕业项目演示、监考证据,会产生成千上万个媒体文件。学生截图、实验视频、语音咨询、考试片段,都得在评分和申诉结束后按可配置的 TTL 策略自动过期。学年末删除一个班级时,必须清掉 100% 关联的 Cloud Storage blob 和 20 多个 Firestore 子集合,一条孤儿记录都不留。
一条命令可复现的 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 小时热内存缓存,把 GenkitGoogle开源的AI应用开发框架,用于构建、测试和部署AI工作流,支持多种模型和工具集成。(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,背后是一条端到端流水线:
- 建项目、绑账单:GCP 项目不存在就新建,然后把账单账户挂上去。
- 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)。 - 跨代码库配置同步(
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。 - 零冷启动 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端到端测试把关。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。




