Agent 页面接进真实应用以后有个容易被忽略的问题端侧到底怎么知道自己能调用哪些能力。这些能力如果只埋在服务端代码里端侧看不到参数要求也看不到风险高低就只能把按钮写死在页面上服务端每加一个能力端侧跟着改一次。这次把本机 Agent Gateway 里的工具整理成一份能力注册表。每个能力都带上名称、描述、输入结构、风险等级、要不要确认、返回示例。鸿蒙端不写死按钮改成从/tools动态读取这份注册表再按注册表里的信息决定哪些能直接执行。能力先得有一份能被读懂的说明这些能力最终都会落到某个执行入口查时间、看机器状态、检查 URL、写待办、检索材料、巡检设备。入口写在哪儿是次要的关键是入口前面有没有一份端侧和模型都能读懂的说明。本机 Gateway 先跑起来所有能力从这里统一暴露。模型 Key 只留在 Gateway 环境变量里鸿蒙端只跟 Gateway 通信也不去猜服务端有什么接口而是先读/tools返回的能力清单。清单里每个工具都有一组固定字段。拿http_check举例它带着风险等级和参数要求不是给个 URL 就完事{name:http_check,description:检查一个 HTTP 地址是否能访问用于发布前接口巡检。,riskLevel:medium,needConfirm:true,inputSchema:{required:[url]}}这几个字段直接决定端侧怎么处理这个能力。riskLevel决定页面上怎么标风险needConfirm决定能不能直接点执行inputSchema交给 Gateway 做参数校验。模型能选哪个工具但选不出清单以外的东西。普通接口列表告诉开发者有这么个 URL就够了。这份注册表要多担一层把能力做什么、要什么参数、调用前要不要确认、返回大概什么样都写进去让端侧和模型直接读。低风险能力可以直接执行time_now和local_status是低风险工具只读本机 Gateway 状态不改数据也不碰外部系统端侧可以放开让它们直接执行。先在终端里调time_now和local_status返回里有当前时间、时区、系统版本、机器架构、磁盘使用率还有一个 requestId。这个 requestId 有用处。就算是低风险工具也得保留一次调用的身份。终端、端侧页面、Gateway 日志里出现同一个 requestId一次调用才追得回来排查的时候不用光靠时间点去猜是哪一次。风险低不代表能跳过校验。Gateway 该先查工具在不在、参数合不合 schema一步都不少。端侧按钮只是个入口真正卡边界的地方在服务端。调一个没注册的工具直接被拒为了看边界在哪故意调一个根本没注册的工具{tool:delete_everything,arguments:{}}Gateway 回了TOOL_NOT_FOUND没有去做任何模糊匹配也没把这事丢给模型自由发挥。这一关挺要紧。智能体系统最怕的就是看起来什么都懂模型把用户一句话解释成一个不存在的能力再指望服务端临时兜底。有了注册表工具名必须来自白名单白名单以外的一律不执行。参数校验必须压在服务端note_search要一个query参数。故意不传Gateway 返回ARGUMENT_MISSING: query把参数补上工具才正常跑。这道校验放在服务端不能只放在 ArkUI 页面里。端侧可以做更友好的提示但它不能当唯一的防线。只要请求能到 Gateway就得按同一份 schema 校验参数不然换个入口绕过页面就能钻空子。注册表里的inputSchema顺带也让端侧知道每个工具要什么参数。当前页面先展示了基础字段已经能说明能力边界是从服务端来的不是写死在某个按钮上。鸿蒙端从注册表把能力列表生成出来端侧页面没把工具写死在数组里。核心就一个loadTools()请求/tools把返回的tools塞进State tools交给List渲染。页面做成了一个轻量的能力控制台顶上是工具总数、可直接执行数、需要确认数中间能按全部、低风险、需确认筛每一条列出名称、描述、风险等级和确认策略。加载完鸿蒙端显示 8 个工具7 个可直接执行1 个需要确认。这几个数字不是页面里写死的是从 Gateway 返回的注册表算出来的。这个页面比单纯列一排按钮更像个能力控制台。用户不光知道有这个能力还能看到它的风险和执行条件。像http_check这种中风险工具端侧只给个锁定状态不让直接点低风险和中风险在同一个页面里被明确分开。执行结果回到同一个面板点time_now的执行按钮页面拼一个tool-registry-加时间戳的 requestIdPOST 到/tools/call底部最近执行区域直接回一段完整 JSON。页面不跳转、不弹窗结果就留在当前这块。time_now结果短适合先验证一遍端侧的执行闭环local_status会带回系统、机器架构、磁盘使用率和检查时间能证明这次调用真的读到了机器状态。结果留在同一个面板里用户能把点了哪个工具和工具回了什么对上号。端侧又压了一层比风险等级更保守的白名单注册表里 8 个工具7 个标的是低风险。但页面并没有把这 7 个全放开让点。判断一个工具能不能执行的canRun除了看needConfirm还额外卡了一个工具名白名单canRun(tool:ToolItem):boolean{return!tool.needConfirm(tool.nametime_now||tool.namelocal_status)}意思是哪怕注册表说某个工具低风险端侧这一版也只放开自己实际验证过的time_now和local_status其余低风险工具在页面上显示成只读需要确认的工具显示成锁定。同样是不能点端侧用两种文案分开一个是这一版还没放开一个是风险更高、要走确认流程。风险等级是服务端给的建议端侧在这个建议上又收了一道宁可少放不抢跑。可执行、只读、锁定这三种状态落到代码里就是一个if (this.canRun(tool))分支加一个三元判断页面不用为每个工具单独写逻辑全靠注册表字段加这一层端侧策略推出来。筛选也是同一个思路visibleTools()按filterMode过滤lowRiskCount()、confirmCount()直接在State tools上算顶部那三个统计数、中间那份列表都跟着注册表返回的数据走没有一个是写死的常量。能力边界这样就定住了走到这一步本机 Gateway 这边的能力边界已经清楚模型只能从注册表里挑工具挑不出清单以外的东西参数不合 schema 会在 Gateway 被挡下中风险工具在注册表里就标好了要确认。端侧这边也不再靠写死的按钮加一个能力、改一个能力的描述或风险等级页面重新拉一次/tools就跟着变不用动 ArkTS 代码。这份注册表是本机自建的不是什么正式的能力发布流程。但它把加能力时最容易漏的几件事凑齐了能力怎么描述、什么时候该触发、要哪些参数、风险多高、返回什么。少任何一样端侧要么得靠写死的按钮硬对接要么退回到模型随口说个接口名、服务端临时补救。把这些信息集中进注册表、让端侧去读加能力和改能力这件事才从改两处代码变成改一处配置。
把工具变成可发现能力:鸿蒙端动态能力注册表实践
Agent 页面接进真实应用以后有个容易被忽略的问题端侧到底怎么知道自己能调用哪些能力。这些能力如果只埋在服务端代码里端侧看不到参数要求也看不到风险高低就只能把按钮写死在页面上服务端每加一个能力端侧跟着改一次。这次把本机 Agent Gateway 里的工具整理成一份能力注册表。每个能力都带上名称、描述、输入结构、风险等级、要不要确认、返回示例。鸿蒙端不写死按钮改成从/tools动态读取这份注册表再按注册表里的信息决定哪些能直接执行。能力先得有一份能被读懂的说明这些能力最终都会落到某个执行入口查时间、看机器状态、检查 URL、写待办、检索材料、巡检设备。入口写在哪儿是次要的关键是入口前面有没有一份端侧和模型都能读懂的说明。本机 Gateway 先跑起来所有能力从这里统一暴露。模型 Key 只留在 Gateway 环境变量里鸿蒙端只跟 Gateway 通信也不去猜服务端有什么接口而是先读/tools返回的能力清单。清单里每个工具都有一组固定字段。拿http_check举例它带着风险等级和参数要求不是给个 URL 就完事{name:http_check,description:检查一个 HTTP 地址是否能访问用于发布前接口巡检。,riskLevel:medium,needConfirm:true,inputSchema:{required:[url]}}这几个字段直接决定端侧怎么处理这个能力。riskLevel决定页面上怎么标风险needConfirm决定能不能直接点执行inputSchema交给 Gateway 做参数校验。模型能选哪个工具但选不出清单以外的东西。普通接口列表告诉开发者有这么个 URL就够了。这份注册表要多担一层把能力做什么、要什么参数、调用前要不要确认、返回大概什么样都写进去让端侧和模型直接读。低风险能力可以直接执行time_now和local_status是低风险工具只读本机 Gateway 状态不改数据也不碰外部系统端侧可以放开让它们直接执行。先在终端里调time_now和local_status返回里有当前时间、时区、系统版本、机器架构、磁盘使用率还有一个 requestId。这个 requestId 有用处。就算是低风险工具也得保留一次调用的身份。终端、端侧页面、Gateway 日志里出现同一个 requestId一次调用才追得回来排查的时候不用光靠时间点去猜是哪一次。风险低不代表能跳过校验。Gateway 该先查工具在不在、参数合不合 schema一步都不少。端侧按钮只是个入口真正卡边界的地方在服务端。调一个没注册的工具直接被拒为了看边界在哪故意调一个根本没注册的工具{tool:delete_everything,arguments:{}}Gateway 回了TOOL_NOT_FOUND没有去做任何模糊匹配也没把这事丢给模型自由发挥。这一关挺要紧。智能体系统最怕的就是看起来什么都懂模型把用户一句话解释成一个不存在的能力再指望服务端临时兜底。有了注册表工具名必须来自白名单白名单以外的一律不执行。参数校验必须压在服务端note_search要一个query参数。故意不传Gateway 返回ARGUMENT_MISSING: query把参数补上工具才正常跑。这道校验放在服务端不能只放在 ArkUI 页面里。端侧可以做更友好的提示但它不能当唯一的防线。只要请求能到 Gateway就得按同一份 schema 校验参数不然换个入口绕过页面就能钻空子。注册表里的inputSchema顺带也让端侧知道每个工具要什么参数。当前页面先展示了基础字段已经能说明能力边界是从服务端来的不是写死在某个按钮上。鸿蒙端从注册表把能力列表生成出来端侧页面没把工具写死在数组里。核心就一个loadTools()请求/tools把返回的tools塞进State tools交给List渲染。页面做成了一个轻量的能力控制台顶上是工具总数、可直接执行数、需要确认数中间能按全部、低风险、需确认筛每一条列出名称、描述、风险等级和确认策略。加载完鸿蒙端显示 8 个工具7 个可直接执行1 个需要确认。这几个数字不是页面里写死的是从 Gateway 返回的注册表算出来的。这个页面比单纯列一排按钮更像个能力控制台。用户不光知道有这个能力还能看到它的风险和执行条件。像http_check这种中风险工具端侧只给个锁定状态不让直接点低风险和中风险在同一个页面里被明确分开。执行结果回到同一个面板点time_now的执行按钮页面拼一个tool-registry-加时间戳的 requestIdPOST 到/tools/call底部最近执行区域直接回一段完整 JSON。页面不跳转、不弹窗结果就留在当前这块。time_now结果短适合先验证一遍端侧的执行闭环local_status会带回系统、机器架构、磁盘使用率和检查时间能证明这次调用真的读到了机器状态。结果留在同一个面板里用户能把点了哪个工具和工具回了什么对上号。端侧又压了一层比风险等级更保守的白名单注册表里 8 个工具7 个标的是低风险。但页面并没有把这 7 个全放开让点。判断一个工具能不能执行的canRun除了看needConfirm还额外卡了一个工具名白名单canRun(tool:ToolItem):boolean{return!tool.needConfirm(tool.nametime_now||tool.namelocal_status)}意思是哪怕注册表说某个工具低风险端侧这一版也只放开自己实际验证过的time_now和local_status其余低风险工具在页面上显示成只读需要确认的工具显示成锁定。同样是不能点端侧用两种文案分开一个是这一版还没放开一个是风险更高、要走确认流程。风险等级是服务端给的建议端侧在这个建议上又收了一道宁可少放不抢跑。可执行、只读、锁定这三种状态落到代码里就是一个if (this.canRun(tool))分支加一个三元判断页面不用为每个工具单独写逻辑全靠注册表字段加这一层端侧策略推出来。筛选也是同一个思路visibleTools()按filterMode过滤lowRiskCount()、confirmCount()直接在State tools上算顶部那三个统计数、中间那份列表都跟着注册表返回的数据走没有一个是写死的常量。能力边界这样就定住了走到这一步本机 Gateway 这边的能力边界已经清楚模型只能从注册表里挑工具挑不出清单以外的东西参数不合 schema 会在 Gateway 被挡下中风险工具在注册表里就标好了要确认。端侧这边也不再靠写死的按钮加一个能力、改一个能力的描述或风险等级页面重新拉一次/tools就跟着变不用动 ArkTS 代码。这份注册表是本机自建的不是什么正式的能力发布流程。但它把加能力时最容易漏的几件事凑齐了能力怎么描述、什么时候该触发、要哪些参数、风险多高、返回什么。少任何一样端侧要么得靠写死的按钮硬对接要么退回到模型随口说个接口名、服务端临时补救。把这些信息集中进注册表、让端侧去读加能力和改能力这件事才从改两处代码变成改一处配置。