ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【共创稿事节】HarmonyOS 7 Agent 开发实战 12:人机协同与人工接管

【共创稿事节】HarmonyOS 7 Agent 开发实战 12:人机协同与人工接管

系列说明:本文是「HarmonyOS 7 Agent 开发实战」系列的收官之作。它讲的是在自治 Agent 上叠加一道「人类审批闸门」——Agent 提出敏感动作,人类在动作真正落地前通过、拒绝或修改。它和 13-应用Skill化(把能力封装成可复用技能)以及 14-A2A(Agent 之间协同)关注点不同:本文解决的是「这个动作该不该由人拍板」,而后两者解决的是「能力如何组织、多个 Agent 如何协作」。三者可以叠加,但彼此独立。

自治 Agent 真正跑起来之后,最让人睡不安稳的,往往是那一两个带副作用的动作:退款、删除账号、修改配置、对外发送通知。第 03 篇的 Tool Calling 让模型能自动调用工具,但「自动」这两个字意味着:只要模型判断失误,危害会立刻发生且难以撤回。人机协同(Human-in-the-Loop,HITL)的核心思路很简单——把敏感动作从「自动执行」改成「先提请求、等人拍板」。本文用纯 ArkTS 实现一个可测的审批状态机,不依赖任何大模型也能跑通核心逻辑,最后再把它接回 LLM 客户端,让 Agent 的「提议」与人类的「决策」串成一条闭环。

一、为什么需要人工接管

把第 03 篇的 Tool Calling 和本文放在一起对比,差异一目了然:

  • 03 篇(自动执行):模型决定调用refund_order,框架直接执行,钱当场退出去。
  • 本文(人工接管):模型决定调用refund_order,框架先生成一条ApprovalRequest(状态PENDING),停在半空,等人类在界面上点「通过 / 拒绝 / 修改」,才决定动作是否真正发生。

这条「半空中的停顿」就是人机协同的价值所在。它把「模型的可信度」和「动作的后果」解耦:模型依然负责理解与提议,但凡涉及不可逆、有成本的动作,最终按钮握在人手里。需要强调的是,这层闸门是纯状态机,和 LLM 是否在线没有关系——即便模型完全失灵,已经PENDING的请求也不会因为超时而被偷偷执行,这正是它比「直接调工具」更安全的地方。

那么哪些动作该加闸门?一个简单的判据是「可逆性 × 成本」:删除、退款、对外发送这类不可撤回或代价高的动作,一律进PENDING;而「读取订单状态」「查库存」这类只读、零副作用的查询,继续走第 03 篇的自动 Tool Calling 即可,不必惊动人类。闸门加得太少会漏风险,加得太多又会把人累垮——把 gate 当成白名单而非黑名单来设计,通常最省力,也最容易向业务方解释清楚。

二、核心设计:一个审批状态机

我们把一次「待人类接管」的请求抽象成两个类。ApprovalRequest是数据单元,字段只读、状态通过 getter 暴露;HumanInLoopAgent是状态机,负责推进状态并保证幂等。

exportenumApprovalStatus{PENDING='PENDING',// 待人类审批APPROVED='APPROVED',// 已通过,可执行原方案REJECTED='REJECTED',// 已拒绝,操作中止EDITED='EDITED'// 已修改,执行人类改后的方案}exportclassApprovalRequest{privatereadonly_id:string;privatereadonly_action:string;privatereadonly_payload:string;private_status:ApprovalStatus;private_editedPayload?:string;constructor(id:string,action:string,payload:string){this._id=id;this._action=action;this._payload=payload;this._status=ApprovalStatus.PENDING;}getid():string{returnthis._id;}getaction():string{returnthis._action;}getpayload():string{returnthis._payload;}getstatus():ApprovalStatus{returnthis._status;}setstatus(s:ApprovalStatus){this._status=s;}getpending():boolean{returnthis._status===ApprovalStatus.PENDING;}// EDITED 后返回人类改过的方案,否则返回原始方案geteffectivePayload():string{returnthis._status===ApprovalStatus.EDITED&&this._editedPayload!==undefined?this._editedPayload:this._payload;}}

状态机只认四个事件:requestApproval(创建)、onApprove(通过)、onReject(拒绝)、onEdit(修改)。写操作全部带「幂等保护」——只有当请求仍处于PENDING时才真正生效,否则一律返回 no-op。

exportclassHumanInLoopAgent{privaterequests:Map<string,ApprovalRequest>=newMap();privateseq:number=0;requestApproval(action:string,payload:string):ApprovalRequest{this.seq++;constid='apr_'+this.seq.toString();constreq=newApprovalRequest(id,action,payload);this.requests.set(id,req);// 存为 PENDINGreturnreq;}onApprove(id:string):{proceeded:boolean;payload:string}{constreq=this.requests.get(id);if(req===undefined||!req.pending)return{proceeded:false,payload:''};req.status=ApprovalStatus.APPROVED;return{proceeded:true,payload:req.payload};// 用原方案继续}onReject(id:string):{aborted:boolean}{constreq=this.requests.get(id);if(req===undefined||!req.pending)return{aborted:false};req.status=ApprovalStatus.REJECTED;return{aborted:true};// 操作中止}onEdit(id:string,newPayload:string):{proceeded:boolean;payload:string}{constreq=this.requests.get(id);if(req===undefined||!req.pending)return{proceeded:false,payload:''};req.editedPayload=newPayload;req.status=ApprovalStatus.EDITED;return{proceeded:true,payload:newPayload};// 用改后方案继续}}

三、幂等性:重复操作是 no-op

生产环境里,人类的点击可能重复:网络抖动后界面重绘、用户手快点了两次「通过」。如果第二次点击又触发一次退款,后果不堪设想。我们的写方法用一句if (req === undefined || !req.pending)把已决定的请求挡在门外——APPROVED之后再onApprove,返回{ proceeded: false };REJECTED之后再onReject,返回{ aborted: false }。状态只前进一次,副作用只发生一次。这套不变量我们会在第四节用 Hypium 单测锁死,保证重构时不会退化。

EDITED的状态也遵循同一原则:人类觉得模型给的退款金额不对,可以在卡片里改成正确金额再「按修改执行」,状态变成EDITED,effectivePayload立即返回改后的内容,业务侧永远读effectivePayload而不是原始payload,从而避免「展示是改后的、执行却是原的」这种割裂 bug。

四、演示页 Index.ets

演示页只做三件事:点按钮让 Agent 提议一个敏感操作;把PENDING请求渲染成一张审批卡片;根据人类点击回调onApprove / onReject / onEdit,并更新状态与结果文案。这里用MockLlmClient(实现LlmClient但不联网)让「Agent 提议」这一步在离线环境也能跑通。

privatepropose():void{constreq=this.agent.requestApproval('退款','订单 ORDER_20261012,退款 888.00 元');this.currentId=req.id;this.currentStatus=req.status;// PENDINGthis.hasRequest=true;}privatedoApprove():void{constr=this.agent.onApprove(this.currentId);if(r.proceeded){this.currentStatus=ApprovalStatus.APPROVED;this.outcome='已通过,执行:'+r.payload;}else{this.outcome='该请求已处理,重复操作被忽略';// 幂等保护生效}}

界面上只有PENDING状态才显示「通过 / 拒绝 / 修改」三个按钮;一旦状态改变,按钮隐藏,只留下结果。这样从视觉上就杜绝了「对一个已决定的请求再次下手」的可能,和状态机的幂等保护形成双重保险。

五、测试:用 Hypium 锁住状态机

人机协同的可靠性几乎全部来自这个状态机,所以测试重点就是它。底座层(重试、解析、记忆、消息模型)沿用前几篇的 sanity 用例,新增的是审批状态机的特性用例。所有用例纯逻辑、不联网,可在 DevEco 的 ohosTest 里一键运行。

it('onApprove 用原方案继续并置 APPROVED',0,()=>{constagent=build();constreq=agent.requestApproval('退款','订单 A');constr=agent.onApprove(req.id);expect(r.proceeded).assertTrue();expect(r.payload).assertEqual('订单 A');expect(agent.getStatus(req.id)).assertEqual(ApprovalStatus.APPROVED);});it('幂等:已批准后再 approve 是 no-op',0,()=>{constagent=build();constreq=agent.requestApproval('退款','订单 A');agent.onApprove(req.id);constr=agent.onApprove(req.id);expect(r.proceeded).assertFalse();});

覆盖点包括:创建即PENDING、onApprove继续并置APPROVED、onReject中止并置REJECTED、onEdit用改后方案并置EDITED,以及「已决定后再操作是 no-op」「isPending仅在PENDING为真」。命令行侧还有一份verify-node.cjs镜像同一套逻辑,提交前用 Node 跑一遍即可确认纯逻辑无误。

六、12 篇系列回顾与组合

把这 12 篇串起来,正好是一条「从能对话到能上岗」的进阶路线:

  1. 01 云端大模型 API 接入底座:统一LlmClient抽象、重试与解析。
  2. 02 Agent 工程脚手架与对话 UI 骨架:ConversationStore、ChatUIMessage、页面骨架。
  3. 03 Tool Calling 让 AI 查实时订单与库存:模型自主调用工具(本文的对照篇)。
  4. 04 云端 RAG 搭企业私有知识库问答:检索增强,让回答有据可依。
  5. 05 流式输出:逐字渲染,体验更顺。
  6. 06 对话记忆:长上下文裁剪与多轮连贯。
  7. 07 多 Agent 协作:分工编排复杂任务。
  8. 08 结构化输出:稳定 JSON,便于下游消费。
  9. 09 效果评测:用数据集量化好坏。
  10. 10 韧性设计:超时、降级、熔断。
  11. 11 并发控制:限流与队列,稳住吞吐。
  12. 12 人机协同与人工接管(本文):敏感动作加人工闸门。

把它们组合到生产,典型形态是:02 的脚手架搭好界面,01 的底座连模型,03/04/07 提供「能力」,08 保证输出可被程序消费,05/06 改善体验,09/10/11 兜住质量与稳定,最后用本文的HumanInLoopAgent在「有能力」和「敢放手」之间加一道安全阀。这样出来的就是一个既聪明、又让人放心的 HarmonyOS 7 Agent 应用。

七、避坑要点

  • 不要把审批状态存在 UI 局部变量里:状态必须是单一可信源(本例的Map),UI 只渲染它,否则界面重绘会让「已决定」的请求又变回可操作。
  • 业务侧永远读effectivePayload:人类改过之后,执行用的载荷要和展示一致,别让原payload悄悄溜进执行路径。
  • 超时不要自动放行:PENDING等待人类期间,既不要自动APPROVED也不要自动REJECTED,宁可升级提醒让人来处理。
  • 幂等写在状态机内部:不要依赖「前端禁用按钮」来防重复提交,后端/状态机这一层必须自己能挡住二次写。

参考与出处

  • HarmonyOS 应用开发文档:ArkTS 语言基础 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-get-started-V5
  • HarmonyOS 应用开发文档:ArkUI 声明式开发概述 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkui-overview-V5
  • HarmonyOS 应用开发文档:应用测试(Hypium 单元测试) https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/unit-test-V5

最后一句:当 Agent 学会「先问一句再动手」,它才真正从演示玩具成长为可以托付业务的生产系统。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进