【听见课堂 HarmonyOS NEXT 实战系列 08】AI 候选为什么必须经过人:课堂任务的人机协同闭环

课堂里一句“下周交实验报告”,经过实时字幕、板书 OCR 或端侧规则整理后,很容易被系统包装成一条看起来完整的任务:标题、截止时间、来源都有了。此时最危险的设计不是识别失败,而是系统把这条结果直接写进正式任务中心,让学生在没有察觉的情况下接受一个错误事实。

听见课堂没有把 AI 输出当成任务,而是把它定义成 候选。候选可以编辑、拒绝、确认;只有人工确认后,confirmed 才会成为数据层的稳定边界,正式任务中心也只读取已确认记录。本文结合 P08“任务确认”、P09“任务中心”、ClassroomServiceRelationalClassroomRepository,拆解这条人机协同闭环。

AI 候选必须经过人

一、为什么“识别正确率很高”仍不能直接写任务

课堂任务不是普通文本。它会触发提醒、安排学习时间,甚至影响迟交判断。即使模型整体准确率很高,一次局部错误仍可能带来真实后果:

  • “周五交”被听成“周四交”;
  • “选做第三题”被整理成“完成第三题”;
  • 老师在举例时说到的日期被误判为截止时间;
  • OCR 把公式编号、页码或板书箭头拼进任务标题;
  • 手语或动作候选只有姿态相似,并不具备完整语义证据。

因此,系统必须区分两类对象:

对象 含义 是否进入正式任务中心 谁拥有最终决定权
AI 候选 基于字幕、OCR 或规则生成的建议 用户
已确认任务 用户检查并确认过的本机记录 用户

这里的核心不是在按钮前加一句“AI 生成”,而是让候选与事实处在不同的数据状态、查询路径和交互权限中。

二、用 confirmed 建立候选与事实的硬边界

项目的 TaskItem 没有设计成一段随意传递的文本,而是保留标题、截止时间、来源、确认状态、完成状态和可计算的截止时间:

export class TaskItem {
  id: string;
  title: string;
  dueText: string;
  source: string;
  confirmed: boolean;
  completed: boolean;
  dueAtMillis: number;
}

其中 confirmed 回答“这条记录是否经过人工确认”,completed 回答“已确认任务是否完成”。二者不能合并:候选任务甚至还没有资格进入完成/未完成状态机。

数据层的 tasks 表也把两个字段分别持久化:

CREATE TABLE tasks (
  id TEXT PRIMARY KEY,
  course_id TEXT NOT NULL,
  title TEXT NOT NULL,
  due_text TEXT NOT NULL,
  due_at_ms INTEGER NOT NULL DEFAULT 0,
  source TEXT NOT NULL,
  confirmed INTEGER NOT NULL,
  completed INTEGER NOT NULL,
  sort_order INTEGER NOT NULL
)

这意味着“候选”和“正式任务”不是两个视觉标签,而是同一领域对象在不同可信阶段的明确状态。

候选到已确认事实的人机协同生命周期

三、读取时就分流,而不是页面里临时隐藏

ClassroomService 从 Repository 取得任务后,分别暴露候选集合与已确认集合:

async getCandidateTasks(): Promise<Array<TaskItem>> {
  const tasks: Array<TaskItem> = await this.repository.getTasks();
  return tasks.filter((item: TaskItem) => !item.confirmed);
}

async getConfirmedTasks(): Promise<Array<TaskItem>> {
  const tasks: Array<TaskItem> = await this.repository.getTasks();
  return tasks.filter((item: TaskItem) => item.confirmed);
}

P08 消费候选集合,P09 的任务中心快照则再次只保留 confirmed 记录,再计算待办、完成、过期、日期分组和日历状态。这样即使某个页面重构,也不应该因为少写一个 if 就把未确认候选混入正式列表。

一个更稳妥的原则是:可信边界越重要,越要在靠近业务层的位置重复表达。 页面可以用颜色说明状态,Service 必须用查询规则保护状态,Repository 则负责让状态修改可靠落盘。

四、编辑发生在确认之前

AI 可能识别到了任务意图,却把标题或时间整理错。此时用户需要修改候选,而不是先确认再去正式任务中心补救。

P08 的编辑流程先把候选值复制到页面草稿:

private startCandidateTaskEdit(task: TaskItem): void {
  this.selectedCandidateTaskId = task.id;
  this.editingCandidateTaskId = task.id;
  this.taskDraftTitle = task.title;
  this.taskDraftDueText = task.dueText;
}

保存时,页面只做空值提示;ClassroomService.updateTask() 负责去除首尾空白、解析截止时间,Repository 再执行真正的数据写入。更关键的是,RelationalClassroomRepository.updateTask() 会检查当前记录:

const task: TaskItem | undefined = await this.findTask(taskId);
if (task === undefined || task.confirmed) {
  return false;
}

已经确认的任务不能继续走“修改候选”通道。这条限制避免了页面状态滞后时越权修改正式事实,也迫使产品为正式任务的编辑建立另一套清晰规则,而不是复用候选入口。

五、确认是一条业务命令,不是 UI 选中态

用户点击确认后,P08 调用 Service,写入成功才刷新全局数据并给出“任务已确认并保存”:

private async confirmCandidateTask(taskId: string): Promise<void> {
  if (await this.service.confirmTask(taskId)) {
    this.lastConfirmedTaskId = taskId;
    this.selectedCandidateTaskId = '';
    this.editingCandidateTaskId = '';
    await this.refreshData();
    this.notice = '任务已确认并保存';
  }
}

这段流程有三个值得保留的细节:

  1. 只有 Repository 返回成功才改变后续界面;
  2. 确认后重新读取 canonical data,而不是手工从候选数组移动到正式数组;
  3. 编辑态、选中态被同步清理,避免旧草稿继续指向已经确认的对象。

所谓 canonical data,就是数据层里唯一可信的任务状态。写入以后让所有页面重新从它计算,能减少候选数、正式任务数、回顾时间线和任务中心统计之间的漂移。

六、拒绝也要可恢复,但当前实现边界必须说清

用户可能确认某条候选无效。P08 支持“暂不加入”和立即撤销,但当前拒绝状态保存在页面的 hiddenCandidateTaskIds 中:

private rejectCandidateTask(taskId: string): void {
  if (!this.hiddenCandidateTaskIds.includes(taskId)) {
    this.hiddenCandidateTaskIds = [...this.hiddenCandidateTaskIds, taskId];
  }
  this.notice = '候选任务已暂不加入;本次演示可立即撤销。';
}

private undoRejectedCandidates(): void {
  this.hiddenCandidateTaskIds = [];
  this.notice = '已撤销拒绝,候选任务重新显示。';
}

这是一项明确的演示级能力:拒绝后当前页面隐藏,用户可立即恢复;它并没有写入 RelationalStore。应用重启或页面状态重建后,候选仍可能出现。

如果要升级为生产设计,应在任务表增加稳定的候选处置状态,例如 candidate / rejected / confirmed,并记录 reviewedAt、处置原因和来源版本。此时“撤销拒绝”也应成为 Repository 命令,而不是只清空页面数组。

七、确认之后仍允许撤销,而且要重置下游状态

误点确认并不罕见。项目记录最后一次确认的任务 ID,允许立即撤销。Repository 的 unconfirmTask() 不只把 confirmed 改回 0,还把 completed 重置为 0:

async unconfirmTask(taskId: string): Promise<boolean> {
  const task: TaskItem | undefined = await this.findTask(taskId);
  if (task === undefined || !task.confirmed) {
    return false;
  }
  const values: ValuesBucket = { confirmed: 0, completed: 0 };
  return this.updateById(TABLE_TASKS, taskId, values);
}

这是一个很小但重要的状态不变量:任务一旦回到候选,就不应保留“已完成”这种只属于正式任务的下游状态。否则重新确认时,用户会看到一条刚刚进入任务中心却已经完成的记录。

当前 UI 的撤销入口围绕“最后一次确认”设计,适合短时纠错。若要支持任意历史任务的撤销确认,还需要增加审计提示、关联提醒处理和并发写入保护。

八、正式任务中心只接受已确认数据

P09 不是把 P08 的列表换个样式。getTaskCenterSnapshot() 首先过滤 task.confirmed,然后才计算:

  • 截止时间 dueAtMillis
  • 待办 / 已完成 / 已过期
  • 今天 / 本周 日期分组;
  • 日历日期键;
  • 来源证据时间;
  • 排序与统计数量。

这条顺序决定了系统语义:先成为用户认可的任务,再参与业务计算。如果对所有候选先计算提醒和逾期,系统事实上已经把候选当成事实,只是视觉上还写着“待确认”。

任务中心的完成/恢复也各自通过 Service 修改数据,并保留一次立即撤销。人机协同不只发生在 AI 输出的第一步,还要延伸到用户对正式任务状态的持续控制。

九、确认必须带着证据进入下一页

只展示任务标题和截止日期还不够。当用户怀疑“这条任务从哪里来”时,系统需要回到原始课堂上下文。

听见课堂在 TaskItem.source 中保留来源,任务中心可展开来源证据,也能跳转课堂回顾时间线,并用稳定的 task-<id> 找到对应节点。课堂回顾则根据 confirmed 展示“已人工确认”或“待人工确认”。

因此,一条可信任务至少应回答四个问题:

问题 对应数据
它说了什么 titledueText
它来自哪里 source、课程 ID
谁决定它成为事实 confirmed 与人工操作
现在处于什么状态 completed、过期计算

证据回看不是装饰性的“详情”,而是用户纠错和系统问责的入口。

十、把同一闭环扩展到 OCR、低置信度字幕与手语候选

候选边界不应只服务于任务抽取。多模态无障碍应用里,只要模型输出会影响用户记录,就可以复用同一原则:

OCR 校对

Core Vision OCR 输出先进入复核页,用户查看原图、旋转、切页、编辑文字,再保存复核后的内容。低置信度结果不应直接覆盖已有板书。

低置信度字幕

字幕可标记“不确定”,让回顾页保留异常状态。若要把字幕自动归纳为重点,应先生成重点候选,不能静默修改课堂原文。

手语或动作候选

人体骨骼规则、手部关键点或时序分类器的输出都应先成为候选。当前没有真实手部模型时,Provider 返回 unavailable、分类保持 unknown,更不能制造正式语义。

通用决策表

输出类型 默认状态 人工动作 正式写入条件
OCR 文字 待复核 对照原图、编辑 用户保存
任务抽取 候选 编辑、拒绝、确认 confirmed = true
低置信度字幕 不确定 回看、修正 用户接受或保留标记
手语/动作结果 候选或 unknown 选择、纠错 真实模型可用且用户确认

十一、验收人机协同闭环要测什么

仅测试“点击确认后列表多一条”远远不够。建议至少覆盖:

  1. 空标题、空截止时间不能保存候选编辑;
  2. 编辑成功后,刷新与重启仍能回读;
  3. 已确认任务不能继续走候选编辑接口;
  4. 未确认候选不会进入任务中心统计和提醒;
  5. 撤销确认后回到候选,completed 被清零;
  6. 完成/恢复状态可以立即撤销;
  7. 来源证据能从任务中心回到正确时间线节点;
  8. AI 建议关闭后,候选入口隐藏但正式任务不受影响;
  9. 数据写入失败时不显示“已保存”;
  10. 应用重启后再次核对候选、确认与完成状态。

其中第 2、10 项需要真实 RelationalStore 与重启回读,不能只靠页面内数组证明。

十二、当前项目的已验证与未验证边界

根据现有项目代码和记录,可以确认:

  • TaskItem、Service、Repository 与 RelationalStore 已形成确认状态链;
  • 候选编辑、确认、撤销确认、正式任务完成/恢复都有真实业务调用;
  • 任务中心与回顾只按状态消费数据,并保留来源入口;
  • 页面内拒绝和撤销拒绝目前是临时状态,尚未持久化;
  • AI 候选的真实生成准确率、长课堂误识别率和跨设备表现不能由这套状态机证明;
  • 手部关键点和真实手语时序模型尚未接入,不能把模拟候选写成真实识别结果。

这组边界恰好说明人机协同的价值:系统无需假装模型永不出错,也不能因为模型尚未完善就放弃可用流程。它用候选、确认、可撤销和证据回看,把不确定能力安全地交给用户控制。

总结

AI 候选必须经过人,不是一句免责声明,而是一条贯穿领域模型、Service 查询、Repository 写入、页面交互和证据回看的工程边界。听见课堂通过 confirmed 把候选与正式任务分开,通过确认后重读 canonical data 避免多页面漂移,通过撤销确认修复误操作,也诚实保留了“拒绝尚未持久化”的现状。

下一篇将继续沿着这条可信边界,拆解麦克风、图片、字幕、板书、任务、导出和删除如何组成一条真正的本机优先隐私数据流。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐