BasicAI 工作流标注绩效实现方案
BasicAI SaaS 的工作流标注是为了满足专业的标注团队对大量数据进行标注,既然是团队工作,那么就需要监管团队成员的工作进度和质量,也就是工作绩效管理。由于工作绩效需要细到标注对象(Object)的粒度,并且还需要按工作阶段和时间周期统计,导致计算逻辑非常复杂,并且计算量也很大。工作绩效还用于给标注人员结算工资,因此其准确性要求也很高。

BasicAI 是全球首个开源多模态训练数据平台,通过提供 AI 赋能的软件工具、数万项目提炼的本体中心和丰富的数据治理特性,来加速多模态训练数据的处理效率,进而提高 AI 工程师的建模效率。
需求及限制
相比于数据流只适合于算法工程师少量数据标注,工作流是为了满足专业的标注团队对大量数据进行标注,同时还需要对标注进度和质量进行监管。标注团队管理员创建 Task 来对某个数据集中的全部或部分数据按指定的要求进行标注,并从团队中分配一些标注人员来完成这些工作。整个标注过程包含标注(Annotate)、审核(Review)、验收(Accept)等多个 Stage(阶段),其中审核可以有多个,每个标注人员可以参与其中的一个或多个阶段,验收由甲方客户来完成。如果审核和验收不通过,可以打回到前面的阶段。绩效分为完成绩效和提交绩效两类,完成绩效用于结算(给标注人员算钱),只需在某个 Data 验收通过后计算其相关绩效即可,而提交绩效用于监管过程,包括进度和质量。
- Task 完成绩效不分 Stage,Worker 完成绩效、Task 提交绩效和 Worker 提交绩效分 Stage(不汇总 ALL Stage);
- Task 完成绩效、Worker 完成绩效、Task 提交绩效和 Worker 完成绩效都按 Tool 和 Class 细分;
- 完成绩效可以选择任意时间段来汇总查看,提交绩效不行,因为单个 Data 可能在多个时间点产生提交绩效,汇总时需要去重,无法通过简单的累加来实现;
- Edit 和 Review Object 在每个 Stage 只算作最后一个人的绩效;
- 重置结果时需清空 Data 绩效事件和提交绩效,只有打回到第一个 Stage 时才允许清空结果;
- 导入的初始结果算作 Import Object 绩效;
库表设计
设计思路
产品功能上要求能够按 Task、Data Type 和 Scenario Type,以及任意时间段来汇总绩效,并且部分绩效指标还需要以天、周、月等周期来汇总,这么复杂的查询条件使得难以通过预计算来提前算出各种条件组合的查询结果,只能存储较细粒度的绩效并在查询时实时聚合来实现。考虑到数据是以 Data 为粒度流转,并且各个 Data 之间的绩效不会互相影响,可以直接累加,因此以 Data 作为绩效存储粒度是合适的。对于完成绩效,Data 的完成时间决定了其绩效所属时间点,因此可以根据完成时间来判断某个 Data 的绩效是否属于某个时间段或者周期。对于提交绩效,反应的是过程绩效,由于单个 Data 的提交绩效会分布在多个时间点(提交时间),且不能把多个时间点的提交绩效简单累加来得到某个时间段或周期的提交绩效(比如同一天内 Edit 和 Review 同一个 Object 只能算一个),因此只能预先按预设周期汇总好单个 Data 的提交绩效(需要在周期内去重)。查询的时候可以按预设周期查看提交绩效,但不能选择任意时间段来查看。
完成绩效
task_completed_performance (Task 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
scene_id | long | Scene ID,Null 表示非连续帧 | |
completed_at | datetime | 完成时间 | |
is_data_valid | bool | Data 是否有效 | |
submit_count | int | 提交次数 | |
redo_count | int | 重做次数,包括 Reject 和 Reassign | |
reset_count | int | 重置次数,Reject 和 Reassign 时选择了重置结果 | |
is_correct | bool | 标注是否准确 | |
create_object_total_count | int | 创建对象总数 | |
create_object_error_count | int | 创建对象错误数 |
- Task Data 通过率 = Task Data 数量 * Task Stage 数量 / 提交次数;
- Reject Data 数量和 Reset Data 数量分别依据 redo_count 和 reset_count 是否大于 0 来计算;
task_completed_performance_by_tool (按 Tool 细分的 Task 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
is_data_valid | bool | Data 是否有效 | |
completed_at | datetime | 完成时间 | |
tool_type | string | Tool 类型 | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
task_completed_performance_by_class (按 Class 细分的 Task 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
is_data_valid | bool | Data 是否有效 | |
completed_at | datetime | 完成时间 | |
class_id | long | Class ID | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
task_worker_completed_performance (Worker 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
scene_id | long | Scene ID,Null 表示非连续帧 | |
worker_id | long | Worker ID | |
is_data_valid | bool | Data 是否有效 | |
is_final_worker | bool | 是否为最后工作者,决定了完成 Data 的数量 | |
completed_at | datetime | 完成时间 | |
working_duration | int | 工作时长,秒 | |
submit_count | int | 提交次数 | |
redo_count | int | 重做次数,包括 Reject 和 Reassign | |
reset_count | int | 重置次数,Reject 和 Reassign 时选择了重置结果 | |
create_object_total_count | int | 创建对象总数 | |
create_object_error_count | int | 创建对象错误数 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_total_count | int | 审核对象总数 | |
review_object_error_count | int | 审核对象错误数 | |
accept_object_count | int | 接收对象的数量 |
- Worker Stage 通过率 = 1 - Stage 重做次数 / Stage 提交次数;
task_worker_completed_performance_by_tool (按 Tool 细分的 Worker 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
worker_id | long | Worker ID | |
is_data_valid | bool | Data 是否有效 | |
completed_at | datetime | 完成时间 | |
tool_type | string | Tool 类型 | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
task_worker_completed_performance_by_class (按 Class 细分的 Worker 完成绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
worker_id | long | Worker ID | |
is_data_valid | bool | Data 是否有效 | |
completed_at | datetime | 完成时间 | |
class_id | long | Class ID | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
提交绩效
task_submitted_performance (Task 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
scene_id | long | Scene ID,Null 表示非连续帧 | |
submit_at | datetime | 提交时间 | |
submit_count | int | 提交次数 | |
redo_count | int | 重做次数,包括 Reject 和 Reassign | |
reset_count | int | 重置次数,Reject 和 Reassign 时选择了重置结果 |
- Stage Data 通过率 = Stage Data 数量 / Stage 提交次数;
- All Stage Data 通过率 = All Stage Data 记录数 / All Stage 提交次数;
- All Stage 的可通过聚合多个 Stage 来得到;
task_submitted_performance_by_tool (按 Tool 细分的 Task 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
period_type | string | 统计周期力度,day、week、month or all | |
period_index | int | 统计周期序号,比如 20221114 | |
tool_type | string | Tool 类型 | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
task_submitted_performance_by_class (按 Class 细分的 Task 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
period_type | string | 统计周期粒度,day、week、month or all | |
period_index | int | 统计周期序号,比如 20221114 | |
class_id | long | Class ID | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
worker_submitted_performance (Worker 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
scene_id | long | Scene ID,Null 表示非连续帧 | |
worker_id | long | Worker ID | |
period_type | string | 统计周期粒度,day、week、month or all | |
period_index | int | 统计周期序号,比如 20221114 | |
is_final_worker | bool | 是否为当前周期内的最后工作者,决定了完成 Data 的数量 | |
working_duration | int | 工作时长,秒 | |
submit_count | int | 提交次数 | |
redo_count | int | 重做次数,包括 Reject 和 Reassign | |
reset_count | int | 重置次数,Reject 和 Reassign 时选择了重置结果 | |
create_object_total_count | int | 创建对象总数 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_total_count | int | 审核对象总数 | |
accept_object_count | int | 接收对象的数量 |
- Worker Stage 通过率 = 1 - Stage 重做次数 / Stage 提交次数;
worker_submitted_performance_by_tool (按 Tool 细分的 Worker 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
worker_id | long | Worker ID | |
period_type | string | 统计周期力度,day、week、month or all | |
period_index | int | 统计周期序号,比如 20221114 | |
tool_type | string | Tool 类型 | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
worker_submitted_performance_by_class (按 Class 细分的 Worker 提交绩效)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
stage_id | long | Stage ID,不能为 Null | -100 表示 Need Acceptance Stage |
data_id | long | Data ID | |
data_type | string | Data 类型 | |
worker_id | long | Worker ID | |
period_type | string | 统计周期粒度,day、week、month or all | |
period_index | int | 统计周期序号,比如 20221114 | |
class_id | long | Class ID | |
create_object_count | int | 创建对象的数量 | |
edit_object_count | int | 编辑对象的数量 | |
delete_object_count | int | 删除对象的数量 | |
review_object_count | int | 审核对象的数量 | |
accept_object_count | int | 接收对象的数量 |
绩效事件
task_performance_event (Task 绩效事件)
字段 | 类型 | 描述 | 备注 |
id | long | ID | |
team_id | long | Team ID | |
task_id | long | Task ID | |
data_id | long | Data ID | |
events | json | 事件列表,单个绩效事件数据结构参考下方描述 |
绩效事件数据结构
属性 | 类型 | 描述 | 备注 |
type | string | 类型 | |
workerId | long | Worker ID | |
stageId | long | Stage ID,Null 表示 Accept 阶段 | |
payload | json | 内容,依具体事件类型而定 | |
time | datetime | 事件发生时间 |
绩效事件
为了减少前后端沟通协作成本,绩效事件都在后端产生,但需要前端配合来采集一些数据,比如工作时长、Object 修改时间等。对于 Object 相关事件,考虑到自动保存,如果每次保存时都产生一系列的创建、编辑和删除事件,那么事件数量会急剧增加,因此最好是只在提交的时候产生这些事件。后端可以在标注结果表里增加一个草稿字段,保存时修改草稿字段,提交时对比草稿和正式字段的内容来得到 Object 的差异并写入相关事件,然后使用草稿内容覆盖正式内容并清空草稿。为了方便后端知道 Object 是否有修改,前端需要维护一个 Object 的版本号(从 1 开始,每次有修改就 +1),否则后端得一一去比对 Object 的所有属性。工作时长也有类似情况,自动保存时也会产生大量工作时长事件,为了减少事件数量,可以在标注结果表里维护一个自上次提交以来的累计工作时长,每次保存时只需要更新该字段,不产生事件,提交时使用当前的累计工作时长产生一个事件并重置工作时长。Reject 和 Reassign 时需要把标注结果表里的草稿内容转为正式并记录相关事件,工作时长也类似。
导入 Object
从其它来源导入 Object,首次提交结果时产生,包含初始化数据里的所有 Object,无论其是否被编辑或删除。
{
"type": "IMPORT_OBJECT",
"workerId": 1,
"stageId": 1,
"payload": {
"objects": [
{
"id": "1",
"toolType": "3D_OBJECT",
"classId": 1
},
{
"id": "2",
"toolType": "2D_BOX",
"classId": 2
}
]
},
"time": "2022-11-11 15:40:00"
}
创建 Object
{
"type": "CREATE_OBJECT",
"workerId": 1,
"stageId": 1,
"payload": {
"objects": [
{
"id": "1",
"toolType": "3D_OBJECT",
"classId": 1
},
{
"id": "2",
"toolType": "2D_BOX",
"classId": 2
}
]
},
"time": "2022-11-11 15:40:00"
}
编辑 Object
{
"type": "EDIT_OBJECT",
"workerId": 1,
"stageId": 1,
"payload": {
"objects": [
{
"id": "1",
"toolType": "3D_OBJECT",
"classId": 1
}
]
},
"time": "2022-11-11 15:40:00"
}
删除 Object
{
"type": "DELETE_OBJECT",
"workerId": 1,
"stageId": 1,
"payload": {
"objects": [
{
"id": "2",
"toolType": "2D_BOX",
"classId": 2
}
]
},
"time": "2022-11-11 15:40:00"
}
审核 Object
需要记录所有被审核的 Objects,审核时如果有创建、修改和删除 Objects 操作,需单独记录相应事件。
{
"type": "REVIEW_OBJECT",
"workerId": 1,
"stageId": 1,
"payload": {
"objects": [
{
"id": "1",
"toolType": "3D_OBJECT",
"classId": 1
}
]
},
"time": "2022-11-11 15:40:00"
}
接受 Object
{
"type": "ACCEPT_OBJECT",
"workerId": 1,
"stageId": null,
"payload": {
"objects": [
{
"id": "1",
"toolType": "3D_OBJECT",
"classId": 1
}
]
},
"time": "2022-11-11 15:40:00"
}
错误 Object
Accept 时记录。
{
"type": "ERROR_OBJECT",
"workerId": 1,
"stageId": null,
"payload": {
"objects": [
{
"id": "1"
}
]
}
"time": "2022-11-11 15:40:00"
}
提交 Data
{
"type": "SUBMIT_DATA",
"workerId": 1,
"stageId": 1,
"payload": null,
"time": "2022-11-11 15:40:00"
}
拒绝 Data
{
"type": "REJECT_DATA",
"workerId": 1,
"stageId": 2,
"payload": {
// Stage 回退路径,顺序
"stageBackPath": [1],
"isReset": false,
"currentWorker": 2,
"isChangeWorker": false
},
"time": "2022-11-11 15:40:00"
}
重分配 Data
{
"type": "REASSIGN_DATA",
"workerId": 1,
"stageId": 2,
"payload": {
// Stage 回退路径,顺序
"stageBackPath": [1],
"isReset": false,
"currentWorker": 2,
"toWorker": 3
},
"time": "2022-11-11 15:40:00"
}
工作时长
前端每次保存(Save)时上报自上次保存以来的工作时长(注意中间可能有暂停),后端累计到下一次提交的工作时长里,然后在提交(Submit)时后端使用累计的工作时长生成本次提交的工作时长事件并重置累计工作时长。
{
"type": "WORKING_DURATION",
"workerId": 1,
"stageId": 1,
"payload": {
// 工作时长,单位秒
"duration": 3600
},
"time": "2022-11-11 15:40:00"
}
计算流程
批量计算

流式计算
