鸿蒙应用 AI 对话实战:用蓝耘 MaaS 给礼物 App 装上「多轮对话选礼大脑」
鸿蒙 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
usage:prompt_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元
→ 既有品牌质感,又有现金实用,寓意"一杯子一辈子"🥂
usage:prompt_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:
- 多轮对话 — 取最近 10 条历史构建上下文,AI 能记住前面聊了什么
- 6 模型热切换 — 同一个
chat()函数,只改 model 参数,DeepSeek/Kimi/Qwen/GLM/MiniMax 随意切 - 聊天界面 — 双色气泡(橙=用户,蓝=AI)、Loading 动画、快捷提示词、自动滚动
- 跨页面联动 — 首页快捷场景 → AI 对话页预填提示词,三步变一步
- 鸿蒙原生 — 零第三方依赖,只用
@kit.NetworkKit的 http 模块
蓝耘 MaaS 的价值在于:你不需要为每个模型写不同的代码。一个 Key、一个 URL、一个函数,六个模型随意切。 对话式 AI 的复杂度在 UI 层(消息管理、气泡渲染、滚动控制),而不在 API 层——因为蓝耘 MaaS 把 API 层简化到了极致。
更多推荐





所有评论(0)