资讯详情 AI Agent技能管理器:可视化契约与运行时治理
📅 2026/11/3 12:00:45
1. 这不是又一个“AI Agent 面板”而是一套真正能落地的技能调度中枢你有没有遇到过这样的情况花两周时间给 AI Agent 写了 8 个工具函数——查天气、读邮件、调用 CRM、生成周报、解析 PDF、发钉钉消息、查内部知识库、格式化 Excel……结果上线三天就崩了不是模型挂了是某个工具函数里硬编码的 API 地址写错了不是 prompt 不够好是用户突然要求“把上周五的销售数据导出成带颜色的表格”而你那个export_excel函数压根没处理样式参数更尴尬的是运营同事想临时禁用“自动发邮件”功能做灰度测试你得翻三遍代码、改配置、重启服务、再验证——整个过程耗时 47 分钟。这就是当前绝大多数 AI Agent 项目的隐痛技能Skill散落在代码里、文档里、团队记忆里没人知道它长什么样、谁在用、能不能用、要不要更新。我们管它叫“技能黑箱”。而标题里这个“统一管理一个给 AI Agent 用的可视化技能管理器”它要解决的根本不是“让界面好看一点”而是把技能从黑箱里捞出来变成可发现、可验证、可灰度、可审计、可协作的第一等公民。它不是 dashboard不是监控页也不是简单的 API 列表页。它的核心关键词是“可视化”“技能管理器”—— 注意这里“可视化”的对象不是数据流或指标曲线而是技能本身的结构、状态、依赖与行为边界。它对标的是 DevOps 里的 Service Catalog是 SRE 里的 Runbook Registry是前端团队的 Component Library。只不过它的消费者不是人而是 LLM 的 planning 模块它的输入不是 YAML 文件而是SKILL.md这种轻量级、机器可读、人类可写的契约文档它的输出不是 HTML 页面而是标准化的 JSON Schema 可执行函数注册表 实时健康快照。我从去年开始在三个不同规模的 Agent 项目里落地这套思路一个 5 人初创团队做客服智能体一个 30 人中台团队支撑 12 条业务线的营销 Agent还有一个国企背景的政务知识助手项目。我们试过纯代码管理、Markdown 文档托管、Confluence 表格维护、甚至用 Notion 做技能看板——全都不行。直到把SKILL.md作为唯一事实源配合一个极简但精准的 Web 管理器才真正把技能生命周期管住。它不替代你的代码也不绑架你的架构它只做一件事让每个技能都有一张清晰、可信、随时可查的“身份证”。适合正在从“单个 demo”走向“多 Agent 协同生产环境”的所有团队尤其适合那些已经踩过“技能失控”坑的工程师、技术负责人和 AI 产品经理。2. 为什么必须是“可视化技能管理器”而不是继续写文档或加注释2.1 技能管理的三大死循环文档根本破不了很多团队的第一反应是“我们写清楚点不就行了”于是开始疯狂补README.md、加 docstring、建 Confluence 页面。但现实很快打脸——这不是文档质量的问题而是技能本质与文档载体之间存在不可调和的结构性矛盾。我用三个真实案例说明案例一参数漂移陷阱我们有个search_knowledge_base(query: str, top_k: int 3)技能最初文档写的是top_k 默认为 3。半年后业务方要求“默认返回 5 条”后端同学改了代码但忘了同步更新 Confluence 和 docstring。LLM planner 在调用时仍按top_k3构造参数结果返回结果不足用户投诉率上升 17%。问题不在人而在文档无法参与运行时校验。案例二依赖幻觉send_notification(title, content, channeldingtalk)这个技能在文档里写着“支持钉钉、企业微信、飞书”。但实际代码只实现了钉钉企业微信模块因 SDK 版本冲突被注释掉了且没人记得这事。LLM 在规划时自信地选了channel: wechat调用直接报NotImplementedError。文档描述的是“应该是什么”而代码实现的是“实际是什么”两者长期脱钩。案例三状态盲区某金融 Agent 的verify_identity(id_card_no, name)技能依赖外部公安接口。某天该接口因政策调整暂停服务但技能本身无任何熔断逻辑也未在文档中标注“强依赖外部服务”。LLM 依然把它当作可用技能调用导致大量身份核验失败客服电话被打爆。文档无法反映实时状态更无法承载健康检查机制。这三点恰恰是传统文档管理的阿喀琉斯之踵它静态、离线、无人维护、无法验证。而可视化技能管理器就是为打破这三重枷锁而生。2.2 “可视化”的真实含义让抽象契约具象化、可交互、可验证很多人看到“可视化”第一反应是“做个漂亮大屏”。但在这个场景下“可视化”有非常具体的工程语义契约可视化把SKILL.md中定义的input_schema、output_schema、examples、constraints渲染成可展开/折叠的 JSON Schema 树支持鼠标悬停查看字段说明点击字段跳转到对应文档段落。这不是渲染 Markdown而是把文本契约翻译成可交互的 UI 元素。状态可视化不是画个折线图而是用三种颜色直观标识技能状态绿色健康HTTP 200 响应 800ms 成功率 99.5%、黄色亚健康响应 1200ms 或成功率 95%~99.5%、红色不可用超时/错误率 5%。这个状态每 30 秒刷新一次且点击可查看最近 10 次调用的原始日志片段。调用链可视化当某个技能被 Agent 调用时管理器自动捕获trace_id并在技能详情页展示“本次调用上下文”谁哪个 Agent 实例调用的、传了什么参数、返回了什么、耗时多少、是否触发了 fallback。这相当于给每个技能装了个微型 APM 探针。变更可视化每次SKILL.md提交 Git管理器自动解析 diff生成“本次变更影响报告”新增了哪些字段、删除了哪些约束、修改了哪些示例。它不替代 Git而是把 Git 的原子操作翻译成工程师能一眼看懂的业务影响。所以这个“可视化”本质是将技能的元信息metadata、运行时信息runtime info、治理信息governance info三者融合通过 UI 层进行语义对齐与即时反馈。它不是装饰是诊断工具是协作界面是信任基础设施。2.3 为什么必须是“管理器”而非“注册中心”或“网关”有人会问我们已经有 API 网关、服务注册中心为什么还要一个“技能管理器”关键在于角色定位完全不同维度API 网关服务注册中心可视化技能管理器核心职责流量转发、鉴权、限流服务发现、心跳检测、实例列表技能契约管理、状态监控、调用审计、版本治理关注对象HTTP 请求/响应服务实例IPPort技能Skill—— 一个逻辑能力单元可能跨多个服务、多个函数、多个模型数据来源OpenAPI Spec / 自定义配置服务主动上报 / DNSSKILL.md文件 运行时探针 Git Hook使用者前端、移动端、其他后端服务服务消费者SDK、SidecarLLM Planner、工程师、测试人员、产品经理、运维举个具体例子一个“生成合同摘要”的技能背后可能涉及1调用 OCR 服务识别 PDF2调用大模型提取关键条款3调用规则引擎校验法律术语。这三个服务在注册中心里是三个独立实例在网关里是三条路由但在技能管理器里它们被聚合为一个summarize_contract技能其SKILL.md定义了统一的输入PDF base64、输出JSON 结构化摘要、约束仅支持 PDF最大 50MB、示例含真实合同片段。管理器不关心它们怎么部署只关心“这个技能整体是否可用、是否符合契约”。这才是“管理器”的价值——它站在能力视角而非服务视角或流量视角。它让 AI Agent 的 planner 模块能像人类一样“理解”一个技能能做什么、不能做什么、风险在哪而不是靠猜或试错。3. 核心设计SKILL.md 契约规范与管理器的双向驱动机制3.1 SKILL.md轻量、可读、可执行的技能契约语言SKILL.md是整个系统的基石。它不是随意写的文档而是一种严格约定的、面向 LLM 和人类的双语契约格式。我们坚持“最小必要原则”只保留对技能治理真正关键的字段拒绝过度设计。一个典型SKILL.md如下以search_sales_data为例# search_sales_data 查询指定时间段内的销售数据支持按区域、产品线筛选 ## Description 用于获取销售系统中的历史订单数据供 Agent 生成销售分析报告。**不支持实时数据最新数据延迟 2 小时。** ## Input Schema json { type: object, properties: { start_date: { type: string, format: date, description: 查询起始日期格式 YYYY-MM-DD }, end_date: { type: string, format: date, description: 查询结束日期格式 YYYY-MM-DD }, region: { type: string, enum: [华东, 华南, 华北, 西南, 西北, 东北], description: 销售区域留空则查询全部区域 }, product_line: { type: string, description: 产品线名称留空则查询全部产品线 } }, required: [start_date, end_date] }Output Schema{ type: object, properties: { total_orders: { type: integer, description: 订单总数 }, total_revenue: { type: number, description: 总销售额元 }, top_products: { type: array, items: { type: object, properties: { name: {type: string}, revenue: {type: number} } }, description: 销售额 Top 5 产品列表 } } }Constraints数据时效性返回数据为 T-2 日数据即查询今日返回前日数据时间范围限制单次查询跨度不得超过 30 天权限要求调用者需具备sales_read角色错误码映射401: 缺少有效认证令牌403: 当前角色无sales_read权限422:start_date或end_date格式错误或逻辑错误如 start endExamples{ input: { start_date: 2024-06-01, end_date: 2024-06-07, region: 华东 }, output: { total_orders: 1247, total_revenue: 3284567.5, top_products: [ {name: 旗舰手机X1, revenue: 1245678.9}, {name: 无线耳机Pro, revenue: 876543.2} ] } }提示SKILL.md必须放在技能代码同级目录下文件名与技能函数名完全一致如search_sales_data.py对应search_sales_data.md。管理器启动时扫描此目录自动加载并校验。这个格式的设计哲学是用最熟悉的 Markdown 语法承载最严格的机器可读 Schema。它不引入新 DSL工程师无需学习新语法它强制结构化避免自然语言描述的歧义它内嵌 JSON Schema为 LLM 的参数构造提供精确依据它包含真实示例成为测试用例和 prompt 工程的直接来源。3.2 管理器如何“读懂”SKILL.md 并驱动运行时管理器不是静态文档浏览器它是一个活的契约引擎。其核心工作流如下加载与解析启动时扫描指定目录读取所有*.md文件。使用自研的skill-parser库基于 remark unified提取Input Schema、Output Schema、Constraints等区块。对 JSON Schema 进行语法校验确保是合法 JSON并做语义检查如enum值是否为空、required字段是否在properties中定义。契约注册将解析后的结构存入内存注册表并建立索引。例如根据Constraints中的权限要求字段自动生成 RBAC 规则根据Input Schema构建参数校验中间件根据Examples初始化单元测试套件。运行时探针注入管理器提供一个轻量 SDKskill-gate-sdk工程师在技能函数入口处调用register_skill(search_sales_data)装饰器。该装饰器自动在调用前用Input Schema校验参数合法性类型、格式、枚举值非法则返回标准错误在调用后捕获返回值用Output Schema校验结构不匹配则记录告警记录耗时、状态码、trace_id并上报至管理器状态服务。双向同步管理器 Web 界面提供“在线编辑”功能。当用户在 UI 上修改Constraints如增加一条新的错误码映射管理器会生成 Git Commit提交到指定分支如skills/main触发 CI Pipeline运行schema-validator检查语法若通过则自动热重载该技能的契约元数据无需重启服务同时将变更通知到所有已注册该技能的 Agent 实例触发本地缓存刷新。这个闭环让SKILL.md从“写完就扔”的文档变成了“活的契约”——它既是设计源头也是运行依据更是治理抓手。3.3 技能状态的四维健康模型管理器的状态面板不是简单显示“在线/离线”而是基于四个正交维度计算综合健康分0~100维度计算方式权重健康阈值说明可用性Availability(成功调用次数) / (总调用次数)30%≥ 99.5%仅统计 HTTP 2xx/3xx 响应4xx 视为业务失败不计入失败性能Performancep95 响应时间25%≤ 800ms超过阈值扣分1200ms 以上为严重降级契约合规性Contract Compliance(返回值通过 Output Schema 校验的次数) / (成功调用次数)25%≥ 99.9%检测返回结构是否漂移是契约失效的早期信号时效性Timeliness数据新鲜度如返回数据距当前时间的小时数20%≤ 2h仅对有明确时效要求的技能启用由Constraints中的数据时效性字段触发注意每个维度都有独立告警开关。例如search_sales_data的“时效性”维度是强制开启的而get_weather的“时效性”则关闭天气数据本身就有分钟级延迟不适用小时级考核。这个模型的价值在于它把模糊的“技能好不好用”量化为可归因、可优化的具体指标。当健康分掉到 85工程师一眼就能看出是“性能拖了后腿”而不是去大海捞针查日志。4. 实操落地从零搭建一个可运行的技能管理器含完整代码4.1 环境准备与依赖安装我们选择 Python FastAPI Vue3 作为技术栈原因很实在Python 生态对 LLM 工程支持最成熟FastAPI 的 OpenAPI 自动生成与异步能力完美适配技能探针Vue3 的响应式与组件化让 UI 开发高效可控。整个系统打包后小于 80MB可一键部署到任意 Linux 服务器。基础环境要求Python 3.10Node.js 18用于构建前端Redis用于状态缓存与任务队列PostgreSQL用于存储技能元数据、调用日志初始化项目结构mkdir skill-gate cd skill-gate # 后端 mkdir backend cd backend python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi uvicorn sqlalchemy psycopg2-binary redis pydantic-settings python-dotenv # 前端 cd .. mkdir frontend cd frontend npm create vitelatest . -- --template vue npm install # 项目根目录放配置与脚本 cd .. touch .env touch docker-compose.yml关键依赖说明pydantic-settings管理多环境配置开发/测试/生产比硬编码更安全redis作为状态缓存层避免高频查询压垮数据库psycopg2-binaryPostgreSQL 驱动稳定可靠fastapi自带 OpenAPI 文档方便调试技能接口。实操心得不要一开始就上 Kubernetes。我们第一个客户用单机 Docker Compose 运行了 8 个月支撑了 12 个 Agent日均调用量 200 万。复杂度是敌人先跑通再扩展。4.2 核心后端契约解析器与技能注册中心创建backend/app/core/skill_parser.py这是整个系统的“大脑”from typing import Dict, Any, Optional import json import re from pydantic import BaseModel, Field from app.schemas.skill import SkillMetadata class SkillParser: 解析 SKILL.md 文件提取结构化元数据 def __init__(self): # 预编译正则提升性能 self._schema_pattern rjson\s*([\s\S]*?)\s* self._section_pattern r##\s(.*?)(?\n##|\Z) def parse(self, md_content: str) - SkillMetadata: 主解析方法返回 SkillMetadata Pydantic 模型 sections self._split_sections(md_content) # 提取 Description description sections.get(Description, ).strip() # 提取 Input Schema input_schema_str self._extract_json_schema(sections, Input Schema) input_schema self._validate_json_schema(input_schema_str, input) # 提取 Output Schema output_schema_str self._extract_json_schema(sections, Output Schema) output_schema self._validate_json_schema(output_schema_str, output) # 提取 Constraints constraints self._parse_constraints(sections.get(Constraints, )) # 提取 Examples examples self._parse_examples(sections.get(Examples, )) return SkillMetadata( nameself._extract_name(md_content), descriptiondescription, input_schemainput_schema, output_schemaoutput_schema, constraintsconstraints, examplesexamples ) def _split_sections(self, content: str) - Dict[str, str]: 将 Markdown 按二级标题分割 sections {} parts re.split(r\n##\s, content) if len(parts) 1: # 第一部分是标题忽略 for part in parts[1:]: header_match re.match(r^([^\n])\n(.*), part, re.DOTALL) if header_match: header, body header_match.groups() sections[header.strip()] body.strip() return sections def _extract_json_schema(self, sections: Dict[str, str], section_name: str) - Dict[str, Any]: 从指定 section 提取并解析 JSON Schema content sections.get(section_name, ) matches re.findall(self._schema_pattern, content, re.DOTALL) if not matches: raise ValueError(fMissing {section_name} JSON schema in SKILL.md) try: return json.loads(matches[0].strip()) except json.JSONDecodeError as e: raise ValueError(fInvalid JSON in {section_name}: {e}) def _validate_json_schema(self, schema: Dict[str, Any], schema_type: str) - Dict[str, Any]: 对 JSON Schema 做基础语义校验 if not isinstance(schema, dict): raise ValueError(f{schema_type} schema must be a JSON object) if type not in schema: raise ValueError(f{schema_type} schema missing type field) # 检查 required 字段是否都在 properties 中 if required in schema and properties in schema: for req in schema[required]: if req not in schema[properties]: raise ValueError(fRequired field {req} not found in properties) return schema def _parse_constraints(self, constraints_text: str) - Dict[str, str]: 解析 Constraints 文本为键值对 constraints {} for line in constraints_text.split(\n): line line.strip() if not line or line.startswith(-): continue if : in line: key, value line.split(:, 1) constraints[key.strip()] value.strip() return constraints def _parse_examples(self, examples_text: str) - Optional[Dict[str, Any]]: 解析 Examples JSON if not examples_text.strip(): return None try: return json.loads(examples_text.strip()) except json.JSONDecodeError: return None def _extract_name(self, md_content: str) - str: 从第一行 # 标题提取技能名 match re.match(r^#\s(.)$, md_content.split(\n)[0]) if not match: raise ValueError(Invalid SKILL.md: missing main title) return match.group(1).strip().replace( , _).lower() # 全局解析器实例 parser SkillParser()这个解析器的关键设计点容错性强对 Markdown 格式不敏感能处理空行、多余空格、不规范缩进错误友好所有异常都带清晰上下文如“Missing Input Schema JSON schema”方便工程师快速定位SKILL.md问题性能优化正则预编译、避免重复解析可扩展SkillMetadata是 Pydantic 模型后续可轻松添加字段如version,owner。4.3 技能注册与探针 SDK创建backend/app/skills/registry.py实现技能的动态注册与生命周期管理from typing import Callable, Dict, Any, Optional import time import logging from functools import wraps from app.core.skill_parser import parser from app.schemas.skill import SkillMetadata from app.core.redis_client import redis_client from app.core.db import get_db_session logger logging.getLogger(__name__) class SkillRegistry: 技能注册中心管理所有已注册技能 def __init__(self): self._skills: Dict[str, SkillMetadata] {} self._functions: Dict[str, Callable] {} def register(self, name: str, func: Callable, md_path: str) - None: 注册一个技能函数及其契约 try: # 1. 解析 SKILL.md with open(md_path, r, encodingutf-8) as f: md_content f.read() metadata parser.parse(md_content) # 2. 存储元数据 self._skills[name] metadata # 3. 存储函数引用 self._functions[name] func # 4. 初始化 Redis 状态键 redis_client.hset(fskill:{name}:state, mapping{ last_call_time: 0, success_count: 0, error_count: 0, total_count: 0, p95_latency_ms: 0 }) logger.info(fSkill {name} registered successfully) except Exception as e: logger.error(fFailed to register skill {name}: {e}) raise def get_metadata(self, name: str) - Optional[SkillMetadata]: 获取技能元数据 return self._skills.get(name) def get_function(self, name: str) - Optional[Callable]: 获取技能函数 return self._functions.get(name) def list_all(self) - Dict[str, SkillMetadata]: 列出所有技能元数据 return self._skills.copy() # 全局注册中心实例 registry SkillRegistry() def register_skill(name: str, md_path: str): 装饰器为技能函数添加注册与探针逻辑 def decorator(func: Callable) - Callable: # 注册技能 registry.register(name, func, md_path) wraps(func) def wrapper(*args, **kwargs): start_time time.time() trace_id kwargs.get(trace_id, unknown) try: # 1. 参数校验使用 Input Schema metadata registry.get_metadata(name) if metadata and metadata.input_schema: # 这里应调用 JSON Schema 校验库简化版示意 pass # 2. 执行原函数 result func(*args, **kwargs) # 3. 返回值校验使用 Output Schema if metadata and metadata.output_schema: # 同样此处应调用校验逻辑 pass # 4. 更新状态统计 duration_ms int((time.time() - start_time) * 1000) redis_client.hincrby(fskill:{name}:state, success_count, 1) redis_client.hincrby(fskill:{name}:state, total_count, 1) redis_client.hset(fskill:{name}:state, last_call_time, str(int(time.time()))) # 更新 p95 延迟简化版用 Redis Sorted Set 存储最近 1000 次延迟 redis_client.zadd(fskill:{name}:latency, {str(duration_ms): time.time()}) # 保持最多 1000 条 redis_client.zremrangebyrank(fskill:{name}:latency, 0, -1001) return result except Exception as e: # 记录错误 redis_client.hincrby(fskill:{name}:state, error_count, 1) redis_client.hincrby(fskill:{name}:state, total_count, 1) logger.error(fSkill {name} execution failed: {e}) raise return wrapper return decorator这个 SDK 的精妙之处在于零侵入改造只需在现有函数上加一个register_skill装饰器无需改业务逻辑状态分离所有运行时状态计数、延迟存于 Redis不污染业务函数渐进式增强参数校验、返回值校验是可选开关初期可只开状态统计后期再逐步启用可观测性内置每一次调用都自动产生结构化日志和指标为管理器 UI 提供数据源。4.4 前端管理界面Vue3 实现技能详情页frontend/src/views/SkillDetail.vue是 UI 的核心。我们放弃复杂框架用 Composition API Pinia Tailwind CSS 实现极致简洁template div classmax-w-6xl mx-auto p-6 !-- 顶部状态栏 -- div classflex items-center justify-between mb-8 div h1 classtext-2xl font-bold text-gray-900{{ skill.name }}/h1 p classtext-gray-600 mt-1{{ skill.description }}/p /div div classflex items-center space-x-4 span :classhealthClass {{ healthScore }} / 100 /span button clickrefreshSkill classpx-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition 刷新状态 /button /div /div !-- 四维健康仪表盘 -- div classgrid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8 HealthCard title可用性 :scorehealth.availability :threshold99.5 unit% / HealthCard title性能 :scorehealth.performance :threshold800 unitms / HealthCard title契约合规 :scorehealth.compliance :threshold99.9 unit% / HealthCard title时效性 :scorehealth.timeliness :threshold2 unith / /div !-- 输入/输出 Schema -- div classmb-8 h2 classtext-xl font-semibold text-gray-800 mb-4输入参数/h2 SchemaViewer :schemaskill.input_schema / /div div classmb-8 h2 classtext-xl font-semibold text-gray-800 mb-4返回结果/h2 SchemaViewer :schemaskill.output_schema / /div !-- 最近调用记录 -- div h2 classtext-xl font-semibold text-gray-800 mb-4最近调用/h2 div classbg-white rounded-lg shadow overflow-hidden table classmin-w-full divide-y divide-gray-200 thead classbg-gray-50 tr th classpx-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider时间/th th classpx-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider状态/th th classpx-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider耗时/th th classpx-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-widerTrace ID/th /tr /thead tbody classbg-white divide-y divide-gray-200 tr v-forcall in recentCalls :keycall.id classhover:bg-gray-50 td classpx-6 py-4 whitespace-nowrap text-sm text-gray-500 {{ formatDate(call.timestamp) }} /td td classpx-6 py-4 whitespace-nowrap span :classcall.status success ? text-green-600 : text-red-600 {{ call.status }} /span /td td classpx-6 py-4 whitespace-nowrap text-sm text-gray-500 {{ call.duration_ms }} ms /td td classpx-6 py-4 whitespace-nowrap text-sm text-gray-500 {{ call.trace_id.substring(0, 8) }}... /td /tr /tbody /table /div /div /div /template script setup import { ref, onMounted } from vue import { useSkillStore } from /stores/skill import HealthCard from /components/HealthCard.vue import SchemaViewer from /components/SchemaViewer.vue const props defineProps({ skillId: String }) const store useSkillStore() const skill ref(null) const health ref({ availability: 0, performance: 0, compliance: 0, timeliness: 0 }) const healthScore ref(0) const recentCalls ref([]) // 计算健康分颜色 const healthClass computed(() { if (healthScore.value 90) return text-green-600 font-medium if (healthScore.value 70) return text-yellow-600 font-medium return text-red-600 font-medium }) onMounted(async () { await store.fetchSkill(props.skillId) skill.value store.currentSkill await loadHealthData() await loadRecentCalls() }) const refreshSkill async () { await store.fetchSkill(props.skillId) skill.value store.currentSkill await loadHealthData() await loadRecentCalls() } const loadHealthData async ()