HarmonyOS 鸿蒙Next搭建AI聊天小助手(娱乐)

发布于 1周前 作者 yuanlaile 最后一次编辑是 5天前 来自 鸿蒙OS

HarmonyOS 鸿蒙Next搭建AI聊天小助手(娱乐)

运行环境

运行真机型号 IDE工具 API版本
Laval 开发者手机 oriole devecostudio-windows-5.0.3.403 11

AI模型采用百度千帆大模型平台

通过HTTP POST方式获取相应的能力,具体可参考API在线调试 - 千帆大模型平台

AI模型 模式
对话模型 Yi-34B-Chat (免费)良心👍👍👍👍👍 单轮
图像理解模型 Fuyu-8B(免费)良心👍👍👍👍👍 单轮

运行效果:

无图片识别对话效果:

有图片识别对话效果:

完整视频:HarmonyOS NEXT 搭建AI聊天小助手(娱乐)哔哩哔哩bilibili

开发步骤

1.搭建UI界面

整体布局内部分为:聊天框:Scroll容器;输入框:Row容器

在Scroll中采用ForEach的方式来显示聊天记录:

Column() {
    //聊天显示框
    Scroll() {
        Column({ space: 20 }) {
            ForEach(this.HumanMessage, (tokenToAIMessage: string, index: number) => {
                this.tokenWithAI(index)
            })
        }.width('100%').margin({ top: 20 })
    }.layoutWeight(10).height('95%').align(Alignment.Top)
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

当中的设计想法是,每一次的循环渲染对应着一次发送问题接收回答的整个过程,图中就对应了3次循环。

细心的同学会发现,我在代码当中循环遍历的数组是this.HumanMessage表示我们说给AI的消息,而渲染的时候我却只用了循环遍历的下标index做为传参进行处理,而不去使用this.HumanMessage里面的tokenToAIMessage内容,这是因为在每次收发消息的过程中,我会将question和answer存放在对应的数组中,这样还可以实现一键清除聊天记录的效果。

那这和index有什么关系呢?

思考一下,每一次的对话,我说一句存在HumanMessage数组里面,AI回答一句存在AIMessage数组里面,那对于数组来说,两个消息的下标index就都是相等的0、1、2、3、4、5.......

那么在渲染的时候,我只需要将index给到对应的数组就可以显示出对应的消息了,并且在没有收到AI消息的时候,会显示一个加载的小圈圈

  [@Builder](/user/Builder)
  tokenWithAI(index: number) {
      Row({ space: 5 }) {
        Text(this.HumanMessage[index])
          .backgroundColor('#61e15d')
          .width('78%')
          .fontSize(18)
          .padding(13)
          .borderRadius(10)
        Image($r('app.media.ic_user_portrait')).width(30).margin({ top: 10 }).fillColor('#ff2fa0b1')
      }.width('90%').justifyContent(FlexAlign.End).alignItems(VerticalAlign.Top)
  <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.AIMessage[index]) {
    Row({ space: <span class="hljs-number"><span class="hljs-number">5</span></span> }) {
      Image($r(<span class="hljs-string"><span class="hljs-string">'app.media.ic_gallery_ai_photography'</span></span>)).width(<span class="hljs-number"><span class="hljs-number">30</span></span>).margin({ top: <span class="hljs-number"><span class="hljs-number">10</span></span> })
      Row() {
        Text(<span class="hljs-keyword"><span class="hljs-keyword">this</span></span>.AIMessage[index]).fontSize(<span class="hljs-number"><span class="hljs-number">18</span></span>)
      }
      .backgroundColor(<span class="hljs-string"><span class="hljs-string">'#fefefe'</span></span>)
      .width(<span class="hljs-string"><span class="hljs-string">'78%'</span></span>)
      .padding(<span class="hljs-number"><span class="hljs-number">13</span></span>)
      .borderRadius(<span class="hljs-number"><span class="hljs-number">10</span></span>)
    }.width(<span class="hljs-string"><span class="hljs-string">'90%'</span></span>).justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Top)
  } <span class="hljs-keyword"><span class="hljs-keyword">else</span></span> {
    Row({ space: <span class="hljs-number"><span class="hljs-number">5</span></span> }) {
      Image($r(<span class="hljs-string"><span class="hljs-string">'app.media.ic_gallery_ai_photography'</span></span>)).width(<span class="hljs-number"><span class="hljs-number">30</span></span>).margin({ top: <span class="hljs-number"><span class="hljs-number">10</span></span> })
      Row() {
        LoadingProgress()
          .color(<span class="hljs-string"><span class="hljs-string">'#2b2b2b'</span></span>).width(<span class="hljs-string"><span class="hljs-string">'10%'</span></span>)
      }
      .backgroundColor(<span class="hljs-string"><span class="hljs-string">'#fefefe'</span></span>)
      .width(<span class="hljs-string"><span class="hljs-string">'78%'</span></span>)
      .padding(<span class="hljs-number"><span class="hljs-number">13</span></span>)
      .borderRadius(<span class="hljs-number"><span class="hljs-number">10</span></span>)
    }.width(<span class="hljs-string"><span class="hljs-string">'90%'</span></span>).justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Top)
  }
}

<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

2.获取AI服务能力

我的方法应该是最简单粗暴也是最笨的方法:异步嵌套

获取百度AI的能力需要两层密钥:

第一层:如何获取AKSK 提供的API_KEY和SECRET_KEY,然后调用

  getAccessToken(): Promise<string> {
    //采用异步方式发起请求
    return new Promise((resole, reject) => {
      // 1.创建HTTP请求对象
      let httpRequest = http.createHttp()
      //2.向FunctionGraph发送请求
      httpRequest.request(
        'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + API_KEY +
          '&client_secret=' + SECRET_KEY, //HTTP请求的URL地址
        {
          method: http.RequestMethod.POST
        } //请求方式为GET
      ).then(resp => { //如果请求成功
        //状态码200表示获取数据成功,将数据返回
        if (resp.responseCode === 200) {
          resole(resp.result.toString())
          httpRequest.destroy();
        } else { //此时返回的异常状态码,则获取数据失败
          reject('从云端获取数据失败')
          httpRequest.destroy();
        }
      })
    })
  }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

来获取一个访问凭证access_token鉴权

第二层:拿到access_token之后,将提问的数据进行JSON格式的封装

messages: [
  {
    role: "user",
    content: "" //初始为空,等待更新
  }
]
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

然后调用

  TokenWithYi34BChat(MessageData:string): Promise<string> {
    //采用异步方式发起请求
    return new Promise((resolve, reject) => {
      // 1.创建HTTP请求对象
      let httpRequest = http.createHttp()
      //2.向FunctionGraph发送请求
      httpRequest.request(
        "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/yi_34b_chat?access_token=" + access_token,
        {
          method: http.RequestMethod.POST,
          header: {
            'Content-Type': 'application/json'
          },
          // 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定
          extraData: MessageData
        }
      )
        .then(resp => { //如果请求成功
          //状态码200表示获取数据成功,将数据返回
          if (resp.responseCode === 200) {
            resolve(resp.result.toString())
          } else { //此时返回的异常状态码,则获取数据失败
            reject('从云端获取数据失败')
            httpRequest.destroy();
          }
        })
    })
  }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

由于两次调用HTTP都采用了异步的方法,有时就会出现access_token还没拿到就调用TokenWithYi34BChat,很显然会出问题,于是便有了异步的嵌套,朋友亲切的称为是💩山,也尝试过其他办法,让getAccessToken和TokenWithYi34BChat融合在一起用一个异步来处理,但效果都不尽人意。

  MainPromise(MessageData:string): Promise<string> {
    return new Promise((resolve, reject) => {
      this.getAccessToken().then(data => {
        access_token = JSON.parse(data.toString())['access_token']
      })
      setTimeout(() => {
        this.TokenWithYi34BChat(MessageData).then(data => {
          access_result = JSON.parse(data.toString())['result']
          console.info('result  ' + access_result)
          if (access_result != '') {
            resolve(access_result)
          } else {
            reject('从云端获取数据失败')
          }
        })
      }, 2000)
    })
  }
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

有思路的大佬咱们可以聊聊,没思路的大佬可以凑合着用吧,有时也会在第一次调用出现undifine的问题,增加一下setTimeout的时间可以缓解。

3.进行对话

获取输入框中的数据

TextInput({
  placeholder: '请输入对话内容',
  text: this.tokenMessage,
  controller: this.TextInputcontroller
})                
    .onChange((value: string) => {
    this.tokenMessage = value
})
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

将this.tokenMessage保存到this.HumanMessage用于显示,再将this.tokenMessage保存为JSON格式

interface MessageList {
  messages: Message[];
}

interface Message { role: string; content: string; }

export default class TextToJson { private messageList: MessageList;

constructor() { this.messageList = { messages: [ { role: “user”, content: “” //初始为空,等待更新 } ] }; } updateMessageContent(newContent: string) { if (this.messageList.messages.length > 0) { this.messageList.messages[0].content = newContent; } } getMessageList(): MessageList { return this.messageList; } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

最后将保存的JSON数据发送给Yi34B模型等待拿到data数据,之后将data数据保存到this.AIMessage中用于显示。

this.HumanMessage.push(this.tokenMessage)
this.Yi34BMessageToJson.updateMessageContent(this.tokenMessage)
this.Yi34BChat.MainPromise(JSON.stringify(this.Yi34BMessageToJson.getMessageList()))
    .then(data => {
    this.AIMessage.push(data)
    })
this.tokenMessage = ''
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

4.待优化

待优化项 优化内容 是否完成
API调用时的异步嵌套 简化调用步骤,并且现阶段获取结果的延迟平均在3.3秒左右,需尽可能减少延迟时间,提升响应速度。
发送按钮控件 当AI在进行回答时,发送按钮应当无法点击。
代码规范化 封装代码当中的魔鬼数以及部分代码可以进行模块化处理
输入显示效果 键盘显示时顶出输入框,浏览时scroll指定到最后问答的内容

更多关于HarmonyOS 鸿蒙Next搭建AI聊天小助手(娱乐)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

1 回复

更多关于HarmonyOS 鸿蒙Next搭建AI聊天小助手(娱乐)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS上搭建AI聊天小助手(娱乐版),你可以利用鸿蒙系统的分布式AI能力,结合华为HiAI引擎。首先,确保你的鸿蒙设备支持AI能力并已更新到最新系统版本。接着,使用华为DevEco Studio开发工具,利用Java或JS(ArkTS)编写应用逻辑,集成华为ML Kit或自定义训练模型进行对话处理。注意处理好用户隐私和数据安全。如果问题依旧没法解决请加我微信,我的微信是itying888。

更多关于HarmonyOS 鸿蒙Next搭建AI聊天小助手(娱乐)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


回到顶部