【共创季稿事节】HarmonyOS6.1让大模型帮你想「今天吃什么」:一款鸿蒙 AI 智能菜谱应用的结构化输出实战

本文完整记录一款「AI 智能菜谱」鸿蒙应用在鸿蒙 PC 上从架构设计、大模型结构化输出、四大功能页实现到真机运行的全过程。重点剖析一个 AI 应用开发中的核心命题——如何让大模型稳定输出结构化 JSON,并把它渲染成精致的原生 UI。含大量可复现的 ArkTS 代码与真机运行截图。

在这里插入图片描述

在这里插入图片描述

一、从一个日常痛点说起

“今天吃什么?”——这大概是当代人每天最高频、也最令人头疼的问题之一。冰箱里躺着几样食材,却不知道能凑出什么菜;想做点新花样,又懒得翻菜谱 App 一页页找。

大语言模型的出现,让这个问题有了优雅的解法:把你有的食材告诉 AI,让它帮你想菜谱。这听起来简单,但要做成一款体验好的应用,中间隔着一个关键的技术鸿沟——

大模型天生输出的是自然语言文本。如果我直接把它返回的一大段文字丢给用户,体验和在聊天框里问没什么区别:食材、步骤、用量混在一起,没有结构、无法交互、不能收藏。而一款真正的菜谱应用,需要的是结构化的数据:菜名、难度、耗时、用料清单(还要区分"你已有的"和"需要买的")、分步做法、采购清单……

所以这篇文章的核心,不是"怎么调用大模型 API"(那只是基础),而是:如何驯服大模型,让它稳定地吐出结构化 JSON,再把这份 JSON 渲染成体验精致的鸿蒙原生界面。 这是 AI 应用从"能用"到"好用"的分水岭。

我用 HarmonyOS(ArkTS/ArkUI)实现了这款应用,全程在鸿蒙 PC(HUAWEI MateBook Pro) 上开发运行。先看成品——这是应用的主界面,用户添加了"鸡蛋、番茄、土豆、豆腐",AI 立刻推荐了番茄炒蛋、土豆烧豆腐、番茄豆腐汤三道菜:

AI 智能菜谱主界面

可以看到,AI 的输出不是一段文字,而是三张结构清晰的菜谱卡片,每张都带着耗时、难度、以及"缺几样食材"的智能提示。这正是"结构化输出 + 原生渲染"的效果。下面我完整拆解它是怎么实现的。
在这里插入图片描述

二、架构设计:一个 AI 应用的分层

在写代码前先做架构。一款接入大模型的应用,最容易犯的错是把"网络请求、JSON 解析、业务逻辑、UI 渲染"全揉在一个页面里。这样的代码改一处牵一发,无法维护。

我把工程分为清晰的四层:

entry/src/main/ets/
├── entryability/     应用入口(生命周期、窗口、安全区)
├── common/           通用层(主题、AI 配置、复用组件)
├── model/            数据与服务层(AI 服务、数据模型、会话态)
└── pages/            UI 层(四个功能页)

具体到文件:

├── common/
│   ├── Theme.ets           温暖美食橙主题
│   ├── AIConfig.ets        ★AI 服务配置 + 结构化输出提示词
│   └── RecipeCard.ets      菜谱卡片组件(推荐/收藏复用)
├── model/
│   ├── Recipe.ets          菜谱数据模型 + 收藏/偏好持久化
│   ├── RecipeAI.ets        ★AI 服务:请求 + JSON 解析(核心)
│   └── RecipeSession.ets   页面间的菜谱传递
└── pages/
    ├── Index.ets           四 Tab 主壳
    ├── CookTab.ets         做菜首页(食材输入 + 推荐)
    ├── DetailPage.ets      菜谱详情
    ├── FavTab.ets          收藏
    └── ProfileTab.ets      我的/设置

分层的核心思想是单一职责:UI 层只管展示和交互,绝不直接碰网络;所有与大模型的交互收敛在 RecipeAI;数据结构和持久化在 Recipe;配置集中在 AIConfig。这样当我要调整提示词、换模型、或改 UI 时,改动都被限制在一层里。
在这里插入图片描述

三、核心中的核心:让大模型输出结构化 JSON

这是全文最重要的部分。我们的目标是:让大模型不要返回"番茄炒蛋做法:首先……"这样的散文,而是返回一个我们能直接解析的 JSON 对象。

3.1 用提示词定义数据契约

实现这一点的关键,是在系统提示词里把 JSON 的结构(schema)明确地"喂"给模型,并强硬要求它只输出 JSON。我把这段提示词放在 AIConfig 里:

export class AIConfig {
  static readonly endpoint: string =
    'https://xxxxxxxx/compatible-mode/v1/chat/completions';
  static readonly apiKey: string = 'sk-****************';
  static readonly model: string = 'qwen-plus';

  /** 系统提示词:约束模型只输出结构化菜谱 JSON */
  static readonly systemPrompt: string =
    '你是一位专业的家常菜大厨兼营养师。用户会告诉你现有的食材和偏好,' +
    '你需要基于这些食材推荐 2 到 3 道菜,尽量充分利用已有食材。' +
    '必须严格只输出 JSON,不要输出任何多余文字、解释或 markdown 代码块标记。' +
    'JSON 格式如下:' +
    '{"recipes":[{' +
    '"name":"菜名",' +
    '"desc":"一句话简介",' +
    '"difficulty":"简单|中等|困难",' +
    '"minutes":预计分钟数(整数),' +
    '"tags":["标签1","标签2"],' +
    '"ingredients":[{"name":"用料名","amount":"用量","have":true或false}],' +
    '"steps":["步骤1","步骤2"],' +
    '"shopping":["需要额外购买的食材1","食材2"]' +
    '}]}' +
    '其中 have 字段:用户已有的食材标 true,需要额外买的标 false;' +
    'shopping 列出所有 have 为 false 的食材。';

  static readonly temperature: number = 0.8;
  static readonly maxTokens: number = 1500;
  static readonly timeoutMs: number = 40000;
}

这段提示词有几个精心设计的点,值得逐条说明:

其一,给出完整的 JSON 骨架。 我没有含糊地说"请返回结构化数据",而是把每一个字段、每一层嵌套都写清楚。大模型对这种"填空题"式的明确 schema 遵循度非常高——你越具体,它越听话。

其二,明令禁止多余输出。 “必须严格只输出 JSON,不要输出任何多余文字、解释或 markdown 代码块标记”——这一句至关重要。大模型有个习惯,喜欢在 JSON 外面裹一层 ` ```json … ````代码块,或者在前面加一句"好的,这是为你推荐的菜谱:"。这些都会破坏 JSON 解析,必须明确禁止。(当然,禁止了也不能全信,代码侧还要兜底,后面讲。)

其三,用字段承载业务语义。 have 字段是这个应用的灵魂——它让模型在推荐时就判断"哪些食材用户有、哪些要买",shopping 则汇总要买的。这把"我有啥/还缺啥"这个业务逻辑,直接交给模型在生成阶段完成,而不是等数据回来后我再去比对。善用字段设计,把业务语义嵌入数据契约,是提示工程的高级技巧。

其四,temperature: 0.8 菜谱推荐希望有一定创意和多样性(同样的食材,多点几次能有不同建议),所以温度设得比问答类应用略高。

3.2 组织请求

有了配置,RecipeAI.generate 负责把用户的食材、人数、偏好组织成请求。这里要注意 ArkTS 的强类型约束——所有对象字面量都必须有显式的 interface,否则编译报 arkts-no-untyped-obj-literals

interface ChatMessage {
  role: string;
  content: string;
}

interface ChatRequest {
  model: string;
  messages: ChatMessage[];
  temperature: number;
  max_tokens: number;
}

static async generate(ingredients: string[], servings: number, pref: string): Promise<RecipeResult> {
  // 把用户输入拼成一句自然语言
  const userMsg =
    `我现在有这些食材:${ingredients.join('、')}` +
    `用餐人数:${servings} 人。` +
    (pref !== '' ? `我的偏好/忌口:${pref}` : '') +
    `请推荐 2-3 道菜。`;

  const req: ChatRequest = {
    model: currentModel(),
    messages: [
      { role: 'system', content: AIConfig.systemPrompt },   // schema 约束
      { role: 'user', content: userMsg }                     // 用户诉求
    ],
    temperature: AIConfig.temperature,
    max_tokens: AIConfig.maxTokens
  };
  // ... 发起请求
}

注意这里的分工:system 消息负责"你要输出什么格式",user 消息负责"我要什么内容"。这种职责分离让提示词清晰、可复用——换个应用场景,只要改 system 里的 schema 就行。

3.3 发起网络请求

用鸿蒙 @kit.NetworkKit 的 http 模块发 POST 请求,采用 OpenAI 兼容协议:

const client = http.createHttp();
try {
  const resp = await client.request(AIConfig.endpoint, {
    method: http.RequestMethod.POST,
    header: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + currentKey()
    },
    extraData: JSON.stringify(req),
    connectTimeout: AIConfig.timeoutMs,
    readTimeout: AIConfig.timeoutMs
  });

  if (resp.responseCode !== 200) {
    return { ok: false, recipes: [], error: `请求失败(${resp.responseCode}` };
  }
  const content = RecipeAI.extractContent(`${resp.result}`);
  const recipes = RecipeAI.parseRecipes(content);
  if (recipes.length === 0) {
    return { ok: false, recipes: [], error: '未能解析出菜谱,请换个食材再试' };
  }
  return { ok: true, recipes: recipes, error: '' };
} catch (e) {
  return { ok: false, recipes: [], error: '网络异常,请检查连接后重试' };
} finally {
  client.destroy();   // 释放资源,避免泄漏
}

菜谱生成通常比普通问答耗时更长(要生成多道菜的完整结构),所以我把超时设到 40 秒,max_tokens 也给到 1500。返回结果统一封装成 RecipeResult { ok, recipes, error },让 UI 层能优雅地处理成功和失败两种情况。

下图是点击"生成菜谱"后的加载状态——按钮变为"正在为你想菜谱…"并禁用,防止重复提交:

生成菜谱加载状态

四、驯服大模型:JSON 解析的容错艺术

拿到模型返回的文本后,要把它解析成 Recipe[]。这一步看似简单(JSON.parse 一下嘛),实则是整个应用最容易翻车的地方

因为大模型再怎么被约束,也不是 100% 听话。实践中它可能:

  • 在 JSON 外面裹一层 ` ```json … ````markdown 代码块;
  • 在 JSON 前面加一句"好的,为你推荐:";
  • 某个字段偶尔缺失或类型不对。

如果直接 JSON.parse(rawText),遇到上面任何一种情况都会抛异常、整个功能崩溃。所以解析层必须做足容错

4.1 先提取纯净的 JSON

我的策略是:不管模型返回什么,只截取第一个 { 到最后一个 } 之间的内容,把外层的 markdown 标记、寒暄语全部剥掉:

private static parseRecipes(content: string): Recipe[] {
  let text = content.trim();
  // 剥离 ```json```包裹、前后寒暄——只留 JSON 主体
  const start = text.indexOf('{');
  const end = text.lastIndexOf('}');
  if (start >= 0 && end > start) {
    text = text.substring(start, end + 1);
  }
  try {
    const obj = JSON.parse(text) as Record<string, Object>;
    const arr = obj['recipes'] as Object[];
    if (arr === undefined) { return []; }
    const result: Recipe[] = [];
    for (const item of arr) {
      result.push(RecipeAI.toRecipe(item as Record<string, Object>));
    }
    return result;
  } catch (e) {
    return [];   // 解析失败返回空,由上层提示用户重试
  }
}

这个"截取首尾大括号"的技巧很朴素但极其有效,能挡掉绝大多数模型不听话的情况。

4.2 逐字段安全提取

即使 JSON 能解析,也不能假设每个字段都完美存在。所以 toRecipe 里对每个字段都做了兜底:

private static toRecipe(r: Record<string, Object>): Recipe {
  const ings: Ingredient[] = [];
  const rawIngs = r['ingredients'] as Object[];
  if (rawIngs !== undefined) {
    for (const it of rawIngs) {
      const o = it as Record<string, Object>;
      ings.push({
        name: `${o['name'] ?? ''}`,
        amount: `${o['amount'] ?? ''}`,
        have: o['have'] === true      // 严格判断布尔
      });
    }
  }
  return {
    name: `${r['name'] ?? '未命名菜品'}`,       // 缺失给默认名
    desc: `${r['desc'] ?? ''}`,
    difficulty: `${r['difficulty'] ?? '简单'}`,  // 缺失给默认难度
    minutes: Number(r['minutes'] ?? 20),
    tags: RecipeAI.toStrArr(r['tags']),
    ingredients: ings,
    steps: RecipeAI.toStrArr(r['steps']),
    shopping: RecipeAI.toStrArr(r['shopping'])
  };
}

每个字段都用 ?? 默认值 兜底,数组字段用统一的 toStrArr 转换。这样即使模型漏了某个字段,应用也只是显示一个默认值,而不会崩溃。“永远不要相信外部输入"这条工程铁律,在 AI 应用里要升级为"永远不要完全相信模型输出”。

正是这套"提示词约束 + 解析容错"的组合拳,让应用能稳定地把 AI 的输出变成可靠的结构化数据。下图展示了另一组食材(豆腐、面条、米饭、牛肉、胡萝卜)的推荐结果,可以看到即使食材组合完全不同,输出结构依然稳定:

不同食材的推荐结果

牛肉胡萝卜炖豆腐、牛肉胡萝卜炒面、牛肉胡萝卜米饭煲——AI 很聪明地围绕这几样食材做了不同的组合,每道菜的描述也生动到位(“软嫩豆腐吸饱牛肉与胡萝卜的鲜甜汤汁”)。

五、功能页一:做菜首页

首页承担"输入食材 → 触发生成 → 展示推荐"的完整流程。

5.1 标签式食材输入

食材用标签(chip)的形式管理,可点击删除,还提供常见食材的快捷添加。核心是一个 @State ingredients: string[],配合 Flex 自动换行布局:

@State ingredients: string[] = ['鸡蛋', '番茄'];   // 内置示例,开箱即用
@State input: string = '';

addIng(): void {
  const v = this.input.trim();
  if (v === '' || this.ingredients.includes(v)) { return; }  // 去重
  this.ingredients.push(v);
  this.ingredients = this.ingredients.slice();   // 触发 UI 刷新
  this.input = '';
}

这里 this.ingredients = this.ingredients.slice() 是 ArkUI 的关键技巧——对 @State 数组做浅拷贝赋值才能可靠触发重渲染,直接 push 有时不生效。

UI 上,已添加的食材是橙色可删标签,下方是快捷添加区(土豆、青椒、牛肉……点一下即添加)。这种设计降低了输入成本,用户不用一个个手打。
在这里插入图片描述

5.2 触发生成与结果展示

点击生成按钮,调用 RecipeAI.generate,根据返回结果更新 UI:

async onGenerate(): Promise<void> {
  if (this.ingredients.length === 0) {
    promptAction.showToast({ message: '请先添加至少一种食材', duration: 1500 });
    return;
  }
  this.loading = true;
  this.results = [];
  const res = await RecipeAI.generate(this.ingredients, this.servings, this.pref);
  this.loading = false;
  if (!res.ok) {
    promptAction.showToast({ message: res.error, duration: 2000 });
    return;
  }
  this.results = res.recipes;
  RecipeSession.setResults(res.recipes);
}

loading 状态驱动按钮文案与禁用,results 一旦有值就渲染成菜谱卡片列表。成功、失败(网络/解析错误)都有对应处理,不会让用户面对无响应的界面。

六、功能页二:菜谱卡片与详情页

在这里插入图片描述

6.1 复用的菜谱卡片组件

推荐列表和收藏列表都要展示菜谱卡片,所以我把它抽成独立组件 RecipeCard,通过 @Prop 接收数据、回调传递点击:

@Component
export struct RecipeCard {
  @Prop recipe: Recipe;
  onTap?: (r: Recipe) => void;
  // ... 展示菜名、简介、耗时、难度、缺料提示
}

卡片上有个体现细节的设计——根据 shopping 数组动态显示食材状态

if (this.recipe.shopping.length > 0) {
  Text('🛒 缺 ' + this.recipe.shopping.length + ' 样')  // 缺料,橙色提示
} else {
  Text('✓ 食材齐全')                                     // 齐全,绿色提示
}

难度也用颜色区分(简单绿、中等黄、困难红),用户扫一眼就能判断这道菜适不适合现在做。

6.2 详情页:结构化数据的完整呈现

详情页是"结构化输出"价值的集中体现。因为数据是结构化的,我可以把它渲染成层次分明的几个区块:标题标签区、用料清单、采购清单、分步做法

用料清单里,have 字段的价值就体现出来了——已有的食材标绿色 ✓,需要买的标橙色 🛒:

ForEach(this.recipe.ingredients, (ing: Ingredient) => {
  Row() {
    Text(ing.have ? '✓' : '🛒')
      .fontColor(ing.have ? C.accent : C.gold)   // 已有绿 / 待买橙
    Text(ing.name).layoutWeight(1)
    Text(ing.amount).fontColor(C.textSub)
  }
}, (ing: Ingredient) => ing.name)

制作步骤则用带序号圆标的列表,清晰易读。下图是"番茄炒蛋"的完整详情页:

菜谱详情页

可以看到,一份原本是 AI 文本的菜谱,被完整地结构化呈现:15 分钟、简单、#快手 #下饭 #素食 标签,用料清单区分了已有的鸡蛋番茄(✓)和需买的盐糖油葱(🛒),下方是"还需购买"的采购清单和四步做法。右上角还能一键收藏。这就是结构化输出相比纯文本的降维打击——数据可以被精确地摆放、着色、交互。

6.3 页面间的数据传递

从首页/收藏点击某道菜进入详情,我用一个轻量的 RecipeSession 单例来传递当前菜谱,避免复杂的路由传参:

export class RecipeSession {
  private static current: Recipe | null = null;
  static setCurrent(r: Recipe): void {
    RecipeSession.current = r;
    AppStorage.setOrCreate('currentRecipeName', r.name);  // 触发详情页 @Watch 刷新
  }
  static getCurrent(): Recipe | null { return RecipeSession.current; }
}

配合 Tab 切换(点击卡片切到"菜谱"Tab),实现了流畅的浏览体验。

七、功能页三、四:收藏与个性化设置

7.1 收藏:本地持久化

喜欢的菜谱可以收藏,用 Preferences 存到本地。RecipeStore 封装了收藏的增删查:

static async addFav(r: Recipe): Promise<void> {
  const list = await RecipeStore.listFavorites();
  if (!list.some((x: Recipe) => x.name === r.name)) {   // 去重
    list.unshift(r);
    await RecipeStore.saveFav(list);
  }
}

收藏页展示所有收藏,支持左滑删除。收藏的是完整的菜谱结构,所以离线也能查看已收藏菜谱的完整做法——这是结构化存储的又一好处。

7.2 设置:让偏好影响 AI

"我的"页最有价值的是饮食偏好功能。用户勾选"清淡少油、无辣、素食、低卡"等标签,或自定义输入,这些偏好会被拼进发给 AI 的请求里:

const userMsg = `我现在有这些食材:${ingredients.join('、')}` +
  `用餐人数:${servings} 人。` +
  (pref !== '' ? `我的偏好/忌口:${pref}` : '') +   // 偏好注入
  `请推荐 2-3 道菜。`;

这样 AI 推荐就会考虑用户的忌口和口味——素食者不会被推荐红烧肉,怕辣的不会收到麻辣香锅。用一个简单的偏好字段,就让推荐从"通用"变成"个性化",这正是大模型灵活性的体现。设置页还提供模型服务配置(可运行时覆盖密钥和模型)。下图是设置页:

我的与设置页

顶部橙色渐变的"美食家"卡片,下面是饮食偏好标签、智能菜谱服务配置(显示当前模型),整体延续了温暖的美食橙色调。

八、应用入口与鸿蒙 PC 适配

8.1 入口初始化

EntryAbilityonWindowStageCreate 里初始化存储、配置沉浸式全屏、计算安全区:

async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
  await RecipeStore.init(this.context);
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) { return; }
    const win = windowStage.getMainWindowSync();
    win.setWindowLayoutFullScreen(true);
    const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
    const bottom = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
    AppStorage.setOrCreate('safeTop', px2vp(top.topRect.height));
    AppStorage.setOrCreate('safeBottom', px2vp(bottom.bottomRect.height));
  });
}

8.2 鸿蒙 PC 上的表现

本文所有截图都是在华为 MateBook Pro(鸿蒙 PC) 上运行的。应用以窗口形式跑在电脑桌面,同一套 ArkTS 代码无需修改就能适配 PC 大屏。

能做到这一点,靠的是几个原则:module.json5 里声明 "deviceTypes": ["phone", "tablet", "2in1"] 把 PC 形态纳入;UI 全程用弹性布局(layoutWeight、百分比、Flex 换行),不写死尺寸;安全区逻辑统一处理手机刘海与 PC 窗口边框。

对菜谱这类应用来说,PC 大屏其实体验更好:食材输入用物理键盘更顺手,菜谱详情的多区块内容在大窗口里一览无余,做菜时把电脑架在厨房台面上看菜谱也很实用。

九、四 Tab 主壳

最后用 Tabs 把四个页面组织起来。这里有个巧妙设计——"菜谱"Tab 就是详情页,从做菜或收藏点击某道菜时,通过回调切到该 Tab 展示:

Tabs({ barPosition: BarPosition.End, index: this.current }) {
  TabContent() { CookTab({ onOpenDetail: () => { this.current = 1; } }) }
    .tabBar(this.bar('做菜', '🍳', 0))
  TabContent() { DetailPage({ onBack: () => { this.current = 0; } }) }
    .tabBar(this.bar('菜谱', '📖', 1))
  TabContent() { FavTab({ onOpenDetail: () => { this.current = 1; } }) }
    .tabBar(this.bar('收藏', '♥', 2))
  TabContent() { ProfileTab() }.tabBar(this.bar('我的', '👤', 3))
}
.scrollable(false)   // 禁用滑动切换,避免与页面内滚动冲突

十、踩坑与经验小结

开发过程中的几个关键经验,供参考:

坑一:大模型不老实输出 JSON。 这是 AI 结构化应用最大的坑。解决靠"提示词强约束 + 解析层截取首尾大括号 + 逐字段兜底"三重防线,缺一不可。只靠提示词,模型偶尔还是会加 markdown 包裹;只靠解析,提示词不给力时 JSON 结构会乱。

坑二:ArkTS 严格类型。 所有对象字面量必须有显式 interfaceChatRequestRecipeIngredient 都得定义好,否则 arkts-no-untyped-obj-literals 编译不过。这比普通 TypeScript 严格得多。

坑三:@State 数组刷新。 增删食材、更新结果列表时,要 this.arr = this.arr.slice() 触发刷新。

坑四:AI 请求要给足超时。 生成多道完整菜谱比普通问答慢,readTimeout 给到 40 秒,否则容易超时失败。

坑五:网络与解析都要兜底。 把结果统一封装成 { ok, recipes, error },任何失败都返回友好中文提示,绝不让用户对着空白或崩溃的界面。

十一、可扩展方向与总结

这款应用已经是一个四页齐全、可真机运行的完整作品。继续深化的方向:

  1. 食材图像识别:拍一张冰箱照片,用多模态大模型识别出食材,自动填入——彻底免去手动输入。
  2. 营养分析:让模型额外返回每道菜的热量、蛋白质等,做健康管理。
  3. 一键生成采购清单:把多道菜的 shopping 合并去重,生成一份完整购物清单。
  4. 流式输出:菜谱逐字生成,减少等待焦虑。
  5. 本地菜谱缓存:把生成过的菜谱缓存,相同food组合直接复用,省 token。

回顾整个项目,它虽然是个"想吃啥"的小工具,却完整演示了 AI 应用开发最核心的一课——如何把大模型不确定的自然语言输出,转化为确定的、结构化的、可交互的应用数据。这套"提示词定义 schema + 解析层容错 + 结构化渲染"的方法论,适用于几乎所有需要 AI 生成结构化内容的场景:不只是菜谱,还有行程规划、简历生成、数据报表、表单填充……
在这里插入图片描述

如果你也想在 HarmonyOS 上做一款 AI 应用,我最想强调的一句话是:大模型给你的是"可能性",而工程给你的是"确定性"。把提示词写扎实、把解析层做健壮,才能把 AI 的灵光一现,变成用户手里稳定好用的产品。

希望这篇实战对你有帮助。如果你也在鸿蒙上探索 AI 应用,欢迎交流。

Logo

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

更多推荐