【HarmonyOS学习笔记】2026-08-04 | 端插件新装饰器与CreateRecord全链路验证
date: 2026-08-04
tags: HarmonyOS, 端插件, InsightIntentEntry, 小艺开放平台, CreateRecord, LLM, 参数链路
type: 实战笔记
一、@InsightIntentEntry 新装饰器 vs legacy
端插件有两种实现方式,我一开始用的是 legacy 方式,踩坑后查到了新 API。
对比
| 特性 | legacy InsightIntentExecutor | @InsightIntentEntry (since API 20) |
|---|---|---|
| 装饰器 | 无 | @InsightIntentEntry |
| 意图分发 | 单个Executor按intentName switch分发 | 每个意图独立文件+类 |
| 参数注入 | 手动从param取值 | 框架自动注入同名public属性 |
| 注册方式 | insightIntents数组 + srcEntry指向Executor | insightIntentsSrcEntry数组 + srcEntry指向各意图文件 |
| metadata | ohos.insight_intent | ohos.insight_intent |
新版代码示例
arkts
@InsightIntentEntry({
intentName: 'CreateRecord',
domain: 'com.example.myapp',
intentVersion: 1,
displayName: '创建记录',
llmDescription: '创建一条记录,关联到所属父级',
keywords: ['记录', '新增', '创建'],
abilityName: 'EntryAbility',
executeMode: insightIntent.ExecuteMode.UI_ABILITY_BACKGROUND,
parameters: {
title: { type: 'string' },
parentId: { type: 'string' },
scheduledDate: { type: 'string' }
}
})
export class CreateRecordImpl extends InsightIntentEntryExecutor<CreateRecordResult> {
public title: string = ''
public parentId: string = ''
public scheduledDate: string = ''
async onExecute(): Promise<insightIntent.IntentResult<CreateRecordResult>> {
// title/parentId/scheduledDate 已由框架自动注入
}
}
insight_intent.json 两种格式
legacy格式(insightIntents数组,所有意图指向同一个Executor):
json
{
"insightIntents": [
{
"intentName": "UpdateRecord",
"domain": "com.example.myapp",
"intentVersion": 1,
"srcEntry": "./ets/entryability/IntentExecutor.ets",
"uiAbility": { "ability": "EntryAbility", "executeMode": "background" }
}
]
}
新格式(insightIntentsSrcEntry数组,每个意图指向独立文件):
json
{
"insightIntentsSrcEntry": [
{ "srcEntry": "./ets/insightintents/CreateRecordImpl.ets" }
]
}
新装饰器的3种UI能力
| 装饰器 | 功能 | 适用场景 |
|---|---|---|
| InsightIntentPage | 端插件触发时打开App指定页面 | 深度链接跳转 |
| InsightIntentLink | 端插件触发时打开深度链接 | 页面导航 |
| InsightIntentForm | 端插件绑定到桌面卡片(Widget) | 桌面卡片展示 |
踩坑注意点
新API比legacy更简洁:自动注入参数、独立文件避免switch分发。 但要注意:
- 属性名必须与parameters的key一致,否则框架无法注入
- llmDescription影响LLM何时调用这个意图,要认真写
- keywords帮助LLM匹配用户意图,要覆盖常见同义词
二、端插件三种执行模式
模式对比
| 模式 | 能渲染UI? | 说明 |
|---|---|---|
| 后台模式(background) | ❌ | 纯数据处理,不显示任何界面 |
| 前台模式(foreground) | ✅ | 收到WindowStage,可加载全屏自定义页面 |
| UI扩展模式(UIExtension) | ✅ | 收到UIExtensionContentSession,可渲染自定义UI内容 |
踩坑:页面操控选错导致报错
现象:后台存储类端插件在平台上选择"涉及端侧app页面操控=是"后,小艺调用时报错:"应用未安装或未升级到最新版本"。
根因:"页面操控=是"意味着端插件执行时需要以前台模式拉起App的UI页面。但后台存储操作不需要UI,选择"是"后系统尝试前台拉起App,可能因签名、权限等原因拉不起来,就报"未安装"。
教训 :端插件工具的"页面操控"选项必须按实际需求选择。 后台存储类工具选"否",只有真正需要展示UI的工具才选"是"。
三、CreateRecord 三轮验证
第一轮:参数全空
onExecute START: title=, parentId=, scheduledDate=
create: record rec_5 created
LLM知道要调CreateRecord工具,但不填任何参数。
根因:平台上参数的description=""、required=false。LLM不知道填什么,也不觉得必须填。
修复:补上清晰的description,title和parentId设为required。
第二轮:title有值,parentId和scheduledDate空
onExecute START: title=去XX地点做XX事, parentId=, scheduledDate=
用户说了时间信息,但LLM没有单独提取到scheduledDate。
根因:scheduledDate的description太弱("计划日期,如明天"),对optional参数,LLM倾向跳过或把时间信息融进title而不是单独提取。
修复:加强description为"时间,如明天,2026-08-04,如果没有默认当前日期"。
第三轮:三个参数都有值
onExecute START: title=明天要做XX事, parentId=XX父级, scheduledDate=明天
create: record rec_11 created ✅
onExecute START: title=明天要做YY事, parentId=YY父级, scheduledDate=明天
create: record rec_12 created ✅
✅ 全链路跑通:LLM调工具→系统路由→端侧执行→存储→返回code:0
遗留问题
| 问题 | 说明 | 修复方向 |
|---|---|---|
| parentId填的是描述而非ID | LLM把"XX父级"填到parentId,而非"parent_1"这样的ID | parentId描述改为"父级ID,由SetParent工具返回的parentId值";或改为parentTitle让LLM填描述,端侧自己查ID |
| LLM假装调工具 | 小艺回复"创建成功"但端侧没执行记录,LLM用文字模拟了工具调用 | 端插件返回值包含明确的执行标记(_source+操作结果),提示词约束"必须基于工具返回值回复,不得编造结果" |
三轮测试的教训
| 踩坑 | 根因 | 教训 |
|---|---|---|
| 参数全空 | description="" + required=false | description像给新人写文档一样写清楚,required严格按业务设置 |
| optional参数被跳过 | description太弱,LLM倾向跳过或合并到其他字段 | optional参数的description要更强,特别是时间/日期等容易融进title的信息 |
| parentId填描述 | description写"属于哪个父级",LLM理解为描述文字 | 参数名和description要匹配LLM的理解方式,"parentId"要说明"由XX工具返回的ID值" |
| 页面操控选错 | 后台工具选"是"导致前台拉起失败 | 后台存储类选"否",真正需要UI才选"是" |
四、参数定义链路:三处一致性
开发过程中我一直困惑"参数到底在哪边定义",直到三轮测试后才理清。
完整链路
平台侧参数配置 端侧代码
────────────── ──────
description + required @InsightIntentEntry的parameters
→ LLM看这个 → 框架看这个
→ 决定提取什么参数 → 声明参数Schema
→ 决定填不填 → 必须与平台同名一致
执行器类public属性
→ 框架自动注入同名参数值
→ 属性名须与parameters key一致
平台参数定义=LLM的说明书,端侧属性=框架的接收器,两者必须同名一致。
三处参数定义对比
| 位置 | 作用 | 谁看 | 示例 |
|---|---|---|---|
| 平台工具配置的参数 | 告诉LLM"你可以填这些参数" | LLM | title: String, required, "记录描述" |
| @InsightIntentEntry的parameters | 声明参数Schema给框架 | 系统/框架 | {"title": {"type": "string"}} |
| 执行器类的public属性 | 接收注入的参数值 | 框架注入 | public title: string |
三处必须同名且类型一致。平台说有3个参数,端侧就要有3个同名属性。
踩坑:修改平台参数后必须重新发布
现象:修改平台参数description后直接测试,LLM行为没有变化。
根因:平台上的工具参数配置属于插件元数据,修改后需要重新发布才能生效。LLM读取的是已发布版本的元数据,不是草稿。
教训 :修改平台工具参数后必须重新发布插件,否则LLM看到的还是旧配置。 这个生效周期可能包含审核时间,修改前要确认好再提交。
我补充的经验:云上更改≠实机生效
调试中我进一步发现,小艺开放平台会因为缓存或网络等不确定因素 ,导致云上更改和实机测试实际版本不一致。
这不是简单的"改完要重新发布"这么明确------即使重新发布了,也可能因为缓存/网络延迟,实机拿到的还是旧版本。
处理方式:等待、刷新、或者重新发布。改完平台配置后,要留出传播时间,必要时强制刷新或重新发布,确认实机版本与云上一致后再测试。否则会出现"明明改了,LLM行为却不变"的假象,容易误判为代码问题。
五、LLM行为踩坑
5.1 LLM有时"假装"调工具
现象:小艺回复"已为你创建了一个明天去XX的记录",但端侧没有任何onExecute日志。看起来像调了端插件,实际没调。
根因:LLM有时会选择不调用工具,而是用文字描述"已经做了"来回复用户。这在以下场景更容易发生:
- 插件刚重新发布,LLM的缓存还没更新
- LLM对工具调用的信心不足(参数不确定等)
- LLM觉得文字回复就够了
验证方法:
| 方法 | 真实调用 | LLM假装 |
|---|---|---|
| 查端侧日志 | 有onExecute START |
没有 |
| 查返回值 | 有_source和_timestamp |
无返回值 |
| 查小艺回复 | "工具返回结果:..." | "已为你创建..." |
5.2 parentId填了描述而非ID
现象:parentId有值了,但填的是父级描述而非父级ID。
根因:parentId的description是"属于哪个父级",LLM理解为"父级的描述文字"。因为只测试了单个工具,没有SetParent先创建父级,LLM不知道parentId应该是什么格式的ID。
修复方向:
- 改description为"父级ID,由SetParent工具返回的parentId值",让LLM知道这是ID
- 或者改字段名为parentTitle,让LLM填描述,端侧自己根据描述查找或创建父级
- 多个工具配合使用时,先有父级才有父级ID
教训 :参数名和description要匹配LLM的理解方式。 "parentId"对LLM来说可能理解为"父级的描述"而非"父级的ID",除非description明确说明格式和来源。
5.3 返回值可观测标记
端插件返回结果包含两个调试字段:
arkts
result: {
actionId: actionId,
_source: 'MyEndPlugin',
_timestamp: Date.now()
}
用途:
_source:确认数据来自端插件真实执行,而非LLM模拟_timestamp:端侧处理时间戳,可对比调用时序
5.4 我补充的经验:幻觉问题的双方案
大模型幻觉问题会导致一个很坑的现象:明明没有调用端插件,返回的答案却说调过了端插件。小艺回复"已创建成功",但端侧根本没有执行记录------这是LLM在"编"结果,不是真实执行。
针对这个问题,我有两个方案:
方案①:在端插件侧加入详细日志
端插件每次执行都记录关键日志(onExecute进入、参数值、执行结果、返回值),作为"是否真的执行过"的铁证。出现疑似幻觉时,直接查端侧日志就能判定------有日志就是真调了,没日志就是LLM编的。
方案②:在云端大模型侧加上经过验证的明确且稳定的可观测标记
在端插件的返回值(会回到云端LLM上下文里)中加入稳定、明确的可观测标记,例如 _source(数据来源)+ _timestamp(执行时间)。这个标记经过验证、不会丢失、格式稳定,LLM的回复必须能对上这些标记,才能证明端插件真的执行过。
两个方案配合使用:端侧日志是"是否执行"的物理证据,云端可观测标记是"LLM是否基于真实返回作答"的逻辑证据。光有日志,LLM照样能编;光有标记,查不到端侧明细。两个一起上,才能确认更改实际准确性。
5.5 平台参数配置的三类陷阱
| 陷阱类型 | 表现 | 防御方法 |
|---|---|---|
| 参数description空 | LLM不知道填什么,参数全空 | description像给新人写文档一样写清楚 |
| required设置错误 | LLM跳过必填参数或强填可选参数 | 严格按业务需求设置required |
| 页面操控选错 | 后台工具选"是"导致报错 | 后台存储类选"否",真正需要UI才选"是" |
5.6 端插件调试的三类陷阱
| 陷阱类型 | 表现 | 防御方法 |
|---|---|---|
| LLM假装调工具 | 小艺说成功但端侧没执行 | 端插件返回_source+操作结果,提示词约束"必须基于工具返回值回复,不得编造结果" |
| 改参数没发布 | 修改后LLM行为不变 | 修改后必须重新发布插件;且注意缓存/网络导致云上更改与实机版本不一致,需等待/刷新/重新发布 |
| 参数两端不一致 | 框架注入不到属性 | 平台参数名、@InsightIntentEntry的parameters key、执行器public属性三处同名 |
六、经验法则汇总
- 遇到端插件参数为空 --- 先查平台description是否为空、required是否正确
- 遇到小艺说成功但端侧没执行 --- LLM可能在假装,查端侧日志或返回值可观测标记
- 修改平台参数后 --- 必须重新发布插件,否则LLM看到的还是旧配置
- 参数三处定义 --- 平台参数名、@InsightIntentEntry的parameters key、执行器public属性必须同名一致
- optional参数的description要更强 --- 特别是时间/日期等容易融进title的信息,description要明确说明"从用户话中提取单独填入"
- 后台存储类工具选"页面操控=否" --- 选"是"会导致系统尝试前台拉起App,增加约束和失败风险
- 端插件返回值加可观测标记 ---
_source标识来源、_timestamp标识时序,防止LLM编造结果 - 云上更改≠实机生效 --- 小艺开放平台会因缓存或网络导致云上配置和实机版本不一致,需要等待、刷新或重新发布后再测试
- 幻觉问题双方案 --- 端插件侧加详细日志(物理证据)+ 云端大模型侧加经过验证的明确稳定的可观测标记(逻辑证据),两者配合确认更改实际准确性
- 新API(@InsightIntentEntry)比legacy更优 --- 自动注入参数、独立文件避免switch分发
- 提示词中递归逻辑必须显式约束 --- 明确写出"缺口→递归→直到明天可行",否则LLM倾向跳步
先画参数链路图再写代码,先查SDK .d.ts再写import,能省很多调试时间。
学习小结:端插件开发的核心是理解"三处一致"------平台参数配置(LLM的说明书)、@InsightIntentEntry的parameters(框架的Schema)、执行器public属性(注入的接收器)必须同名一致。CreateRecord三轮验证揭示了LLM行为规律:description决定LLM填什么,required决定LLM填不填,optional参数必须强化description否则被跳过。两个最深的坑:一是平台更改与实机生效之间存在缓存/网络导致的不一致,要等待/刷新/重新发布;二是LLM幻觉会"假装"调工具,需要端侧详细日志+云端经过验证的稳定可观测标记双方案配合,才能确认更改实际准确性。
懿路向前 · AI辅助整理
2026-08-04