鸿蒙 AI 对话实战:用蓝耘 MaaS 给礼物 App 装上「多轮对话选礼大脑」

当别人还在用表单填参数调 AI 时,我们直接在鸿蒙里做了一个完整的聊天界面——多轮对话、模型热切换、快捷提示词、Markdown 清洗,全部原生 ArkTS 实现,后端只靠蓝耘 MaaS 一个 API Key。
在这里插入图片描述

一、为什么礼物 App 需要 AI?

送礼是门玄学。预算 500 送女友、300 送爸爸、800 送朋友结婚——每个人、每个场合、每个预算,最优解都不同。

传统礼物 App 只能做静态推荐:编辑写好的榜单、用户手动筛选。但送礼这件事本质是对话

“她喜欢文艺和摄影” → “那拍立得怎么样?” → “会不会太普通?” → “加点手账本呢?”

这种来回沟通,表单做不到,只有多轮对话才能胜任。而多轮对话的核心是上下文记忆——AI 要记住前面聊了什么。

本文完整拆解:如何在 HarmonyOS 上用蓝耘 MaaS 实现一个真正的多轮对话式 AI 选礼助手
在这里插入图片描述

在这里插入图片描述

二、先看效果:蓝耘 MaaS 真实调用

在这里插入图片描述

以下内容全部来自蓝耘 MaaS API 的真实返回。

蓝耘 MaaS(元生代 MaaS)模型即服务(Model-as-a-Service)平台,它以 OpenAI 兼容的 Chat Completions 协议统一封装了 DeepSeek、Kimi、Qwen、智谱 GLM、MiniMax 等多家主流大模型的 API 调用,开发者只需一个 API Key即可用同一套 SDK(如 openai Python 库)按需切换调用不同模型,无需各自对接各家厂商,本质是一个多模型聚合的一站式大模型推理网关
https://maas.lanyun.net/#/model/modelSquare
在这里插入图片描述

2.1 调用方式

蓝耘 MaaS 兼容 OpenAI Chat Completions 协议:

curl -X POST https://maas-api.lanyun.net/v1/chat/completions \
  -H "Authorization: Bearer sk-你的Key" \
  -d '{
    "model": "deepseek-v4-flash",
    "messages": [
      {"role": "system", "content": "你是蓝耘AI选礼助手..."},
      {"role": "user", "content": "我女朋友下周过生日,预算500元,她喜欢文艺和摄影,请推荐3个礼物方案。"}
    ],
    "max_tokens": 2048,
    "temperature": 0.8
  }'

2.2 调用 1:deepseek-v4-flash — 送女友生日礼物

Prompt我女朋友下周过生日,预算500元左右,她喜欢文艺和摄影,请推荐3个礼物方案

蓝耘 AI 真实返回

好的!😊 根据你女朋友的文艺摄影爱好和500元预算,我为你精选了3个走心又实用的礼物方案:

方案一:Lomography 拍立得相机(如 Diana F+ 或 Lomo’Instant)

  • 推荐理由:文艺复古造型,自带柔焦与漏光效果,出片有胶片感,非常适合喜欢摄影的女生
  • 预估价格:¥380-480(含一盒相纸)

方案二:手工牛皮相机背带 + 文艺帆布相机包组合

  • 推荐理由:如果她已有相机,一条质感牛皮背带能瞬间提升文艺气质;搭配复古帆布包,日常出门装相机、笔记本都很方便
  • 预估价格:¥200-400

方案三:摄影画册《光影集》+ 复古胶卷钥匙扣

  • 推荐理由:一本大师级摄影集能让她在翻页中感受光影美学,附赠复古胶卷造型钥匙扣
  • 预估价格:¥150-250 + ¥30-50

usageprompt_tokens: 75, completion_tokens: 458, reasoning_tokens: 34, total: 533

2.3 调用 2:kimi-k2.5 — 同一 Key 换模型,送朋友结婚

只改 "model": "kimi-k2.5",其余参数不动:

Prompt好朋友要结婚了,预算800元,请推荐合适的结婚礼物

蓝耘 AI 真实返回

恭喜你的朋友!🎉 800元预算可以选到既体面又有心意的结婚礼物:

🏠 实用家电类
| 礼物 | 参考价格 | 亮点 |
| 戴森吹风机/卷发棒 | 约800元 | 高端实用,送礼有面子 |
| 胶囊咖啡机 | 500-800元 | 提升生活幸福感 |
| 智能扫拖机器人 | 700-800元 | 解放双手,懒人福音 |

🍽️ 品质餐具/厨具

  • 双立人/WMF锅具套装 — 600-800元,"锅"满幸福
  • Wedgwood骨瓷香槟杯 — 500-800元,仪式感强

💡 最稳妥选法:Wedgwood香槟杯一对(约600元)+ 红包200元
→ 既有品牌质感,又有现金实用,寓意"一杯子一辈子"🥂

usageprompt_tokens: 60, completion_tokens: 476, total: 536

2.4 关键发现

观察 说明
同一个 API Key 两次调用用完全相同的 Key
同一个 URL 都是 https://maas-api.lanyun.net/v1/chat/completions
只改 model 字段 请求体其他参数不变
模型各有特色 DeepSeek 推理细致 · Kimi 表格输出规整
usage 透明 含 reasoning_tokens,成本精确可控

这就是蓝耘 MaaS 的核心价值:一次对接,六个模型随意切,代码零改动。

在这里插入图片描述

三、项目架构

在这里插入图片描述

64-gift-mgr/
├── entry/src/main/ets/
│   ├── common/
│   │   ├── LanYunAI.ets      ← 蓝耘 MaaS 服务层 + Prompt + 快捷词
│   │   └── Theme.ets         ← 暖橙主色 + 蓝耘品牌色
│   ├── pages/
│   │   ├── Index.ets         ← 3 Tab(首页 · AI选礼 · 我的)
│   │   ├── HomeTab.ets       ← 倒计时 + AI入口 + 创意/时间轴切换
│   │   ├── AITab.ets         ← 对话式 AI 选礼助手(核心)
│   │   └── ProfileTab.ets    ← 蓝耘平台信息展示
│   └── entryability/
│       └── EntryAbility.ets  ← 安全区全区初始化
└── module.json5              ← 网络权限

3 Tab 架构

Tab 功能 AI 集成
首页 倒计时 + 快捷功能 + 礼物创意 + 送礼时间轴 AI 入口大卡片(3 个快捷场景)
AI选礼 多轮对话聊天界面 核心页面,6 模型热切换
我的 统计 + 菜单 + 关于 蓝耘平台信息卡

四、核心:对话式 AI 选礼助手

4.1 交互设计

与表单式 AI 不同,这里做的是真正的聊天界面

在这里插入图片描述

设计要点

  • 用户消息:橙色右气泡(应用主色)
  • AI 消息:蓝耘蓝左气泡 + 蓝耘头像(品牌色区分)
  • Loading 状态:AI 气泡内显示「蓝耘 AI 正在思考…」+ 转圈动画
  • 快捷提示词:横滑标签,点击即发送
  • 底部输入栏:TextInput + 蓝耘渐变发送按钮

4.2 消息气泡实现

// 用户消息气泡(右对齐,橙色渐变)
@Builder
UserBubble(m: Msg) {
  Row() {
    Column() {
      Text(m.content)
        .fontSize(14).fontColor('#FFFFFF').lineHeight(22)
        .padding({ left: 14, right: 14, top: 10, bottom: 10 })
        .borderRadius(D.rMd)
        .linearGradient(C.gradPrimary)    // 暖橙渐变
    }.constraintSize({ maxWidth: '78%' })  // 限制最大宽度

    Blank().layoutWeight(1)               // 右对齐
  }.width('100%')
}

// AI 消息气泡(左对齐,蓝耘蓝)
@Builder
AIBubble(m: Msg) {
  Row({ space: 8 }) {
    // 蓝耘头像
    Column() {
      Text('耘').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
    }
    .width(30).height(30).borderRadius(8)
    .linearGradient(C.gradLanYun)          // 蓝耘渐变
    .justifyContent(FlexAlign.Center)
    .alignSelf(ItemAlign.Start)

    Column() {
      if (m.loading) {
        Row({ space: 6 }) {
          LoadingProgress().width(16).height(16).color(C.lanYun)
          Text('蓝耘 AI 正在思考…').fontSize(13).fontColor(C.textDim)
        }
        .padding(12).backgroundColor(C.lanYunBubble).borderRadius(D.rMd)
      } else {
        Text(this.stripMd(m.content))
          .fontSize(14).fontColor(C.text).lineHeight(23)
          .padding(12).backgroundColor(C.lanYunBubble).borderRadius(D.rMd)
      }
    }.constraintSize({ maxWidth: '80%' })
  }.width('100%')
}

双色气泡策略:用户消息用 App 主色(暖橙),AI 消息用蓝耘品牌色(蓝),用户一眼能区分「这是我说的话」和「这是 AI 回的话」。
在这里插入图片描述

4.3 多轮对话:上下文记忆

这是对话式 AI 的核心。每次发送时,把最近 10 条消息作为上下文一起发给蓝耘 MaaS:

private doSend(userText: string): void {
  // 1. 添加用户消息到列表
  this.msgId++;
  this.messages.push({ id: this.msgId, role: 'user', content: userText, loading: false });
  this.messages = this.messages.slice();

  // 2. 添加 AI loading 占位消息
  this.msgId++;
  const aiMsgId = this.msgId;
  this.messages.push({ id: aiMsgId, role: 'assistant', content: '', loading: true });
  this.messages = this.messages.slice();
  this.sending = true;

  // 3. 构建对话历史(System + 最近 10 条)
  const history: ChatMessage[] = [
    { role: 'system', content: GIFT_SYSTEM_PROMPT }
  ];
  const recent = this.messages
    .filter(m => m.id < aiMsgId && m.content.length > 0)
    .slice(-10);                    // 只取最近 10 条,控制 token 消耗
  for (const m of recent) {
    history.push({ role: m.role, content: m.content });
  }

  // 4. 调用蓝耘 MaaS
  const model = LAN_YUN_MODELS[this.currentModel];
  chat(history, model).then((reply: string) => {
    // 5. 替换 loading 消息为真实回复
    const idx = this.messages.findIndex(m => m.id === aiMsgId);
    if (idx >= 0) {
      this.messages.splice(idx, 1, {
        id: aiMsgId, role: 'assistant',
        content: reply, loading: false,
      });
      this.messages = [...this.messages];   // 触发 UI 更新
    }
    this.sending = false;
    this.scrollToBottom();
  });
}

为什么取最近 10 条而不是全部?

  • Token 消耗:每条历史都会计入 prompt_tokens,全量发送成本高
  • 上下文窗口:模型有最大上下文限制,10 条足够覆盖大多数对话场景
  • 响应速度:历史越短,首 token 延迟越低

4.4 System Prompt 设计

export const GIFT_SYSTEM_PROMPT: string =
  '你是「蓝耘 AI 选礼助手」,集成在礼物管理 App 中,由蓝耘元生代 MaaS 平台驱动。' +
  '你的任务是根据收礼人信息、场合、预算等,推荐合适的礼物方案。' +
  '回答要实用、有条理,使用中文,适当使用 emoji。' +
  '推荐礼物时请给出:礼物名称、推荐理由、预估价格。' +
  '如果用户预算有限,请优先推荐性价比高的选择。';

5 句话设定了:身份、平台来源、任务、输出格式、特殊策略。简洁但完整。

4.5 快捷提示词

6 个预置提示词,横滑展示,点击即发送:

export const QUICK_PROMPTS: QuickPrompt[] = [
  { emoji: '👩', label: '送女友', prompt: '我女朋友下周过生日,预算500元左右...' },
  { emoji: '👨', label: '送爸爸', prompt: '父亲节快到了,预算300元,爸爸喜欢喝茶和钓鱼...' },
  { emoji: '👶', label: '送孩子', prompt: '侄子满月了,预算200元...' },
  { emoji: '🧑', label: '送朋友', prompt: '好朋友要结婚了,预算800元...' },
  { emoji: '💐', label: '写贺卡', prompt: '帮我写一段生日贺卡文案...' },
  { emoji: '💰', label: '预算规划', prompt: '这个月有3个人需要送礼...' },
];

用户不用打字,点一下就能开始对话。这对移动端体验至关重要。


在这里插入图片描述

五、蓝耘 MaaS 服务层

5.1 核心调用函数

export async function chat(
  messages: ChatMessage[],
  model: string = DEFAULT_MODEL,
  maxTokens: number = 2048,
  temperature: number = 0.8
): Promise<string> {
  const client = http.createHttp();

  // 超时兜底:90 秒
  const timeoutPromise = new Promise<string>((_, reject) => {
    setTimeout(() => reject(new Error('请求超时(90s)')), 90000);
  });

  const requestPromise = new Promise<string>(async (resolve, reject) => {
    try {
      const resp = await client.request(BASE_URL + '/chat/completions', {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + API_KEY,
        },
        extraData: JSON.stringify({
          model, messages, max_tokens: maxTokens,
          temperature, stream: false,
        }),
        connectTimeout: 30000,
        readTimeout: 60000,
      });

      if (resp.responseCode !== 200) {
        resolve('[请求失败] 状态码: ' + resp.responseCode);
        return;
      }

      const json: ChatResponse = JSON.parse(`${resp.result}`);
      const msg = json.choices?.[0]?.message;
      if (!msg) { resolve('[蓝耘 AI 返回为空]'); return; }

      // content 优先,reasoning_content 兜底
      const content = msg.content ?? '';
      const reasoning = msg.reasoning_content ?? '';
      resolve(content.length > 0 ? content
            : reasoning.length > 0 ? reasoning
            : '[蓝耘 AI 返回为空]');
    } catch (e) {
      reject(e);
    }
  });

  try {
    return await Promise.race([requestPromise, timeoutPromise]);
  } catch (e) {
    return '[请求异常] ' + (e as Error).message;
  } finally {
    client.destroy();   // 关键:防止资源泄漏
  }
}

5.2 鸿蒙特有的三个坑

原因 解决
http.createHttp() 内存泄漏 每次调用创建新实例 finally { client.destroy(); }
请求卡死不返回 鸿蒙 http 超时机制不完善 Promise.race + 90s 兜底
DeepSeek 返回空 content 推理模型内容在 reasoning_content fallback 到 reasoning_content

5.3 模型列表与热切换

export const LAN_YUN_MODELS: string[] = [
  'deepseek-v4-flash',              // 快速响应 · 日常推荐
  '/maas/deepseek-ai/DeepSeek-V3.2', // 深度推理 · 复杂选礼
  'kimi-k2.5',                      // 超长上下文 · 详细方案
  'qwen3.6-flash',                  // 极速生成 · 短文本
  '/maas/zhipuai/GLM-5.2',          // 中文优化 · 自然表达
  'minimax-m3',                     // 创意发散 · 独特推荐
];

用户在聊天页点右上角 ⚙️ 弹出模型选择器,切换后下次对话即用新模型。同一个 chat() 函数,只改 model 参数,6 个模型随意切。


六、首页:AI 入口 + 创意/时间轴

6.1 AI 入口大卡片

在这里插入图片描述

首页核心是蓝耘 AI 入口卡片——主入口 + 3 个快捷场景:

@Builder
AIEntry() {
  Column({ space: 0 }) {
    // 主入口:点击跳转 AI 对话页
    Row({ space: 14 }) {
      Column() { Text('耘').fontSize(22).fontColor('#FFFFFF') }
        .width(52).height(52).borderRadius(14)
        .linearGradient(C.gradLanYun)

      Column({ space: 4 }) {
        Text('蓝耘 AI 选礼助手').fontSize(16).fontWeight(FontWeight.Bold)
        Text('不知道送什么?问蓝耘 AI').fontSize(12).fontColor(C.textDim)
      }.layoutWeight(1)

      Text('→').fontSize(20).fontColor(C.lanYun)
    }
    .padding(16).onClick(() => { goAI(''); })

    Divider().color(C.stroke)

    // 三个快捷场景:点击跳转并预填提示词
    Row() {
      this.AISubEntry('👩', '送女友', '我女朋友下周过生日...')
      this.AISubEntry('👨', '送爸爸', '父亲节快到了...')
      this.AISubEntry('🧑', '送朋友', '好朋友要结婚了...')
    }
  }
  .backgroundColor(C.card).borderRadius(D.rLg)
  .border({ width: 1, color: C.lanYunSoft })
}

6.2 跨页面预填提示词

点击「送女友」等快捷场景,跳转到 AI 对话页并自动填充对应提示词:

// HomeTab — 点击快捷场景
function goAI(prompt: string): void {
  AppStorage.setOrCreate('aiPresetPrompt', prompt);
  AppStorage.setOrCreate('currentTab', 1);   // 切到 AI Tab
}

// AITab — 进入时读取预填
aboutToAppear(): void {
  if (this.presetPrompt.length > 0) {
    this.inputText = this.presetPrompt;      // 自动填入输入框
  }
}

用户点「送女友」→ 跳到 AI 页 → 输入框已填好提示词 → 直接点发送 → AI 开始推荐。三步变一步。
在这里插入图片描述

6.3 创意/时间轴切换

首页底部用 Tab 切换两种视图:

  • 礼物创意:卡片列表,带爱心收藏,点击切换 liked 状态
  • 送礼时间轴:竖向时间轴 + 汇总统计(待送/总预算/本周)
@Builder
TimelineList() {
  Column() {
    // 汇总统计
    Row() {
      Column() { Text('4').fontSize(20).fontColor(C.primary); Text('待送礼物') }
      Column() { Text('¥1800').fontSize(20).fontColor(C.accent); Text('总预算') }
      Column() { Text('1').fontSize(20).fontColor(C.danger); Text('本周') }
    }

    // 时间轴
    ForEach(this.timeline, (e: TimelineEvent, idx: number) => {
      Row({ space: 12 }) {
        Column() {
          Row().width(12).height(12).backgroundColor(C.primary).borderRadius(6)  // 圆点
          if (idx < this.timeline.length - 1) {
            Column().width(2).height(48).backgroundColor(C.stroke)               // 连线
          }
        }
        // 事件卡片...
      }
    })
  }
}

七、主题设计:暖橙 + 蓝耘蓝双色系

在这里插入图片描述

export class C {
  // 应用主色:暖橙(礼物温暖)
  static readonly primary: string = '#FF6B35';
  static readonly accent: string = '#FF9558';
  static readonly gradPrimary: LinearGradient = {
    angle: 135, colors: [['#FF6B35', 0.0], ['#FF9558', 1.0]]
  };

  // 蓝耘品牌色(AI 组件专用)
  static readonly lanYun: string = '#2563EB';
  static readonly lanYunDeep: string = '#1E40AF';
  static readonly lanYunBubble: string = '#EFF6FF';   // AI 气泡背景
  static readonly gradLanYun: LinearGradient = {
    angle: 135, colors: [['#2563EB', 0.0], ['#3B82F6', 0.5], ['#0EA5E9', 1.0]]
  };
}
组件 色系 效果
用户消息气泡 暖橙渐变 右对齐,代表「我说的」
AI 消息气泡 蓝耘蓝 左对齐 + 蓝耘头像,代表「AI 回的」
AI 入口卡片 蓝耘蓝边框 与普通功能卡区分
倒计时卡片 暖橙渐变 App 原生功能
发送按钮 蓝耘渐变 AI 操作入口

八、Markdown 清洗

大模型返回带 Markdown 标记,鸿蒙 Text 组件不解析,需要清洗:

private stripMd(md: string): string {
  let t = md;
  t = t.replace(/\*\*(.+?)\*\*/g, '$1');      // **粗体**
  t = t.replace(/\*(.+?)\*/g, '$1');           // *斜体*
  t = t.replace(/^#{1,6}\s+/gm, '');           // # 标题
  t = t.replace(/^(\d+)\.\s+/gm, '$1. ');      // 有序列表
  t = t.replace(/^[-*]\s+/gm, '· ');           // 无序列表 → ·
  t = t.replace(/`(.+?)`/g, '$1');             // `代码`
  return t;
}

清洗后 **方案一** 变成 方案一- 推荐理由 变成 · 推荐理由,在 Text 组件中显示干净整洁。


九、模型选择器

底部弹层,6 个模型带描述:

@Builder
ModelSheet() {
  Stack({ alignContent: Alignment.Bottom }) {
    // 遮罩
    Column().width('100%').height('100%').backgroundColor('#80000000')
      .onClick(() => { this.showModelSheet = false; })

    // 选择器
    Column({ space: 16 }) {
      Row() {
        Text('切换蓝耘 MaaS 模型').fontSize(16).fontWeight(FontWeight.Bold)
        Text('✕').onClick(() => { this.showModelSheet = false; })
      }.justifyContent(FlexAlign.SpaceBetween)

      ForEach(LAN_YUN_MODELS, (m: string, idx: number) => {
        Row({ space: 10 }) {
          Column({ space: 2 }) {
            Text(m).fontSize(13)
              .fontColor(this.currentModel === idx ? C.lanYun : C.textSub)
            Text(this.modelDesc(idx)).fontSize(10).fontColor(C.textDim)
          }.layoutWeight(1)

          if (this.currentModel === idx) {
            Text('✓').fontSize(16).fontColor(C.lanYun).fontWeight(FontWeight.Bold)
          }
        }
        .padding(12).borderRadius(D.rSm)
        .backgroundColor(this.currentModel === idx ? C.lanYunSoft : C.cardSoft)
        .onClick(() => {
          this.currentModel = idx;
          this.showModelSheet = false;
          promptAction.showToast({ message: '已切换至 ' + m });
        })
      })
    }
    .padding(20).backgroundColor(C.card)
    .borderRadius({ topLeft: D.rXl, topRight: D.rXl })
  }
}

每个模型带一行描述(快速响应 · 日常推荐 / 深度推理 · 复杂选礼),用户不用懂技术也能选。


十、完整调用链路

在这里插入图片描述

十一、踩坑总结

原因 解决方案
http.createHttp() 内存泄漏 每次创建新实例 finally { client.destroy(); }
请求卡死不返回 鸿蒙 http 超时不完善 Promise.race + 90s 兜底
DeepSeek 返回空 content 推理模型内容在 reasoning_content fallback 读取
@State 无法跨 Tab 组件内部状态不共享 @StorageLink + AppStorage
Markdown 显示混乱 Text 不解析 Markdown stripMd() 正则清洗
splice 后 UI 不更新 ArkUI 数组监听机制 splice[...arr] 重新赋值
对话历史过长 token 消耗大 只取最近 10 条
气泡宽度溢出 长文本撑满屏幕 constraintSize({ maxWidth: '80%' })

十二、与表单式 AI 的对比

对比项 表单式 AI(如纪念日 App) 对话式 AI(本文)
交互方式 选参数 → 点生成 → 看结果 自然对话,来回追问
上下文记忆 无,每次独立 有,最近 10 轮
用户体验 像填表 像聊天
适用场景 标准化任务(祝福语/选礼) 探索性任务(不知道送什么)
实现复杂度 中(需管理消息列表 + 滚动)
AI 能力利用 单轮 多轮,充分发挥大模型能力

对话式的核心优势:用户可以追问。第一次推荐不满意,直接说「太贵了,有没有便宜点的」,AI 记住上下文继续推荐。这是表单式做不到的。


十三、总结

这个礼物 App 展示了蓝耘 MaaS 在鸿蒙上的另一种集成范式——对话式 AI

  1. 多轮对话 — 取最近 10 条历史构建上下文,AI 能记住前面聊了什么
  2. 6 模型热切换 — 同一个 chat() 函数,只改 model 参数,DeepSeek/Kimi/Qwen/GLM/MiniMax 随意切
  3. 聊天界面 — 双色气泡(橙=用户,蓝=AI)、Loading 动画、快捷提示词、自动滚动
  4. 跨页面联动 — 首页快捷场景 → AI 对话页预填提示词,三步变一步
  5. 鸿蒙原生 — 零第三方依赖,只用 @kit.NetworkKit 的 http 模块

蓝耘 MaaS 的价值在于:你不需要为每个模型写不同的代码。一个 Key、一个 URL、一个函数,六个模型随意切。 对话式 AI 的复杂度在 UI 层(消息管理、气泡渲染、滚动控制),而不在 API 层——因为蓝耘 MaaS 把 API 层简化到了极致。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐