轻规划鸿蒙开发实战17:小艺智能体 Schema 定义与意图槽位抽取(Intent Slot Filling)实战

背景介绍

在 HarmonyOS 6 时代,应用智能化的最佳表现之一,就是将应用的功能深度融入到系统级助理——小艺智能体中。

对于“轻规划”(AeroPlan)的用户,我们不仅提供了在 App 内手动创建项目的常规入口,更打通了小艺智能体的自然语言唤醒通道。

例如用户对小艺说:

“帮我用轻规划安排一个 3 天的上海旅行路线。”
“在轻规划里创建一个下周开始学习鸿蒙 NEXT 的计划,工期 10 天。”

当用户说出这些口语化的语句时,小艺智能体可以通过语义理解,准确提取出“旅行路线”或“学习鸿蒙 NEXT”作为项目主题(projectName),提取出“3天”或“10天”作为工期槽位(expectedDays)。最后,小艺直接将这些参数通过系统的“意图通道”送给“轻规划”,无感拉起应用并进入自动导入确认界面。
轻规划鸿蒙开发实战17:小艺智能体 Schema 定义与意图槽位抽取(Intent Slot Filling)实战.png
今天,我们将实战演练如何定义意图 Schema、注册意图槽位以及在端侧处理意图反序列化写入的全过程。


1. 架构纵览:小艺自然语言意图转换与槽位分发管线

从用户的语音输入到应用前台重绘并导入,数据的完整传递链条如下:

轻规划鸿蒙开发实战17:小艺智能体 Schema 定义与意图槽位抽取(Intent Slot Filling)实战-1.png

1.1 核心步骤解析

  1. 自然语言输入(Natural Language Input):用户通过语音或键盘向小艺发送指令,表述其希望进行某项规划活动。
  2. 意图匹配与槽位填充(Intent Matching & Slot Filling):系统底层的大模型以及 NLP 引擎解析用户的表述,将其与在系统中注册的 intent_schema.json 文件进行语义匹配,识别出对应的意图 ID (如 com.aeroplan.intent.CREATE_PLAN),并提取出关联的核心参数(Slots)。
  3. 拉起应用分发意图(Want Parameter Distribution):系统通过 Want 基础架构拉起目标 App。若 App 已运行在后台,则会通过 onNewWant 回调分发;若 App 未启动,则会通过 onCreate 路径分发。所有的槽位参数都会以 Key-Value 形式存放在 Want.parameters 字典中。
  4. 数据清洗与安全拦截(Sanitization & Validation):由于自然语言的随意性,小艺提取出的参数可能存在非标准格式(如提取出空值、溢出数字或非预期日期格式)。应用宿主端(EntryAbility)需要进行数据清洗以规避边界写入的稳定性风险。
  5. 前台导入消费(Front-end Consumption):将清洗后的标准实体存入 AppStorage,激活前台 UI 的订阅监听器,自动弹出导入预览浮窗,降低用户二次录入的摩擦力。

1.2 技术方案对比分析

为了更清晰地理解小艺智能体意图架构与传统架构的差异,我们可以通过下表进行对比:

维度 传统 Scheme/URI 路由 小艺意图槽位抽取 (Intent Slot Filling)
触发介质 必须由外部网页/App 点击特定硬编码链接 用户通过日常口语自然语言唤醒,支持模糊表达与近义词
参数提取 发起端必须精准拼接 Query 串,容错率低 系统智能解析文本结构,多级实体匹配自动转换为强类型 Slot
参数校验 依赖网关或路由层的正则匹配,易漏校验 支持 Schema 契约约束,配合端侧 Sanitizer 形成防御式编程
用户体验 路径长,用户必须在固定界面寻找入口 一步直达,系统底层感知,应用前台直接展示推荐导入卡片
系统融合度 孤立的应用级路由,系统无法感知业务 与系统级助理深度绑定,享受小艺智能体多模态推荐分发

2. 编写 Schema 契约文件:定义意图与槽位(Slots)

在鸿蒙工程的 HAP 模块中,我们必须在 src/main/resources/base/profile 目录下,新建一个 intent_schema.json 契约配置文件。这相当于我们向系统公示的应用意图白皮书。

2.1 意图声明 Schema 核心定义

{
  "intents": [
    {
      "name": "com.aeroplan.intent.CREATE_PLAN",
      "slots": [
        {
          "name": "projectName",
          "type": "string",
          "mandatory": true,
          "description": "需要创建的年度规划项目或计划的名称"
        },
        {
          "name": "expectedDays",
          "type": "number",
          "mandatory": false,
          "description": "该计划预期的工期天数"
        },
        {
          "name": "startDate",
          "type": "string",
          "mandatory": false,
          "description": "计划的预期起始日期(格式:YYYY-MM-DD)"
        }
      ]
    }
  ]
}

2.2 核心字段设计决策

  • name: 意图的全局唯一标识。采用反向域名风格(如 com.aeroplan.intent.CREATE_PLAN),防止与其他应用的意图冲突。
  • slots: 槽位数组,用来定义我们期望从小艺解析出的业务参数。
  • mandatory: 设定为 true 的槽位(如 projectName),当小艺发现用户的自然语言中缺少该核心信息时,会主动向用户追问(如:“请问您要创建什么计划呢?”),直到收集完成才触发分发。
  • type: 声明数据类型(如 stringnumber)。系统级转换时会依此做初步类型校正。

3. 宿端接收意图:EntryAbility 劫持与槽位数据清洗

当小艺识别意图成功后,系统会通过一个携带有意图名和参数槽位值的 Want 对象唤醒我们的 EntryAbility。我们需要实现意图捕获逻辑,并结合防御式编程过滤可能存在的异常输入。

3.1 意图处理核心代码

import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  
  /**
   * 当 Ability 实例创建时触发,用于处理冷启动场景下的意图分发
   * @param want 系统传入的意图参数包,包含小艺抽取的槽位数据
   * @param launchParam 启动参数
   */
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 调用内部意图分发拦截机
    this.handleXiaoyiIntent(want);
  }

  /**
   * 当 Ability 实例已存在于后台,再次被拉起时触发,用于处理热启动场景
   * @param want 系统传入的更新后的意图参数包
   * @param launchParam 启动参数
   */
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 覆盖式意图处理,确保最新的自然语言指令能立即获得前台响应
    this.handleXiaoyiIntent(want);
  }

  /**
   * 解析并过滤小艺智能体传递过来的 Intent 槽位参数
   * @param want 原始 Want 报文
   */
  private handleXiaoyiIntent(want: Want) {
    // 1. 判断触发的 Want Action 是否属于小艺意图定义,防止非合规行为引起的误触发
    const intentName = want.action;
    if (intentName === "com.aeroplan.intent.CREATE_PLAN") {
      console.info("EntryAbility", "Xiaoyi intent received, extracting slots...");

      const parameters = want.parameters;
      if (parameters) {
        // 2. 从 Want 键值对中安全抽取槽位填充值,添加空值判定和类型规整
        const rawProjectName = parameters['projectName'] as string;
        const rawExpectedDays = parameters['expectedDays'] as number;
        const rawStartDate = parameters['startDate'] as string;

        // 3. 进入格式清洗与类型安全过滤,保证下游核心数据库持久化时不产生异常中断
        const sanitizedProjectName = this.sanitizeProjectName(rawProjectName);
        const sanitizedExpectedDays = this.sanitizeExpectedDays(rawExpectedDays);
        const sanitizedStartDate = this.sanitizeDate(rawStartDate);

        // 4. 将解析清洗后的结构化槽值存入 AppStorage 全局存储,以激活前台响应式订阅
        AppStorage.setOrCreate('xiaoyiProjectName', sanitizedProjectName);
        AppStorage.setOrCreate('xiaoyiExpectedDays', sanitizedExpectedDays);
        AppStorage.setOrCreate('xiaoyiStartDate', sanitizedStartDate);
        // 标记有未消费的小艺导入请求,促使主页自动唤起确认卡片组件
        AppStorage.setOrCreate('hasPendingXiaoyiIntent', true);
        
        console.info("EntryAbility", `Staged Xiaoyi intent slots: ${sanitizedProjectName}, ${sanitizedExpectedDays} days`);
      }
    }
  }

  /**
   * 项目名称字符过滤器,限制长度并过滤非法控制符,避免界面排版错位或溢出风险
   * @param name 原始输入的项目名
   * @returns 规整后的安全项目名
   */
  private sanitizeProjectName(name: string): string {
    if (!name || name.trim().length === 0) {
      return "未命名新规划";
    }
    // 截断超长字符串,最大支持 30 个字符长度限制,多余的过滤掉
    const trimmed = name.trim();
    return trimmed.length > 30 ? trimmed.substring(0, 30) : trimmed;
  }

  /**
   * 预期天数数值过滤器,规避非法负数或超大数值引发的逻辑死循环和系统稳定性风险
   * @param days 原始槽位数值
   * @returns 合理区间的天数结果
   */
  private sanitizeExpectedDays(days: number | undefined): number {
    // 若自然语言未提取出天数或者解析失败,默认设置兜底工期为 7 天
    if (days === undefined || isNaN(days)) {
      return 7;
    }
    // 业务规则限制:项目周期最短 1 天,最长支持 365 天,防止越界输入破坏甘特图排版
    if (days < 1) {
      return 1;
    }
    if (days > 365) {
      return 365;
    }
    return Math.floor(days); // 强制转为整数
  }

  /**
   * 日期格式清洗层,识别并归一化日期串,防止绕过格式校验导致的系统崩溃风险
   * @param dateStr 原始传入的日期字符串
   * @returns 标准化 YYYY-MM-DD 日期
   */
  private sanitizeDate(dateStr: string | undefined): string {
    // 校验日期是否为标准 YYYY-MM-DD 格式
    const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
    if (!dateStr || !dateRegex.test(dateStr)) {
      const today = new Date();
      // 动态拼装符合国际标准的 YYYY-MM-DD 字符,补足前导零
      const year = today.getFullYear();
      const month = (today.getMonth() + 1).toString().padStart(2, '0');
      const day = today.getDate().toString().padStart(2, '0');
      const formatToday = `${year}-${month}-${day}`;
      
      console.warn("EntryAbility", `Invalid date slot value: ${dateStr}, auto fallbacked to: ${formatToday}`);
      return formatToday;
    }
    return dateStr;
  }
}

4. 极客避坑:Slot Type 转换失败与类型边界安全

小艺在将语义转换为 Want 参数包时,受用户口语多样性的影响,有可能传过来出乎意料的数据格式。例如:

  • 用户说:“下周开始”,小艺可能会把 startDate 格式化为非标准字符串(如 2026-W24next_week)。
  • 用户说:“过几天”,天数槽位可能获取到 NaN 或者无意义的超长字符串。

如果我们在端侧不做强类型判定与数据清洗,直接硬塞给数据库的 Date 字段,很容易由于字符串格式不符导致数据库写异常,甚至由于类型转换失败直接引发应用闪退(Crash)。

4.1 防御式安全过滤机制

我们在获取槽位值后,必须通过上述 Sanitizer 对所有外部传入数据进行边界过滤与边界限制。这遵循了以下安全开发原则:

  1. 输入验证(Input Validation):绝不信任任何外部实体的输入值,不管是来自网络报文还是系统底座。
  2. 安全兜底(Safe Fallbacks):当遇到非法入参或解析错误时,采用高容错率的默认值(如今天、默认周期 7 天),保证核心业务回路依然通畅,规避应用运行时的不合规表现。
  3. 数据一致性保护:在将内存字段落库之前完成所有转换,确保持久化实体类型符合 RdbStore 或 Preferences 的物理 Schema 规范。

5. 交互落地:UI 侧根据 Slot 状态触发自动导入确认弹窗

数据清洗并 staging 到 AppStorage 后,前台 UI 页面应当自动监听此状态,并以非侵入式的 UI 交互弹窗向用户请求最终确认。

5.1 UI 交互层实现

下面提供了一个标准的 UI 页面触发样例,演示如何消费我们在 EntryAbility 中 staged 的小艺槽位数据:

import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  // 订阅来自 EntryAbility 写入的全局状态变量
  @StorageLink('hasPendingXiaoyiIntent') hasPendingIntent: boolean = false;
  @StorageLink('xiaoyiProjectName') projectName: string = '';
  @StorageLink('xiaoyiExpectedDays') expectedDays: number = 7;
  @StorageLink('xiaoyiStartDate') startDate: string = '';

  build() {
    Stack() {
      Column({ space: 16 }) {
        // 主页常驻面板组件
        Text("轻规划 — 您的智能时间管家")
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 40 })

        Button("查看我的所有规划")
          .width('80%')
          .height(48)
          .onClick(() => {
            // 查看明细交互逻辑
          })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)

      // 5.2 浮窗拦截器:当小艺意图就绪时,动态浮现导入确认卡片
      if (this.hasPendingIntent) {
        Column() {
          Text("检测到来自小艺的创建请求")
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#222222')
            .margin({ bottom: 12 })

          Column({ space: 8 }) {
            Text(`项目名称: ${this.projectName}`)
              .fontSize(14)
              .fontColor('#555555')
            Text(`预估工期: ${this.expectedDays}`)
              .fontSize(14)
              .fontColor('#555555')
            Text(`启动日期: ${this.startDate}`)
              .fontSize(14)
              .fontColor('#555555')
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .margin({ bottom: 20 })

          Row({ space: 12 }) {
            Button("取消")
              .layoutWeight(1)
              .height(40)
              .backgroundColor('#EAEAEA')
              .fontColor('#666666')
              .onClick(() => {
                // 用户拒绝导入,静默清理缓存,关闭浮窗
                this.dismissXiaoyiIntent();
              })

            Button("确认导入")
              .layoutWeight(1)
              .height(40)
              .backgroundColor('#007DFF')
              .fontColor('#FFFFFF')
              .onClick(() => {
                // 执行持久化写入动作
                this.savePlanToDatabase();
              })
          }
          .width('100%')
        }
        .width('90%')
        .padding(20)
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .shadow({ radius: 20, color: 'rgba(0, 0, 0, 0.15)', offsetX: 0, offsetY: 8 })
        .align(Alignment.Center)
      }
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 静默重置状态,销毁悬浮卡片
   */
  private dismissXiaoyiIntent() {
    this.hasPendingIntent = false;
    AppStorage.setOrCreate('hasPendingXiaoyiIntent', false);
  }

  /**
   * 模拟将清洗后的数据写入底层数据库
   */
  private savePlanToDatabase() {
    // 此处可调用本地轻量级数据库 (Relational Database) 写入逻辑
    console.info("IndexUI", `Saving plan to RDB: ${this.projectName}`);
    
    // 显示系统 Toast 通知用户导入成功
    promptAction.showToast({
      message: '项目导入成功!',
      duration: 2000
    });

    // 写入成功后清理状态机,隐藏弹窗
    this.dismissXiaoyiIntent();
  }
}

轻规划鸿蒙开发实战17:小艺智能体 Schema 定义与意图槽位抽取(Intent Slot Filling)实战-2.png

6. 总结与下期预告

通过在鸿蒙工程中注册小艺智能体 Schema 配置文件、并在宿端 EntryAbility 实现槽位填充意图拦截与异常数据格式过滤,我们为“轻规划”拓展了自然语言极速创建计划的智能化大门。

意图导入成功后,前台 UI 页面需要在多种屏幕设备(包括折叠屏展开态、平板)上进行高保真的甘特图自适应展现。

在下一篇文章中,我们将涉足高级组件开发与折叠屏适配:富文本与 Markdown AST 组件在折叠屏展开态下的栅格响应自适应适配! 敬请期待。

Logo

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

更多推荐