# HarmonyOS NEXT 体验官 # 搭建AI聊天小助手(娱乐) 原创

时空未宇
发布于 2024-8-3 15:34
浏览
0收藏

运行环境

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

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

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

运行效果:

无图片识别对话效果:
# HarmonyOS NEXT 体验官 # 搭建AI聊天小助手(娱乐)-鸿蒙开发者社区
有图片识别对话效果:
# HarmonyOS NEXT 体验官 # 搭建AI聊天小助手(娱乐)-鸿蒙开发者社区
完整视频:HarmonyOS NEXT 搭建AI聊天小助手(娱乐)哔哩哔哩bilibili

开发步骤

1.搭建UI界面

# HarmonyOS NEXT 体验官 # 搭建AI聊天小助手(娱乐)-鸿蒙开发者社区

整体布局内部分为:聊天框: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)

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

# HarmonyOS NEXT 体验官 # 搭建AI聊天小助手(娱乐)-鸿蒙开发者社区

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

那这和index有什么关系呢?

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

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

  @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)

      if (this.AIMessage[index]) {
        Row({ space: 5 }) {
          Image($r('app.media.ic_gallery_ai_photography')).width(30).margin({ top: 10 })
          Row() {
            Text(this.AIMessage[index]).fontSize(18)
          }
          .backgroundColor('#fefefe')
          .width('78%')
          .padding(13)
          .borderRadius(10)
        }.width('90%').justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Top)
      } else {
        Row({ space: 5 }) {
          Image($r('app.media.ic_gallery_ai_photography')).width(30).margin({ top: 10 })
          Row() {
            LoadingProgress()
              .color('#2b2b2b').width('10%')
          }
          .backgroundColor('#fefefe')
          .width('78%')
          .padding(13)
          .borderRadius(10)
        }.width('90%').justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Top)
      }
    }

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();
        }
      })
    })
  }

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

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

messages: [
  {
    role: "user",
    content: "" //初始为空,等待更新
  }
]

然后调用

  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();
          }
        })
    })
  }

由于两次调用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)
    })
  }

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

3.进行对话

获取输入框中的数据

TextInput({
  placeholder: '请输入对话内容',
  text: this.tokenMessage,
  controller: this.TextInputcontroller
})                
    .onChange((value: string) => {
    this.tokenMessage = value
})

将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;
  }
}

最后将保存的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 = ''

4.待优化

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

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2024-8-15 08:36:35修改
2
收藏
回复
举报
回复
    相关推荐