给小艺装上数学能力:数易 App 接入智能体 FunctionComponent 与 A2A 双通路实战

给小艺装上数学能力:数易 App 接入智能体 FunctionComponent 与 A2A 双通路实战

摘要 :数易是一款数学可视化 App(函数拟合、学习空间、计算工具、练习挑战)。本文记录把它接入小艺智能体的完整过程:用 FunctionComponent 在应用内嵌一个「问小艺」按钮(三档按钮形态 + 可用性探测 + 四类错误码降级),再用 AgentExtensionAbility 声明 type: "agent" 扩展能力,通过 A2A 协议让小艺反向调用 App 的 7 个数学技能(问题分析、方程求解、导数/矩阵/行列式验证、知识总结)。附真实代码、错误码处理与 6 个踩坑。

目录

  • 背景与前置
  • [通路一:App → 小艺(FunctionComponent 嵌入)](#通路一:App → 小艺(FunctionComponent 嵌入) "#%E9%80%9A%E8%B7%AF%E4%B8%80app--%E5%B0%8F%E8%89%BAfunctioncomponent-%E5%B5%8C%E5%85%A5")
  • 可用性探测与四类错误降级
  • [通路二:小艺 → App(AgentExtensionAbility + A2A)](#通路二:小艺 → App(AgentExtensionAbility + A2A) "#%E9%80%9A%E8%B7%AF%E4%BA%8C%E5%B0%8F%E8%89%BA--appagentextensionability--a2a")
  • 踩坑与排错
  • 结论与适用边界
  • 参考资料

背景与前置

数易是什么:数易(EasyMath)是一款数学可视化应用,五个 Tab:函数拟合、学习空间、数学之美、计算工具、练习挑战。核心场景是「看得见的数学」------2D/3D 函数图像、参数滑杆实时重绘、极限逼近与积分黎曼和可视化。

为什么接小艺:数学问题天然适合「提问 → 解析 → 求解」。用户在数易里看到一个函数图像,最自然的下一个动作是「问一句这题怎么解」。与其自己造一个问答入口,不如把问题交给系统级的小艺智能体,让它在用户的自然语言里拿到上下文。

版本环境

项 本文环境
设备 手机 / 平板(Mate 80 实测)
系统 HarmonyOS 5.1+
开发工具 DevEco Studio 26
关键 Kit @kit.AgentFrameworkKit(FunctionComponent / FunctionController)、@kit.AbilityKit(AgentExtensionAbility)

两条通路的分工(这是本文的核心认知):

方向 组件 作用
App → 小艺 FunctionComponent 在 App 界面内嵌按钮,用户点击直接把当前问题抛给小艺
小艺 → App AgentExtensionAbility(A2A 协议) 小艺(或其它智能体)反向调用 App 的技能,如「帮我解这个方程」

只做第一条,用户得离开 App 去小艺;只做第二条,用户在 App 里没有入口。两条都做,才是完整闭环。

实际落地后的两个界面(数易「学习空间」章节内):

入口------底部的「问小艺」按钮,出现在每节知识点的思考题下方:

效果------点击后拉起小艺里的「数易-学习伙伴」智能体,直接回答当前问题:

这两张图正好对应下文的两条通路:按钮由 FunctionComponent 提供,而智能体能根据当前章节内容作答,靠的是 App 通过扩展能力反哺给小艺的业务知识。

通路一:App → 小艺(FunctionComponent 嵌入)

1. 封装一个可复用的胶囊按钮

FunctionComponent 是平台提供的声明式组件,通过 @Builder 包装成 WrappedBuilder 才能在业务组件里复用:

typescript 复制代码
import { FunctionComponent, FunctionController, ButtonType } from '@kit.AgentFrameworkKit'

export enum AgentButtonPreset { CIRCLE = 0, CAPSULE = 1, ICON_ABOVE_TITLE = 2 }

@Builder
export function AgentCapsuleBuilderFn(options: AgentCapsuleOptions) {
  FunctionComponent({
    agentId: options.agentId,
    onError: (err: BusinessError): void => { options.onError?.(err) },
    options: {
      title: options.title,
      queryText: options.queryText,
      buttonType: options.preset === AgentButtonPreset.CIRCLE ? ButtonType.CIRCLE :
        (options.preset === AgentButtonPreset.ICON_ABOVE_TITLE ? ButtonType.ICON_ABOVE_TITLE : ButtonType.CAPSULE)
    },
    controller: options.controller ?? undefined
  })
}

三档按钮形态对应三种界面语境:CAPSULE (胶囊,正文流内最常用)、CIRCLE (圆形,浮在图表角落)、ICON_ABOVE_TITLE(图标在上文字在下,工具栏位)。

数易的做法是业务侧自定义入口 + 平台组件按需渲染 :知识点思考题下方那颗「✨ 问小艺」按钮(见上文图 1)由业务侧用 SymbolGlyph + Text 绘制,保证风格与 App 一致;点击后由 FunctionComponent(能力可用时)打开智能体对话框,或走降级逻辑(能力不可用时复制问题)。这样做的好处是「入口长什么样」与「智能体能不能用」解耦------按钮外观稳定,能力差异只影响点击后的行为。

2. 会话控制器:把「打开/关闭」变成可监听的业务事件

FunctionController 是这套接入的状态中枢:既能探测能力,也能监听对话框生命周期。

typescript 复制代码
export class AgentKitSession {
  private controller: FunctionController = new FunctionController()

  isSupport(context: common.UIAbilityContext, agentId: string): Promise<boolean> {
    return this.controller.isAgentSupport(context, agentId)
  }
  onOpened(cb: () => void): void { this.controller.on('agentDialogOpened', cb) }
  onClosed(cb: () => void): void { this.controller.on('agentDialogClosed', cb) }
  dispose(): void {
    this.controller.off('agentDialogOpened')
    this.controller.off('agentDialogClosed')
  }
}

有两个细节值得强调:

  • 必须成对 off :on('agentDialogOpened') 这类监听在组件销毁时若不移除,页面切走再回来会重复触发(数易实测表现为「点一次按钮弹两次对话框」);
  • agentId 是云端智能体身份:它在小艺开发平台侧创建,App 侧只是一个字符串常量。我们的做法是集中定义单点常量,避免散落在多个页面:
typescript 复制代码
const CLOUD_AGENT_ID: string = 'agent4****************************' // 云端智能体 ID(脱敏)

可用性探测与四类错误降级

这是整篇最容易被忽略、但对线上质量影响最大的一段。智能体能力并非所有机型/账号都可用 ,直接渲染 FunctionComponent 可能白屏或点击无反应。

探测在前,渲染在后

typescript 复制代码
private async initAgent(ctx: common.UIAbilityContext): Promise<void> {
  const supported: boolean = await CommonCapability.isAgentAvailable(ctx, CLOUD_AGENT_ID)
  this.agentChecked = true
  if (!supported) return                    // 不可用:保持降级按钮
  try {
    const ns: ESObject = await import('./AgentKitEntry')   // 按需加载
    const session: AgentKitSession = ns.createSession()
    this.session = session
    this.capsuleBuilder = ns.agentCapsuleBuilder
    this.capsuleOptions = this.buildCapsuleOptions(session)
    this.agentReady = true
  } catch (err) {
    hilog.error(0x0001, 'AskXiaoYi', 'load agent kit entry failed')
  }
}

三个关键设计:

  1. isAgentSupport 先行 :返回 false 时根本不加载组件模块;
  2. 动态 import():智能体能力只在手机/平板端用,动态加载避免其它设备形态的白加载与包体积浪费;
  3. agentChecked 与 agentReady 双状态:探测中、探测完成、能力可用三个状态分开,UI 才不会在探测期间闪烁。

四类错误码,四种用户话术

onError 回调里的错误码不能笼统提示「失败了」,要按用户该做什么来分:

typescript 复制代码
private notifyAgentError(code: number): void {
  if (code === 1022400011) {          // 隐私未授权
    appSnackBarWarning(ui, $r('app.string.learning_xiaoyi_err_privacy'))
  } else if (code === 1022400012) {   // 账号异常
    appSnackBarWarning(ui, $r('app.string.learning_xiaoyi_err_account'))
  } else if (code === 1022400013) {   // 网络异常
    appSnackBarError(ui, $r('app.string.learning_xiaoyi_err_network'))
  } else {
    appSnackBarError(ui, $r('app.string.learning_xiaoyi_unavailable'))
  }
}
错误码 含义 用户可采取的动作
1022400011 隐私授权未完成 引导去设置页授权
1022400012 账号状态异常 引导登录/切换账号
1022400013 网络异常 提示检查网络,可重试
其它 能力不可用 降级方案(见下)

降级不是死路 :能力不可用时,按钮退化为普通按钮------点击把当前问题复制到剪贴板并提示「已复制,可粘贴到小艺」:

typescript 复制代码
private notifyUnavailable(): void {
  const head = res.getStringSync($r('app.string.learning_xiaoyi_unavailable'))
  if (this.questionText.length === 0) { appSnackBarWarning(ui, head); return }
  CommonCapability.copyToClipboard(this.questionText)
    .then(() => appSnackBarWarning(ui, head + tail))
    .catch(() => appSnackBarWarning(ui, head + copyFailedTail))
}

这一步让「不支持智能体的设备」也有可用路径,而不是给用户一个点了没反应的灰按钮。

通路二:小艺 → App(AgentExtensionAbility + A2A)

第一条通路是「用户在小艺里问」,第二条是「小艺反过来调用 App 的能力」。后者靠扩展能力实现。

1. 声明一个 type 为 agent 的扩展能力

module.json5 里要声明扩展能力,type 必须是 agent,并挂官方 metadata:

json5 复制代码
"extensionAbilities": [
  {
    "name": "AgentExtAbility",
    "srcEntry": "./ets/agentextability/AgentExtensionAbility.ets",
    "type": "agent",
    "exported": true,
    "description": "agent",
    "icon": "$media:appicon_easymath_starticon",
    "metadata": [
      { "name": "ohos.extension.agent", "resource": "$profile:agent_config" }
    ]
  }
]

2. 实现 AgentExtensionAbility 的四个生命周期

typescript 复制代码
export default class AgentExtAbility extends AgentExtensionAbility {
  private protocolHandler: A2AProtocolHandler = new A2AProtocolHandler()
  private skillDispatcher: SkillDispatcher = createDefaultDispatcher()
  private sessionState: A2ASessionState = new A2ASessionState()

  onCreate(want: Want): void { this.knowledgeBase.load(this.context.resourceManager) }
  onConnect(want: Want, proxy: common.AgentHostProxy): void { this.comProxy = proxy }
  onData(proxy: common.AgentHostProxy, data: string): void {
    /* 收到 A2A 请求 → 解析 → 分派到具体技能 → 回写结果 */
  }
  onDisconnect(want: Want, proxy: common.AgentHostProxy): void {
    this.comProxy = null
    this.sessionState = new A2ASessionState()   // 会话状态清零
  }
}

onData 收到的是 A2A 协议的 JSON(请求里 params.message.parts[].text 承载自然语言),解析后交给技能分派器。

一条选型说明 :官方 A2A 开发指南提供了 createA2AServer()(@kit.AgentFrameworkKit)封装,可直接处理 method 分发、TaskState 状态与 Artifact 回传。数易最终选择自己实现协议处理器(A2AProtocolHandler + SkillDispatcher),原因是:数学技能有 7 个细分 handler,需要按关键词做自定义路由,并把每个 handler 的结果组装成带步骤说明的自定义报文------官方的固定 method 语义反而要为它绕一层。如果只是标准问答场景,直接用 createA2AServer 更快;需要自定义路由与报文结构时才值得手写。

3. 技能分派:7 个数学技能各管一件事

数易把「数学能力」拆成互相独立的技能,用关键词路由:

typescript 复制代码
const dispatcher = new SkillDispatcher()
dispatcher.register({ name: 'problem-analysis', handler: handleProblemAnalysis, keywords: [...] })
dispatcher.register({ name: 'equation-formulation', handler: handleEquationFormulation, ... })
dispatcher.register({ name: 'equation-solving', handler: handleEquationSolving, ... })
dispatcher.register({ name: 'derivative-verification', handler: handleDerivativeVerification, ... })
dispatcher.register({ name: 'matrix-verification', handler: handleMatrixVerification, ... })
dispatcher.register({ name: 'determinant-verification', handler: handleDeterminantVerification, ... })
dispatcher.register({ name: 'knowledge-summary', handler: handleKnowledgeSummary, ... })
技能 做什么 典型用户问法
问题分析 判断题目类型、拆解条件 「这道题考什么」
方程列式 把自然语言条件转成方程 「三倍加二是十七」→ 3x+2=17
方程求解 求解并给出步骤 「解一下这个方程」
导数验证 校验用户的求导结果 「我算的导数对不对」
矩阵验证 校验矩阵运算 「这个矩阵乘法对吗」
行列式验证 校验行列式结果 「行列式算得对吗」
知识总结 归纳用到的知识点 「总结一下这类题的做法」

为什么拆这么细:每个 handler 是一个纯函数式的「输入文本 → 结果」,便于单测、便于按用户问题精准命中,也避免一个巨大 prompt 里塞所有能力导致回答发散。

4. 教程知识库:把 App 内容变成智能体的回答素材

除了技能,数易还接了一个 TutorialKnowledgeBase------从 resourceManager 加载 App 内的教程文本,供「知识总结」类问题直接引用。好处是回答口径与 App 内一致,不会出现「智能体说的和 App 教的不一样」。

踩坑与排错

坑 1:能力不支持时直接渲染组件 → 白屏/无反应

  • 现象:部分机型进入页面,按钮区域空白或点了没反应。
  • 根因 :未先调 isAgentSupport 就渲染 FunctionComponent。
  • 解法:先探测、后渲染;探测期间用降级按钮占位(见上文双状态设计)。

坑 2:错误码一律提示「网络错误」,用户被误导

  • 现象:用户明明有网,却被提示检查网络。
  • 根因 :把 1022400011(隐私)/1022400012(账号)也归到网络类。
  • 解法:按码分支给不同话术与不同引导动作(授权页 / 登录页 / 重试)。

坑 3:监听未解绑,点一次弹两次

  • 现象:页面返回再进入后,点击按钮弹出两个对话框。
  • 根因 :controller.on('agentDialogOpened') 注册后未 off,组件重建时叠加。
  • 解法 :在 aboutToDisappear 里成对 off,或把 controller 生命周期绑定到页面级单例。

坑 4:extensionAbilities 忘了 exported: true 或 type 写错

  • 现象 :小艺侧调用不到 App 能力,onConnect 不触发。
  • 根因 :扩展能力声明不完整;type 必须是 agent,且 metadata 要挂 ohos.extension.agent。
  • 解法 :按官方模板声明,并在真机上用 hilog 验证 onCreate → onConnect → onData 三步是否依次出现。

坑 5:A2A 会话状态跨请求残留

  • 现象:第二次提问带上了上一次的上下文,答案莫名跑偏。
  • 根因:会话状态对象未在断开时重置。
  • 解法 :onDisconnect 里重建 A2ASessionState,把「一次连接 = 一次会话」作为契约。

坑 6:把 agentId 散落在各页面

  • 现象:换智能体时漏改一处,某些页面仍连旧智能体。
  • 根因 :agentId 硬编码在多个组件里。
  • 解法:集中到单点常量(或配置),全工程只此一处引用。

结论与适用边界

这套接入适合谁:

  • App 内已有「结构化提问场景」(数学、法律、健康自查这类有明确输入→输出形态的领域);
  • 想让用户「不用离开 App」就能用上系统级智能体;
  • 同时希望智能体反过来调用 App 的专业能力(A2A),形成双向闭环。

不适合的场景:

  • 纯展示型页面,没有需要问答的上下文------硬塞按钮反而干扰;
  • 没有成体系的能力可拆成技能------A2A 那条通路会流于形式;
  • 设备覆盖要求极高且无法接受降级体验------需先评估目标机型的能力覆盖。

经验总结 :小艺接入的工程量不在「调通」,而在降级路径与错误话术。调通只需半天,把「不支持、未授权、账号异常、网络异常」四种情况都给出用户能行动的出口,才是能上线状态的那部分。

参考资料

官方文档(2026-10-29 逐条核验,均 HTTP 200)

工程内引用

  • entry/src/main/ets/components/learning/AgentKitEntry.ets --- FunctionComponent 封装
  • entry/src/main/ets/components/learning/AskXiaoYiButton.ets --- 按钮与降级
  • entry/src/main/ets/agentextability/ --- AgentExtensionAbility 与 7 个技能 handler

🔗 发布链接

  • 中小企业IT网(首发):www.cbismb.com/harmonyos/a...
  • 平台状态:审核中(2026-10-29 21:47 发布)
  • 站内分类:教育学习 | 站内标签:HarmonyOS、小艺、agent、Skill
相关推荐
Dream-Y.ocean1 小时前
鸿蒙PC平台逻控 PLC 编辑器适配实战:基于 Electron 壳方案的纯 JavaScript 梯形图编程与仿真引擎开发
javascript·编辑器·harmonyos
OH_TPC4 小时前
HarmonyOS APP开发---"好物清单"购物清单App,需要用到这个库
harmonyos
贾伟康6 小时前
【鸿蒙心迹】个人开发者上架复盘:备案、合规与审核指南没写透的细节
华为·harmonyos
不爱学英文的码字机器7 小时前
2026鸿蒙远程控制软件完全选购指南:从入门到精通选型攻略
华为·harmonyos
贾伟康1 天前
【共创稿事节】HarmonyOS 7.0 BindContentCover全屏模态:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui
KevinChaos1 天前
声音明信片实验室 03|HarmonyOS 7.0 音频空间化:双版本试听无缝切换
harmonyos
贾伟康1 天前
【共创稿事节】HarmonyOS 7.0 Menu菜单与上下文:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui
行者-全栈开发1 天前
Flutter 插件 device_calendar_plus 开源鸿蒙适配实战:从权限弹窗失灵到循环日程跑通
flutter·跨平台·harmonyos·arkts·日历·适配·开源鸿蒙
贾伟康1 天前
【共创稿事节】HarmonyOS 7.0 BindSheet半模态浮层:从接入到生产验证
性能优化·实战·harmonyos·arkts·arkui