鸿蒙原生 ArkTS AI 对话应用实战:从零构建「AI 万能手册」

一、引言

随着大语言模型(LLM)技术的快速发展,将 AI 对话能力集成到原生应用中已成为移动开发的刚需。从智能客服到个人助理,从学习辅导到情感陪伴,AI 对话正在改变用户与应用的交互方式。

本文将以一个完整的「AI 万能手册」应用为依托,详细讲解如何在 HarmonyOS NEXT(API 24)上使用 ArkTS 原生框架构建一个支持流式响应的 AI 对话应用。内容涵盖:

  • 项目架构设计与页面导航
  • 原生 HTTP 网络请求(@kit.NetworkKit)
  • SSE(Server-Sent Events)流式响应解析
  • 声明式 UI 的聊天气泡组件设计
  • 系统提示词(System Prompt)工程
  • 状态管理与流式文本拼接
  • 编译错误排查与 ArkTS 语法避坑

二、项目架构与入口

2.1 应用入口 EntryAbility

鸿蒙应用的入口是 UIAbility 的子类。每个应用有且只有一个 EntryAbility,它负责应用的初始化、生命周期管理和页面加载。

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'App', 'Ability onCreate');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(0x0000, 'App', 'Ability onWindowStageCreate');
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'App',
          'Failed to load content: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(0x0000, 'App', 'Succeeded in loading content.');
    });
  }

  onForeground(): void { /* 应用进入前台 */ }
  onBackground(): void { /* 应用进入后台 */ }
}

关键点:

  • loadContent('pages/Index') 加载 Index.ets 页面。路径不含 .ets 后缀。
  • hilog 是鸿蒙的日志工具,来自 @kit.PerformanceAnalysisKit。
  • 生命周期 onCreate → onWindowStageCreate → onForeground → onBackground 的顺序是固定的。

2.2 项目文件结构

复制代码
entry/src/main/ets/
├── entryability/
│   └── EntryAbility.ets           # 应用入口
└── pages/
    ├── Index.ets                   # AI 聊天主界面
    ├── AIChatService.ets           # AI 服务层(网络请求 + SSE 解析)
    ├── RunnerPage.ets
    └── TabsBottomDemo.ets

2.3 应用架构分层

应用采用经典的三层架构:

复制代码
Index.ets(UI 层)
    ↓ 调用 queryAI() / cancelAI()
AIChatService.ets(服务层)
    ↓ @kit.NetworkKit HTTP 请求
GitCode AI API(AI 模型层)

这种分层的好处是:

  • UI 层只关心渲染和用户交互,不关心网络细节
  • 服务层封装所有网络和解析逻辑,可独立测试和复用
  • AI 模型层可随时切换不同的 API 供应商

三、AI 服务层:AIChatService.ets

服务层是整个应用的「发动机」。它负责将用户的聊天消息通过 HTTP POST 请求发送到 AI API,并通过 SSE(Server-Sent Events)流式接收和解析 AI 的回复。

3.1 数据模型定义

首先定义聊天的核心数据结构:

typescript 复制代码
/** 聊天消息结构体 */
export interface ChatMessage {
  role: string;      // 'system' | 'user' | 'assistant'
  content: string;   // 消息文本内容
}

/** 请求体结构体 */
export interface ChatCompletionRequest {
  model: string;                          // 模型名称
  messages: ChatMessage[];                // 消息历史
  stream: boolean;                        // 是否流式
  max_tokens: number;                     // 最大生成长度
  temperature: number;                    // 温度参数(0~2)
  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 的 role 字段遵循 OpenAI API 规范:system(系统提示词)、user(用户)、assistant(AI)
  • AICallbacks 使用回调模式而不是 Promise,因为 SSE 是流式的------数据会分多次到达,Promise 只能 resolve 一次
  • stream: true 是关键标记,告诉 API 以 SSE 格式返回响应

3.2 系统提示词工程

系统提示词(System Prompt)是决定 AI 行为和回答质量的关键。一个好的系统提示词应该包含:

  1. 角色定义:AI 是谁?
  2. 能力范围:能解决什么问题?
  3. 回答结构:如何组织回答?
  4. 风格约束:语气、长度、格式等
typescript 复制代码
const SYSTEM_PROMPT: string =
  '你是一位名叫「AI 万能手册」的智能生活助手,擅长解决用户生活中遇到的各种问题。' +
  '你的回答应遵循以下原则:\n\n' +
  '1. 领域覆盖:涵盖但不限于以下生活场景------\n' +
  '   · 情感关系:恋爱沟通、家庭矛盾、友谊维护\n' +
  '   · 职场工作:职业规划、沟通技巧、压力管理\n' +
  '   · 学习成长:学习方法、时间管理、技能提升\n' +
  '   · 健康生活:饮食营养、运动健身、睡眠调节\n' +
  '   · 日常生活:家务技巧、旅行攻略、消费决策\n' +
  '   · 心理情绪:焦虑缓解、情绪管理、自我调节\n' +
  '   · 社交人际:社交技巧、聚会礼仪、沟通话术\n' +
  '   · 法律常识:消费维权、租房合同、劳动权益(仅作参考,建议咨询专业律师)\n\n' +
  '2. 回答结构:每个回答包含------\n' +
  '   · 共情理解:先理解用户的处境和情绪\n' +
  '   · 问题分析:简要分析问题的关键点\n' +
  '   · 实用建议:提供 1~3 条具体可执行的建议\n' +
  '   · 温馨提示:如有需要,补充注意事项\n\n' +
  '3. 回答风格:\n' +
  '   · 语气温和亲切,像一位有经验的朋友在聊天\n' +
  '   · 用词通俗易懂,避免过于专业的术语\n' +
  '   · 建议具体可行,不空泛不鸡汤\n' +
  '   · 涉及法律/医疗等专业领域时,务必注明「建议咨询专业人士」\n\n' +
  '4. 格式要求:\n' +
  '   · 使用中文回答\n' +
  '   · 适当使用 emoji 让回答更生动\n' +
  '   · 分点排列,方便阅读\n' +
  '   · 保持回答简洁,一般不超过 500 字\n\n' +
  '请记住:你的使命是用智慧和温度,帮助用户解决生活中的每一个问题。';

提示词工程要点:

  1. 角色明确:定义 AI 的名字和使命,让回答有「人格」
  2. 范围清晰:明确列出 8 大生活领域,避免 AI 回答跑题
  3. 结构约束:要求「共情→分析→建议→提示」的四段式结构
  4. 风格约束:温和亲切、通俗易懂、具体可行
  5. 安全边界:法律/医疗领域必须标注「建议咨询专业人士」
  6. 格式约束:中文 + emoji + 分点 + 500 字上限

3.3 SSE 流式响应解析

SSE(Server-Sent Events)是一种服务器推送协议,允许服务器向客户端持续发送数据流。AI 对话中的「打字机效果」就是通过 SSE 实现的。

SSE 数据格式如下:

复制代码
data: {"choices":[{"delta":{"content":"你好"}}]}

data: {"choices":[{"delta":{"content":",我"}}]}

data: {"choices":[{"delta":{"content":"是AI"}}]}

data: [DONE]

每条消息以 data: 开头,以 \n\n 结尾。[DONE] 标记流结束。

typescript 复制代码
/**
 * 解析 SSE data 行,提取 content 增量
 */
function parseSSEDataLine(line: string): string | null {
  const jsonStr = line.slice(5).trim();      // 去掉 "data:" 前缀
  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;
}

解析逻辑:

  1. 去掉 data: 前缀(前 5 个字符)
  2. 将剩余部分解析为 JSON
  3. 提取 choices[0].delta.content(流式格式)
  4. 如果 delta 不存在,尝试 message.content(非流式回退)

3.4 非流式回退机制

并非所有的 HarmonyOS 设备都能正确触发 dataReceive 事件。为了应对这种情况,我们设计了「非流式回退」机制:

typescript 复制代码
// ---- 非流式回退 ----
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);
    } else {
      callbacks.onError(`无法解析响应: ${preview}`);
    }
  }
}

回退优先级:

  1. 流式 dataReceive(最佳体验,逐字显示)
  2. SSE 格式解析(dataReceive 未触发时,从完整响应体中解析 SSE)
  3. 非流式 JSON 解析(API 返回完整 JSON)
  4. 错误提示(以上全部失败)

3.5 完整请求流程

typescript 复制代码
export function queryAI(
  callbacks: AICallbacks,
  messages: ChatMessage[],
): void {
  // 1. 取消上一次未完成的请求
  if (httpRequestTask) {
    httpRequestTask.destroy();
    httpRequestTask = null;
  }

  // 2. 创建 HTTP 请求
  const httpRequest = http.createHttp();
  httpRequestTask = httpRequest;

  // 3. 构建完整消息列表(系统提示词 + 对话历史)
  const fullMessages: ChatMessage[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    ...messages,
  ];

  // 4. 构建请求体
  const requestBody: ChatCompletionRequest = {
    model: 'deepseek-ai/DeepSeek-V3',
    messages: fullMessages,
    stream: true,
    max_tokens: 2048,
    temperature: 0.6,
  };

  // 5. 注册 SSE 监听
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
    // 逐块接收 SSE 数据
  });

  httpRequest.on('dataEnd', () => {
    // 数据接收完毕
  });

  // 6. 发起 POST 请求
  httpRequest.request(API_URL, {
    method: http.RequestMethod.POST,
    header: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      Accept: 'text/event-stream',   // ← 关键:要求 SSE 流式响应
    },
    extraData: JSON.stringify(requestBody),
    connectTimeout: 30000,
    readTimeout: 120000,
  }, callback);
}

四、UI 层:Index.ets 聊天界面

UI 层负责渲染聊天界面和处理用户交互。主要包含三个区域:顶部标题栏、消息列表区、底部输入区。

4.1 聊天气泡组件 ChatBubble

聊天气泡是聊天界面的核心视觉元素。我们需要区分用户消息和 AI 消息:

typescript 复制代码
@Component
struct ChatBubble {
  private msg: ChatMessage = { role: 'user', content: '' };
  private isLoading: boolean = false;

  build() {
    Column() {
      if (this.msg.role === 'user') {
        // ── 用户消息:蓝色气泡,右对齐 ──
        Row() {
          Blank()                          // 左侧弹簧,将气泡推到右侧
          Column() {
            Text(this.msg.content)
              .fontSize(15).fontColor('#ffffff')
          }
          .padding(14)
          .backgroundColor('#2d5f8a')      // 蓝色气泡
          .borderRadius({
            topLeft: 16, topRight: 4,       // 右上角小圆角 → "尾巴"效果
            bottomLeft: 16, bottomRight: 16
          })
          .constraintSize({ maxWidth: '80%' })
        }
        .width('100%')
      } else {
        // ── AI 消息:灰色气泡 + 🤖 头像,左对齐 ──
        Row() {
          // AI 头像
          Column() { Text('🤖').fontSize(20) }
            .width(36).height(36)
            .backgroundColor('#eef2f7').borderRadius(18)
            .margin({ right: 8 })

          // 气泡内容
          Column() {
            if (this.isLoading && !this.msg.content) {
              // ★ 正在输入动画:三个小圆点依次闪烁 ★
              Row() {
                ForEach([0, 1, 2], (idx: number) => {
                  Circle()
                    .width(6).height(6).fill('#2d5f8a')
                    .opacity(0.4 + idx * 0.3)     // 透明度依次递增
                    .margin({ left: 2, right: 2 })
                })
              }
              .padding(14).backgroundColor('#f0f4f8').borderRadius(16)
            } else {
              Text(this.msg.content)
                .fontSize(15).fontColor('#1a1a2e')
            }
          }
          .padding(14)
          .backgroundColor('#f0f4f8')      // 灰色气泡
          .borderRadius({
            topLeft: 4, topRight: 16,       // 左上角小圆角 → "尾巴"效果
            bottomLeft: 16, bottomRight: 16
          })
          .constraintSize({ maxWidth: '70%' })

          Blank()                          // 右侧弹簧
        }
        .width('100%')
      }
    }
    .margin({ bottom: 12 })
  }
}

设计要点:

  • 用户气泡右对齐 :使用 Blank() 占据左侧空间,将气泡推到右侧
  • AI 气泡左对齐 :左侧放头像,右侧放气泡,使用 Blank() 占据右侧空间
  • 圆角方向差异:用户气泡的右上角小圆角(4px),AI 气泡的左上角小圆角(4px),模拟聊天气泡的「尾巴」
  • 加载动画 :isLoading 为 true 且内容为空时,显示三个渐变透明度的圆点

4.2 状态管理

页面级状态使用 @State 装饰器管理:

typescript 复制代码
@Entry
@Component
struct AIUniversalManualPage {
  @State messages: ChatMessage[] = [];       // 聊天消息列表
  @State inputText: string = '';             // 输入框文本
  @State isLoading: boolean = false;          // 是否正在请求 AI
  @State currentAIResponse: string = '';      // 流式拼接中的 AI 回复
}

@State 的作用域规则:

  • @State 变量变化时,所在组件的 build() 方法会重新执行
  • 只有被 @State 装饰的变量才能触发 UI 更新
  • 普通成员变量变化不会触发重渲染

4.3 发送消息流程

typescript 复制代码
sendMessage(text: string): void {
  if (this.isLoading || !text.trim()) return;

  // 1. 添加用户消息到列表
  const userMsg: ChatMessage = { role: 'user', content: text.trim() };
  this.messages.push(userMsg);
  this.inputText = '';           // 清空输入框
  this.isLoading = true;
  this.currentAIResponse = '';   // 清空之前的回复

  // 2. 构建对话历史(保留最近 20 条)
  const history: ChatMessage[] = this.messages
    .filter(m => m.content !== '')
    .slice(-20);

  // 3. 调用 AI 服务
  queryAI(
    {
      onData: (content: string) => {
        // 流式拼接:每收到一个 token 就追加到 currentAIResponse
        this.currentAIResponse += content;
      },
      onDone: () => {
        // 流式结束:将完整回复加入消息列表
        if (this.currentAIResponse) {
          this.messages.push({
            role: 'assistant',
            content: this.currentAIResponse,
          });
        }
        this.currentAIResponse = '';
        this.isLoading = false;
      },
      onError: (errMsg: string) => {
        // 错误处理:显示友好错误信息
        this.messages.push({
          role: 'assistant',
          content: `😅 抱歉,我遇到了问题:${errMsg}`,
        });
        this.isLoading = false;
      },
    },
    history,
  );
}

关键流程:

  1. 用户消息立即加入 messages 列表 → 气泡立即显示
  2. isLoading = true → 显示「正在输入」动画
  3. onData 每次被调用时,currentAIResponse 更新 → UI 中流式显示文字
  4. onDone 被调用时,将完整回复加入 messages → 固化为静态气泡
  5. onError 时显示错误信息

4.4 空白引导页

当用户首次打开应用时,显示引导页而不是空列表:

typescript 复制代码
if (this.messages.length === 0) {
  Column() {
    Text('🤖').fontSize(56)
    Text('AI 万能手册').fontSize(22).fontWeight(FontWeight.Bold)
    Text('解决你生活中的各种问题').fontSize(14).fontColor('#666')

    // 6 大能力卡片
    Row() {
      ForEach(features, (item: FeatureItem) => {
        Column() {
          Text(item.icon).fontSize(24)
          Text(item.text).fontSize(11).fontColor('#555')
        }
        .width(72).height(72)
        .backgroundColor('#f5f7fa').borderRadius(12)
      })
    }
    .justifyContent(FlexAlign.Center)

    // 4 个快捷问题按钮
    Row() {
      ForEach(QUICK_QUESTIONS, (q: string) => {
        Text(q).padding(12)
          .backgroundColor('#eef2f7').borderRadius(16)
          .onClick(() => { this.sendMessage(q); })
      })
    }
  }
}

五、编译错误排查与 ArkTS 语法避坑

在开发过程中,我们遇到并修复了多个编译错误。以下是最常见的 7 类错误及其解决方案。

5.1 类型推断错误

错误信息:

复制代码
Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)

原因 :ArkTS 默认禁止 any 类型,要求显式类型注解。

修复:

typescript 复制代码
// ❌ 错误:隐式 any
ForEach(data, (item) => { ... })

// ✅ 正确:显式类型
ForEach(data, (item: FeatureItem) => { ... })

// 或使用 as 断言
ForEach([
  { icon: '💕', text: '情感关系' },
] as FeatureItem[], (item: FeatureItem) => { ... })

5.2 属性不存在

错误信息:

复制代码
Property 'maxWidth' does not exist on type 'ColumnAttribute'

原因:ArkTS 组件的属性名可能与预期不同。

修复:

typescript 复制代码
// ❌ 错误:maxWidth 不存在
Column() { }
  .maxWidth('80%')

// ✅ 正确:使用 constraintSize
Column() { }
  .constraintSize({ maxWidth: '80%' })

5.3 Flex 容器属性差异

错误信息:

复制代码
Property 'flexDirection' does not exist on type 'FlexAttribute'. Did you mean 'direction'?
Property 'flexWrap' does not exist on type 'FlexAttribute'
Property 'justifyContent' does not exist on type 'FlexAttribute'

原因 :在 HarmonyOS NEXT API 24 中,Flex 组件的属性集非常有限。justifyContent、flexDirection 等属性仅在 Row 和 Column 上可用。

修复:

typescript 复制代码
// ❌ 错误:Flex 没有 justifyContent
Flex() { }
  .justifyContent(FlexAlign.Center)

// ✅ 正确:使用 Row 替代
Row() { }
  .justifyContent(FlexAlign.Center)

总结 :ArkUI 中 Flex、Row、Column 的关系:

  • Row = 弹性水平容器(支持 justifyContent、alignItems、layoutWeight)
  • Column = 弹性垂直容器(支持 justifyContent、alignItems、layoutWeight)
  • Flex = 底层弹性容器(API 24 中属性有限,大部分场景用 Row/Column 替代)

5.4 overlay 回调格式

错误信息:

复制代码
Argument of type '{ builder: () => TextAttribute; }' is not assignable to parameter of type 'string | CustomBuilder | ComponentContent<Object>'

原因 :overlay() 方法接受一个 CustomBuilder 函数,而不是一个包含 builder 属性的对象。

修复:

typescript 复制代码
// ❌ 错误:对象格式
Circle()
  .overlay({
    builder: () => Text('↑').fontColor('#ffffff')
  })

// ✅ 正确:直接传函数
Circle()
  .overlay((): void => {
    Text('↑').fontSize(20).fontColor('#ffffff')
  })

5.5 私有属性初始化

警告信息:

复制代码
Property 'msg' is private and can not be initialized through the component constructor

原因 :在 ArkTS 中,组件的 private 属性不能通过构造函数参数初始化。

修复 :这只是一个警告,不影响运行。可以将 private 改为 public 或移除访问修饰符:

typescript 复制代码
// 有警告:private 属性不能通过构造函数传值
@Component
struct ChatBubble {
  private msg: ChatMessage = { role: 'user', content: '' };
}

// 无警告:移除 private
@Component
struct ChatBubble {
  msg: ChatMessage = { role: 'user', content: '' };
}

5.6 文本字符区分

问题描述:发送按钮显示汉字「上」而非箭头符号「↑」。

原因 :在代码中写了 Text("上") 而不是 Text('↑')。

修复:

typescript 复制代码
// ❌ 显示汉字
Text("上").fontSize(20)

// ✅ 显示箭头
Text('↑').fontSize(20)

Unicode 箭头符号需要用单引号包裹,"上" 是汉字,'↑' 是箭头符号。

5.7 权限配置

警告信息:

复制代码
To use this API, you need to apply for the permissions: ohos.permission.INTERNET

原因 :网络请求需要 ohos.permission.INTERNET 权限。

修复 :在 module.json5 中添加:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

六、ArkTS 组件设计模式

6.1 @Builder 自定义构建器

@Builder 是 ArkTS 中复用 UI 片段的核心机制。它类似于一个不返回值的函数,可以在 build() 方法中直接被调用:

typescript 复制代码
@Component
struct MyComponent {
  @State count: number = 0;

  build() {
    Column() {
      this.MyButton('点击', '#2d5f8a')
    }
  }

  @Builder
  MyButton(label: string, color: string) {
    Button(label)
      .backgroundColor(color)
      .fontColor('#ffffff')
      .onClick(() => { this.count++ })
  }
}

在底部 Tabs 导航中,我们大量使用 @Builder 来自定义 Tab 样式:

typescript 复制代码
.tabBar(this.BottomTabBuilder(idx, tab))

@Builder
BottomTabBuilder(index: number, tab: TabData) {
  Column() {
    Stack() {
      Text(tab.icon).fontSize(22)
      if (tab.badge > 0) {
        Text(`${tab.badge}`)
          .backgroundColor('#e74c3c').borderRadius(8)
      }
    }
    Text(tab.label).fontSize(10)
  }
}

6.2 @BuilderParam 插槽模式

@BuilderParam 是 ArkTS 的「插槽」机制,允许父组件向子组件注入 UI 片段:

typescript 复制代码
@Component
struct SectionCard {
  @BuilderParam content?: () => void;

  build() {
    Column() {
      // ... 标题和描述
      if (this.content) {
        this.content()     // ← 渲染注入的内容
      }
      // ... 代码块
    }
  }
}

// 使用
SectionCard({
  content: MyCustomContent()    // ← 注入自定义内容
})

这种模式在演示页面中广泛使用,实现了「通用卡片容器 + 可变内容」的优雅分离。

@State 是组件的内部状态,@Link 是父组件向子组件传递的引用状态:

typescript 复制代码
@Component
struct Child {
  @Link value: number;

  build() {
    Button(`值: ${this.value}`)
      .onClick(() => { this.value++ })  // ★ 修改 @Link 会同步到父组件 ★
  }
}

@Component
struct Parent {
  @State count: number = 0;

  build() {
    Column() {
      Child({ value: this.count })      // 传递 @State 给 @Link
    }
  }
}

七、AI 对话应用最佳实践

7.1 对话历史管理

复制代码
// 保留最近 N 条消息,避免超出 token 上限
const history = messages
  .filter(m => m.content !== '')       // 过滤空消息
  .slice(-20);                          // 最多保留 20 条

原则:

  • 保留系统提示词 + 最近 20 条对话
  • 过滤掉空的 AI 回复(流式拼接过程中产生的临时空字符串)
  • 系统提示词始终在消息列表最前面

7.2 请求取消机制

当用户快速发送多条消息时,需要取消上一次未完成的请求:

typescript 复制代码
let httpRequestTask: http.HttpRequest | null = null;

export function cancelAI(): void {
  if (httpRequestTask) {
    try {
      httpRequestTask.destroy();
    } catch (_) { /* ignore */ }
    httpRequestTask = null;
  }
}

在 UI 层,提供「停止」按钮和「清空」按钮都调用 cancelAI():

typescript 复制代码
// 停止按钮(仅加载中可见)
if (this.isLoading) {
  Text('停止').onClick(() => {
    cancelAI();
    this.isLoading = false;
  })
}

// 清空按钮
Text('清空').onClick(() => {
  cancelAI();
  this.messages = [];
  this.currentAIResponse = '';
  this.isLoading = false;
})

7.3 错误处理策略

typescript 复制代码
onError: (errMsg: string) => {
  // 向用户显示友好错误信息,而非崩溃或静默失败
  this.messages.push({
    role: 'assistant',
    content: `😅 抱歉,我遇到了问题:${errMsg}`,
  });
  this.isLoading = false;
}

错误处理原则:

  1. 绝不崩溃:所有网络错误都应捕获并显示
  2. 友好提示:使用 emoji 和口语化表达
  3. 显示错误详情:帮助用户判断是网络问题还是 API 问题
  4. 恢复状态:确保 isLoading 被重置,UI 恢复正常

八、性能优化

8.1 流式渲染 vs 全量渲染

流式渲染(SSE)的核心优势是「逐字显示」,用户无需等待完整的 AI 回复即可开始阅读。这在长回复场景下大幅提升了用户体验。

实现原理:

  1. AI 服务器将回复拆成多个 token
  2. 每个 token 到达后,onData 回调被触发
  3. currentAIResponse 累加新内容
  4. @State 触发 UI 重渲染
  5. 气泡中的文字实时更新

8.2 对话历史截断

AI 模型的 token 上限是有限的(DeepSeek-V3 支持 32K tokens)。如果不截断历史,过长的对话会导致 API 调用失败。我们的策略是保留最近 20 条消息,这在实际使用中已足够维持上下文连贯性。

8.3 请求超时配置

typescript 复制代码
connectTimeout: 30000,    // 连接超时 30 秒
readTimeout: 120000,      // 读取超时 120 秒

较长的读取超时时间是因为 AI 生成完整回复可能需要 10-60 秒(取决于回复长度和服务器负载)。


九、ArkTS vs TypeScript/JavaScript 差异总结

特性 TypeScript ArkTS
any 类型 允许 ❌ 禁止
unknown 类型 允许 ❌ 禁止
联合类型 支持 有限支持
Flex 组件属性 --- 仅支持基础属性
Row/Column --- 支持 justifyContent/alignItems
overlay() 对象格式 函数格式
@State 无 必须用于响应式数据
私有属性传参 允许 警告
模块导入 ES Module @kit.xxx 格式

十、总结与展望

本文通过一个完整的「AI 万能手册」应用,详细讲解了在 HarmonyOS NEXT(API 24)上使用 ArkTS 构建 AI 对话应用的完整流程。从项目架构、服务层设计、SSE 流式解析、UI 组件设计到编译错误排查,覆盖了全链路开发要点。

核心技术点回顾:

  1. 网络层 :使用 @kit.NetworkKit 的 http 模块发起 SSE 请求
  2. 数据层:通过回调模式实现流式数据拼接
  3. UI 层 :@State 驱动消息列表实时更新
  4. 提示词工程:通过 System Prompt 精确控制 AI 回答行为和质量
  5. 错误处理:三层回退机制保障网络异常时的可用性

AI 对话应用只是鸿蒙原生 AI 集成的起点。后续可以扩展的方向包括:

  • 加入 TTS(文本转语音)能力,让 AI「说」出回复
  • 接入图片理解模型,支持多模态交互
  • 结合本地数据库,实现对话持久化
  • 加入语音输入,提升输入效率