# 端侧问答模型

#### 概述

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

*   使用自建的云端大模型。
    
*   采用Kit提供的端侧问答模型能力。
    

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

*   免除云端大模型的运维成本。
    
*   增强用户数据安全性（数据在端侧处理）。
    

#### 约束与限制

1.  当前端侧模型问答仅支持PC/2in1设备类型。
    
2.  当前端侧模型问答仅对企业开发者提供申请能力。
    

#### 接口说明

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

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

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

#### 开发步骤

#### 申请权限

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

#### 功能开发

1.  调用init接口，拉起本地AI模型管理。
    
2.  本地AI模型管理首次拉起，弹出隐私声明界面，同意后下载默认模型。
    
3.  本地AI模型管理非首次拉起，打开设置>系统>本地AI模型管理，下载默认模型。
    
4.  等待模型下载完成后，调用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.info('modelChat finished');
        this.addMessage('assistant', this.assistantContent);
        this.isProcessing = false;
      }

    };
    try {
      console.info('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%')
  }
}
```