2841 words
14 minutes

特斯拉级系统设计面试题(六):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 内部团队
  • 协同标注:多人协作避免重复劳动
  • 质量控制:抽样质检、交叉验证、模型预标注 + 人工修正
  • 成本优化:自动标注、人机协同、标注复用

面试误区:很多候选人把标注系统等同于”任务分配 + 提交”,没有考虑质检循环、争议处理、模型预标注、数据回流等关键环节。


题目重述#

题目:设计特斯拉自动驾驶路测数据标注系统,支持:

  1. PB 级数据:日均 1PB 新增数据、累计 50PB+ 历史数据
  2. 多模态标注:2D 框、3D 框、语义分割、点云标注、行为标注
  3. 万人并发:3-5 万标注员、众包模式
  4. 严格质量:关键场景标注准确率 > 99%、普通场景 > 95%
  5. 任务调度:公平分配、紧急任务插队、按难度分级
  6. 协同工作:多人协同标同一 clip、避免重复
  7. 成本控制:单 clip 标注成本持续下降
  8. 快速反馈:标注结果快速回流到训练 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. 任务调度引擎#

@Service
class 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,互相校验
*/
@Service
class 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. 质量控制#

/**
* 质检工作流:分层质检
*/
@Service
class 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 辅助)#

/**
* 模型预标注:用最新模型先标一遍,人工修正
*/
@Service
class 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:标注成本怎么降?#

三层降本

  1. 模型预标注:最新模型先标 70%,人工修 30%
  2. 难度分级:简单任务用初级标注员(便宜)
  3. 标注复用:同一 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 篇 — 特斯拉超级充电会员服务系统(权益引擎、实时生效、账单统计)

特斯拉级系统设计面试题(六):PB 级自动驾驶路测数据标注系统 — 任务调度、质量控制与人机协同
https://cyber.cc.cd/posts/blogs/tesla-system-design-06-data-annotation/
Author
Lance
Published at
2026-06-17
License
CC BY-NC-SA 4.0