鸿蒙应用开发之ArkTs集成AI大模型与Markdown流式渲染教程(API 20)
概述
本文章将深度讲解如何在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)}@Builderprivate 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 })}@Builderprivate 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 })}@Builderprivate 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});
}