鸿蒙应用开发之ArkTs集成AI大模型与Markdown流式渲染教程(API 20)
本文章将深度讲解如何在HarmonyOS NEXT(OpenHarmony 同样适用)中基于ArkTS、API 20开发具备AI大模型对话能力的完整应用。包含从基础对话实现到高级流式输出、Markdown实时渲染的完整解决方案,提供企业级应用开发的最佳实践。创建// 消息角色枚举// 消息数据类// API请求响应模型data?
·
概述
本文章将深度讲解如何在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
});
}
更多推荐

所有评论(0)