# 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)
当中的设计想法是,每一次的循环渲染对应着一次发送问题和接收回答的整个过程,图中就对应了3次循环。
细心的同学会发现,我在代码当中循环遍历的数组是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指定到最后问答的内容 | 否 |