【听见课堂 HarmonyOS NEXT 实战系列 08】AI 候选为什么必须经过人:课堂任务的人机协同闭环
课堂里一句“下周交实验报告”,经过实时字幕、板书 OCR 或端侧规则整理后,很容易被系统包装成一条看起来完整的任务:标题、截止时间、来源都有了。此时最危险的设计不是识别失败,而是系统把这条结果直接写进正式任务中心,让学生在没有察觉的情况下接受一个错误事实。
听见课堂没有把 AI 输出当成任务,而是把它定义成候选。候选可以编辑、拒绝、确认;只有人工确认后,confirmed才会成为数据层的稳定边界,正式任务中心也只读取已确认记录。本文结合 P08“任务确认”、P09“任务中心”、ClassroomService与RelationalClassroomRepository,拆解这条人机协同闭环。
一、为什么“识别正确率很高”仍不能直接写任务
课堂任务不是普通文本。它会触发提醒、安排学习时间,甚至影响迟交判断。即使模型整体准确率很高,一次局部错误仍可能带来真实后果:
- “周五交”被听成“周四交”;
- “选做第三题”被整理成“完成第三题”;
- 老师在举例时说到的日期被误判为截止时间;
- OCR 把公式编号、页码或板书箭头拼进任务标题;
- 手语或动作候选只有姿态相似,并不具备完整语义证据。
因此,系统必须区分两类对象:
| 对象 | 含义 | 是否进入正式任务中心 | 谁拥有最终决定权 |
|---|---|---|---|
| AI 候选 | 基于字幕、OCR 或规则生成的建议 | 否 | 用户 |
| 已确认任务 | 用户检查并确认过的本机记录 | 是 | 用户 |
这里的核心不是在按钮前加一句“AI 生成”,而是让候选与事实处在不同的数据状态、查询路径和交互权限中。
二、用confirmed建立候选与事实的硬边界
项目的TaskItem没有设计成一段随意传递的文本,而是保留标题、截止时间、来源、确认状态、完成状态和可计算的截止时间:
exportclassTaskItem{id:string;title:string;dueText:string;source:string;confirmed:boolean;completed:boolean;dueAtMillis:number;}其中confirmed回答“这条记录是否经过人工确认”,completed回答“已确认任务是否完成”。二者不能合并:候选任务甚至还没有资格进入完成/未完成状态机。
数据层的tasks表也把两个字段分别持久化:
CREATETABLEtasks(idTEXTPRIMARYKEY,course_idTEXTNOTNULL,titleTEXTNOTNULL,due_textTEXTNOTNULL,due_at_msINTEGERNOTNULLDEFAULT0,sourceTEXTNOTNULL,confirmedINTEGERNOTNULL,completedINTEGERNOTNULL,sort_orderINTEGERNOTNULL)这意味着“候选”和“正式任务”不是两个视觉标签,而是同一领域对象在不同可信阶段的明确状态。
三、读取时就分流,而不是页面里临时隐藏
ClassroomService从 Repository 取得任务后,分别暴露候选集合与已确认集合:
asyncgetCandidateTasks():Promise<Array<TaskItem>>{consttasks:Array<TaskItem>=awaitthis.repository.getTasks();returntasks.filter((item:TaskItem)=>!item.confirmed);}asyncgetConfirmedTasks():Promise<Array<TaskItem>>{consttasks:Array<TaskItem>=awaitthis.repository.getTasks();returntasks.filter((item:TaskItem)=>item.confirmed);}P08 消费候选集合,P09 的任务中心快照则再次只保留confirmed记录,再计算待办、完成、过期、日期分组和日历状态。这样即使某个页面重构,也不应该因为少写一个if就把未确认候选混入正式列表。
一个更稳妥的原则是:可信边界越重要,越要在靠近业务层的位置重复表达。页面可以用颜色说明状态,Service 必须用查询规则保护状态,Repository 则负责让状态修改可靠落盘。
四、编辑发生在确认之前
AI 可能识别到了任务意图,却把标题或时间整理错。此时用户需要修改候选,而不是先确认再去正式任务中心补救。
P08 的编辑流程先把候选值复制到页面草稿:
privatestartCandidateTaskEdit(task:TaskItem):void{this.selectedCandidateTaskId=task.id;this.editingCandidateTaskId=task.id;this.taskDraftTitle=task.title;this.taskDraftDueText=task.dueText;}保存时,页面只做空值提示;ClassroomService.updateTask()负责去除首尾空白、解析截止时间,Repository 再执行真正的数据写入。更关键的是,RelationalClassroomRepository.updateTask()会检查当前记录:
consttask:TaskItem|undefined=awaitthis.findTask(taskId);if(task===undefined||task.confirmed){returnfalse;}已经确认的任务不能继续走“修改候选”通道。这条限制避免了页面状态滞后时越权修改正式事实,也迫使产品为正式任务的编辑建立另一套清晰规则,而不是复用候选入口。
五、确认是一条业务命令,不是 UI 选中态
用户点击确认后,P08 调用 Service,写入成功才刷新全局数据并给出“任务已确认并保存”:
privateasyncconfirmCandidateTask(taskId:string):Promise<void>{if(awaitthis.service.confirmTask(taskId)){this.lastConfirmedTaskId=taskId;this.selectedCandidateTaskId='';this.editingCandidateTaskId='';awaitthis.refreshData();this.notice='任务已确认并保存';}}这段流程有三个值得保留的细节:
- 只有 Repository 返回成功才改变后续界面;
- 确认后重新读取 canonical data,而不是手工从候选数组移动到正式数组;
- 编辑态、选中态被同步清理,避免旧草稿继续指向已经确认的对象。
所谓 canonical data,就是数据层里唯一可信的任务状态。写入以后让所有页面重新从它计算,能减少候选数、正式任务数、回顾时间线和任务中心统计之间的漂移。
六、拒绝也要可恢复,但当前实现边界必须说清
用户可能确认某条候选无效。P08 支持“暂不加入”和立即撤销,但当前拒绝状态保存在页面的hiddenCandidateTaskIds中:
privaterejectCandidateTask(taskId:string):void{if(!this.hiddenCandidateTaskIds.includes(taskId)){this.hiddenCandidateTaskIds=[...this.hiddenCandidateTaskIds,taskId];}this.notice='候选任务已暂不加入;本次演示可立即撤销。';}privateundoRejectedCandidates():void{this.hiddenCandidateTaskIds=[];this.notice='已撤销拒绝,候选任务重新显示。';}这是一项明确的演示级能力:拒绝后当前页面隐藏,用户可立即恢复;它并没有写入 RelationalStore。应用重启或页面状态重建后,候选仍可能出现。
如果要升级为生产设计,应在任务表增加稳定的候选处置状态,例如candidate / rejected / confirmed,并记录reviewedAt、处置原因和来源版本。此时“撤销拒绝”也应成为 Repository 命令,而不是只清空页面数组。
七、确认之后仍允许撤销,而且要重置下游状态
误点确认并不罕见。项目记录最后一次确认的任务 ID,允许立即撤销。Repository 的unconfirmTask()不只把confirmed改回 0,还把completed重置为 0:
asyncunconfirmTask(taskId:string):Promise<boolean>{consttask:TaskItem|undefined=awaitthis.findTask(taskId);if(task===undefined||!task.confirmed){returnfalse;}constvalues:ValuesBucket={confirmed:0,completed:0};returnthis.updateById(TABLE_TASKS,taskId,values);}这是一个很小但重要的状态不变量:任务一旦回到候选,就不应保留“已完成”这种只属于正式任务的下游状态。否则重新确认时,用户会看到一条刚刚进入任务中心却已经完成的记录。
当前 UI 的撤销入口围绕“最后一次确认”设计,适合短时纠错。若要支持任意历史任务的撤销确认,还需要增加审计提示、关联提醒处理和并发写入保护。
八、正式任务中心只接受已确认数据
P09 不是把 P08 的列表换个样式。getTaskCenterSnapshot()首先过滤task.confirmed,然后才计算:
- 截止时间
dueAtMillis; 待办 / 已完成 / 已过期;今天 / 本周日期分组;- 日历日期键;
- 来源证据时间;
- 排序与统计数量。
这条顺序决定了系统语义:先成为用户认可的任务,再参与业务计算。如果对所有候选先计算提醒和逾期,系统事实上已经把候选当成事实,只是视觉上还写着“待确认”。
任务中心的完成/恢复也各自通过 Service 修改数据,并保留一次立即撤销。人机协同不只发生在 AI 输出的第一步,还要延伸到用户对正式任务状态的持续控制。
九、确认必须带着证据进入下一页
只展示任务标题和截止日期还不够。当用户怀疑“这条任务从哪里来”时,系统需要回到原始课堂上下文。
听见课堂在TaskItem.source中保留来源,任务中心可展开来源证据,也能跳转课堂回顾时间线,并用稳定的task-<id>找到对应节点。课堂回顾则根据confirmed展示“已人工确认”或“待人工确认”。
因此,一条可信任务至少应回答四个问题:
| 问题 | 对应数据 |
|---|---|
| 它说了什么 | title、dueText |
| 它来自哪里 | source、课程 ID |
| 谁决定它成为事实 | confirmed与人工操作 |
| 现在处于什么状态 | completed、过期计算 |
证据回看不是装饰性的“详情”,而是用户纠错和系统问责的入口。
十、把同一闭环扩展到 OCR、低置信度字幕与手语候选
候选边界不应只服务于任务抽取。多模态无障碍应用里,只要模型输出会影响用户记录,就可以复用同一原则:
OCR 校对
Core Vision OCR 输出先进入复核页,用户查看原图、旋转、切页、编辑文字,再保存复核后的内容。低置信度结果不应直接覆盖已有板书。
低置信度字幕
字幕可标记“不确定”,让回顾页保留异常状态。若要把字幕自动归纳为重点,应先生成重点候选,不能静默修改课堂原文。
手语或动作候选
人体骨骼规则、手部关键点或时序分类器的输出都应先成为候选。当前没有真实手部模型时,Provider 返回unavailable、分类保持unknown,更不能制造正式语义。
通用决策表
| 输出类型 | 默认状态 | 人工动作 | 正式写入条件 |
|---|---|---|---|
| OCR 文字 | 待复核 | 对照原图、编辑 | 用户保存 |
| 任务抽取 | 候选 | 编辑、拒绝、确认 | confirmed = true |
| 低置信度字幕 | 不确定 | 回看、修正 | 用户接受或保留标记 |
| 手语/动作结果 | 候选或unknown | 选择、纠错 | 真实模型可用且用户确认 |
十一、验收人机协同闭环要测什么
仅测试“点击确认后列表多一条”远远不够。建议至少覆盖:
- 空标题、空截止时间不能保存候选编辑;
- 编辑成功后,刷新与重启仍能回读;
- 已确认任务不能继续走候选编辑接口;
- 未确认候选不会进入任务中心统计和提醒;
- 撤销确认后回到候选,
completed被清零; - 完成/恢复状态可以立即撤销;
- 来源证据能从任务中心回到正确时间线节点;
- AI 建议关闭后,候选入口隐藏但正式任务不受影响;
- 数据写入失败时不显示“已保存”;
- 应用重启后再次核对候选、确认与完成状态。
其中第 2、10 项需要真实 RelationalStore 与重启回读,不能只靠页面内数组证明。
十二、当前项目的已验证与未验证边界
根据现有项目代码和记录,可以确认:
TaskItem、Service、Repository 与 RelationalStore 已形成确认状态链;- 候选编辑、确认、撤销确认、正式任务完成/恢复都有真实业务调用;
- 任务中心与回顾只按状态消费数据,并保留来源入口;
- 页面内拒绝和撤销拒绝目前是临时状态,尚未持久化;
- AI 候选的真实生成准确率、长课堂误识别率和跨设备表现不能由这套状态机证明;
- 手部关键点和真实手语时序模型尚未接入,不能把模拟候选写成真实识别结果。
这组边界恰好说明人机协同的价值:系统无需假装模型永不出错,也不能因为模型尚未完善就放弃可用流程。它用候选、确认、可撤销和证据回看,把不确定能力安全地交给用户控制。
总结
AI 候选必须经过人,不是一句免责声明,而是一条贯穿领域模型、Service 查询、Repository 写入、页面交互和证据回看的工程边界。听见课堂通过confirmed把候选与正式任务分开,通过确认后重读 canonical data 避免多页面漂移,通过撤销确认修复误操作,也诚实保留了“拒绝尚未持久化”的现状。
下一篇将继续沿着这条可信边界,拆解麦克风、图片、字幕、板书、任务、导出和删除如何组成一条真正的本机优先隐私数据流。