# HarmonyOS NEXT AI 对话助手应用开发实战 —— 基于 ArkTS 的流式 AI 聊天应用

开发环境: DevEco Studio 5.0+ / HarmonyOS NEXT 6.1.1(API 24)
开发语言: ArkTS(基于 TypeScript 的鸿蒙原生声明式语言)
网络框架:@kit.NetworkKit(原生 HTTP + SSE 流式解析)
AI 模型: DeepSeek-V3(兼容 OpenAI API 格式)
一、引言
在移动互联网时代,AI 对话助手已经成为应用开发的标配功能。从通用的 ChatGPT 到垂直领域的专业助手,AI 正在改变用户与应用的交互方式。HarmonyOS NEXT 作为新一代国产操作系统,提供了完整的原生网络通信能力,使得开发者可以在 ArkTS 中直接实现 AI API 的调用和流式响应展示。
本文将通过一个女友对话助手应用的完整实现,系统性地讲解在 HarmonyOS NEXT 上如何:
- 构建聊天式的 UI 界面,包括消息气泡、输入框、流式文字展示
- 使用
@kit.NetworkKit原生 HTTP 模块调用 AI API - 解析 SSE(Server-Sent Events)流式响应
- 实现非流式回退机制,兼容不同版本的网络行为
- 管理复杂的异步状态(加载、流式追加、取消、错误处理)
1.1 应用功能预览
女友对话助手是一款专注于帮助男性用户分析女友话语、提供高情商回复建议的 AI 工具。用户输入女友说的话,AI 会:
- 共情分析:理解女友当下的情绪状态
- 需求解读:指出话语背后隐含的情感需求
- 回复建议:提供 1~3 条具体可用的回复话术
应用提供了丰富的示例场景供用户快速尝试:
"女朋友说「我没事」的时候到底是什么意思?"
"女友生气了说「别管我」,该怎么回?"
"她问我「你前女友好看还是我好看」该怎么回答?"
"女友说「今天好累」怎么回复最暖心?"
二、项目结构
2.1 文件组织
女友对话助手涉及两个核心文件:
entry/src/main/ets/pages/
├── index4.ets # 主页面:聊天 UI + 交互逻辑
└── AIChatService.ets # AI 服务层:网络请求 + SSE 解析
这种分层架构将 UI 层(index4.ets)和网络层(AIChatService.ets)分离,职责清晰:
| 文件 | 职责 | 代码量 |
|---|---|---|
index4.ets |
UI 布局、状态管理、用户交互、Builder 组件 | 400 行 |
AIChatService.ets |
HTTP 请求、SSE 流式解析、非流式回退、取消逻辑 | 330 行 |
2.2 页面路由注册
在 main_pages.json 中注册新页面:
{
"src": [
"pages/Index",
"pages/index1",
"pages/index3",
"pages/index4"
]
}
三、AI 服务层设计(AIChatService.ets)
3.1 架构概览
AIChatService.ets 是整个应用的网络通信层,负责:
- 构建符合 OpenAI API 格式的请求体
- 通过
@kit.NetworkKit的http模块发起 POST 请求 - 解析 SSE(Server-Sent Events)流式响应,逐 token 回调
- 提供非流式回退机制作为兼容方案
- 支持请求取消
3.2 配置与数据类型
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
/** API 配置常量 */
const API_URL = 'https://api-ai.gitcode.com/v1/chat/completions';
const API_KEY = 'YOUR_API_KEY_HERE'; // 需要替换为真实的 API Key
关键类型定义:
/** 聊天消息结构 */
export interface ChatMessage {
role: string; // "system" | "user" | "assistant"
content: string;
}
/** 请求体结构 */
export interface ChatCompletionRequest {
model: string;
messages: ChatMessage[];
stream: boolean;
max_tokens: number;
temperature: number;
top_p: number;
frequency_penalty: number;
thinking_budget: number;
}
/** SSE 解析回调 */
export interface AICallbacks {
onData: (text: string) => void; // 收到新 token
onDone: () => void; // 流式完成
onError: (errMsg: string) => void; // 错误处理
}
核心设计决策:
ChatMessage接口完全兼容 OpenAI 的 message 格式,使得可以调用任何兼容的 APIAICallbacks采用回调模式而非 Promise,因为 SSE 流式需要多次回调stream: true开启流式响应,让 AI 回复逐字显示
3.3 系统提示词设计
系统提示词(System Prompt)是 AI 行为的核心控制手段:
const SYSTEM_PROMPT: string =
'你是一位资深的恋爱沟通顾问,擅长帮助男生分析女友话语背后的情感需求,' +
'并提供真诚、体贴、有温度的高情商回复建议。\n\n' +
'以下是你要遵循的原则:\n' +
'1. 先共情:理解女友当下的情绪(开心、难过、生气、焦虑等)。\n' +
'2. 再分析:指出她话语中隐含的需求(被关注、被理解、被重视、安全感等)。\n' +
'3. 给建议:提供 1~3 条具体可用的回复话术,并说明每条话术的适用场景。\n' +
'4. 语气温柔但真诚,避免油腻或过度讨好。\n' +
'5. 如果涉及矛盾冲突,优先建议冷静沟通而非道歉敷衍。\n\n' +
'请用中文回复,保持简洁实用。';
提示词设计的五大原则覆盖了:
- 情感分析(情绪 + 需求)
- 实用输出(具体话术 + 场景说明)
- 风格控制(温柔真诚,不油腻)
- 价值观引导(冷静沟通优于敷衍道歉)
3.4 SSE 流式解析
SSE(Server-Sent Events)是一种基于 HTTP 的流式传输协议,AI 模型逐 token 向客户端推送数据:
data: {"choices":[{"delta":{"content":"我"}}]}
data: {"choices":[{"delta":{"content":"理解"}}]}
data: {"choices":[{"delta":{"content":"你的"}}]}
data: [DONE]
单行解析函数:
function parseSSEDataLine(line: string): string | null {
// 移除 "data:" 前缀(5个字符)
const jsonStr = line.slice(5).trim();
if (!jsonStr) return null;
try {
const parsed = JSON.parse(jsonStr) as Record<string, Object>;
const choices = parsed.choices as Object[];
if (choices && choices.length > 0) {
const choice = choices[0] as Record<string, Object>;
// 兼容 delta(流式)和 message(非流式)两种格式
const delta = choice.delta as Record<string, Object>;
if (delta) return delta.content as string;
const message = choice.message as Record<string, Object>;
if (message) return message.content as string;
}
} catch (_) { /* JSON 解析失败,跳过 */ }
return null;
}
完整响应体解析(非流式回退):
function parseFullSSEBody(body: string): string {
let result = '';
const lines = body.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('data:')) {
if (trimmed === 'data:[DONE]') break;
const content = parseSSEDataLine(trimmed);
if (content) result += content;
}
}
return result;
}
function parseNonStreamingBody(body: string): string | null {
try {
const parsed = JSON.parse(body) as Record<string, Object>;
const choices = parsed.choices as Object[];
if (choices && choices.length > 0) {
const choice = choices[0] as Record<string, Object>;
const message = choice.message as Record<string, Object>;
if (message) return message.content as string;
}
} catch (_) { /* not JSON */ }
return null;
}
3.5 核心请求逻辑(带流式+回退)
queryAI 函数是整个服务层的核心,实现了三段式请求策略:
export function queryAI(
callbacks: AICallbacks,
messages: ChatMessage[],
): void {
// === 第一步:取消上一次未完成的请求 ===
if (httpRequestTask) {
httpRequestTask.destroy();
httpRequestTask = null;
}
const httpRequest = http.createHttp();
httpRequestTask = httpRequest;
// === 第二步:构建请求体 ===
const fullMessages: ChatMessage[] = [
{ role: 'system', content: SYSTEM_PROMPT },
...messages,
];
const requestBody: ChatCompletionRequest = {
model: 'deepseek-ai/DeepSeek-V3',
messages: fullMessages,
stream: true,
max_tokens: 2048,
temperature: 0.6,
top_p: 0.95,
};
let isDone = false;
let receivedAnyData = false;
let buffer = '';
// === 第三步:注册事件监听 ===
// 方式 A:dataReceive 事件 —— 流式数据推送
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
const text = arrayBufferToString(data);
buffer += text;
receivedAnyData = true;
// 按行拆解(SSE 以 \n 分隔)
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // 最后一行可能不完整
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) continue;
if (trimmed === 'data:[DONE]') {
if (!isDone) { isDone = true; callbacks.onDone(); }
continue;
}
const content = parseSSEDataLine(trimmed);
if (content) callbacks.onData(content);
}
});
// === 第四步:发起 POST 请求 ===
httpRequest.request(
API_URL,
{
method: http.RequestMethod.POST,
header: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
extraData: JSON.stringify(requestBody),
connectTimeout: 30000,
readTimeout: 120000,
},
(err, resp) => {
if (err) { /* 错误处理 */ return; }
if (resp.responseCode !== 200) { /* HTTP 错误处理 */ return; }
// 方式 B:非流式回退
if (!receivedAnyData && resp.result) {
const bodyStr = typeof resp.result === 'string'
? resp.result
: arrayBufferToString(resp.result as ArrayBuffer);
// 先尝试 SSE 格式解析
const sseContent = parseFullSSEBody(bodyStr);
if (sseContent) {
callbacks.onData(sseContent);
} else {
// 再尝试非流式 JSON 格式
const jsonContent = parseNonStreamingBody(bodyStr);
if (jsonContent) {
callbacks.onData(jsonContent);
}
}
if (!isDone) { isDone = true; callbacks.onDone(); }
}
},
);
}
为什么需要非流式回退?
HarmonyOS 不同版本的 http 模块对 SSE 的支持存在差异。在某些版本中,dataReceive 事件可能不会被触发。为了确保兼容性,当 receivedAnyData 标记为 false 时,我们从 request 回调的完整响应体中解析数据。
取消请求:
export function cancelAI(): void {
if (httpRequestTask) {
httpRequestTask.destroy();
httpRequestTask = null;
}
}
destroy() 方法会立即终止 HTTP 连接,适用于用户主动取消或超时场景。
3.6 ArrayBuffer 解码
function arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Arr = new Uint8Array(buffer);
let text = '';
for (let i = 0; i < uint8Arr.length; i++) {
text += String.fromCharCode(uint8Arr[i]);
}
return text;
}
http.on('dataReceive') 回调的数据类型是 ArrayBuffer,需要手动解码为字符串。这个函数逐字节转换,支持包括中文在内的所有 Unicode 字符。
四、主页面设计(index4.ets)
4.1 整体布局
主页面采用经典的三段式聊天 UI 布局:
┌──────────────────────────────────┐
│ 💕 女友对话助手 │
│ 高情商回复从这里开始 │ ← 标题栏(白色背景)
├──────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ 用户消息气泡(粉红色) │ → │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ ← AI回复气泡(白色背景) │ │ ← 聊天区(Scroll,可滚动)
│ └─────────────────────────┘ │
│ │
│ 💭 正在思考... │ ← 加载动画
│ │
│ ⚠️ 错误提示(红色) │ ← 错误提示
│ │
├──────────────────────────────────┤
│ ┌──────────────────┐ ┌────┐ │
│ │ 输入女友说的话... │ │发送│ │ ← 输入区(白色背景)
│ └──────────────────┘ └────┘ │
│ ⏹ 取消回复 │ ← 取消按钮(加载时显示)
└──────────────────────────────────┘
4.2 状态变量设计
@Component
struct Index {
@State messageList: ChatMessage[] = []; // 聊天历史
@State inputText: string = ''; // 输入框内容
@State isLoading: boolean = false; // AI 回复中
@State streamingContent: string = ''; // 流式实时内容
@State errorMsg: string = ''; // 错误提示
private sampleQuestions: string[] = [ // 示例问题列表
'女朋友说「我没事」的时候到底是什么意思?',
'女友生气了说「别管我」,该怎么回?',
'她问我「你前女友好看还是我好看」该怎么回答?',
'女友说「今天好累」怎么回复最暖心?',
'她说「你是不是不爱我了」该怎么回应?',
'女朋友突然不回消息了,我该怎么办?',
];
}
状态流转关系:
用户发送 → isLoading=true → 流式追加 streamingContent → 完成 → 加入 messageList → isLoading=false
↓
用户点取消 → cancelAI() → isLoading=false
↓
发生错误 → errorMsg 显示
4.3 build() 主布局
build() {
Column() {
// 顶部标题栏
this.buildHeader();
// 聊天消息区(弹性填充)
Column() { this.buildChatArea() }
.layoutWeight(1);
// 底部输入区
this.buildInputArea();
}
.width('100%')
.height('100%')
.backgroundColor('#FFF0E8') // 暖粉色背景
}
设计亮点:
- 暖粉色背景(
#FFF0E8)营造温馨感性的聊天氛围 layoutWeight(1)使聊天区占满输入栏以上的所有空间- 标题栏和输入区各自独立
@Builder,结构清晰
4.4 标题栏
@Builder
buildHeader() {
Column() {
Text('💕 女友对话助手')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#D4587A') // 主色调:玫瑰粉
.margin({ top: 12 })
Text('高情商回复从这里开始')
.fontSize(13)
.fontColor('#E8A0B0') // 副色调:浅粉
.margin({ top: 2, bottom: 4 })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.backgroundColor('#FFFFFF') // 白色背景
}
标题栏使用双层文字设计:主标题 + 副标语。颜色方案围绕主色 #D4587A(玫瑰粉)展开,配合副色 #E8A0B0(浅粉),形成柔和女性化的视觉感受。
4.5 聊天区
@Builder
buildChatArea() {
Scroll() {
Column() {
// 欢迎消息(消息列表为空时显示)
if (this.messageList.length === 0) {
this.buildWelcomeMessage();
}
// 历史消息气泡
ForEach(this.messageList, (msg: ChatMessage) => {
if (msg.role === 'user') {
this.buildUserBubble(msg.content);
} else {
this.buildAIBubble(msg.content);
}
}, (msg: ChatMessage) => msg.role + '#' + msg.content)
// 流式 AI 回复(实时追加)
if (this.isLoading && this.streamingContent) {
this.buildAIBubble(this.streamingContent + ' ▍');
}
// 加载中占位
if (this.isLoading && !this.streamingContent) {
this.buildLoadingIndicator();
}
// 错误提示
if (this.errorMsg) {
this.buildErrorBubble(this.errorMsg);
}
Blank().height(12) // 底部留白
}
.width('100%')
.padding(12)
}
.scrollBar(BarState.Auto)
}
消息列表渲染要点:
- 空状态 → 欢迎界面:当
messageList.length === 0时显示欢迎消息和示例按钮 - 历史消息 → 气泡遍历:使用
ForEach遍历所有历史消息 - 流式内容 → 实时追加:在
isLoading && streamingContent时显示正在累积的 AI 回复,尾部加▍闪烁光标 - 加载态 → 占位动画:
isLoading && !streamingContent表示等待第一个 token - 错误态 → 红色气泡:显示错误信息
4.6 欢迎界面
欢迎界面是用户打开应用后看到的第一个屏幕,包含示例场景按钮:
@Builder
buildWelcomeMessage() {
Column() {
Text('💬').fontSize(48).margin({ top: 24, bottom: 8 })
Text('还在为不知如何回复女友消息而发愁?')
.fontSize(15).fontColor('#C08A9A')
Text('把女友对你说的话贴进来,我帮你分析情绪,给出高情商回复建议 ❤️')
.fontSize(13).fontColor('#D4A5B3')
.padding({ left: 20, right: 20 })
Text('试试这些常见场景 👇')
.fontSize(13).fontColor('#E8A0B0')
.margin({ bottom: 8 })
// 示例问题按钮
ForEach(this.sampleQuestions, (q: string) => {
Button(q)
.fontSize(13).fontColor('#D4587A')
.backgroundColor('#FFF5F0')
.border({ width: 1, color: '#F5D0D8' })
.borderRadius(18).height(40).width('90%')
.margin({ bottom: 8 })
.onClick(() => { this.inputText = q; })
}, (_q: string, i?: number) => (i ?? 0).toString())
// 换一批按钮
Button('🎲 换一批场景')
.fontSize(12).fontColor('#E8A0B0')
.backgroundColor('transparent')
.border({ width: 1, color: '#F0D0D8' })
.borderRadius(16).height(34)
.margin({ top: 4, bottom: 16 })
.onClick(() => { this.rotateSamples(); })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
示例问题轮换算法:
rotateSamples(): void {
const arr: string[] = this.sampleQuestions;
// 确定性洗牌(伪随机,每次结果不同但可重现)
for (let i: number = 0; i < arr.length; i++) {
const j: number = (i * 7 + 3) % arr.length;
const tmp: string = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
this.sampleQuestions = [...arr]; // 触发 @State 更新
}
使用线性同余法(LCG)实现伪随机排列,不需要额外的随机数 API。每次调用 rotateSamples 都会产生不同的排列顺序。
4.7 消息气泡设计
用户气泡(右对齐,粉红色):
@Builder
buildUserBubble(content: string) {
Column() {
Row() {
Blank() // 左占位,推动气泡靠右
Text(content)
.fontSize(15).fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor('#D4587A') // 主色粉
.borderRadius({
topLeft: 18, topRight: 4, // 右上角小圆角("尾巴"效果)
bottomLeft: 18, bottomRight: 18
})
.maxLines(20).lineHeight(22)
}
.width('100%')
.margin({ top: 6 })
}
}
AI 气泡(左对齐,白色):
@Builder
buildAIBubble(content: string) {
Column() {
Row() {
Text(content)
.fontSize(15).fontColor('#3D2B1F') // 深棕色文字
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#F5E0E5' })
.borderRadius({
topLeft: 4, topRight: 18, // 左上角小圆角("尾巴"效果)
bottomLeft: 18, bottomRight: 18
})
.lineHeight(22)
Blank() // 右占位,推动气泡靠左
}
.width('100%')
.margin({ top: 6 })
}
}
气泡设计哲学:
用户气泡和 AI 气泡在视觉上形成镜像对称:
| 特征 | 用户气泡 | AI 气泡 |
|---|---|---|
| 对齐 | 右对齐 | 左对齐 |
| 背景色 | #D4587A(粉) |
#FFFFFF(白) |
| 文字色 | #FFFFFF |
#3D2B1F(深棕) |
| 尖角位置 | 右上角 | 左上角 |
| 边框 | 无 | #F5E0E5 精致边框 |
这种对话式 UI的经典设计,让用户能一目了然地分辨消息的发送方。
4.8 加载与错误状态
加载动画:
@Builder
buildLoadingIndicator() {
Column() {
Row() {
Text('💭 正在思考...')
.fontSize(14).fontColor('#E8A0B0')
.padding(16)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#F5E0E5' })
.borderRadius({ topLeft: 4, topRight: 18, bottomLeft: 18, bottomRight: 18 })
Blank()
}
.width('100%')
.margin({ top: 6 })
}
}
错误提示:
@Builder
buildErrorBubble(errMsg: string) {
Column() {
Row() {
Text('⚠️ ' + errMsg)
.fontSize(13).fontColor('#C94A4A') // 红色文字
.padding(12)
.backgroundColor('#FFF0F0') // 浅红背景
.border({ width: 1, color: '#F5C0C0' })
.borderRadius(12)
Blank()
}
.width('100%')
.margin({ top: 6 })
}
}
4.9 输入区
@Builder
buildInputArea() {
Column() {
Row() {
// 文本输入框
TextInput({
placeholder: '输入女友说的话...',
text: this.inputText
})
.layoutWeight(1)
.height(44).fontSize(15)
.fontColor('#3D2B1F')
.placeholderColor('#D4C5B0')
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#F0D0D8' })
.borderRadius(22) // 胶囊圆角
.padding({ left: 16 })
.enabled(!this.isLoading)
.onChange((val: string) => { this.inputText = val; })
.onSubmit(() => { this.handleSend(); })
Blank().width(8)
// 发送按钮
Button() {
Text('发送').fontSize(14).fontColor('#FFFFFF')
}
.width(60).height(44)
.backgroundColor(this.isLoading ? '#E8C0C8' : '#D4587A')
.borderRadius(22)
.enabled(!this.isLoading && this.inputText.trim().length > 0)
.onClick(() => { this.handleSend(); })
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor('#FFFFFF')
.border({ width: { top: 1 }, color: '#F0D0D8' })
// 取消按钮(仅加载时显示)
if (this.isLoading) {
Button('⏹ 取消回复')
.fontSize(13).fontColor('#B0A595')
.backgroundColor('#F5F0E8')
.border({ width: 1, color: '#E8DDD0' })
.borderRadius(16).height(32).width(120)
.margin({ bottom: 6 })
.onClick(() => { this.handleCancel(); })
}
}
.width('100%')
}
交互细节:
- 发送按钮颜色随状态变化:加载时变为
#E8C0C8(浅粉灰),正常时为#D4587A(主色) - 双重发送触发:点击按钮 + 键盘回车(
onSubmit)均可发送 - 输入为空禁用:
enabled条件包含inputText.trim().length > 0,防止发送空白消息 - 加载时禁用输入:
enabled(!this.isLoading)防止用户在 AI 回复时输入新消息 - 取消按钮条件渲染:仅
isLoading为 true 时显示
五、核心交互逻辑
5.1 发送消息(handleSend)
handleSend(): void {
const msg: string = this.inputText.trim();
if (!msg) return;
// 清空输入
this.inputText = '';
// 添加用户消息到历史
const userMsg: ChatMessage = { role: 'user', content: msg };
this.messageList = [...this.messageList, userMsg];
// 重置状态
this.streamingContent = '';
this.errorMsg = '';
this.isLoading = true;
// 调用 AI(携带完整聊天历史)
queryAI({
onData: (text: string) => {
this.streamingContent += text; // 流式追加
},
onDone: () => {
if (this.streamingContent) {
const aiMsg: ChatMessage = {
role: 'assistant',
content: this.streamingContent
};
this.messageList = [...this.messageList, aiMsg];
}
this.streamingContent = '';
this.isLoading = false;
},
onError: (errMsg: string) => {
this.errorMsg = errMsg;
this.isLoading = false;
},
}, this.messageList);
}
数据流时序图:
用户点击发送
│
├──→ 1. inputText 清空
├──→ 2. messageList 追加用户消息
├──→ 3. isLoading = true
├──→ 4. streamingContent = ''
│
├──→ queryAI() 发起网络请求
│ │
│ ├──→ onData(text): streamingContent += text (多次触发)
│ │
│ ├──→ onDone():
│ │ ├──→ messageList 追加 AI 回复
│ │ ├──→ streamingContent = ''
│ │ └──→ isLoading = false
│ │
│ └──→ onError(errMsg):
│ ├──→ errorMsg = errMsg
│ └──→ isLoading = false
│
└──→ UI 自动刷新(@State 驱动)
为什么将流式内容复制到 messageList?
streamingContent 是一个临时变量,用于实时显示 AI 正在输出的文字。当 onDone 回调触发时,我们将完整的 streamingContent 作为一个 ChatMessage 加入 messageList。这样做的原因是:
messageList是消息历史,@State 数组采用不可变更新([...list, newMsg])- 如果逐字追加到
messageList,每次onData都会创建新数组,性能开销大 - 使用
streamingContent(字符串)做临时累积,只有最终完成时才更新数组
5.2 取消请求(handleCancel)
handleCancel(): void {
cancelAI(); // 终止 HTTP 请求
this.isLoading = false;
// 保留已收到的流式内容
if (this.streamingContent) {
const aiMsg: ChatMessage = {
role: 'assistant',
content: this.streamingContent
};
this.messageList = [...this.messageList, aiMsg];
}
this.streamingContent = '';
}
取消操作的设计原则是保留部分结果而非完全丢弃。如果 AI 已经输出了部分回复,取消后这部分内容仍然作为完整的 AI 回复加入聊天记录。这比完全丢弃给用户更好的体验。
六、网络层与 UI 层的协奏
6.1 模块导出与导入
// AIChatService.ets — 导出
export { queryAI, cancelAI, ChatMessage };
// index4.ets — 导入
import { queryAI, cancelAI, ChatMessage } from './AIChatService';
ArkTS 使用 ES Module 规范的 import/export 语法。同一个模块内的文件使用相对路径引用。
6.2 异步模式的演进
在 ArkTS 中处理网络请求有三种常见模式:
| 模式 | 适用场景 | 本例使用 |
|---|---|---|
| Callback 回调 | 一次性事件(如 request 完成) | request() 的 err 回调 |
| Event 事件监听 | 多次触发(如 SSE 流式) | on('dataReceive') |
| Promise async/await | 串联异步操作 | 暂未使用 |
queryAI 函数采用了事件 + 回调的混合模式:
- 流式数据通过
httpRequest.on('dataReceive', callback)事件监听 - 完成/错误通过
AICallbacks接口中的回调通知调用方 - 取消通过独立的
cancelAI()函数
七、主题与样式系统
7.1 色彩体系
女友对话助手采用了暖粉色调的色彩方案:
| 色值 | 用途 | 样例 |
|---|---|---|
#FFF0E8 |
页面背景 | 暖白粉底色 |
#D4587A |
主色(标题、用户气泡、发送按钮) | 玫瑰粉 |
#E8A0B0 |
副色(标语、占位文字) | 浅粉 |
#C08A9A |
欢迎文字 | 中粉 |
#D4A5B3 |
欢迎说明 | 柔和粉 |
#FFF5F0 |
示例按钮背景 | 极浅粉 |
#F5D0D8 |
示例按钮边框 | 粉边框 |
#FFFFFF |
AI 气泡背景、标题栏、输入区背景 | 白色 |
#3D2B1F |
AI 气泡文字 | 深棕色 |
#C94A4A |
错误提示文字 | 红色 |
#FFF0F0 |
错误提示背景 | 浅红色 |
#D4C5B0 |
输入框占位文字 | 米色 |
7.2 字体与圆角系统
// 字号规范
标题 → fontSize: 22
主内容 → fontSize: 15
次要内容 → fontSize: 13 ~ 14
// 圆角规范
按钮/气泡 → borderRadius: 18 ~ 22
输入框 → borderRadius: 22(胶囊形)
示例按钮 → borderRadius: 18
小型按钮 → borderRadius: 16
// 线条
边框宽度 → 1px
边框颜色 → '#F0D0D8' / '#F5D0D8'
分割线宽度 → 1px
7.3 消息气泡的差异圆角
// 用户气泡(尖角在右上)
.borderRadius({
topLeft: 18, topRight: 4, // 右上角几乎直角
bottomLeft: 18, bottomRight: 18
})
// AI 气泡(尖角在左上)
.borderRadius({
topLeft: 4, topRight: 18, // 左上角几乎直角
bottomLeft: 18, bottomRight: 18
})
这种差异圆角(asymmetric rounded corners)是 IM 类应用气泡 UI 的标志性设计:指向对侧的角几乎为直角,形成"对话尾巴"的视觉效果。
八、构建验证与调试
8.1 编译检查
hvigorw assembleHap --daemon=false --analyze=false
构建输出示例:
Finished :entry:default@CompileResource...
Finished :entry:default@CompileArkTS... after 1 s 920 ms
Finished :entry:default@PackageHap... after 589 ms
BUILD SUCCESSFUL in 4 s 200 ms
8.2 常见编译问题
问题 1:网络模块未导入
// ❌ 错误:找不到 http
import { http } from '@kit.NetworkKit';
// ✅ 正确:@kit.NetworkKit 是系统内置 Kit,无需额外安装
问题 2:ArrayBuffer 解码性能
arrayBufferToString 中使用逐字节 String.fromCharCode,在小数据量下性能可接受。对于超大响应体,可以考虑使用 TextDecoder API(如果在目标 SDK 版本中可用)。
问题 3:API Key 泄露
当前 API Key 硬编码在源码中,这是一个安全风险。生产环境中应该:
- 将 API Key 存储在安全配置中
- 通过后端服务代理转发,避免前端直接持有
- 使用 HarmonyOS 的安全存储 API(如
@kit.UniversalKeystoreKit)加密存储
九、扩展方向
基于当前架构,可以从以下方向扩展:
9.1 语音消息支持
集成语音识别(ASR)API,支持用户通过语音输入问题。HarmonyOS NEXT 提供了 @kit.VoiceRecognitionKit 可以实现语音转文字。
9.2 多种 AI 角色切换
当前应用专注于"女友对话助手",可以扩展为多角色平台:
┌─────────────────────────┐
│ 选择 AI 角色 │
│ │
│ 💕 女友对话助手 │
│ 👔 职场沟通导师 │
│ 📚 学习辅导老师 │
│ 🏠 家庭关系顾问 │
│ 🧘 心理咨询师 │
└─────────────────────────┘
每个角色对应不同的系统提示词,切换时替换 SYSTEM_PROMPT 即可。
9.3 会话历史持久化
使用 HarmonyOS 的 Preferences 或关系型数据库(RDB)存储聊天记录,支持用户关闭应用后恢复对话。
9.4 情感分析可视化
在 AI 回复的同时,增加情感分析的可视化展示——用表情符号或百分比显示识别到的情绪维度(开心、难过、生气、焦虑等)。
9.5 消息搜索与收藏
支持用户搜索聊天历史中的关键词,以及收藏有用的回复建议。
十、总结
本文通过一个完整的 AI 对话助手的实现,系统地展示了在 HarmonyOS NEXT 上开发 AI 聊天应用的完整流程。从 AIChatService 网络层的 SSE 流式解析、非流式回退、请求取消,到 index4 UI 层的聊天气泡、流式文字展示、状态管理,涵盖了 ArkTS 开发中的多个关键技术点。
核心知识点回顾
| 技术点 | 应用位置 | 关键代码 |
|---|---|---|
| SSE 流式解析 | AIChatService.ts |
parseSSEDataLine() + on('dataReceive') |
| 非流式回退 | AIChatService.ts |
parseFullSSEBody() + parseNonStreamingBody() |
| HTTP 请求取消 | AIChatService.ts |
httpRequest.destroy() |
| @State 数组更新 | index4.ets |
[...this.messageList, newMsg] |
| 流式文字累积 | index4.ets |
this.streamingContent += text |
| 差异圆角气泡 | index4.ets |
borderRadius({ topLeft, topRight, ... }) |
| 条件渲染 | index4.ets |
if (this.isLoading) { ... } |
| FlexWrap 布局 | index4.ets |
Flex({ wrap: FlexWrap.Wrap }) |
ArkTS 开发 AI 应用的优势
- 原生网络支持:
@kit.NetworkKit提供了完整的 HTTP 客户端能力,包括事件驱动的流式数据接收 - 声明式 UI 与流式数据天然契合:
@State+streamingContent的组合让 AI 逐字展示变得非常简单 - 类型安全:ArkTS 的严格类型检查减少了 API 调用中的数据格式错误
- 丰富的 UI 组件:
TextInput、Button、Scroll、Flex等组件开箱即用
HarmonyOS NEXT 正在逐步完善其 AI 能力生态。随着更多 AI Kit 的开放,未来在鸿蒙原生应用中集成 AI 功能将变得更加便捷。希望本文能为你在 HarmonyOS NEXT 上开发 AI 应用提供有价值的参考和启发。
更多推荐



所有评论(0)