news 2026/10/14 3:32:54

【共创稿事节】HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【共创稿事节】HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架

上篇把云端大模型接进了 HarmonyOS 7(API 26),底座能稳定返回一段文本。但真实业务里的 Agent 是多轮、有记忆、有界面的:用户问一句,Agent 结合上下文答一句;历史越长,上下文管理越要精细。不少团队的第一步,是把消息数组、系统提示词、网络调用全混进Index.ets——改一处崩三处,第二个人接手根本不敢动。

本文继续用可开箱即跑的 DevEco 工程,把这一层收敛成一套Agent 工程脚手架:ConversationStore管对话记忆与上下文裁剪,Agent管编排,ChatUIMessage管界面状态映射,最后用一套聊天页骨架把三者串起来。底层复用上篇的LlmClient(华为云盘古 / OpenAI 兼容),本篇代码聚焦"Agent 该怎么组织",纯逻辑全部配了可脱离设备运行的 Hypium 单测。

一、整体架构:三层拆分,各管各的

脚手架的价值在于拆:把易变的界面、稳定的编排、可替换的底座分成三层,每层只依赖下一层的抽象。

  • UI 层:聊天页骨架 +ChatUIMessage视图模型,只管把状态画出来。
  • Agent 编排层:Agent持有系统提示词和ConversationStore,一次send()完成"落用户消息 → 调底座 → 落助手回复"。
  • 云端底座层:复用上篇LlmClient抽象,超时、重试、解析已收口,本篇零改动直接复用。

这样拆完,换界面不影响编排,换模型厂商不影响记忆管理——三层各自独立演进,这就是脚手架该有的样子。

二、对话记忆容器:系统消息常驻,历史自动裁剪

ConversationStore是脚手架里最容易被写错的部分。两个关键点:系统消息必须永远留在发给模型的消息序列里(丢了它,Agent 的角色设定就失效);多轮历史不能无限堆(上下文窗口撑爆,费用也跟着涨)。

// feature/agent/ConversationStore.ets(节选)exportclassConversationStore{privatemessages:ChatMessage[]=[];privatesystemPrompt:string;// 最多保留的对话消息条数(不含系统消息),超出从头部丢弃最旧的privatemaxMessages:number;constructor(systemPrompt:string='',maxMessages:number=40){this.systemPrompt=systemPrompt;this.maxMessages=maxMessages;if(systemPrompt.length>0){this.messages.push({role:Role.SYSTEM,content:systemPrompt});}}addUser(text:string):void{this.messages.push({role:Role.USER,content:text});this.trim();}addAssistant(text:string):void{this.messages.push({role:Role.ASSISTANT,content:text});this.trim();}// 系统消息常驻;对话消息超出上限时,丢弃最旧的部分privatetrim():void{constconv=this.messages.filter(m=>m.role!==Role.SYSTEM);if(conv.length<=this.maxMessages)return;constkeepConv=conv.slice(conv.length-this.maxMessages);this.messages=[...(this.systemPrompt.length>0?[{role:Role.SYSTEM,content:this.systemPrompt}]:[]),...keepConv];}clear():void{this.messages=this.systemPrompt.length>0?[{role:Role.SYSTEM,content:this.systemPrompt}]:[];}}

getMessages()返回完整序列(系统 + 对话)直接喂给底座;getConversation()只返回对话消息,给 UI 渲染用。裁剪只动对话消息,系统消息雷打不动——这个细节用单测锁死。

三、Agent 编排:一次 send 走通完整往返

有了记忆容器,Agent本身出乎意料地薄——它只做一件事:把"组织请求 → 调底座 → 回写记忆"的固定顺序封装成send()。

// feature/agent/Agent.etsexportclassAgent{readonlystore:ConversationStore;privateclient:LlmClient;privatemodel:string;privatetemperature:number;constructor(client:LlmClient,systemPrompt:string,model:string,temperature:number=0.7){this.client=client;this.store=newConversationStore(systemPrompt);this.model=model;this.temperature=temperature;}asyncsend(userText:string):Promise<string>{this.store.addUser(userText);constreq:ChatRequest={model:this.model,messages:this.store.getMessages(),temperature:this.temperature};constresp:ChatResponse=awaitthis.client.chat(req);this.store.addAssistant(resp.text);returnresp.text;}}

注意send()里发出去的是store.getMessages()——即带系统提示词的完整历史,而不是只有当前这句。很多"Agent 记不住上文"的 bug,根子就在这里只发了单句。

四、UI 视图模型:把领域消息翻译成界面状态

领域层的ChatMessage只有角色和内容,但界面关心更多:这条是谁说的、发出去没有、是成功还是失败、几点发的。ChatUIMessage负责这层翻译,避免在 ArkUI 组件里散落一堆判断。

// feature/agent/ChatUIMessage.ets(节选)exportenumMsgStatus{PENDING='pending',DONE='done',ERROR='error'}exportclassChatUIMessage{readonlyid:string;readonlyrole:BubbleRole;text:string;status:MsgStatus;readonlycreatedAt:number;staticfromDomain(m:ChatMessage):ChatUIMessage{constrole=m.role===Role.USER?BubbleRole.USER:m.role===Role.ASSISTANT?BubbleRole.ASSISTANT:BubbleRole.SYSTEM;returnnewChatUIMessage(role,m.content,MsgStatus.DONE,0,'');}isUser():boolean{returnthis.role===BubbleRole.USER;}isPending():boolean{returnthis.status===MsgStatus.PENDING;}isError():boolean{returnthis.status===MsgStatus.ERROR;}formattedTime():string{// HH:mm,UI 直接展示constd=newDate(this.createdAt);returnd.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0');}}

PENDING状态对应"思考中…"的占位气泡,ERROR对应红色的失败提示——状态先建模,界面才有章法。

五、对话 UI 骨架:气泡列表 + 输入区 + 发送状态机

页面骨架分两块:消息列表和输入区。发送逻辑是一个小状态机——先落两个气泡(用户气泡立即完成,助手气泡置PENDING),请求回来更新助手气泡,失败标ERROR,全程用sending锁住按钮防重复提交。

// pages/Index.ets(节选)privateasynchandleSend():Promise<void>{consttext=this.inputText.trim();if(text.length===0||this.sending)return;this.inputText='';constuserMsg=newChatUIMessage(BubbleRole.USER,text,MsgStatus.DONE);constbotMsg=newChatUIMessage(BubbleRole.ASSISTANT,'',MsgStatus.PENDING);this.messages=[...this.messages,userMsg,botMsg];// 重新赋值触发刷新this.sending=true;try{constreply=awaitthis.agent.send(text);botMsg.text=reply;botMsg.status=MsgStatus.DONE;}catch(e){botMsg.text='出错了:'+(easError).message;botMsg.status=MsgStatus.ERROR;}finally{this.messages=[...this.messages];this.sending=false;}}

气泡渲染用List + ForEach,用户消息靠右蓝底、助手消息靠左灰底,@Builder Bubble一个模板搞定两种角色。唯一的 ArkUI 惯例要记住:@State数组直接 push 不一定触发刷新,改完重新赋值this.messages = [...this.messages]最稳。

六、纯逻辑单测:不联网也能绿

脚手架的三块核心全是纯逻辑,Hypium 单测不需要网络、不需要 API Key,模拟器上秒过。

// ohosTest/ets/test/LlmClient.test.ets(节选)it('超出上限裁剪最旧对话,系统消息保留',0,()=>{consts=newConversationStore('sys',2);s.addUser('1');s.addAssistant('A');s.addUser('2');s.addAssistant('B');s.addUser('3');s.addAssistant('C');expect(s.getMessages()[0].role).assertEqual(Role.SYSTEM);expect(s.getConversation().length).assertEqual(2);expect(s.getConversation()[1].content).assertEqual('C');});it('send 落库用户/助手并走通底座',0,async()=>{constmock=newMockLlmClient('假回复');// 继承 LlmClient,sendOnce 返回固定 JSONconstagent=newAgent(mock,'sys','m',0.7);constreply=awaitagent.send('你好');expect(reply).assertEqual('假回复');constall=agent.store.getMessages();expect(all[1].role).assertEqual(Role.USER);expect(all[2].role).assertEqual(Role.ASSISTANT);});

工程里共 19 条断言:底座复用 9 条(重试 6 + 解析 3),新增ConversationStore4 条、Agent2 条(含错误传播)、ChatUIMessage4 条,全部不发起网络请求。Agent的测试通过继承LlmClient写了一个MockLlmClient,sendOnce直接返回固定 JSON,顺带把"底座解析 → Agent 落库"的整条链路验证了。

七、跑起来:三步

  1. DevEco Studio 26 打开工程,右键ohosTest → Run,确认 19 条断言全绿。
  2. 打开pages/Index.ets,把buildAgent()里的baseUrl/apiKey/model换成自己的云端网关配置。
  3. 连真机或 HarmonyOS 7 模拟器运行,输入框打字、点发送,即可看到多轮对话气泡——历史会带着系统提示词一起发给模型,Agent 记得住上文。

以下是 V 哥真机测试截图:

算是跑起来了^^

八、五个高频坑(搭脚手架前先看)

  1. 系统提示词别写死在页面里:收进ConversationStore,new Agent时统一传入,换页面不丢人设。
  2. 多轮历史会撑爆上下文窗口:设maxMessages自动裁剪最旧对话,系统消息常驻。
  3. 气泡状态不建模,UI 一团乱:用ChatUIMessage区分pending / done / error,别在组件里写散装 if。
  4. @State数组直接 push 不刷新:改完重新赋值this.messages = [...this.messages]。
  5. 每个页面都 new 一个客户端:Agent复用同一个LlmClient实例,配置只写一处。

参考与出处

  • 上篇《HarmonyOS 7 Agent 开发实战 01:云端大模型 API 接入底座》(本工程复用其LlmClient抽象)
  • 华为开发者官网 · ArkTS 声明式 UI 开发指南(@Component / @State / List / ForEach)
  • 华为开发者官网 · 应用测试框架 Hypium(ohosTest 单测)

最后一句:脚手架不是 scaffolding 的花架子,而是让每一行新代码都有明确的归宿;Agent 的能力可以慢慢长,骨架先要站得直。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:32:49

小公司大数据面试实战指南:Kafka/Flink/Hive故障排查与最小可行架构

1. 为什么这份“上海小公司大数据面试题汇总”值得你花一整晚细读我带过三届校招实习生&#xff0c;也帮五家不同行业的中小团队做过技术选型评估。每次聊到“大数据岗位怎么准备”&#xff0c;几乎所有人都会下意识点开大厂面经、翻看《Hadoop权威指南》目录&#xff0c;或者刷…

作者头像 李华
网站建设 2026/10/14 3:32:34

LVS 凭什么比 Nginx 快?四种工作模式与调度算法拆开讲

Nginx 和 HAProxy 做负载均衡&#xff0c;都要把数据包从内核态提到用户态、处理完再送回去&#xff1b;LVS 直接在内核里改完地址就转发了&#xff0c;这一步之差就是数量级的差距。这篇不碰实验环境&#xff0c;只把原理讲透&#xff1a;先分清「管理工具」与「转发引擎」谁在…

作者头像 李华
网站建设 2026/10/14 3:31:23

WPS AI 办公落地:AI 抽取、翻译、AIPPT 三个真实场景怎么接

在日常办公场景中&#xff0c;我们常常陷入一种“低效忙碌”的怪圈&#xff1a;为了制作一份演示文稿&#xff0c;需要在 PPT 软件里反复调整排版、对齐文本框&#xff0c;耗费数小时却只完成了内容的堆砌&#xff1b;面对堆积如山的合同文档&#xff0c;只能依靠肉眼逐行扫描&…

作者头像 李华
网站建设 2026/10/14 3:31:08

20261013_205810_大模型预训练如何进行:从数据准备到小模型训练实践

没有人把世界上所有问题的答案逐条写给模型&#xff0c;它为什么还能接话、解释概念、写文章&#xff1f;答案的一部分&#xff0c;藏在模型正式提供服务之前的大规模学习中。 这篇先把事情讲简单&#xff1a;模型用一组可调整的数值处理输入&#xff0c;这些数值叫参数。训练…

作者头像 李华