
不同于调用现成SDK,本文将手写原生网络层、解析SSE数据流,并利用ArkUI状态管理实现“打字机”效果,打造一个纯血鸿蒙智能助手。
在鸿蒙NEXT剥离AOSP、确立“原生智能”基调的背景下,AI能力接入已成为ArkTS应用开发的刚需。DeepSeek 以其极高的性价比和媲美GPT-4的推理能力,成为中小型鸿蒙应用接入大模型的首选。
市面上大多教程仅展示http.request请求非流式接口,这在用户体验上存在致命缺陷——用户需等待模型完整生成(往往超10秒)才能看到结果。本文将深入鸿蒙的网络底层,利用onDataReceive回调实现实时流式(SSE)输出,并解决ArkUI高频刷新带来的性能瓶颈。
@ohos.net.http 与 @ohos.util,无第三方库。关键权限配置 (entry/src/main/module.json5):
鸿蒙Next严格管控网络权限,需在requestPermissions中声明,且默认禁止明文流量(如需本地调试可配置networkSecurityConfig)。
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
]
}
}对接DeepSeek的Chat Completion API,定义请求与响应实体。
// model/ChatEntity.ets
export class Message {
role: string; // 'system' | 'user' | 'assistant'
content: string;
constructor(role: string, content: string) {
this.role = role;
this.content = content;
}
}
export class ChatRequest {
model: string = 'deepseek-chat';
messages: Message[] = [];
stream: boolean = true;
max_tokens: number = 2048;
temperature: number = 0.7;
}
// 用于解析流式响应的delta
export class StreamDelta {
role?: string;
content?: string;
}HarmonyOS的http模块提供了onDataReceive回调,这是我们实现流式的关键。难点在于分片数据可能截断JSON,必须维护buffer进行粘包处理。
封装 DeepSeekStreamService:
// service/DeepSeekService.ets
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
import { Message, ChatRequest } from '../model/ChatEntity';
export type OnChunkCallback = (chunk: string, isEnd: boolean) => void;
export class DeepSeekService {
private httpRequest: http.HttpRequest;
private buffer: string = ''; // 用于处理不完整的消息块
constructor() {
this.httpRequest = http.createHttp();
}
/**
* 发起流式对话请求
*/
streamChat(request: ChatRequest, onChunk: OnChunkCallback): void {
const url = 'https://api.deepseek.com/chat/completions'; // 请替换为您的代理地址或官方地址
const apiKey = 'sk-xxxxxx'; // 建议放入加密Preferences中
this.httpRequest.request(url, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
extraData: JSON.stringify(request),
expectDataType: http.HttpDataType.STRING, // 关键:接收文本流
usingCache: false,
connectTimeout: 60000,
readTimeout: 60000,
// 【核心】流式数据接收回调
onDataReceive: (data: ArrayBuffer) => {
const decoder = new util.TextDecoder('utf-8');
const chunkStr = decoder.decode(new Uint8Array(data), { stream: true });
this.parseStreamData(chunkStr, onChunk);
},
// 请求结束回调
onEnd: (data: http.HttpResponse) => {
// 处理残余buffer(如果最后一行没有换行)
if (this.buffer.length > 0) {
this.extractContentFromLine(this.buffer, onChunk);
this.buffer = '';
}
onChunk('', true); // 通知结束
}
}, (err: BusinessError) => {
console.error(`请求失败: ${JSON.stringify(err)}`);
onChunk(`网络错误: ${err.message}`, true);
});
}
/**
* 解析SSE文本块 (格式: data: {...}\n\n)
*/
private parseStreamData(raw: string, callback: OnChunkCallback): void {
// 将新数据追加到缓冲区
this.buffer += raw;
// 按行分割(SSE标准以双换行符分隔,但实测单换行较多)
const lines = this.buffer.split('\n');
// 保留最后一行(可能不完整)
this.buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '') continue;
// 处理 data: 前缀
if (trimmed.startsWith('data: ')) {
const jsonStr = trimmed.substring(6);
if (jsonStr === '[DONE]') {
callback('', true);
return;
}
try {
const parsed = JSON.parse(jsonStr);
const delta = parsed.choices?.[0]?.delta;
if (delta?.content) {
callback(delta.content, false);
}
} catch (e) {
console.warn('JSON解析失败,忽略该行:', jsonStr);
}
}
}
}
// 辅助方法:处理最后残余行
private extractContentFromLine(line: string, callback: OnChunkCallback): void {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.substring(6);
const parsed = JSON.parse(jsonStr);
const content = parsed.choices?.[0]?.delta?.content;
if (content) callback(content, false);
} catch (e) { /* ignore */ }
}
}
}在ArkUI中,若每收到一个字符就刷新整个List或Text,会导致UI卡顿。解决方案:
@State管理当前正在输出的一条消息内容。@ObjectLink优化列表项渲染,仅更新变化项。页面结构 (Index.ets):
import { DeepSeekService } from '../service/DeepSeekService';
import { Message } from '../model/ChatEntity';
@Entry
@Component
struct ChatPage {
@State messages: Message[] = [];
@State inputText: string = '';
@State currentAssistantMsg: string = ''; // 正在流式输出的文本
@State isStreaming: boolean = false;
private service: DeepSeekService = new DeepSeekService();
private scroller: Scroller = new Scroller();
build() {
Column() {
// 消息列表
List({ scroller: this.scroller }) {
ForEach(this.messages, (msg: Message, index: number) => {
ListItem() {
this.MessageItem(msg)
}
}, (item: Message, index: number) => index.toString())
// 正在流式显示的占位气泡
if (this.isStreaming) {
ListItem() {
Row() {
Text(this.currentAssistantMsg || '思考中...')
.padding(10)
.backgroundColor('#F0F0F0')
.borderRadius(10)
.width('80%')
.textAlign(TextAlign.Start)
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 10 })
}
}
}
.width('100%')
.layoutWeight(1)
.onAreaChange(() => {
this.scroller.scrollToEdge(Edge.Bottom); // 滚动到底部
})
// 输入区
Row() {
TextInput({ text: this.inputText, placeholder: '输入问题...' })
.onChange((val) => this.inputText = val)
.layoutWeight(1)
Button('发送')
.enabled(!this.isStreaming)
.onClick(() => this.sendMessage())
}
.padding(10)
}
.height('100%')
}
@Builder
MessageItem(msg: Message) {
Row() {
Text(msg.content)
.padding(10)
.backgroundColor(msg.role === 'user' ? '#007AFF' : '#E9E9EB')
.fontColor(msg.role === 'user' ? '#FFFFFF' : '#000000')
.borderRadius(10)
.maxWidth('80%')
.wordBreak(WordBreak.BREAK_ALL)
}
.width('100%')
.justifyContent(msg.role === 'user' ? FlexAlign.End : FlexAlign.Start)
.margin({ top: 10 })
}
private sendMessage() {
if (this.inputText.trim() === '') return;
// 添加用户消息
this.messages.push(new Message('user', this.inputText));
const question = this.inputText;
this.inputText = '';
this.isStreaming = true;
this.currentAssistantMsg = '';
// 构建请求(携带历史上下文)
const history = this.messages.map(m => new Message(m.role, m.content));
// 注意:这里需要将用户刚发的包含进去,但由于push后messages已更新,需复制
const requestMessages = this.messages.map(m => new Message(m.role, m.content));
this.service.streamChat(
{ messages: requestMessages, stream: true } as ChatRequest,
(chunk: string, isEnd: boolean) => {
if (isEnd) {
// 流结束:将当前累积的文本存入消息列表
if (this.currentAssistantMsg.length > 0) {
this.messages.push(new Message('assistant', this.currentAssistantMsg));
}
this.isStreaming = false;
this.currentAssistantMsg = '';
this.scroller.scrollToEdge(Edge.Bottom);
} else {
// 逐字追加(打字机效果)
this.currentAssistantMsg += chunk;
// 列表滚动到底部(通过改变状态触发)
this.scroller.scrollToEdge(Edge.Bottom);
}
}
);
}
}request参数中配置caPath忽略证书校验,或使用http明文(需配置networkSecurityConfig)。推荐:使用云函数/网关代理DeepSeek接口,避免在前端暴露apiKey。messages中content总长度,截断最早的user/assistant对话。http.createHttp()在每次请求结束后务必调用destroy(),否则会导致内存飙升。
// 在 onEnd 或 onError 中执行 this.httpRequest.destroy();Text组件原生不支持Markdown。若需显示代码块,建议引入RichEditor或自定义解析器,将**粗体**转换为Span节点(篇幅原因,本文不展开,但这是商业级应用的分水岭)。在 Dayu 800 真机(麒麟9010)上测试:
onDataReceive高频刷新(每秒约30-50个chunk),UI丢帧率低于 3%,得益于ArkUI的@State细粒度更新机制,远优于WebView方案。本文从零实现了基于HarmonyOS NEXT原生能力的AI对话助手。相较于直接使用WebView嵌入ChatGPT或依赖第三方SDK,这种方案具有更低的内存占用和更流畅的动画体验。
后续您可扩展的方向:
@ohos.multimodalInput.voice)。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。