积分中心
从自然语言到可确认操作:HarmonyOS 应用接入 Agent/Skill 前的动作契约实践
头像 ccai 2026-09-18 07:42:59    发布
1409 浏览 2 点赞 0 收藏

摘要

让用户用自然语言查询或操作应用,真正困难的部分不是生成一句看起来合理的回答,而是如何把不确定的模型输出转换成可验证、可撤销、可审计的应用动作。本文以“车账本”中的查询和新增记录为例,设计一套与具体模型平台解耦的动作契约,并讨论参数校验、人工确认、幂等处理、权限和隐私边界。本文不假设 HarmonyOS 已提供某个通用的 Agent.create() API,也不宣称任何应用都能直接接入小艺智能体;正式接入仍需以官方开放资格和文档为准。

一、先把“聊天”拆成动作

用户可能说:

  • “帮我算一下本月油费。”
  • “记一笔 68.5 元的加油费。”
  • “把上个月的停车记录删掉。”

这三句话的风险完全不同:

  • 查询月度汇总:通常是只读操作;
  • 新增记录:会改变数据,需要确认;
  • 删除记录:可能造成不可逆损失,默认不自动执行。

因此,模型不应直接调用数据库。中间应增加一个动作网关:

自然语言
   ↓
Agent/Skill 平台适配层
   ↓
应用动作契约
   ↓
参数校验与权限判断
   ↓
用户确认(必要时)
   ↓
业务服务
   ↓
结果与审计记录

本文把 “Skill” 理解为一组可调用动作的约定,不把它等同于某个固定的 HarmonyOS 官方类名。

二、定义最小动作集合

第一版只开放两个动作:


动作类型是否需要确认
ledger.querySummary读取本月或指定月份汇总可按产品策略决定
ledger.createRecord新增一条费用记录必须确认

暂不开放删除、批量导出和修改历史记录。能力少一些,反而更容易完成权限和异常测试。

动作请求可以抽象为:

// ArkTS 示意类型,具体平台接入时按官方协议映射
type ActionName =
  | 'ledger.querySummary'
  | 'ledger.createRecord'

interface ActionRequest {
  requestId: string
  name: ActionName
  version: number
  args: Record<string, string | number>
  source?: string
}

interface ActionResponse {
  requestId: string
  status: 'ok' | 'need_confirmation' | 'rejected' | 'error'
  message: string
  preview?: string
}

每个参数都要有明确约束:

  • amount:大于 0,不能超过业务上限;
  • type:只能是预定义枚举;
  • date:必须是可解析的本地日期;
  • note:限制长度,去除控制字符;
  • requestId:必填且唯一,用于幂等;
  • version:不兼容变更时递增。

三、动作网关不信任模型输出

下面是简化的业务示意代码,不是某个官方 Agent SDK 的调用方式:

class ActionGateway {
  private readonly supported: string[] = [
    'ledger.querySummary',
    'ledger.createRecord'
  ]

  async handle(request: ActionRequest): Promise<ActionResponse> {
    if (request.requestId.length === 0) {
      return this.reject(request, '缺少请求标识')
    }

    if (request.version !== 1) {
      return this.reject(request, '暂不支持该动作版本')
    }

    if (this.supported.indexOf(request.name) < 0) {
      return this.reject(request, '动作未开放')
    }

    if (request.name === 'ledger.querySummary') {
      const month = this.normalizeMonth(request.args['month'])
      if (month === undefined) {
        return this.reject(request, '月份格式不正确')
      }

      const summary = await this.querySummary(month)
      return {
        requestId: request.requestId,
        status: 'ok',
        message: `已查询 ${month} 的汇总`,
        preview: JSON.stringify(summary)
      }
    }

    if (request.name === 'ledger.createRecord') {
      const amount = Number(request.args['amount'])
      if (!Number.isFinite(amount) || amount <= 0) {
        return this.reject(request, '金额不合法')
      }

      const type = String(request.args['type'])
      if (!this.isAllowedType(type)) {
        return this.reject(request, '费用类型不合法')
      }

      return {
        requestId: request.requestId,
        status: 'need_confirmation',
        message: '请确认后保存这笔记录',
        preview: `类型:${type},金额:${amount.toFixed(2)} 元`
      }
    }

    return this.reject(request, '未知动作')
  }

  private reject(
    request: ActionRequest,
    message: string
  ): ActionResponse {
    return {
      requestId: request.requestId,
      status: 'rejected',
      message
    }
  }

  private normalizeMonth(value: string | number): string | undefined {
    const text = String(value)
    return /^\d{4}-\d{2}$/.test(text) ? text : undefined
  }

  private isAllowedType(type: string): boolean {
    return ['fuel', 'maintenance', 'parking', 'other']
      .indexOf(type) >= 0
  }

  private async querySummary(month: string): Promise<Object> {
    // 调用应用自己的只读业务服务
    return {}
  }
}

实际项目中还应检查:

  • 是否为重复的 requestId;
  • 调用方是否具备必要权限;
  • 是否处于允许操作的页面和账户;
  • 参数是否包含敏感数据;
  • 请求是否超时;
  • 业务服务是否已经执行过同一动作。

四、写操作必须有可见确认

当网关返回 need_confirmation 时,页面应展示结构化预览,而不是让用户再次阅读一段自然语言。

确认界面至少应显示:

  • 将要执行的动作;
  • 金额、类别、日期等解析后的字段;
  • 数据保存位置;
  • 调用来源;
  • 取消和确认按钮。

用户点击确认后,应用再调用真正的业务服务。删除、转账、发送消息等高风险动作应采用更严格的二次确认,甚至不开放给 Agent 自动触发。

五、幂等和审计不能省略

网络重试、平台重发或用户重复点击,都可能导致同一个动作到达多次。写操作应以 requestId 或业务幂等 ID 去重:

async function confirmCreate(
  requestId: string,
  record: CostRecord
): Promise<ActionResponse> {
  const existing = await actionStore.find(requestId)

  if (existing !== undefined) {
    return existing.result
  }

  const result = await ledgerService.create(record)

  await actionStore.save({
    requestId,
    action: 'ledger.createRecord',
    result
  })

  return result
}

审计日志不应记录完整的用户备注或模型原始提示词,只保留必要的元数据,例如:

  • 请求 ID;
  • 动作名称和版本;
  • 结果状态;
  • 时间;
  • 失败原因。

六、接入小艺或其他平台时的边界

正式接入时,通常需要根据官方平台要求完成能力描述、参数协议、权限配置和资格申请。可能的入口包括官方 Agent/Skill 平台、深链或扩展能力,但具体方式不能通过猜测 API 得出。

应用侧应坚持以下原则:

  1. 不使用未公开的系统接口;
  2. 不把任意 URI 参数直接转换成写操作;
  3. 在入口处验证调用方、版本和参数;
  4. 平台不可用时,应用仍能手工完成核心功能;
  5. 平台协议变化时,通过适配层升级,不改动领域业务。

如果尚未获得小艺或其他平台的开放资格,本文仍然可以作为应用侧动作建模和安全设计文章发布,但标题和正文应写成“接入前准备”或“动作契约实践”,不能写成“已完成官方接入”。

七、隐私和失败兜底

AI 相关功能尤其需要说明数据边界:

  • 哪些内容在本地解析;
  • 哪些内容会上传到云端;
  • 上传前是否脱敏;
  • 用户是否可以关闭 AI 功能;
  • 网络不可用时如何提示;
  • 模型输出错误时如何撤销或改正。

例如,车账本的金额和备注属于个人数据。日志中不应打印完整备注,也不应将模型请求中的全部历史账目默认上传。

八、测试清单

建议至少覆盖:


场景预期
正常查询月度汇总返回只读结果
新增记录先进入确认态
金额为负数直接拒绝
未知费用类型直接拒绝
重复 requestId不重复写入
用户取消确认不产生数据变化
平台不可用手工入口仍可用
网络中断给出可理解的失败提示
旧版本动作明确拒绝或走兼容适配
删除类请求第一版不开放

九、总结

Agent 接入的核心不是让应用“听起来更聪明”,而是把自然语言转换成有限、可验证、可撤销的动作。先定义动作契约,再做参数校验、人工确认、幂等和审计,最后才连接具体平台,能够降低模型误判和平台变化带来的风险。


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255