端侧问答模型
概述
当应用通过RAG接口进行知识问答时,系统会经过以下处理流程:问题分解、查询改写、知识检索和检索生成,该流程需要与大语言模型(LLM)进行多次交互。应用可选择两种部署方案:
- 使用自建的云端大模型。
- 采用Kit提供的端侧问答模型能力。
选择端侧问答模型方案具有以下优势:
- 免除云端大模型的运维成本。
- 增强用户数据安全性(数据在端侧处理)。
约束与限制
开发者需要申请接口调用。
接口说明
端侧问答模型关键接口如下表所示,具体API说明详见API参考。
| 接口名 | 描述 |
|---|---|
| init(): Promise<boolean> | 初始化端侧问答模型,负责拉起模型管理应用。 |
| chat(info: QuestionInfo, config: Config, callback: AsyncCallback<Answer>): Promise<void> | 与端侧模型进行交互,实现端侧模型的问答功能。 |
模型资源来自Matrix模型库,chat接口默认调用模型为Qwen25-7B-Instruct。
白名单申请
打开华为开发者联盟的“在线提单”页面,填写“概述”,端侧模型问答接口调用申请,问题分类选择“HarmonyOS NEXT > 系统 > Data Augmentation Kit”,描述问题详情并单击“提交问题”。提交问题后,有时需要您进一步澄清问题,请及时关注进展并予以回复,以便更好地解决问题。
1.当前端侧模型问答仅支持PC/2in1设备类型,其它设备类型(Phone、Tablet等)无法使用此能力。
2.为了提供优质的开发体验,当前端侧模型问答接口需要申请,优先处理华为开放生态团队对接的企业方应用。
申请接口调用信息模板:
- 应用名称:xxx。
- bundleName:xxx。
- AppID:xxx。
- 支持PC/2in1设备类型:是或否。
- 华为开放生态团队对接的企业方应用:是或否。
- 当前鸿蒙化进展:xxx。
- 当前已经支持的AI能力:xxx。
- 当前行业与用户影响力:xxx。
- 应用内容信息介绍:xxx。
开发步骤
-
问答过程中,端侧模型与LLM通过http请求交互,因此需要为应用申请网络权限。
// module.json5中配置"requestPermissions"字段"requestPermissions": [{"name": "ohos.permission.INTERNET"}], -
调用init接口,拉起本地AI模型管理。
-
本地AI模型管理首次拉起,弹出隐私声明界面,同意后下载默认模型。
-
本地AI模型管理非首次拉起,打开设置>系统>本地AI模型管理,下载默认模型。
-
等待模型下载完成后,调用chat接口,开始进行端侧问答。
完整示例代码
import { BusinessError } from "@kit.BasicServicesKit";
import { localChatModel } from '@kit.DataAugmentationKit'
type MessageRole = 'system' | 'user' | 'assistant';
interface ChatMessage {
role: MessageRole;
content: string;
}
@Entry
@Component
struct Index {
@State title: string = '端侧大模型问答助手';
@State isStreamMode: boolean = true;
@State messages: ChatMessage[] = [];
@State inputText: string = '';
@State initFlag: boolean = false;
@State isProcessing: boolean = false;
@State assistantContent: string = '';
@State chatCounter: number = 0;
// 页面加载时,拉起模型管理应用
onPageShow() {
console.info('modelChat onPageShow');
this.initModel();
}
private scroller: Scroller = new Scroller();
private scrollToBottom() {
setTimeout(() => {
this.scroller.scrollEdge(Edge.Bottom);
}, 50);
}
private addMessage(role: MessageRole, content: string): void {
const newMessage: ChatMessage = {
role: role,
content: content,
};
this.messages = [...this.messages, newMessage];
}
private async initModel(): Promise<void> {
try {
await localChatModel.init();
this.initFlag = true;
this.addMessage('system', '模型初始化完成!');
} catch (err) {
const error = err as BusinessError;
this.initFlag = false;
this.addMessage('system', `模型初始化出错: ${error.message}`);
}
}
private async DoChat(questionId: number): Promise<void> {
if (!this.inputText.trim() || this.isProcessing) {
return;
}
const userQuestion = this.inputText.trim();
if (!userQuestion) {
return;
}
this.inputText = '';
this.addMessage('user', userQuestion);
this.assistantContent = "思考中...";
this.isProcessing = true;
const questionInfo: localChatModel.QuestionInfo = {
questionId: questionId,
content: userQuestion
};
const localConfig: localChatModel.Config = {
isStream: this.isStreamMode
};
const localChatCallback = async (err: BusinessError, ans: localChatModel.Answer): Promise<void> => {
this.scrollToBottom();
if (err) {
if (this.assistantContent == "思考中...") {
this.assistantContent = "";
this.isProcessing = false;
}
// 模型运行相关错误码
console.error('modelChat Callback failed:', err.message);
this.addMessage('system', `localChatCallback: error code is ${err.code}, ${err.message}`);
this.scrollToBottom();
}
if (ans.content && ans.content.trim() !== '') {
if (this.assistantContent == "思考中...") {
this.assistantContent = "";
}
this.assistantContent += ans.content;
this.scrollToBottom();
}
this.scrollToBottom();
if (ans.isFinished) {
console.log('modelChat finished');
this.addMessage('assistant', this.assistantContent);
this.isProcessing = false;
}
};
try {
console.log('modelChat Starting chat...');
localChatModel.chat(questionInfo, localConfig, localChatCallback);
} catch (err) {
// 入参相关错误码
const error = err as BusinessError;
console.error('modelChat Chat failed:', error.message);
this.addMessage('system', `chat: error code is ${error.code}, ${error.message}`);
this.isProcessing = false;
}
}
private clearChat(): void {
this.messages = [];
}
build() {
Stack({ alignContent: Alignment.Top }) {
Column() {
Row() {
Text(this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1a73e8')
.margin({ left: 12 })
Circle()
.width(10)
.height(10)
.margin({ left: 12 })
.fill(this.initFlag ? '#0f0' : '#f00')
.opacity(0.8)
Text(this.initFlag ? '已就绪' : '未就绪')
.margin({ left: 6 })
.fontSize(12)
.fontColor('#666')
Blank()
Row() {
Button(this.isStreamMode ? '流式' : '非流式')
.width(70)
.height(25)
.fontSize(12)
.margin({ right: 20 })
.backgroundColor(Color.Gray)
.fontColor(Color.White)
.borderRadius(12.5)
.onClick(() => {
this.isStreamMode = !this.isStreamMode;
this.addMessage('system', `已切换至 ${this.isStreamMode ? '流式问答' : '非流式问答'} 模式`);
})
}
.margin({ right: 12 })
}
.width('100%')
.height(50)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 4,
color: '#1a73e888',
offsetX: 0,
offsetY: 2
})
.margin({ bottom: 12 })
// 聊天区域
Scroll(this.scroller) {
Column() {
ForEach(this.messages, (msg: ChatMessage, index: number) => {
if (msg.role === 'system') {
Row() {
Text(msg.content)
.fontSize(14)
.fontColor('#666')
.textAlign(TextAlign.Center)
.padding(8)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: index === 0 ? 0 : 12 })
} else if (msg.role === 'user') {
Row() {
Blank()
Text(msg.content)
.fontSize(16)
.fontColor(Color.White)
.padding(10)
.backgroundColor('#1a73e8')
.borderRadius(12)
}
.width('100%')
.margin({ top: 12 })
.justifyContent(FlexAlign.End)
} else if (msg.role === 'assistant') {
Row() {
Column() {
Text(msg.content)
.fontSize(16)
.fontColor('#333')
.lineHeight(20)
.padding(10)
.backgroundColor(Color.White)
.borderRadius(12)
}
.borderRadius(12)
.margin({ left: 8 })
Blank()
}
.width('100%')
.margin({ top: 12 })
.justifyContent(FlexAlign.Start)
}
}, (msg: ChatMessage) => msg.toString())
// 加载指示器
if (this.isProcessing) {
Row() {
Column() {
Text(this.assistantContent)
.fontSize(16)
.fontColor('#333')
.lineHeight(20)
.padding(10)
.backgroundColor(Color.White)
.borderRadius(12)
}
.borderRadius(12)
.margin({ left: 8 })
Blank()
}
.width('100%')
.margin({ top: 12 })
}
}
.padding(12)
.width('100%')
}
.width('100%')
.layoutWeight(1)
.margin({ bottom: 12 })
// 输入区域
Column() {
Row() {
TextInput({ text: this.inputText, placeholder: '请输入您的问题...' })
.flexGrow(1)
.height(42)
.fontSize(16)
.padding(8)
.backgroundColor(Color.White)
.borderRadius(21)
.width('85%')
.onChange((value: string) => {
this.inputText = value;
})
.onSubmit(() => {
if (!this.isProcessing && this.inputText.trim() !== '') {
const chatId = this.chatCounter++;
this.DoChat(chatId);
}
})
Button('发送')
.width(72)
.height(42)
.fontSize(16)
.margin({ left: 8 })
.backgroundColor('#1a73e8')
.fontColor(Color.White)
.borderRadius(21)
.onClick(() => {
if (!this.isProcessing && this.inputText.trim() !== '') {
const chatId = this.chatCounter++;
this.DoChat(chatId);
}
})
.opacity(this.isProcessing || this.inputText.trim() === '' ? 0.6 : 1)
Button("清空")
.width(72)
.height(42)
.fontSize(16)
.margin({ left: 8 })
.fontColor('#fff')
.backgroundColor('#ea4335')
.borderRadius(18)
.onClick(() => {
this.clearChat();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(8)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 4,
color: '#1a73e888',
offsetX: 0,
offsetY: 2
})
}
.width('100%')
.height('100%')
.padding(12)
.backgroundColor('#f0f5ff')
}
.width('100%')
.height('100%')
}
}