特斯拉级系统设计面试题(六):PB 级自动驾驶路测数据标注系统 — 任务调度、质量控制与人机协同
特斯拉 2024 年披露过一个数字:每天从全球车队回传的路测数据约 30PB,其中需要人工标注的约占 2-5%(约 1PB+)。这些数据被切割成 10-30 秒的”片段”(clip),每个 clip 包含 8 路摄像头视频、激光雷达点云、毫米波雷达、车辆状态数据。一个标注员一天最多标 200-300 个 clip(2D 框) 或 50-80 个 clip(3D 框 + 语义分割),这意味着单日需要 3-5 万名标注员满负荷运转。
数据标注系统是自动驾驶系统的”数据燃料”——标注质量直接决定模型上限。但它又是被人低估的系统:看似简单(标个框),实则是 PB 级数据 + 数十万标注员 + 严格质量管控的复杂工程。核心挑战:
- 海量数据:PB 级存储、亿级 clip、千万级文件
- 多模态:图像、点云、视频、时序数据协同标注
- 质量参差:标注员水平不一、错误难以发现
- 成本压力:标注占自动驾驶研发成本 30%+
- 时效性:新场景必须快速标注入库,否则影响模型迭代
核心考察点
- 数据分层存储:热/温/冷三级,平衡成本与访问速度
- 任务调度:公平调度、优先级、众包 vs 内部团队
- 协同标注:多人协作避免重复劳动
- 质量控制:抽样质检、交叉验证、模型预标注 + 人工修正
- 成本优化:自动标注、人机协同、标注复用
面试误区:很多候选人把标注系统等同于”任务分配 + 提交”,没有考虑质检循环、争议处理、模型预标注、数据回流等关键环节。
题目重述
题目:设计特斯拉自动驾驶路测数据标注系统,支持:
- PB 级数据:日均 1PB 新增数据、累计 50PB+ 历史数据
- 多模态标注:2D 框、3D 框、语义分割、点云标注、行为标注
- 万人并发:3-5 万标注员、众包模式
- 严格质量:关键场景标注准确率 > 99%、普通场景 > 95%
- 任务调度:公平分配、紧急任务插队、按难度分级
- 协同工作:多人协同标同一 clip、避免重复
- 成本控制:单 clip 标注成本持续下降
- 快速反馈:标注结果快速回流到训练 pipeline
请给出整体架构、核心数据模型、关键流程、典型问题处理。
标准回答(架构设计)
1. 整体架构:四层流水线
┌─────────────────────────────────────────────────────────────┐│ 数据接入层 (Vehicle → Cloud) ││ - 数据回传 (HTTPS/MQTT) - 切片 (clip) - 元数据抽取 │└────────────────────────┬────────────────────────────────────┘ │┌────────────────────────▼────────────────────────────────────┐│ 存储层 (分层) ││ - 热数据 (NVMe, 30 天) - 温数据 (HDD, 1 年) - 冷数据 (S3) │└────────────────────────┬────────────────────────────────────┘ │┌────────────────────────▼────────────────────────────────────┐│ 调度层 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ 任务生成 │ │ 分配引擎 │ │ 质量控制 │ │ 模型预标 │ ││ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ ││ └──────────────┴──────────────┴──────────────┘ │└────────────────────────┬────────────────────────────────────┘ │┌────────────────────────▼────────────────────────────────────┐│ 标注工作台 (Web / Desktop App) ││ - 标注员 - 质检员 - 审核员 - 管理员 │└──────────────────────────────────────────────────────────────┘2. 核心数据模型
-- 1. Clip 元数据表CREATE TABLE data_clip ( clip_id VARCHAR(64) NOT NULL, vehicle_id VARCHAR(32) NOT NULL, capture_time DATETIME(3) NOT NULL, duration_sec INT NOT NULL, camera_count TINYINT NOT NULL, lidar_frames INT NOT NULL, scene_tags JSON NULL COMMENT '场景标签: 高速/城市/雨天', difficulty TINYINT NOT NULL COMMENT '1-5 难度等级', priority TINYINT NOT NULL DEFAULT 3 COMMENT '1-5 优先级', status VARCHAR(16) NOT NULL COMMENT 'PENDING/ASSIGNED/LABELED/QC_PASSED/QC_FAILED/ARCHIVED', storage_path VARCHAR(256) NOT NULL, file_size_gb DECIMAL(10, 2) NOT NULL, created_at DATETIME(3) NOT NULL, PRIMARY KEY (clip_id), INDEX idx_status_priority (status, priority, capture_time), INDEX idx_vehicle (vehicle_id, capture_time)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 PARTITION BY RANGE (TO_DAYS(capture_time)) ( PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')), -- 按月分区 );
-- 2. 标注任务表CREATE TABLE annotation_task ( task_id VARCHAR(64) NOT NULL, clip_id VARCHAR(64) NOT NULL, task_type VARCHAR(32) NOT NULL COMMENT '2D_BOX/3D_BOX/SEGMENT/BEHAVIOR/LANE', difficulty TINYINT NOT NULL, estimated_min INT NOT NULL COMMENT '预计耗时(分钟)', required_level VARCHAR(16) NOT NULL COMMENT 'L1/L2/L3 标注员等级', reward DECIMAL(8, 2) NOT NULL COMMENT '单 clip 报酬', status VARCHAR(16) NOT NULL COMMENT 'PENDING/ASSIGNED/SUBMITTED/QC/DONE', assigned_to BIGINT NULL, assigned_at DATETIME(3) NULL, submitted_at DATETIME(3) NULL, PRIMARY KEY (task_id), INDEX idx_status_difficulty (status, difficulty), INDEX idx_assigned (assigned_to, status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 标注结果表CREATE TABLE annotation_result ( result_id BIGINT NOT NULL AUTO_INCREMENT, task_id VARCHAR(64) NOT NULL, clip_id VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, annotation JSON NOT NULL COMMENT '标注内容(框、分割等)', submitted_at DATETIME(3) NOT NULL, qc_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/PASSED/REJECTED/REVIEW', qc_score DECIMAL(5, 2) NULL, qc_user_id BIGINT NULL, qc_notes TEXT NULL, PRIMARY KEY (result_id, submitted_at), INDEX idx_task (task_id), INDEX idx_clip (clip_id), INDEX idx_user_qc (user_id, qc_status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 标注员表CREATE TABLE annotator ( user_id BIGINT NOT NULL, level VARCHAR(16) NOT NULL COMMENT 'L1/L2/L3', accuracy DECIMAL(5, 4) NOT NULL DEFAULT 0 COMMENT '历史准确率', speed DECIMAL(8, 2) NOT NULL DEFAULT 0 COMMENT '单 clip 平均耗时', daily_quota INT NOT NULL DEFAULT 100 COMMENT '每日配额', status VARCHAR(16) NOT NULL COMMENT 'ACTIVE/PAUSED/BANNED', PRIMARY KEY (user_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 任务调度引擎
@Serviceclass TaskDispatchService( private val clipRepo: DataClipRepository, private val taskRepo: AnnotationTaskRepository, private val annotatorRepo: AnnotatorRepository, private val skillMatcher: SkillMatcher, private val loadBalancer: LoadBalancer) { /** * 任务分配(核心调度逻辑) */ fun dispatchTask(taskId: String): DispatchResult { val task = taskRepo.findById(taskId) ?: return DispatchResult.fail("TASK_NOT_FOUND") if (task.status != "PENDING") return DispatchResult.fail("TASK_NOT_PENDING")
// 1. 找到合适的标注员 val candidates = findEligibleAnnotators(task) if (candidates.isEmpty()) { return DispatchResult.fail("NO_ANNOTATOR_AVAILABLE") }
// 2. 多目标评分(公平 + 紧急 + 准确率) val selected = candidates.minBy { annotator -> calculateDispatchScore(annotator, task) }
// 3. 分配任务(加锁防并发) return assignTask(task, selected) }
/** * 找到符合要求的标注员 */ private fun findEligibleAnnotators(task: AnnotationTask): List<Annotator> { return annotatorRepo.findBy { and( Annotator::status eq "ACTIVE", Annotator::level gte task.requiredLevel ) } .filter { it.dailyCompleted < it.dailyQuota } // 未超额 .filter { it.accuracy >= 0.90 } // 准确率门槛 .filter { skillMatcher.hasSkill(it.userId, task.taskType) } // 技能匹配 }
/** * 计算调度得分(越低越优先) */ private fun calculateDispatchScore(annotator: Annotator, task: AnnotationTask): Double { // 1. 负载因子(完成的越少越优先) val loadFactor = annotator.dailyCompleted.toDouble() / annotator.dailyQuota
// 2. 历史准确率(越高越优先) val accuracyFactor = 1.0 - annotator.accuracy
// 3. 速度匹配(任务难度 vs 标注员速度) val speedFactor = (task.estimatedMin - annotator.avgSpeedMin) / task.estimatedMin.toDouble()
// 4. 紧急程度 val urgencyFactor = 1.0 / task.priority
return loadFactor * 0.4 + accuracyFactor * 0.3 + speedFactor * 0.2 + urgencyFactor * 0.1 }}4. 多人协同标注
/** * 协同标注:多人标同一 clip,互相校验 */@Serviceclass CollaborativeAnnotationService( private val redisTemplate: RedisTemplate, private val annotationRepo: AnnotationResultRepository) { /** * 多人标同一 clip 模式:M-of-N 共识 */ fun submitAnnotation(taskId: String, userId: Long, annotation: Annotation): SubmitResult { // 1. 获取该 clip 的所有提交 val existingResults = annotationRepo.findByTaskId(taskId) val requiredSubmissions = 3 // 3 人标
// 2. 保存当前提交 val saved = annotationRepo.save(AnnotationResult( taskId = taskId, userId = userId, annotation = annotation, submittedAt = Instant.now() ))
// 3. 是否到达共识数量 val totalSubmissions = existingResults.size + 1 if (totalSubmissions < requiredSubmissions) { return SubmitResult.pending(totalSubmissions, requiredSubmissions) }
// 4. 计算共识(IoU 加权投票) val consensus = calculateConsensus(existingResults + saved) return SubmitResult.consensus(consensus) }
/** * 共识算法:IoU 加权投票 */ private fun calculateConsensus(results: List<AnnotationResult>): ConsensusAnnotation { val allBoxes = results.flatMap { it.annotation.boxes }
// 按类别分组 val byCategory = allBoxes.groupBy { it.category }
return ConsensusAnnotation( boxes = byCategory.map { (category, boxes) -> val consensusBox = findConsensusBox(boxes) consensusBox } ) }
private fun findConsensusBox(boxes: List<BoundingBox>): BoundingBox { // 找出现次数 >= 2 的框(3 人中至少 2 人同意) val groups = boxes.groupBy { // 用坐标量化到网格 (it.x / 10).toInt() * 10000 + (it.y / 10).toInt() * 100 + (it.width / 10).toInt() * 10 + (it.height / 10).toInt() }
val consensus = groups.filter { it.value.size >= 2 } .map { (key, list) -> // 取平均值 val avgBox = BoundingBox( x = list.map { it.x }.average(), y = list.map { it.y }.average(), width = list.map { it.width }.average(), height = list.map { it.height }.average(), category = list.first().category ) avgBox }
return consensus.first() }}5. 质量控制
/** * 质检工作流:分层质检 */@Serviceclass QualityControlService( private val annotationRepo: AnnotationResultRepository, private val qcRuleEngine: QcRuleEngine, private val autoQaModel: AutoQaModel) { companion object { // 抽样率 private const val NORMAL_SAMPLE_RATE = 0.10 // 普通任务 10% 抽样 private const val KEY_SAMPLE_RATE = 1.00 // 关键任务 100% 全检 }
/** * 提交后质检 */ fun qualityCheck(annotationResult: AnnotationResult): QcResult { val task = taskRepo.findById(annotationResult.taskId)!!
// 1. 自动质检(AI 模型) val autoQa = autoQaModel.evaluate(annotationResult)
// 2. 规则质检 val ruleViolations = qcRuleEngine.check(annotationResult, task)
// 3. 综合判断 if (autoQa.confidence > 0.95 && ruleViolations.isEmpty()) { // AI 高置信度通过 + 无规则违反 → 直接通过 return QcResult.passed(autoQa) }
if (autoQa.confidence < 0.5 || ruleViolations.size > 3) { // AI 低置信度 或 多规则违反 → 直接拒绝 return QcResult.rejected(autoQa, ruleViolations) }
// 4. 抽样规则 val sampleRate = if (task.priority >= 4) KEY_SAMPLE_RATE else NORMAL_SAMPLE_RATE if (Random.nextDouble() < sampleRate) { // 抽样中 → 进入人工审核队列 return QcResult.queued(autoQa, ruleViolations) }
// 未抽中 → 通过 return QcResult.passed(autoQa) }}6. 模型预标注(AI 辅助)
/** * 模型预标注:用最新模型先标一遍,人工修正 */@Serviceclass ModelPreAnnotationService( private val modelInference: ModelInferenceService) { /** * 预标注(提升效率 3-5 倍) */ fun preAnnotate(clip: DataClip): Annotation { // 1. 加载数据 val images = loadImages(clip) val pointClouds = loadPointClouds(clip)
// 2. 模型推理 val predictions = modelInference.predict(images, pointClouds)
// 3. 后处理(NMS、置信度过滤) val filtered = postProcess(predictions, confThreshold = 0.7)
// 4. 转换为标注格式 return Annotation( boxes = filtered.boundingBoxes, segmentations = filtered.masks, confidence = filtered.scores ) }}追问深度
Q1:标注成本怎么降?
答:三层降本:
- 模型预标注:最新模型先标 70%,人工修 30%
- 难度分级:简单任务用初级标注员(便宜)
- 标注复用:同一 clip 跨任务复用(一次标注,多次使用)
Q2:标注员水平参差怎么办?
答:金字塔结构 + 动态评级。
- L1(新手):只能标简单场景
- L2(熟手):可标中等场景
- L3(专家):标高难度 + 质检
// 标注员动态评级fun updateLevel(userId: Long) { val recentAccuracy = calculateRecentAccuracy(userId, last30days = true) val newLevel = when { recentAccuracy >= 0.98 -> "L3" recentAccuracy >= 0.95 -> "L2" else -> "L1" } annotatorRepo.updateLevel(userId, newLevel)}Q3:标注争议怎么处理?
答:仲裁机制。
// 争议处理:5 人标注,3 人同意即通过;分歧太大交专家fun resolveDispute(taskId: String): DisputeResult { val results = annotationRepo.findByTaskId(taskId)
// 计算 IoU 分布 val iouDistribution = results.map { calculateIoU(it, consensus) }
return when { iouDistribution.average() > 0.9 -> DisputeResult.consensus(consensus) iouDistribution.average() > 0.7 -> DisputeResult.partial(consensus) // 部分通过 else -> DisputeResult.expertReview(taskId) // 交专家审核 }}Q4:标注数据如何快速回流到训练?
答:变更数据捕获 (CDC) + 事件流。
// 标注结果变更 → Kafka → 训练 pipeline@KafkaListener(topics = ["annotation.verified"])fun onAnnotationVerified(event: AnnotationVerifiedEvent) { // 1. 转换格式(标注格式 → TFRecord) val trainingData = convertToTrainingFormat(event.annotation, event.clip)
// 2. 上传到训练存储 trainingDataStore.put("training/${event.clipId}", trainingData)
// 3. 通知训练调度 trainingScheduler.notifyNewData(event.clipId, event.taskType)}常见坑
1. 没有分层存储:PB 级数据全在 NVMe 上,成本爆炸。 2. 任务分配不均:有的标注员累死,有的闲死。 3. 质检不严:低质量标注污染训练集,模型上限被卡死。 4. 协同冲突:两人同时改同一 clip,结果丢失。 5. 模型预标注覆盖人工:模型错的地方没人修正。 6. 标注员作弊:用脚本批量提交,质量崩塌。 7. 没有冷数据归档:3 年前的数据还在线上访问,浪费成本。
可执行 Checklist
- 分层存储策略(热/温/冷)
- 任务分配算法(公平 + 紧急)
- 多人协同标注(M-of-N 共识)
- 自动质检(AI 模型 + 规则引擎)
- 人工抽样质检(10% 抽样)
- 模型预标注流程
- 标注员分级 + 动态评级
- 争议处理机制
- 标注数据回流训练 pipeline
- 标注员作弊检测
- 监控指标(标注速度、准确率、争议率)
- 成本核算(单 clip 成本)
写在最后
PB 级数据标注系统是自动驾驶的”数据基础设施”——没有高质量标注,再先进的模型也是空中楼阁。它涉及存储、调度、协同、质检、AI 辅助、成本优化等多个维度,是”看似简单、实则复杂”的典型代表。
三大核心:
- 质量为先:低质量标注比没有标注更糟
- 效率为王:标注成本决定研发节奏
- 协同为要:众包模式必须严格管控
下篇预告:第 7 篇 — 特斯拉超级充电会员服务系统(权益引擎、实时生效、账单统计)