概述

本文章将深度讲解如何在HarmonyOS NEXT(OpenHarmony 同样适用)中基于ArkTS、API 20开发具备AI大模型对话能力的完整应用。包含从基础对话实现到高级流式输出、Markdown实时渲染的完整解决方案,提供企业级应用开发的最佳实践。

项目设置与配置

1. 创建新项目

在DevEco Studio中创建一个新的Empty Ability项目,选择API 20作为编译版本。
已有项目可直接新建页面接入即可

2. 配置网络权限

module.json5文件中添加必要的网络访问权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

3. 添加依赖

package.json中添加必要的依赖项:

{
  "dependencies": {
    "@ohos/net.http": "> 2.0.0",
    "@luvi/lv-markdown-in": "^1.0.0"
  }
}

安装Markdown渲染插件:

ohpm install @luvi/lv-markdown-in

核心实现代码

1. 定义数据模型

创建model/Message.ets文件定义消息数据结构:

// 消息角色枚举
export enum MessageRoleEnum {
  User = 'user',
  Assistant = 'assistant'
}

// 消息数据类
export class MessageVO {
  role: MessageRoleEnum;
  content: string;
  timestamp: number;

  constructor(role: MessageRoleEnum, content: string) {
    this.role = role;
    this.content = content;
    this.timestamp = new Date().getTime();
  }
}

// API请求响应模型
export class AIResponse {
  code: number = 0;
  message: string = '';
  data?: {
    choices: Array<{
      message: {
        role: string;
        content: string;
      }
    }>
  };
}

2. 网络请求工具类

创建utils/HttpUtils.ets处理大模型API请求:

import http from '@ohos.net.http';
import { AIResponse } from '../model/Message';

export class HttpUtils {
  private static readonly BASE_URL = 'https://api.example.com/v1/chat/completions';
  private static readonly API_KEY = 'your-api-key-here';

  // 发送消息到大模型
  static async sendMessage(messages: Array<{role: string, content: string}>): Promise<string> {
    try {
      let httpRequest = http.createHttp();
      
      let response = await httpRequest.request(this.BASE_URL, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.API_KEY}`
        },
        extraData: {
          'model': 'your-model-name',
          'messages': messages,
          'temperature': 0.7,
          'max_tokens': 2000
        }
      });

      if (response.responseCode === 200) {
        let result: AIResponse = JSON.parse(response.result.toString());
        if (result.code === 0 && result.data && result.data.choices.length > 0) {
          return result.data.choices[0].message.content;
        } else {
          throw new Error(result.message || 'API返回数据格式错误');
        }
      } else {
        throw new Error(`HTTP错误: ${response.responseCode}`);
      }
    } catch (error) {
      console.error('请求大模型API失败:', error);
      throw error;
    }
  }
}

3. 流式输出增强实现

创建utils/StreamHttpUtils.ets支持流式输出:

import http from '@ohos.net.http';

export class StreamHttpUtils {
  private static readonly BASE_URL = 'https://api.example.com/v1/chat/completions';
  private static readonly API_KEY = 'your-api-key-here';

  // 流式请求方法
  static async sendMessageStreaming(
    messages: Array<{role: string, content: string}>,
    onChunkReceived: (chunk: string, isComplete: boolean) => void
  ): Promise<void> {
    try {
      let httpRequest = http.createHttp();
      
      let response = await httpRequest.request(this.BASE_URL, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.API_KEY}`,
          'Accept': 'text/event-stream',
          'Cache-Control': 'no-cache'
        },
        extraData: {
          'model': 'your-model-name',
          'messages': messages,
          'temperature': 0.7,
          'max_tokens': 2000,
          'stream': true
        },
        expectDataType: http.HttpDataType.ARRAY_BUFFER
      });

      if (response.responseCode === 200) {
        await this.processStreamResponse(response, onChunkReceived);
      } else {
        throw new Error(`HTTP错误: ${response.responseCode}`);
      }
    } catch (error) {
      console.error('流式请求失败:', error);
      throw error;
    }
  }

  private static async processStreamResponse(
    response: http.HttpResponse, 
    onChunkReceived: (chunk: string, isComplete: boolean) => void
  ): Promise<void> {
    const decoder = new TextDecoder();
    const buffer = response.result as ArrayBuffer;
    const text = decoder.decode(buffer);
    const lines = text.split('\n');
    
    let fullContent = '';
    
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.substring(6);
        if (data === '[DONE]') {
          onChunkReceived('', true);
          break;
        }
        
        try {
          const parsed = JSON.parse(data);
          if (parsed.choices && parsed.choices[0].delta.content) {
            const chunk = parsed.choices[0].delta.content;
            fullContent += chunk;
            onChunkReceived(chunk, false);
          }
        } catch (e) {
          console.warn('解析流数据出错:', e);
        }
      }
    }
    
    onChunkReceived('', true);
  }
}

4. Markdown渲染组件

创建components/MarkdownRenderer.ets

import { LvMarkdownIn } from '@luvi/lv-markdown-in';

@Component
export struct MarkdownRenderer {
  private content: string = '';
  private isRendering: boolean = false;

  setContent(text: string) {
    this.content = text;
    this.isRendering = true;
  }

  clearContent() {
    this.content = '';
    this.isRendering = false;
  }

  build() {
    Column() {
      if (this.isRendering && this.content) {
        LvMarkdownIn({
          text: this.content,
          loadMode: "text",
          loadCallBack: {
            success(r: any) {
              console.info("Markdown渲染成功: " + r.code, r.message);
            },
            fail(r: any) {
              console.error("Markdown渲染失败: " + r.code, r.message);
            }
          }
        })
        .width('100%')
      } else {
        Text('加载中...')
          .fontSize(14)
          .fontColor('#999999')
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }
}

5. 完整聊天页面实现

创建pages/AdvancedChatPage.ets

import { MessageVO, MessageRoleEnum } from '../model/Message';
import { StreamHttpUtils } from '../utils/StreamHttpUtils';
import { MarkdownRenderer } from '../components/MarkdownRenderer';

@Entry
@Component
struct AdvancedChatPage {
  @State messageArr: MessageVO[] = [];
  @State textInputMsg: string = '';
  @State isLoading: boolean = false;
  @State currentStreamingContent: string = '';
  @State isStreaming: boolean = false;
  
  private markdownRenderer: MarkdownRenderer = new MarkdownRenderer();
  private throttledUpdate: Function = this.throttle(this.updateContent.bind(this), 100);

  // 流式发送消息
  private async sendMessageStreaming() {
    if (this.textInputMsg.trim() === '' || this.isLoading) {
      return;
    }

    let userMessage = new MessageVO(MessageRoleEnum.User, this.textInputMsg);
    this.messageArr.push(userMessage);
    
    this.textInputMsg = '';
    this.isLoading = true;
    this.isStreaming = true;
    this.currentStreamingContent = '';

    try {
      let historyMessages = this.messageArr.map(msg => ({
        role: msg.role === MessageRoleEnum.User ? 'user' : 'assistant',
        content: msg.content
      }));

      await StreamHttpUtils.sendMessageStreaming(
        historyMessages,
        (chunk: string, isComplete: boolean) => {
          setTimeout(() => {
            if (chunk) {
              this.currentStreamingContent += chunk;
              this.throttledUpdate(this.currentStreamingContent);
            }
            
            if (isComplete) {
              if (this.currentStreamingContent) {
                this.messageArr.push(
                  new MessageVO(MessageRoleEnum.Assistant, this.currentStreamingContent)
                );
              }
              this.cleanupStreaming();
            }
          }, 0);
        }
      );
    } catch (error) {
      console.error('流式对话失败:', error);
      this.cleanupStreaming();
      this.messageArr.push(
        new MessageVO(MessageRoleEnum.Assistant, '抱歉,对话出现错误,请重试。')
      );
    }
  }

  private cleanupStreaming() {
    this.isLoading = false;
    this.isStreaming = false;
    this.currentStreamingContent = '';
  }

  private updateContent(content: string) {
    this.markdownRenderer.setContent(content);
  }

  private throttle(func: Function, delay: number): Function {
    let timeoutId: number | undefined;
    return (...args: any[]) => {
      if (!timeoutId) {
        timeoutId = setTimeout(() => {
          func.apply(this, args);
          timeoutId = undefined;
        }, delay);
      }
    };
  }

  build() {
    Column() {
      // 聊天消息列表
      List({ space: 10 }) {
        ForEach(this.messageArr, (item: MessageVO, index?: number) => {
          ListItem() {
            this.MessageItem(item)
          }
        }, (item: MessageVO) => item.timestamp.toString())
        
        if (this.isStreaming) {
          ListItem() {
            this.StreamingMessageItem()
          }
        }
      }
      .layoutWeight(1)
      .width('100%')

      // 输入区域
      this.InputArea()
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }

  @Builder
  private MessageItem(message: MessageVO) {
    let isUser = message.role === MessageRoleEnum.User;
    
    Row() {
      if (!isUser) {
        Image($r('app.media.ai_avatar'))
          .width(30)
          .height(30)
          .margin({ right: 10 })
          .borderRadius(15)
      }
      
      if (isUser) {
        Text(message.content)
          .fontSize(16)
          .padding(10)
          .backgroundColor('#007DFF')
          .textColor('#FFFFFF')
          .borderRadius(10)
          .maxLines(0)
          .layoutWeight(1)
      } else {
        Column() {
          MarkdownRenderer({ content: message.content })
            .width('100%')
        }
        .padding(10)
        .backgroundColor('#F0F0F0')
        .borderRadius(10)
        .width('100%')
      }
      
      if (isUser) {
        Image($r('app.media.user_avatar'))
          .width(30)
          .height(30)
          .margin({ left: 10 })
          .borderRadius(15)
      }
    }
    .width('100%')
    .justifyContent(isUser ? FlexAlign.End : FlexAlign.Start)
    .margin({ top: 5, bottom: 5 })
  }

  @Builder
  private StreamingMessageItem() {
    Row() {
      Image($r('app.media.ai_avatar'))
        .width(30)
        .height(30)
        .margin({ right: 10 })
        .borderRadius(15)
      
      Column() {
        this.markdownRenderer
          .width('100%')
        
        if (this.isStreaming) {
          Text('AI正在思考...')
            .fontSize(12)
            .fontColor('#666666')
            .margin({ top: 5 })
        }
      }
      .padding(10)
      .backgroundColor('#F0F0F0')
      .borderRadius(10)
      .width('100%')
    }
    .width('100%')
    .justifyContent(FlexAlign.Start)
    .margin({ top: 5, bottom: 5 })
  }

  @Builder
  private InputArea() {
    Row() {
      TextInput({ placeholder: '输入消息...', text: this.textInputMsg })
        .height(40)
        .layoutWeight(1)
        .fontSize(16)
        .onChange((value: string) => {
          this.textInputMsg = value;
        })
        .onSubmit(() => {
          this.sendMessageStreaming();
        })

      Button(this.isLoading ? '发送中...' : '发送')
        .height(40)
        .margin({ left: 10 })
        .backgroundColor('#007DFF')
        .fontColor('#FFFFFF')
        .onClick(() => {
          this.sendMessageStreaming();
        })
        .enabled(!this.isLoading)
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#FFFFFF')
    .border({ width: 1, color: '#E5E5E5' })
    .borderRadius(20)
  }
}

功能扩展与高级特性

1. 对话历史管理

import preferences from '@ohos.data.preferences';

export class ChatHistoryManager {
  private static readonly PREFERENCES_KEY = 'chat_history';
  
  static async saveHistory(messages: MessageVO[]): Promise<void> {
    try {
      let prefs = await preferences.getPreferences(getContext(), 'chat_store');
      let history = messages.map(msg => ({
        role: msg.role,
        content: msg.content,
        timestamp: msg.timestamp
      }));
      await prefs.put(this.PREFERENCES_KEY, JSON.stringify(history));
      await prefs.flush();
    } catch (error) {
      console.error('保存对话历史失败:', error);
    }
  }
  
  static async loadHistory(): Promise<MessageVO[]> {
    try {
      let prefs = await preferences.getPreferences(getContext(), 'chat_store');
      let historyStr = await prefs.get(this.PREFERENCES_KEY, '[]');
      let history = JSON.parse(historyStr);
      return history.map((item: any) => 
        new MessageVO(item.role, item.content)
      );
    } catch (error) {
      console.error('加载对话历史失败:', error);
      return [];
    }
  }
}

2. 多轮对话上下文优化

private prepareMessages(): Array<{role: string, content: string}> {
  const maxHistory = 10;
  const startIndex = Math.max(this.messageArr.length - maxHistory * 2, 0);
  
  return this.messageArr.slice(startIndex).map(msg => ({
    role: msg.role === MessageRoleEnum.User ? 'user' : 'assistant',
    content: msg.content
  }));
}

3. 自定义Markdown样式

@Component
export struct CustomMarkdownRenderer {
  private content: string = '';
  private customStyles: Object = {
    code: {
      backgroundColor: '#f5f5f5',
      padding: '2px 4px',
      borderRadius: 3,
      fontFamily: 'monospace'
    },
    blockquote: {
      borderLeft: '4px solid #ddd',
      paddingLeft: 10,
      marginLeft: 0,
      color: '#666'
    }
  };

  build() {
    Column() {
      LvMarkdownIn({
        text: this.content,
        loadMode: "text"
      })
      .width('100%')
    }
  }

  setContent(text: string) {
    this.content = text;
  }
}

性能优化与最佳实践

1. 内存管理优化

private cleanupOldMessages() {
  const MAX_MESSAGES = 50;
  if (this.messageArr.length > MAX_MESSAGES) {
    this.messageArr = this.messageArr.slice(-MAX_MESSAGES);
  }
}

aboutToDisappear() {
  // 清理资源
  this.cleanupStreaming();
  this.cleanupOldMessages();
}

2. 错误处理增强

private handleAPIError(error: any) {
  let errorMessage = '网络请求失败,请检查网络连接';
  
  if (error.responseCode === 401) {
    errorMessage = 'API密钥无效,请检查配置';
  } else if (error.responseCode === 429) {
    errorMessage = '请求过于频繁,请稍后重试';
  } else if (error.responseCode >= 500) {
    errorMessage = '服务器内部错误,请稍后重试';
  }
  
  // 显示错误提示
  promptAction.showToast({
    message: errorMessage,
    duration: 3000
  });
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐