目录

端侧问答模型

概述

当应用通过RAG接口进行知识问答时,系统会经过以下处理流程:问题分解、查询改写、知识检索和检索生成,该流程需要与大语言模型(LLM)进行多次交互。应用可选择两种部署方案:

选择端侧问答模型方案具有以下优势:

约束与限制

  1. 当前端侧模型问答仅支持PC/2in1设备类型。

  2. 当前端侧模型问答仅对企业开发者提供申请能力。

接口说明

端侧问答模型关键接口如下表所示,具体API说明详见API参考。

接口名 描述
init(): Promise<boolean> 初始化端侧问答模型,负责拉起模型管理应用。
chat(info: QuestionInfo, config: Config, callback: AsyncCallback<Answer>): Promise<void> 与端侧模型进行交互,实现端侧模型的问答功能。

模型资源来自Matrix模型库,chat接口默认调用模型为Qwen25-7B-Instruct。

开发步骤

[h2]申请权限

申请ohos.permission.INTERNET权限,配置方式请参阅声明权限声明权限。

[h2]功能开发

  1. 调用init接口,拉起本地AI模型管理。

  2. 本地AI模型管理首次拉起,弹出隐私声明界面,同意后下载默认模型。

  3. 本地AI模型管理非首次拉起,打开设置>系统>本地AI模型管理,下载默认模型。

  4. 等待模型下载完成后,调用chat接口,开始进行端侧问答。

示例代码

import { BusinessError } from '@kit.BasicServicesKit';
import { localChatModel } from '@kit.DataAugmentationKit'

type MessageType = 'tips' | 'request' | 'response';
interface ChatMessage {
  type: MessageType;
  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 responseContent: string = '';
  @State chatCounter: number = 0;

  // 页面加载时,拉起模型管理应用
  onPageShow() {
    this.initModel();
  }

  private async initModel(): Promise<void> {
    try {
      this.initFlag = await localChatModel.init();
      // ...
    } catch (error) {
      this.initFlag = false;
      // ...
    }
  }

  private scroller: Scroller = new Scroller();
  // ...

  private async DoChat(questionId: number): Promise<void> {
    if (!this.inputText.trim() || this.isProcessing) {
      return;
    }
    const requestQuestion = this.inputText.trim();
    if (!requestQuestion) {
      return;
    }
    this.inputText = '';
    // ...
    this.responseContent = '思考中...';
    this.isProcessing = true;

    const questionInfo: localChatModel.QuestionInfo = {
      questionId: questionId,
      content: requestQuestion
    };

    const localConfig: localChatModel.Config = {
      isStream: this.isStreamMode
    };
    const localChatCallback = async (err: BusinessError, ans: localChatModel.Answer): Promise<void> => {
      // ...
      if (ans.content && ans.content.trim() !== '') {
        if (this.responseContent == '思考中...') {
          this.responseContent = '';
        }
        this.responseContent += ans.content;
        // ...
      }
      // ...

      if (ans.isFinished) {
        // ...
        this.isProcessing = false;
      }
    };

    try {
      localChatModel.chat(questionInfo, localConfig, localChatCallback);
    } catch (error) {
      // ...
      this.isProcessing = false;
    }
  }

  private clearChat(): void {
    this.messages = [];
  }

  build() {
    Stack({ alignContent: Alignment.Top }) {
      Column() {
        Row() {
          // ...
          Row() {
            Button(this.isStreamMode ? '流式' : '非流式')
              .height(28)
              .fontSize(16)
              .fontWeight(500)
              .backgroundColor('#0c000000')
              .fontColor('#0A59F7')
              .borderRadius(8)
              .onClick(() => {
                this.isStreamMode = !this.isStreamMode;
                // ...
              })
          }
          .margin({ right: 12 })
        }
        .width('100%')
        .height(56)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .shadow({
          radius: 4,
          color: '#1a73e888',
          offsetX: 0,
          offsetY: 2
        })

        // 聊天区域
        Scroll(this.scroller) {
          Column() {
            ForEach(this.messages, (msg: ChatMessage, index: number) => {
              if (msg.type === 'tips') {
                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.type === 'request') {
                Row() {
                  Blank()
                  Text(msg.content)
                    .fontSize(16)
                    .fontColor(Color.White)
                    .padding(10)
                    .backgroundColor('#1a73e8')
                    .borderRadius(8)
                }
                .width('100%')
                .margin({ top: 12 })
                .justifyContent(FlexAlign.End)
              } else if (msg.type === 'response') {
                Row() {
                  Column() {
                    Text(msg.content)
                      .fontSize(16)
                      .fontColor('#333')
                      .lineHeight(20)
                      .padding(10)
                      .backgroundColor(Color.White)
                      .borderRadius(8)
                  }
                  .borderRadius(8)
                  .margin({ left: 8 })

                  Blank()
                }
                .width('100%')
                .margin({ top: 12 })
                .justifyContent(FlexAlign.Start)
              }
            }, (msg: ChatMessage) => msg.toString())

            // 加载指示器
            if (this.isProcessing) {
              Row() {
                Column() {
                  Text(this.responseContent)
                    .fontSize(16)
                    .fontColor('#333')
                    .lineHeight(20)
                    .padding(10)
                    .backgroundColor(Color.White)
                    .borderRadius(8)
                }
                .borderRadius(8)
                .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(8)
              .width('85%')
              .onChange((value: string) => {
                this.inputText = value;
              })
              .onSubmit(() => {
                if (!this.isProcessing && this.inputText.trim() !== '') {
                  const chatId = this.chatCounter++;
                  this.DoChat(chatId);
                }
              })

            Button('发送')
              .height(40)
              .fontSize(16)
              .fontWeight(500)
              .margin({ right: 12 })
              .backgroundColor('#0A59F7')
              .fontColor(Color.White)
              .borderRadius(8)
              .onClick(() => {
                if (!this.isProcessing && this.inputText.trim() !== '') {
                  const chatId = this.chatCounter++;
                  this.DoChat(chatId);
                }
              })
              .opacity(this.isProcessing || this.inputText.trim() === '' ? 0.4 : 1)

            Button('清空')
              .height(40)
              .fontSize(16)
              .fontWeight(500)
              .fontColor('#fff')
              .backgroundColor('#ea4335')
              .borderRadius(8)
              .onClick(() => {
                this.clearChat();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .padding(8)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .shadow({
          radius: 4,
          color: '#1a73e888',
          offsetX: 0,
          offsetY: 2
        })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#f0f5ff')
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }
}