恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
【共创稿事节】HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架
首页
资讯中心
/
【共创稿事节】HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架
【共创稿事节】HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架
发布时间:2026/10/14 3:32:56
上篇把云端大模型接进了 HarmonyOS 7API 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:number40){this.systemPromptsystemPrompt;this.maxMessagesmaxMessages;if(systemPrompt.length0){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{constconvthis.messages.filter(mm.role!Role.SYSTEM);if(conv.lengththis.maxMessages)return;constkeepConvconv.slice(conv.length-this.maxMessages);this.messages[...(this.systemPrompt.length0?[{role:Role.SYSTEM,content:this.systemPrompt}]:[]),...keepConv];}clear():void{this.messagesthis.systemPrompt.length0?[{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:number0.7){this.clientclient;this.storenewConversationStore(systemPrompt);this.modelmodel;this.temperaturetemperature;}asyncsend(userText:string):Promisestring{this.store.addUser(userText);constreq:ChatRequest{model:this.model,messages:this.store.getMessages(),temperature:this.temperature};constresp:ChatResponseawaitthis.client.chat(req);this.store.addAssistant(resp.text);returnresp.text;}}注意send()里发出去的是store.getMessages()——即带系统提示词的完整历史而不是只有当前这句。很多Agent 记不住上文的 bug根子就在这里只发了单句。四、UI 视图模型把领域消息翻译成界面状态领域层的ChatMessage只有角色和内容但界面关心更多这条是谁说的、发出去没有、是成功还是失败、几点发的。ChatUIMessage负责这层翻译避免在 ArkUI 组件里散落一堆判断。// feature/agent/ChatUIMessage.ets节选exportenumMsgStatus{PENDINGpending,DONEdone,ERRORerror}exportclassChatUIMessage{readonlyid:string;readonlyrole:BubbleRole;text:string;status:MsgStatus;readonlycreatedAt:number;staticfromDomain(m:ChatMessage):ChatUIMessage{constrolem.roleRole.USER?BubbleRole.USER:m.roleRole.ASSISTANT?BubbleRole.ASSISTANT:BubbleRole.SYSTEM;returnnewChatUIMessage(role,m.content,MsgStatus.DONE,0,);}isUser():boolean{returnthis.roleBubbleRole.USER;}isPending():boolean{returnthis.statusMsgStatus.PENDING;}isError():boolean{returnthis.statusMsgStatus.ERROR;}formattedTime():string{// HH:mmUI 直接展示constdnewDate(this.createdAt);returnd.getHours().toString().padStart(2,0):d.getMinutes().toString().padStart(2,0);}}PENDING状态对应思考中…的占位气泡ERROR对应红色的失败提示——状态先建模界面才有章法。五、对话 UI 骨架气泡列表 输入区 发送状态机页面骨架分两块消息列表和输入区。发送逻辑是一个小状态机——先落两个气泡用户气泡立即完成助手气泡置PENDING请求回来更新助手气泡失败标ERROR全程用sending锁住按钮防重复提交。// pages/Index.ets节选privateasynchandleSend():Promisevoid{consttextthis.inputText.trim();if(text.length0||this.sending)return;this.inputText;constuserMsgnewChatUIMessage(BubbleRole.USER,text,MsgStatus.DONE);constbotMsgnewChatUIMessage(BubbleRole.ASSISTANT,,MsgStatus.PENDING);this.messages[...this.messages,userMsg,botMsg];// 重新赋值触发刷新this.sendingtrue;try{constreplyawaitthis.agent.send(text);botMsg.textreply;botMsg.statusMsgStatus.DONE;}catch(e){botMsg.text出错了(easError).message;botMsg.statusMsgStatus.ERROR;}finally{this.messages[...this.messages];this.sendingfalse;}}气泡渲染用List ForEach用户消息靠右蓝底、助手消息靠左灰底Builder Bubble一个模板搞定两种角色。唯一的 ArkUI 惯例要记住State数组直接 push 不一定触发刷新改完重新赋值this.messages [...this.messages]最稳。六、纯逻辑单测不联网也能绿脚手架的三块核心全是纯逻辑Hypium 单测不需要网络、不需要 API Key模拟器上秒过。// ohosTest/ets/test/LlmClient.test.ets节选it(超出上限裁剪最旧对话系统消息保留,0,(){constsnewConversationStore(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(){constmocknewMockLlmClient(假回复);// 继承 LlmClientsendOnce 返回固定 JSONconstagentnewAgent(mock,sys,m,0.7);constreplyawaitagent.send(你好);expect(reply).assertEqual(假回复);constallagent.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写了一个MockLlmClientsendOnce直接返回固定 JSON顺带把底座解析 → Agent 落库的整条链路验证了。七、跑起来三步DevEco Studio 26 打开工程右键ohosTest → Run确认 19 条断言全绿。打开pages/Index.ets把buildAgent()里的baseUrl/apiKey/model换成自己的云端网关配置。连真机或 HarmonyOS 7 模拟器运行输入框打字、点发送即可看到多轮对话气泡——历史会带着系统提示词一起发给模型Agent 记得住上文。以下是 V 哥真机测试截图算是跑起来了^^八、五个高频坑搭脚手架前先看系统提示词别写死在页面里收进ConversationStorenew Agent时统一传入换页面不丢人设。多轮历史会撑爆上下文窗口设maxMessages自动裁剪最旧对话系统消息常驻。气泡状态不建模UI 一团乱用ChatUIMessage区分pending / done / error别在组件里写散装 if。State数组直接 push 不刷新改完重新赋值this.messages [...this.messages]。每个页面都 new 一个客户端Agent复用同一个LlmClient实例配置只写一处。参考与出处上篇《HarmonyOS 7 Agent 开发实战 01云端大模型 API 接入底座》本工程复用其LlmClient抽象华为开发者官网 · ArkTS 声明式 UI 开发指南Component / State / List / ForEach华为开发者官网 · 应用测试框架 HypiumohosTest 单测最后一句脚手架不是 scaffolding 的花架子而是让每一行新代码都有明确的归宿Agent 的能力可以慢慢长骨架先要站得直。