基于Hermes协议与MQTT构建开源语音技能:从架构到部署实践
1. 项目概述与核心价值
最近在折腾一个挺有意思的开源项目,叫hermesnest/sister-skill。乍一看这个名字,可能会觉得有点摸不着头脑,又是“赫尔墨斯之巢”,又是“姐妹技能”的,组合在一起像个神秘组织的内部代号。但如果你深入了解一下智能家居、语音助手或者自动化脚本的圈子,就会发现这其实是一个指向性非常明确的“技能包”或“插件集”项目。简单来说,它通常是为某个特定的语音助手平台(比如 Home Assistant 里的 Hermes 协议组件,或者类似的开源语音交互框架)开发的一系列自定义技能。
这些技能,就像是给你的智能语音助手安装的一个个“APP”。官方技能库可能提供了天气、新闻、音乐播放,但如果你想控制一个特定品牌的非标智能灯,或者想用语音触发一个复杂的自动化流程(比如“姐妹,帮我开始周末大扫除模式”,这个模式会依次打开扫地机器人、空气净化器,并播放特定的播客列表),就需要自己开发或集成这样的自定义技能。hermesnest/sister-skill项目,大概率就是这样一个集合,它把一系列相关的、可能由社区贡献的、或者针对特定场景(“姐妹”这个称呼暗示了其交互可能更亲切、拟人化)的技能打包在一起,方便用户一键部署或按需选用。
它的核心价值在于扩展性与场景化。官方平台的能力总有边界,而社区驱动的技能包则能无限延伸这个边界,将语音交互渗透到更个性化、更细分的日常生活和工作场景中。对于开发者而言,这是一个学习语音技能开发、理解意图识别和对话管理的好样本;对于进阶用户,这是打造独一无二智能家居体验的利器。接下来,我们就深入拆解一下这类项目的设计思路、技术实现以及实操中会遇到的那些“坑”。
2. 项目整体设计与架构解析
2.1 核心定位与功能边界
首先必须明确,hermesnest/sister-skill不是一个独立的语音助手,而是一个“技能”或“插件”集合。它的运行依赖于一个底层的语音助手平台,这个平台负责最基础的语音唤醒、语音转文本、文本转语音、以及核心的对话管理框架。项目名称中的hermesnest很可能指向了 Hermes Audio Server 或与之兼容的协议,这是一个在开源语音助手社区(特别是 Rhasspy 项目生态中)广泛使用的、基于 MQTT 的通信协议。它定义了技能与核心对话管理器之间如何交换消息。
因此,这个项目的首要设计原则是“协议兼容”和“松耦合”。每一个技能都应该是一个独立的模块,通过订阅和发布特定的 MQTT 主题(Topic)来与系统交互。例如,当用户说“姐妹,打开客厅的阅读灯”时,对话管理器会将识别出的意图(Intent)发布到某个主题,而监听这个主题的“灯光控制技能”就会接收到这个意图,解析参数(房间:客厅,设备:阅读灯,动作:打开),然后执行相应的操作(比如调用 Home Assistant 的 API),最后将执行结果反馈回去。
hermesnest可能提供了一个技能托管、发现和生命周期管理的“巢穴”(nest),而sister-skill则是住在这个巢穴里的一系列具体技能。这种架构的好处非常明显:
- 独立性:每个技能可以独立开发、测试、部署和更新,一个技能的崩溃不会影响整个系统。
- 可扩展性:任何人都可以按照协议规范编写新的技能,放入“巢穴”中即可被系统识别和使用。
- 技术栈灵活:技能可以用任何语言编写(Python, Node.js, Go等),只要它能连接 MQTT 并处理 JSON 消息即可。
2.2 典型技能结构与数据流
一个标准的技能模块,通常包含以下几个部分:
意图定义(Intents):这是技能的“说明书”,用 YAML 或 JSON 格式定义了这个技能能理解哪些句子。例如,一个天气技能会定义
GetWeather意图,并包含诸如{city}、{date}这样的槽位(Slot,即参数)。# 示例:意图定义片段 intents: GetWeather: utterances: - “今天 [北京] {city} 天气怎么样” - “[上海] {city} 明天会下雨吗” - “查询 {city} 的天气”这些定义需要在对话管理器(如 Rhasspy 的
rhasspy-nlu)中进行训练,以便语音识别后能正确分类。技能逻辑主体:这是一个常驻运行的程序。它的工作流程是:
- 初始化:连接 MQTT 代理(Broker),订阅它关心的意图主题(如
hermes/intent/#)。 - 监听:循环等待 MQTT 消息。
- 处理:收到消息后,解析 JSON 载荷,提取意图名称和槽位值。
- 执行:根据意图和参数,执行核心业务逻辑(如查询天气 API、控制设备、查询数据库)。
- 反馈:将执行结果(成功或失败,附带文本信息)发布到指定的反馈主题(如
hermes/dialogueManager/endSession),以便对话管理器合成语音回复给用户。
- 初始化:连接 MQTT 代理(Broker),订阅它关心的意图主题(如
配置管理:技能通常需要一些配置,比如 API 密钥、设备 ID、服务地址等。这些配置不应硬编码在代码里,而是通过配置文件、环境变量或在部署时注入的方式提供。
本地化支持:一个好的技能包会考虑多语言。
sister-skill中的“姐妹”这个称呼本身就带有本地化色彩,其意图定义和回复语句可能需要支持中文、英文等不同版本。
数据流全景图:
用户语音 -> 语音唤醒 -> 语音转文本 -> 意图识别 (NLU) -> 发布意图到 MQTT (主题: hermes/intent/GetWeather) | v sister-skill (天气模块) 订阅该主题并处理 | v 调用第三方天气 API -> 生成回复文本 | v 发布会话结束消息到 MQTT (主题: hermes/dialogueManager/endSession) | v 文本转语音 -> 音箱播放:“北京今天晴,最高温度25度。”2.3 技术栈选型考量
对于hermesnest/sister-skill这类项目,技术选型是围绕其“协议兼容”和“松耦合”特性展开的。
- 通信层(必须):MQTT是首选。它轻量、低功耗、支持发布/订阅模式,非常适合物联网和事件驱动的技能通信。几乎所有的开源语音助手框架(Rhasspy, Home Assistant Assist)都采用或兼容 Hermes over MQTT。
- 技能开发语言:Python是社区最主流的选择。原因有三:一是生态丰富,有大量现成的库可以处理 HTTP 请求、硬件控制、数据处理等;二是易于上手,适合快速原型开发和社区贡献;三是有成熟的 Hermes 客户端库,如
rhasspy-hermes,能极大简化连接、订阅、发布等底层操作。 - 配置与部署:Docker是理想的部署方式。每个技能可以打包成一个独立的 Docker 镜像,通过环境变量传递配置,通过 Docker Compose 编排多个技能和 MQTT 代理等服务,实现一键部署和版本管理。
- 辅助工具:
- 意图管理:可能需要使用 Rhasspy 的 Web 界面或单独的 NLU 训练工具来管理和训练意图。
- 测试工具:
mosquitto_pub和mosquitto_sub这类 MQTT 客户端命令行工具,是调试技能消息收发的利器。 - 日志:技能内部应使用结构化的日志输出(如 Python 的
logging模块),并配置集中式日志收集(如 ELK 栈),便于排查分布式系统中的问题。
注意:在开始开发或部署前,务必确认你的语音助手核心平台(无论是 Rhasspy、Home Assistant 还是其他)是否支持以及如何配置 Hermes MQTT 协议。这是技能能够“对话”的前提。
3. 核心模块拆解与实现细节
一个像sister-skill这样的集合,内部可能包含多种技能。我们以几个典型的技能为例,深入其实现细节。
3.1 基础框架:技能生命周期管理
任何技能都需要一个稳定的基础框架来处理连接、重连、消息分发和错误处理。在 Python 中,使用rhasspy-hermes库可以快速搭建。
#!/usr/bin/env python3 import paho.mqtt.client as mqtt from rhasspyhermes.client import HermesClient from rhasspyhermes.intent import Intent import asyncio import logging logging.basicConfig(level=logging.INFO) _LOGGER = logging.getLogger(__name__) class SisterSkillBase(HermesClient): def __init__(self, client, skill_id: str): super().__init__(client, skill_id) self.skill_id = skill_id # 注册意图处理函数 self.subscribe_intent("GetWeather", self.handle_get_weather) self.subscribe_intent("ControlLight", self.handle_control_light) async def handle_get_weather(self, intent: Intent): """处理查询天气意图""" city = intent.slots.get("city", {}).get("value", "北京") # 获取槽位值 _LOGGER.info(f"收到天气查询请求,城市: {city}") # 业务逻辑:调用天气API weather_info = await self._fetch_weather(city) # 结束会话,返回语音回复 await self.end_session(intent.session_id, text=f"{city}的天气是{weather_info}") async def handle_control_light(self, intent: Intent): """处理控制灯光意图""" # 类似地,解析房间、设备、动作等槽位 pass async def _fetch_weather(self, city): # 模拟API调用 await asyncio.sleep(0.1) return "晴朗,25摄氏度" async def main(): client = mqtt.Client() # 连接到你的MQTT代理,例如 mosquitto client.connect("localhost", 1883, 60) skill = SisterSkillBase(client, "sister-skill-weather") await skill.start() if __name__ == "__main__": asyncio.run(main())关键点解析:
- 继承
HermesClient:这个基类封装了与 Hermes 协议交互的通用逻辑,如连接、订阅、消息序列化/反序列化。 - 意图注册:通过
subscribe_intent方法,将意图名与对应的处理函数绑定。当收到该意图的消息时,会自动调用处理函数。 - 槽位提取:意图消息中的
slots字段包含了从用户语句中提取的参数。需要安全地获取(使用.get()避免 KeyError)。 - 异步处理:使用
asyncio确保技能在等待网络 I/O(如调用 API)时不会阻塞,可以同时处理其他消息。 - 会话管理:处理完成后,必须调用
end_session来明确告知对话管理器“我的话讲完了”,并附上要合成的文本。这是驱动语音反馈的关键一步。
3.2 典型技能一:智能家居控制
这是最核心的技能之一。它需要桥接语音指令和实际的智能家居设备。
实现要点:
- 设备抽象层:不要直接在技能代码里写死对某个品牌设备 SDK 的调用。应该抽象出一个统一的“设备控制接口”。例如,定义
DeviceController类,它有turn_on(device_id),turn_off(device_id),set_brightness(device_id, value)等方法。 - 后端集成:技能的实现层再去集成具体的后端。最通用的方式是通过 Home Assistant 的 REST API。
在技能处理函数中,就可以这样调用:import aiohttp class HomeAssistantController: def __init__(self, base_url, api_token): self.base_url = base_url.rstrip('/') self.headers = {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"} async def call_service(self, domain, service, data): async with aiohttp.ClientSession() as session: url = f"{self.base_url}/api/services/{domain}/{service}" async with session.post(url, json=data, headers=self.headers) as resp: return await resp.json()await ha_controller.call_service("light", "turn_on", {"entity_id": "light.living_room"})。 - 意图与设备映射:需要一个配置系统,将语音指令中的“客厅主灯”、“卧室空调”等别名,映射到 Home Assistant 中具体的实体 ID(
light.living_room_main,climate.bedroom_ac)。这个映射关系可以放在一个 YAML 配置文件里。 - 状态反馈:好的交互不仅仅是执行命令,还要有状态确认。在控制设备后,可以再次查询设备状态,并将结果包含在回复中。例如:“好的,已打开客厅主灯,当前亮度是70%。”
实操心得:
- 安全第一:Home Assistant 的长期访问令牌(Long-Lived Access Token)要妥善保管,最好通过环境变量传入,不要提交到代码仓库。
- 错误处理:网络调用可能失败,设备可能离线。代码中必须对
aiohttp.ClientError等异常进行捕获,并返回友好的错误提示,如“好像无法连接到客厅的灯,请检查它是否在线。” - 延迟考虑:Wi-Fi 设备响应可能有延迟。在发送控制指令后,可以等待一小段时间(如0.5秒)再查询状态,确保获取到的是最新状态。
3.3 典型技能二:信息查询与播报
例如天气、新闻、日历事件、车辆限行等。这类技能的核心是数据获取与信息提炼。
实现要点:
- 第三方 API 集成:选择稳定、免费或低成本的 API 服务。例如天气可以用和风天气、OpenWeatherMap;新闻可以用 RSS 源或聚合 API。
- 请求优化:
- 缓存:对于更新不频繁的数据(如天气,可缓存10分钟),使用内存缓存(如
functools.lru_cache)或 Redis,避免频繁调用 API 触发限流。 - 异步并发:如果一次查询需要获取多个信息源(如“今天有什么安排?”需要查日历和待办事项),使用
asyncio.gather()并发执行,减少总体响应时间。
- 缓存:对于更新不频繁的数据(如天气,可缓存10分钟),使用内存缓存(如
- 自然语言生成:将获取到的结构化数据(JSON)转换成一句流畅的口语化句子,这是一门艺术。避免直接罗列数据。
- 差示例:“北京:晴,最高温25度,最低温15度,风力3级。”
- 好示例:“北京今天天气不错,是大晴天,最高有25度,早晚稍微凉点,大概15度,有点微风。” 可以准备一些句子模板,根据数据动态填充。
- 配置化:API Key、城市代码、关注的 RSS 源等都应作为配置项。
注意事项:
- API 调用限制:仔细阅读所用 API 的免费套餐限制,并在代码中做好计数和限流,避免意外超限导致服务中断。
- 网络超时:设置合理的请求超时时间(如10秒),并为网络异常提供降级回复,如“暂时无法获取天气信息,请稍后再试。”
- 数据清洗:API 返回的数据可能包含 HTML 标签或异常字符,需要清洗后再用于语音合成,否则 TTS 引擎可能会读出奇怪的内容。
3.4 典型技能三:自定义场景与自动化
这是体现“智能”的高级技能。它不直接控制单个设备,而是触发一个预定义的、复杂的场景或自动化流程。
实现思路:
- 场景定义:在配置文件中定义场景。每个场景有唯一 ID、触发短语和一系列动作。
scenes: morning_routine: trigger: “早上好” # 或更复杂的意图匹配 actions: - service: light.turn_on target: entity_id: light.bedroom data: brightness_pct: 30 - service: media_player.play_media target: entity_id: media_player.kitchen data: media_content_id: “http://example.com/morning-news.mp3” media_content_type: “music” cinema_mode: trigger: “我要看电影” actions: - service: light.turn_off target: area_id: living_room - service: media_player.select_source target: entity_id: media_player.tv data: source: “HDMI 1” - 场景执行引擎:技能收到匹配的意图后,根据场景 ID 查找对应的动作列表,然后顺序或并发地执行这些动作。这里可以直接复用智能家居控制技能的逻辑,调用 Home Assistant 服务。
- 参数化场景:可以让场景支持简单参数。例如,“晚安模式”可以接受一个“延迟关灯时间”的参数。这需要更复杂的意图定义和槽位解析。
高级技巧:
- 原子性与回滚:复杂的场景执行可能中途失败。考虑实现简单的回滚机制,或者确保每个动作是独立的,失败不影响其他已成功执行的动作,并清晰播报哪部分失败了。
- 条件判断:可以在场景定义中加入执行条件。例如,“离家模式”只在检测到所有手机都不在家的地理围栏状态下才执行关闭所有电器的操作。
- 与自动化平台联动:更复杂的逻辑其实更适合在 Home Assistant 的自动化(Automation)或 Node-RED 中实现。技能的角色可以简化为“触发”这个自动化。这样可以利用图形化界面和更强大的逻辑处理能力。
4. 部署、配置与运维实战
4.1 环境准备与依赖安装
假设我们基于 Docker 部署整个sister-skill生态。
基础设施:
- MQTT 代理:我们选择 Eclipse Mosquitto,因为它轻量且稳定。
- 语音助手核心:以 Rhasspy 为例,它集成了语音唤醒、ASR、NLU、TTS 和对话管理。我们需要运行 Rhasspy 的核心服务。
- 技能容器:每个技能一个 Docker 容器。
目录结构:建议的本地开发/部署目录如下:
sister-skills-deploy/ ├── docker-compose.yml # 主编排文件 ├── config/ │ ├── rhasspy/ # Rhasspy 配置文件 │ └── skills/ # 各技能配置 │ ├── weather/ │ │ └── config.yaml │ └── ha_controller/ │ └── config.yaml ├── skills/ # 技能代码目录(通过 volumes 挂载或构建镜像) │ ├── weather-skill/ │ │ ├── Dockerfile │ │ ├── app.py │ │ └── requirements.txt │ └── ha-controller-skill/ │ └── ... └── data/ # 持久化数据(可选)Docker Compose 编排:
version: '3.8' services: mosquitto: image: eclipse-mosquitto:latest container_name: sister-mqtt restart: unless-stopped ports: - "1883:1883" # MQTT 端口 - "9001:9001" # MQTT over WebSockets (可选) volumes: - ./config/mosquitto.conf:/mosquitto/config/mosquitto.conf - ./data/mosquitto:/mosquitto/data - ./log/mosquitto:/mosquitto/log rhasspy: image: rhasspy/rhasspy:latest container_name: sister-rhasspy restart: unless-stopped ports: - "12101:12101" # Web 管理界面 volumes: - ./config/rhasspy:/profiles - ./data/rhasspy:/data depends_on: - mosquitto environment: - MQTT_HOST=mosquitto - MQTT_PORT=1883 - LANGUAGE=zh_CN # 设置中文 weather-skill: build: ./skills/weather-skill container_name: sister-skill-weather restart: unless-stopped depends_on: - mosquitto environment: - MQTT_BROKER=mosquitto - MQTT_PORT=1883 - WEATHER_API_KEY=${WEATHER_API_KEY} # 从.env文件读取 volumes: - ./config/skills/weather:/config ha-controller-skill: build: ./skills/ha-controller-skill container_name: sister-skill-ha restart: unless-stopped depends_on: - mosquitto environment: - MQTT_BROKER=mosquitto - MQTT_PORT=1883 - HA_BASE_URL=${HA_BASE_URL} - HA_ACCESS_TOKEN=${HA_ACCESS_TOKEN} volumes: - ./config/skills/ha_controller:/config使用
.env文件管理敏感信息,并将其加入.gitignore。
4.2 技能配置详解
每个技能都需要独立的配置。以天气技能为例,config.yaml可能包含:
skill: id: "sister-weather" name: "天气查询" # 意图定义文件路径(相对于容器内路径) intent_file: "/config/intents.yaml" # 城市映射表,将口语化城市名映射到API需要的城市ID city_mapping: 北京: "101010100" 上海: "101020100" 广州: "101280101" # API相关 weather_api: provider: "heweather" # 和风天气 base_url: "https://devapi.qweather.com/v7/weather/now" cache_ttl: 600 # 缓存时间,秒在技能启动时,会读取这个配置文件。配置与代码分离,使得调整参数无需重新构建镜像。
4.3 日志、监控与调试
运维这类分布式微服务系统,清晰的日志和监控至关重要。
- 日志聚合:将所有容器的日志输出到标准输出(stdout/stderr),然后使用 Docker 的
json-file日志驱动,或者使用docker-compose logs -f service_name查看。对于生产环境,可以集成Loki+Grafana或ELK栈。 - 技能健康检查:在 Dockerfile 或 docker-compose 中为技能容器添加健康检查,定期发送 MQTT Ping 或检查 HTTP 端点。
healthcheck: test: ["CMD", "python", "-c", "import paho.mqtt.client as mqtt; c=mqtt.Client(); c.connect('localhost', 1883, 5); c.disconnect()"] interval: 30s timeout: 10s retries: 3 start_period: 40s - 调试技巧:
- MQTT 消息监听:使用
mosquitto_sub命令行工具订阅hermes/#主题,可以实时看到所有 Hermes 协议消息,是诊断意图是否被正确发出/接收的终极手段。mosquitto_sub -h localhost -t "hermes/#" -v - 模拟意图发布:使用
mosquitto_pub手动发布一个意图消息,模拟用户语音输入,用于测试技能逻辑。mosquitto_pub -h localhost -t "hermes/intent/GetWeather" -m '{"sessionId":"test123","intent":{"intentName":"GetWeather","confidenceScore":1.0},"slots":[{"slotName":"city","value":{"value":"北京"}}]}' - 技能独立测试:在技能开发阶段,可以写一个简单的测试脚本,模拟 MQTT 消息输入,而不需要启动完整的 Rhasspy 系统。
- MQTT 消息监听:使用
5. 常见问题排查与优化经验
在实际部署和运行sister-skill这类项目时,你会遇到各种各样的问题。下面是一些典型问题及其排查思路。
5.1 技能无响应
现象:说出指令后,语音助手没有反应,或者提示“我没听懂”。
排查步骤:
- 检查 MQTT 连接:首先确认技能容器是否成功连接到了 MQTT 代理。查看技能容器的日志,看是否有连接错误。同时用
mosquitto_sub监听hermes/intent/#,看当你说出指令时,是否有对应的意图消息发布出来。如果没有,问题出在 Rhasspy 的语音识别或意图识别环节。 - 检查意图匹配:如果有意图消息发出,但你的技能没反应,检查技能日志,看是否收到了该消息。确认技能代码中订阅的意图名称(
GetWeather)与 NLU 训练后发布的意图名称完全一致(大小写敏感)。 - 检查槽位解析:技能收到了消息但执行错误,可能是槽位解析问题。在技能处理函数中打印出收到的完整
intent对象,检查slots的结构和内容是否符合预期。有时 NLU 提取的槽位值可能为空或格式不对。 - 检查网络与依赖:如果技能需要调用外部 API(如天气 API)或内部服务(如 Home Assistant),确保网络连通,且 API 密钥、URL 等配置正确。查看是否有网络超时或认证失败的日志。
5.2 响应延迟高
现象:从说完指令到听到回复,间隔时间很长(超过3秒)。
优化方向:
- 技能逻辑优化:
- 异步化:确保所有 I/O 操作(网络请求、数据库查询)都是异步的,使用
async/await,避免阻塞事件循环。 - 缓存:对不常变的数据实施缓存,如天气信息、设备状态(可设置短期缓存)。
- 并行化:如果技能需要多个独立操作,使用
asyncio.gather()并发执行。
- 异步化:确保所有 I/O 操作(网络请求、数据库查询)都是异步的,使用
- 基础设施优化:
- MQTT 代理性能:确保 Mosquitto 运行在性能足够的硬件上,对于大量技能,可以考虑集群部署。
- 容器资源:为技能容器分配足够的 CPU 和内存限制,避免因资源不足导致调度延迟。
- 网络延迟:确保 MQTT 代理、Rhasspy、技能容器、Home Assistant 等所有服务之间的网络延迟尽可能低,最好部署在同一局域网内。
- NLU 优化:Rhasspy 的意图识别如果句子复杂或数量多,也可能成为瓶颈。可以精简意图定义,使用更高效的 NLU 后端(如
fsticuffs替代rasa)。
5.3 意图识别不准
现象:经常错误触发技能,或者该触发时不触发。
解决思路:
- 丰富训练语句:在意图定义文件中,为每个意图提供尽可能多、尽可能口语化、覆盖不同表达方式的例句。这是提升识别准确率最有效的方法。
- 调整置信度阈值:Rhasspy 可以设置意图识别的置信度阈值。如果阈值太低,容易误触发;太高,则容易漏触发。可以在 Rhasspy 的 Web 界面中调整
intent.recognize的阈值。 - 使用槽位同义词:对于槽位值,可以配置同义词列表。例如,用户可能说“北京”、“首都”、“帝都”,都应该映射到同一个城市代码。在 Rhasspy 的
slots目录下配置同义词文件。 - 检查语音识别(ASR):意图识别是基于文本的,如果语音转文本(ASR)就不准,后续全错。确保录音质量,在安静环境下训练语音模型,或尝试不同的 ASR 后端(如
deepspeech,kaldi)。
5.4 技能配置管理混乱
现象:技能越来越多,配置散落在各个容器的环境变量和挂载卷里,难以维护。
最佳实践:
- 集中配置:考虑使用专门的配置管理服务,如
Consul或etcd,但对于中小型项目,一个精心组织的.env文件加上 Docker Compose 的env_file指令已经足够。 - 配置模板化:对于技能配置文件(如
config.yaml),可以使用Jinja2模板,在容器启动时通过环境变量渲染成最终配置。或者使用confd等工具。 - 密钥管理:绝对不要将 API Key 等硬编码或提交到 Git。使用 Docker Secrets(在 Swarm 模式下)或通过 CI/CD 管道在部署时注入环境变量。
- 版本化配置:将技能的配置文件也纳入版本控制(敏感信息除外),与代码版本同步更新。
5.5 技能间通信与依赖
现象:某个技能需要依赖另一个技能的结果才能执行。
解决方案:
- 避免直接依赖:理想情况下,技能应完全独立。如果必须依赖,考虑将公共功能抽象成基础服务。例如,一个“外出建议”技能需要天气和交通信息,那么它应该自己去调用天气 API 和交通 API,而不是依赖天气技能的结果。这样解耦更彻底。
- 通过 MQTT 事件通信:如果一定要通信,可以通过 MQTT 发布/订阅自定义事件主题。例如,天气技能在获取到新数据后,发布到
sister/weather/update主题,其他感兴趣的技能可以订阅。但这增加了系统复杂性,需谨慎设计消息格式和生命周期。 - 使用中央状态机:对于复杂的场景,可以引入一个轻量级的“场景协调器”技能。它订阅用户意图,然后根据场景逻辑,按顺序向其他技能发送一系列控制指令(通过发布特定的意图或事件)。这相当于把复杂的业务流程放在了协调器里,其他技能依然保持简单。
最后,我想分享一点个人体会:构建sister-skill这样的项目,最大的挑战往往不是技术实现,而是如何设计出符合直觉、稳定可靠的交互体验。它要求开发者不仅是一个程序员,还要成为一个产品设计师和用户体验师。从为一个技能编写第一句意图例句开始,你就在定义用户如何与机器对话。多测试、多模拟真实场景,甚至让家人朋友来试用,他们的困惑和反馈是优化技能最宝贵的资源。记住,一个好的语音技能,应该是“润物细无声”的,它在那里,随时待命,准确执行,而不需要用户去记住复杂的命令语法或担心它是否在听。
