从 App 到 Agent:鸿蒙意图框架 Intents Kit 实战开发「小艺出行」

引言:Agent 时代,应用的角色正在被重新定义

在正式展开之前,先描述一个本项目开发过程中验证成功的真实场景。

用户表达一句"帮我加个明天九点去机场的行程",系统随即理解这一意图,从中抽取出目的地、时间等结构化参数,并将其分发给本项目开发的出行应用,由应用完成行程的创建——整个过程中,用户无需主动打开应用,任务即被准确完成
这一场景揭示了 Agent 时代一个根本性的变化:用户不再关心是哪一个应用完成了任务,只关心任务本身是否被正确完成。 应用能够被系统精准调用的前提,是它已向系统"声明"了自身具备处理相应意图的能力。
在这里插入图片描述

而这种向系统声明能力、并由系统完成意图分发的机制,正是本文要探讨的核心:鸿蒙的意图框架(Intents Kit)

笔者从事 App 开发多年,鸿蒙从早期版本一路使用到 HarmonyOS NEXT。过去开发关注的核心始终是页面布局、交互设计与动效实现。而本文希望系统性地记录一次开发范式的转变——从"开发一个供用户主动打开的 App",到"构建一个供系统随时调用的 Agent 能力",并在此过程中探讨"意图即服务"这一理念的工程价值。

为使论述具备可验证性,本文将以一个真实、可编译的 Demo——「小艺出行」——为主线,完整呈现其从零构建到在手机与鸿蒙 PC 双端运行的全过程。
鸿蒙手机端运行截图:
在这里插入图片描述

鸿蒙电脑PC端运行截图:
在这里插入图片描述
在这里插入图片描述

一、开发范式的转变:从"应用中心"到"意图中心"

在进入代码实现之前,有必要先厘清本次开发中最重要的认知升级,因为它决定了后续全部技术选型的方向。

1.1 传统模式:以应用为中心

传统 App 的设计遵循以应用为中心的逻辑。以添加行程为例,用户的操作路径通常是:解锁设备 → 定位应用图标 → 打开应用 → 等待首页加载 → 找到"新增"入口 → 填写表单 → 保存提交。这一链路涉及多个步骤,开发者需要为每一步设计界面、优化性能,并通过埋点分析用户流失环节。

在这一模式下,应用开发的核心目标是获取并留住用户的注意力。应用本身是一个用户必须主动"抵达"的目的地。

1.2 Agent 模式:以意图为中心

Agent 时代的设计遵循以意图为中心的逻辑。同样是添加行程,用户只需表达一句"帮我加个明天九点去机场的行程"即可完成。

在这一过程中,用户全程无需打开任何应用。用户表达"意图",系统中的智能体(小艺)负责理解该意图,并将其路由给最合适的能力提供方执行。

其中的关键在于路由机制。应用不再是孤立的目的地,而是成为系统能力网络中的一个节点。开发者的核心工作,也从"设计吸引用户的界面",转变为"向系统清晰声明:可处理哪些意图、需要哪些参数、执行后返回何种结果"。
在这里插入图片描述

1.3 对开发者的实际影响

这一范式转变可归纳为三个层面:

  • 从流量获取到意图接入:核心不再是将用户"引导进入"应用,而是让系统在恰当的时机调用应用能力。
  • 从界面开发到契约定义:核心产出物从一系列 UI 页面,转变为一份清晰的"意图契约"(能力范围与参数定义)。
  • 从单点体验到全场景触达:一次能力声明,即可在手机、平板、PC、车机等所有小艺可达的设备上生效。

理解这三个层面,再审视意图框架的相关 API,其设计逻辑便清晰可循。

在这里插入图片描述

二、场景选择:为何采用出行助手

在候选场景(记账、点餐、快递查询等)中,最终选定出行助手作为演示场景,主要基于以下几点考量,它们都高度契合意图分发的特性:

  1. 意图表达天然口语化。“加个明天去机场的行程”“我明天有什么安排”"导航去公司"等表达,均为用户的自然语言输入,是意图框架的理想应用对象。
  2. 参数可清晰结构化。目的地、出行方式、时间、备注等要素,能够准确映射到意图框架的参数 schema,便于演示系统从自然语言中抽取结构化参数的过程。
  3. 可演示多意图协同。出行并非单一动作,至少涵盖"添加、查询、导航"三类操作,可完整展示多个意图如何共存与分派,而非孤立的单一功能。
  4. 天然具备跨设备特性。出行场景涉及手机查询、PC 规划、车机导航等多终端协作,契合鸿蒙"一次开发、多端部署"的设计理念。

基于以上考量,本文构建了「小艺出行」:一个纯端侧、无后端依赖、向小艺开放三种出行能力的应用。

在这里插入图片描述

三、Demo 整体架构:三个页面与三个意图

首先给出整体结构概览,后续章节将逐层展开。

三个页面(底部 Tab 切换,深色科技风):

  • 概览:品牌头、"下一程"渐变大卡、今日行程列表,以及一块"小艺能帮你做什么"的引导区。
  • 行程:全部行程的时间线,支持手动新增(底部弹层)、左滑删除、点标签切换完成状态。
  • 我的:把三个意图能力做成了可开关的清单,配上统计卡片和关于信息。

三个意图(开放给小艺的能力):

用户对小艺说 命中意图 App 行为
「加个明天9点去首都机场的行程」 AddTrip 创建行程并跳到行程页
「我明天有什么安排」 QueryTrips 汇总行程、语音式播报
「导航去公司」 NavigateTo 真实拉起地图规划路线

工程目录长这样(只列关键部分):

entry/src/main/
├── ets/
│   ├── entryability/EntryAbility.ets     入口,解析意图 Want、沉浸式全屏
│   ├── common/
│   │   ├── Theme.ets                     设计主题(色板/尺寸)
│   │   └── TripCard.ets                  行程卡组件(多页复用)
│   ├── intents/
│   │   └── TripIntentExecutor.ets        意图执行器(三意图共用,按名分派)
│   ├── model/
│   │   ├── Trip.ets                      行程模型 + Preferences 持久化
│   │   ├── IntentHandler.ets             Want 参数解析
│   │   └── NavHelper.ets                 拉起地图导航
│   └── pages/
│       ├── Index.ets                     三 Tab 主壳 + 意图结果回流
│       ├── OverviewTab.ets / TripsTab.ets / ProfileTab.ets
├── resources/base/profile/insight_intents.json   意图声明
└── module.json5                          注册 intent metadata

在这里插入图片描述

四、核心实现:向系统声明应用能力

这是从 App 到 Agent 转变中最关键的一步。与传统开发首先编写页面不同,本项目首先编写的是一份 JSON 配置文件——意图声明

4.1 意图声明 insight_intents.json

意图声明需要向系统明确三项信息:应用具备哪些意图、每个意图的定义、执行该意图所需的参数。以"添加行程"为例:

{
  "intentName": "AddTrip",
  "domain": "TravelManagement",
  "displayName": "添加行程",
  "llmDescription": "当用户想安排出行、添加行程、记录一次出发时使用。可从表达中提取目的地 destination、出行方式 type、时间 when、备注 note。例如「帮我加个明天9点去首都机场的行程」。",
  "keywords": ["加行程", "安排出行", "添加行程", "去", "出发"],
  "parameters": {
    "type": "object",
    "properties": {
      "destination": { "type": "string", "description": "目的地,必填" },
      "type": { "type": "string", "enum": ["飞机", "高铁", "驾车", "打车", "地铁", "步行"] },
      "when": { "type": "string", "description": "出发时间的自然语言描述" },
      "note": { "type": "string", "description": "备注,可选" }
    },
    "required": ["destination"]
  },
  "executor": "ets/intents/TripIntentExecutor.ets"
}

这段配置中最值得关注的是 llmDescription 字段。它并非面向机器的枚举定义,而是以自然语言撰写、供大模型理解的能力说明。开发者需要清晰地向小艺表达:“在何种场景下应调用本应用,以及需要从用户表达中抽取哪些关键信息”。这代表了一种新的编程范式:开发者编写的是面向 AI 的提示(prompt),而非面向编译器的逻辑。

keywords 用于辅助召回,parameters 则定义了结构化契约——小艺依据该 schema,将用户的口语表达解析为 destination=首都机场type=飞机when=明天9点 等结构化字段。这一抽取过程由系统的大模型完成,开发者无需自行实现任何自然语言处理逻辑。

在这里插入图片描述

4.2 在模块中注册声明

仅有声明文件尚不足够,还需在 module.json5 中通过 metadata 向系统注册该意图声明:

"metadata": [
  { "name": "ohos.ability.intent.metadata", "resource": "$profile:insight_intents" }
]

这条配置即完成了应用向系统声明能力的注册动作。

4.3 意图执行器:能力的具体实现

意图声明解决了"应用能做什么"的问题,执行器则负责"如何执行"。本项目的三个意图共用一个执行器,按意图名称进行分派:

export default class TripIntentExecutor extends InsightIntentExecutor {
  async onExecuteInUIAbilityForegroundMode(
    name: string,
    param: Record<string, Object>,
    pageLoader: object
  ): Promise<insightIntent.ExecuteResult> {
    let message = '';
    if (name === 'QueryTrips') {
      message = await this.doQuery(str(param['when']));
    } else if (name === 'NavigateTo') {
      const dest = str(param['destination']);
      await openNavigation(this.context as common.UIAbilityContext, dest);
      message = `正在为你拉起地图,规划前往「${dest}」的路线…`;
    } else {
      message = await this.doAdd(param);   // AddTrip
    }
    // 结果写入全局状态,供 UI 回流展示
    AppStorage.setOrCreate('lastIntentMsg', message);
    AppStorage.setOrCreate('intentTick', Date.now());
    return { code: 0, result: { message } } as insightIntent.ExecuteResult;
  }
}

需要注意的是最后返回的 result.message——它不仅用于 UI 展示,还可被小艺进行语音播报。因此文案的撰写需要兼顾语音朗读的流畅度。这是与传统 App 开发的又一处差异:返回值需要同时考虑"可视"与"可听"两种呈现方式。

在这里插入图片描述


五、意图结果的 UI 回流机制

意图执行完毕后数据发生变更,而用户当前可能正处于应用的某个页面。如何实现界面的实时、无感更新,是本项目遇到的第一个技术难点。

最初的实现是在页面的 onPageShow 中执行刷新。但实践发现:当小艺(或深链)触发操作时,应用已处于前台显示状态,不会再次触发 onPageShow,导致列表无法更新,必须手动切换页面或重启应用,用户体验较差。

改进方案采用全局状态 + @Watch 监听的响应式机制。执行器将结果写入 AppStorage,页面订阅并监听其变化:

@StorageLink('intentTick') @Watch('onIntentResult') intentTick: number = 0;
@StorageLink('lastIntentMsg') lastIntentMsg: string = '';
@StorageLink('intentJumpTab') intentJumpTab: number = -1;

onIntentResult(): void {
  // 弹出语音式反馈
  promptAction.showToast({ message: '🎙 小艺:' + this.lastIntentMsg, duration: 2800 });
  // 自动跳到相关 Tab(添加→行程页,导航→概览页)
  if (this.intentJumpTab >= 0 && this.intentJumpTab <= 2) {
    this.current = this.intentJumpTab;
  }
}

该方案的核心在于:使用一个递增的 intentTick 时间戳作为触发信号。每次意图处理完成后更新该值,@Watch 随即响应——无论应用是否处于前台,UI 均可即时刷新、弹出反馈并自动跳转至相关页面。
在这里插入图片描述

这种"以时间戳作为信号驱动响应式刷新"的方法,是本项目在调试过程中总结出的一项实用技巧。

六、导航能力的实现:真实拉起地图应用

在初始版本中,"导航"意图仅弹出"正在规划路线"的示例提示。为使其具备实际产品价值,需要实现真实拉起地图应用的能力。

鸿蒙中的应用间跳转通过 startAbility + Want 实现。由于不同设备预装的地图应用存在差异,本项目设计了一条降级调用链:依次尝试 Petal 地图、高德、百度,任一成功即调用,全部失败时则使用系统隐式 Want 作为兜底方案。

export async function openNavigation(context: common.UIAbilityContext, dest: string): Promise<boolean> {
  const schemes = [
    { name: 'PetalMaps', uri: `maps://routes?daddr=${encodeURIComponent(dest)}&type=drive` },
    { name: 'Amap',      uri: `amapuri://route/plan/?dname=${encodeURIComponent(dest)}&t=0` },
    { name: 'BaiduMap',  uri: `baidumap://map/direction?destination=${encodeURIComponent(dest)}&mode=driving` }
  ];
  for (const s of schemes) {
    try {
      await context.startAbility({ uri: s.uri });
      return true;                       // 谁先成功就用谁
    } catch (e) { /* 该地图未安装,继续尝试下一个 */ }
  }
  // 兜底:系统隐式 Want,可能弹出可选地图列表
  try {
    await context.startAbility({ action: 'ohos.want.action.viewData', uri: `geo:0,0?q=${encodeURIComponent(dest)}` });
    return true;
  } catch (e) { return false; }
}

这一设计同样体现了 Agent 化的思路:应用无需自行实现地图功能,而是将"导航"这一子任务委托给系统中最擅长处理它的能力提供方。 应用之间由此从独立运行转变为能力协作。

在这里插入图片描述

七、双端适配:手机与鸿蒙 PC 的一致运行

鸿蒙"一次开发、多端部署"的能力在本项目中得到了充分验证。该 Demo 未为 PC 单独编写任何布局代码,即可在手机与鸿蒙 PC 上直接运行

实现这一效果,依赖于开发之初即遵循的几项原则:

  1. 采用弹性布局而非固定尺寸:宽度使用 layoutWeight、百分比及 Flex 自动换行,避免硬编码 px,从而在 PC 大屏下自然展开。
  2. 安全区适配:通过 getWindowAvoidArea 获取状态栏与导航条高度并存入全局状态,页面统一避让,可正确处理手机刘海与 PC 窗口边框。
  3. 组件化复用:将行程卡 TripCard 抽象为独立组件,供概览页与列表页复用,在不同屏幕尺寸下表现一致。

尤为重要的是,意图能力天然具备跨端特性。同一份 insight_intents.json,在手机与鸿蒙 PC 上均可通过小艺触发——因为意图是注册于"系统"层面,而非某一具体屏幕。这正是第一章所述"一次声明、全场景触达"的实际体现。

本项目从模拟器到真机手机,再到鸿蒙 PC,全程无需修改代码即可运行,充分验证了鸿蒙跨端能力的工程价值。

在这里插入图片描述

八、开发过程中的典型问题与解决方案

在 Demo 的开发过程中遇到了若干典型问题,现选取几个具有代表性的进行记录,供开发者参考。

8.1 语音指令被分发至系统日历

这是最具代表性的一个现象:唤醒小艺添加行程时,行程被添加进系统日历,本应用无任何响应。

经排查,其原因在于:真实小艺是基于华为云端已上架的意图库进行匹配的。 系统日历早已上架了"日程"类意图,而本应用的 AddTrip 仅为本地声明,尚未通过 AppGallery Connect 完成意图框架能力申请与上架,因此小艺云端无法感知本应用具备处理该类意图的能力。
在这里插入图片描述

由此可明确区分两个层面:

  • 本地声明insight_intents.json)仅解决"应用内部知晓自身能力"的问题;
  • 平台上架(在 AGC 申请意图框架能力并提审)才能使"小艺云端知晓应用能力",从而在真实语音场景中完成意图分发。

明确这一机制后,即可采用下述方式在本地验证代码的正确性。

8.2 未上架情况下的意图链路验证

在无法通过语音触发的情况下,可使用 aa start 将意图直接投递至应用——该方式与真实小艺分发所使用的 want.parameters 通道完全一致

# 添加行程(结构化参数,与真实小艺同通道)
hdc shell "aa start -a EntryAbility -b com.example.smarttrip \
  --ps op add --ps destination '首都机场' --ps type 'flight' --ps when 'tomorrow 9'"

初期为简便起见采用 URI 深链 ?a=x&b=y 方式传参,但发现 & 在多层 shell 环境中被解析为命令分隔符,导致仅首个参数能够传入。改用 --ps(string 参数)直接写入 want.parameters 后,问题得以解决。

8.3 命令行中文参数乱码

命令行传递中文参数(如 type=餐饮)经 hdc shell 处理后会出现编码错乱,导致解析失败。需要说明的是,这是命令行终端的编码限制,真实小艺采用结构化参数传递,不受此影响。 本项目的应对方案是:在参数解析中增加英文 key 到中文的映射(如 flight→飞机drive→驾车),本地测试使用英文参数,既规避了乱码,又不影响真实场景。

8.4 环境变量污染导致构建失败

开发过程中曾遇到构建报错 ERR_WORKER_INVALID_EXEC_ARGV: --report-on-fatalerror is not allowed in NODE_OPTIONS,且 DevEco Studio 内部 Sync 亦无法执行。经定位,原因是 NODE_OPTIONS 环境变量被外部工具污染,而 DevEco Studio 作为 GUI 进程继承了启动时的异常环境。解决方案为彻底退出应用、清除该变量后重新启动。此类环境问题虽与代码无关,但排查耗时较长,故一并记录。

九、意图框架的工程价值分析

完成本 Demo 后,笔者对"意图即服务"这一理念有了更为具体的工程层面的认识。

其一,降低了应用的流量获取压力。 开发者无需再刻意引导用户进入应用,只要应用能力可靠、意图声明清晰,系统即可在恰当时机调用应用能力。竞争维度从"应用入口的显著程度"转向"应用能力的可靠程度"。

其二,重新赋予了垂直应用价值。 专注于单一功能并将其做到极致的应用,在传统模式下获客较为困难,而在意图分发机制下,可作为特定垂直意图的最优提供方被系统精准调用,这对独立开发者具有积极意义。

其三,促使开发者重新审视应用的边界。 导航功能的实现表明,应用无需追求功能大而全,完全可以将子任务委托给更专业的能力方。应用之间由此从相互独立转变为协作网络,而小艺则承担了调度中枢的角色。

与此同时,该模式也提出了新的能力要求:开发者需要掌握以自然语言撰写面向 AI 的能力说明(llmDescription)、将返回值优化为适合语音播报的文案、并为跨端场景进行弹性设计。 这些均是传统 App 开发中较少涉及的新课题。


十、结语

回到本文开头所述的场景——用户一句自然语言表达,系统即完成意图理解、参数抽取与应用调用,行程被准确创建。这一过程印证了一个核心观点:在 Agent 时代,一个应用真正的起点,不是它的首个页面,而是它向系统声明的能力契约。

从"开发一个供用户主动打开的 App",到"构建一个供系统随时调用的 Agent 能力",二者之间的差异并非几个 API,而是一整套开发思维的迁移。「小艺出行」Demo 虽仅包含三个页面与三个意图、代码量有限,却完整呈现了这一迁移的全过程,包括其中的技术难点与解决方案。
在这里插入图片描述

HarmonyOS 正在将"意图即服务"构建为每一位开发者均可使用的基础设施。对于长期从事鸿蒙开发的工程师而言,能够在这一范式转变的起点参与实践,具有重要的意义。

对于正在开发鸿蒙应用的开发者,笔者建议:选取一个熟悉的场景,为其编写意图声明并接入小艺。通过这一实践,可以真切地理解到——Agent 时代已经到来,并已具备完整的工程落地条件。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐