
【听见课堂 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():PromiseArrayTaskItem{consttasks:ArrayTaskItemawaitthis.repository.getTasks();returntasks.filter((item:TaskItem)!item.confirmed);}asyncgetConfirmedTasks():PromiseArrayTaskItem{consttasks:ArrayTaskItemawaitthis.repository.getTasks();returntasks.filter((item:TaskItem)item.confirmed);}P08 消费候选集合P09 的任务中心快照则再次只保留confirmed记录再计算待办、完成、过期、日期分组和日历状态。这样即使某个页面重构也不应该因为少写一个if就把未确认候选混入正式列表。一个更稳妥的原则是可信边界越重要越要在靠近业务层的位置重复表达。页面可以用颜色说明状态Service 必须用查询规则保护状态Repository 则负责让状态修改可靠落盘。四、编辑发生在确认之前AI 可能识别到了任务意图却把标题或时间整理错。此时用户需要修改候选而不是先确认再去正式任务中心补救。P08 的编辑流程先把候选值复制到页面草稿privatestartCandidateTaskEdit(task:TaskItem):void{this.selectedCandidateTaskIdtask.id;this.editingCandidateTaskIdtask.id;this.taskDraftTitletask.title;this.taskDraftDueTexttask.dueText;}保存时页面只做空值提示ClassroomService.updateTask()负责去除首尾空白、解析截止时间Repository 再执行真正的数据写入。更关键的是RelationalClassroomRepository.updateTask()会检查当前记录consttask:TaskItem|undefinedawaitthis.findTask(taskId);if(taskundefined||task.confirmed){returnfalse;}已经确认的任务不能继续走“修改候选”通道。这条限制避免了页面状态滞后时越权修改正式事实也迫使产品为正式任务的编辑建立另一套清晰规则而不是复用候选入口。五、确认是一条业务命令不是 UI 选中态用户点击确认后P08 调用 Service写入成功才刷新全局数据并给出“任务已确认并保存”privateasyncconfirmCandidateTask(taskId:string):Promisevoid{if(awaitthis.service.confirmTask(taskId)){this.lastConfirmedTaskIdtaskId;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重置为 0asyncunconfirmTask(taskId:string):Promiseboolean{consttask:TaskItem|undefinedawaitthis.findTask(taskId);if(taskundefined||!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 避免多页面漂移通过撤销确认修复误操作也诚实保留了“拒绝尚未持久化”的现状。下一篇将继续沿着这条可信边界拆解麦克风、图片、字幕、板书、任务、导出和删除如何组成一条真正的本机优先隐私数据流。