ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

hermes-agent实战:用MQTT与Hermes协议搭建本地化语音智能家居代理

2026/9/8 19:20:20 拓冰建站 浏览量
hermes-agent实战:用MQTT与Hermes协议搭建本地化语音智能家居代理 最近后台私信里好几个人都在问同一个名字hermes-agent。有问它和智能音箱有什么区别的有问能不能白嫖一套离线语音控制的还有直接把GitHub链接丢过来让帮忙看看怎么用的。我一开始以为是某个蹭热度的前端脚手架仔细翻完源码和协议文档之后发现这东西其实比大部分语音助手方案都更有意思——它不是一个聊天机器人也不是一个云端技能而是一整套把语音理解、意图分发和设备控制拆开的“代理层”。如果你正在折腾本地化智能家居、想在不开云服务的前提下让音箱自己干活那hermes-agent值得你花一个周末仔细研究。先说结论hermes-agent本身不负责“听懂”它负责的是听懂之后那句“去做”。它将语音助手常见的识别ASR、语义解析NLU、对话管理和设备控制逻辑彻底解耦用一套基于MQTT的Hermes消息协议把这些模块串起来。每个模块负责自己的那一摊agent则像一个调度员在收到“开灯”“开空调”“明天几点下雨”这类解析结果之后根据预设意图去调用对应服务再通过语音合成把结果回给用户。整套链路跑在本地局域网里延迟可控、没有云端账单、数据不出门改造空间也大非常适合嵌入式开发者和智能家居玩家。1. 项目定位与核心设计思路拆解1.1 为什么需要一个“代理”而不是直接用语音识别SDK先想一个实际问题你对着麦克风说了一句“帮我把客厅灯调暗一点”这句话要经过几道工序才能变成真实世界里的那一次PWM调光首先是语音转文字然后要做意图识别——知道“调暗”是一个动作、“客厅灯”是一个目标设备然后才轮到设备侧执行。前两步是AI问题最后一步是工程问题如果硬把所有逻辑塞进一个进程比如在唤醒词引擎里直接写一套设备控制代码那后面每加一个设备、每调整一次说法都要重新编译、重新部署而且任何一个环节崩了整套语音服务就全瘫了。hermes-agent的思路就是把这条链路上的每个节点做成独立模块模块之间只通过消息通信谁挂了就只重启谁互不拖累。这里还涉及一个很重要的取舍设备控制逻辑为什么不让云端的技能平台去做因为智能家居的控制对象在局域网里云端技能要拿到设备控制权要么设备主动连云端要么在路由器上做端口映射两条路都有安全隐患和网络依赖性。hermes-agent从一开始就把控制逻辑放在本地的agent进程里云端只做语音识别和语义解析甚至你也可以把这部分也全部本地化真正下发指令的这一步永远留在家里。用行话说这是“边缘计算”思路的极简实践。1.2 事件驱动架构Hermes协议解决的是消息传递问题在动手写代码之前必须先理解Hermes协议在整个项目里的位置。Hermes协议本质上是一组定义好的MQTT主题Topic和消息格式相当于给语音助手的各个模块之间统一了“语言”。为什么选MQTT而不是HTTP因为语音交互天然是异步的用户说完话识别可能耗时几百毫秒意图解析又要几百毫秒在这个过程中对话还在继续用户可能补充信息或者改口HTTP这种请求-响应模式在这种长链路上非常别扭而MQTT的发布-订阅模型天然契合事件流。打个比方Hermes协议就像是整个语音助手团队的“内部聊天群”ASR模块在群里喊了一声“我转出来一段文字”NLU模块听到了就把文字解析成意图再喊一声“用户想关灯”agent听到这个意图就去把灯关掉然后喊一声“事情办完了”。所有的模块都不需要知道对方在哪儿、用的是什么语言写的只要遵守群里的发言格式就行。这种松耦合的架构对开发者极其友好你可以用Python写agent用C写唤醒词引擎用任意语言实现TTS只要大家发的MQTT消息格式一致就能协同工作。1.3 从整体架构看hermes-agent在哪个位置为了讲清楚我把典型的Hermes语音链路画成一个表格每个环节都有对应的MQTT主题和职责。层级模块典型Topic职责语音输入唤醒词引擎hermes/hotword/detected检测到唤醒词后触发会话语音输入ASR语音识别hermes/asr/textCaptured把音频转成文字语义理解NLU意图解析hermes/nlu/intentParsed把文字解析为意图槽位业务执行Agent代理hermes/intent/接收意图调用设备或服务对话管理对话管理器hermes/dialogueManager/sessionStarted维护会话状态控制流程语音输出TTS语音合成hermes/tts/say把文本转成语音并播放hermes-agent在整个链路里处于“业务执行”这一层但它并不是一个被动的执行者。它需要监听意图消息、维护对话上下文、调用外部API或设备协议再决定是直接结束会话还是追问用户。这也正是agent这个名字的含义它是代表用户去执行动作的“代理”。2. 核心机制拆解意图、槽位、会话闭环2.1 意图与槽位让机器知道你到底想干什么任何一个语音助手项目最先要搞明白的就是“意图”和“槽位”这两个概念。意图就是用户这句话背后的目的比如“turnOnLight”是开灯“queryWeather”是查天气槽位则是完成这个目的需要的关键信息比如“开灯”需要知道是哪个灯“查天气”需要知道是哪个城市、哪一天。Hermes协议对这两者的定义非常清晰一条意图消息的JSON结构大致长这样{ sessionId: a1b2c3, intent: { intentName: home:turnOnLight, confidenceScore: 0.98 }, slots: [ { slotName: room, value: { kind: Custom, value: 客厅 }, rawValue: 客厅 } ], siteId: bedroom, input: 打开客厅的灯 }这里最容易踩坑的是槽位的值不一定是你期望的字符串。比如用户说“打开老妈房间的灯”如果NLU没把“老妈房间”这个叫法映射到具体的实体ID槽位里可能只有一段原始文本你的agent还是不知道要操作哪个设备。所以成熟的实现里NLU配置阶段会有一张“同义词表”把“主卧”“爸妈房间”“大房间”全部映射到同一个实体上。这个动作越早做后续agent处理逻辑就越简单。2.2 会话生命周期一次对话是怎么从开始到结束的和HTTP那种“一来一回”的短连接不同语音对话是有生命周期的。Hermes协议里用“session”这个概念来表示一次完整的交互从用户说唤醒词开始到任务完成或者用户主动退出结束。会话的状态流转大致是这样的用户说唤醒词hotword模块发布消息对话管理器创建一个新session。ASR把后续语音转成文字发布textCaptured。NLU解析文字发布intentParsed。agent收到意图决定是执行操作后立刻关闭会话还是继续追问缺失的槽位。最后agent发布endSession对话管理器清理会话状态。这个流程最精妙的地方在于agent是可以通过“继续会话”来反向控制对话流程的。比如用户只说了一句“开灯”没说哪里的灯agent无法确定目标设备就可以发布一条continueSession让对话管理器通过TTS问用户“请问开哪里的灯”。用户回答后再走一轮ASR→NLU→intent直到槽位齐全、任务完成。这种能力让agent不再是一个单向的指令接收器而是真正具备多轮对话能力的交互体。2.3 Agent之间怎么协作多个agent同时监听实际项目里你不会只写一个agent文件而是倾向于按业务领域拆分成多个agent一个管灯光、一个管空调、一个管闹钟。Hermes协议的意图Topic是分层的不同agent只需订阅自己关心的那部分。比如灯控agent订阅 hermes/intent/home:turnOnLight 和 hermes/intent/home:turnOffLight天气agent订阅 hermes/intent/weather:query闹钟agent订阅 hermes/intent/device:setTimer这样做的好处很明显业务边界清晰、单个agent崩溃不影响其他agent、新增功能只需要加一个新进程而不需要改动旧的。代码仓库的目录结构我也建议按agent拆分每个agent一个文件夹自带配置文件和依赖声明这样后续维护起来不会变成一锅粥。3. 手把手实操从安装到第一个可用agent3.1 环境准备与依赖安装我实操时的环境是树莓派4B4GB版本加一块USB麦克风阵列系统用的是Debian默认的Python 3.9。如果你手里只有一台普通电脑也可以用虚拟麦克风模拟音频输入不影响测试agent逻辑。MQTT broker是整个系统的心脏推荐用mosquitto轻量、稳定、配置少。在Debian系系统上安装只需要一条命令sudo apt install mosquitto mosquitto-clients然后确认它跑起来了systemctl status mosquitto如果用Docker部署更干净的做法是起一个持续运行的容器docker run -d --name mosquitto -p 1883:1883 eclipse-mosquitto:2.0接下来是hermes-agent本身。它提供了官方Python客户端库直接通过pip安装pip install hermes-agent注意这个包名不要和某个JS框架搞混。安装完成后可以从仓库的示例目录里拷贝一份基础模板或者直接用接下来我给的这段代码。3.2 用15行代码写一个最小可用的灯控agent真正写一个agent比我想象中简单得多核心逻辑就是连接MQTT、订阅意图Topic、执行动作、结束会话。下面这段代码我实测可用它监听一个叫home:turnOnLight的意图收到后打印日志、模拟开灯操作、然后语音回复用户import json import paho.mqtt.client as mqtt BROKER_HOST 127.0.0.1 BROKER_PORT 1883 INTENT_TOPIC hermes/intent/home:turnOnLight def on_intent(client, userdata, msg): payload json.loads(msg.payload.decode()) intent payload.get(intent, {}).get(intentName) slots payload.get(slots, []) room None for slot in slots: if slot[slotName] room: room slot[value][value] print(f[agent] 收到意图: {intent}, 房间: {room}) # 这里写真实设备控制逻辑比如通过GPIO或MQTT电源控制 # if room 客厅: # turn_on_light(living_room) # 执行成功结束对话并TTS回复 client.publish(hermes/dialogueManager/endSession, json.dumps({ sessionId: payload.get(sessionId), text: f好的已为您打开{room}的灯 })) def main(): client mqtt.Client() client.on_message on_intent client.connect(BROKER_HOST, BROKER_PORT) client.subscribe(INTENT_TOPIC) client.loop_forever() if __name__ __main__: main()这段代码有几个容易被忽略的细节。第一endSession里的sessionId必须原样带回因为对话管理器是靠它识别会话的如果漏掉或写错TTS消息会无法关联到当前会话表现为“识别到了意图但设备没反应、也没有语音反馈”。第二slots数组不一定按你期望的顺序排列所以要用slotName去匹配而不是固定取第一个元素。第三如果你的agent逻辑执行时间比较长比如要调用一个慢速API建议先回一句“正在处理”再慢慢干活。3.3 多轮追问让agent补齐缺失槽位前面提到如果用户说“开灯”没说哪个房间agent可以选择继续追问。在Hermes协议里这叫做continueSession。我改造一下上面的代码让它具备槽位补齐能力def on_intent(client, userdata, msg): payload json.loads(msg.payload.decode()) session_id payload.get(sessionId) slots {s[slotName]: s[value][value] for s in payload.get(slots, [])} if room not in slots: client.publish(hermes/dialogueManager/continueSession, json.dumps({ sessionId: session_id, text: 请问要打开哪里的灯, intentFilter: [home:turnOnLight, home:turnOffLight] })) return room slots[room] print(f执行开灯操作: {room}) client.publish(hermes/dialogueManager/endSession, json.dumps({ sessionId: session_id, text: f好的已打开{room}的灯 }))这里的关键参数是intentFilter它限制了下一轮用户说话时必须解析成哪些意图如果用户回答“卧室”NLU会继续解析为home:turnOnLight意图只是这次会带上room槽位如果用户回答“帮我看看天气”intentFilter会让系统忽略这个请求或者重新提示。这个机制看起来简单但实际体验上非常关键——没有它多轮对话很容易跑偏。3.4 集成测试方法消息模拟与端到端验证写完agent后不需要急着接麦克风直接用mosquitto_pub发一条模拟意图消息就能测试mosquitto_pub -t hermes/intent/home:turnOnLight -m { sessionId: test, intent: {intentName: home:turnOnLight, confidenceScore: 1.0}, slots: [{slotName: room, value: {kind: Custom, value: 客厅}, rawValue: 客厅}], siteId: test }如果一切正常你的agent终端会打印出日志同时你在另一个监听endSession的终端窗口里能看到回包。监听命令是mosquitto_sub -t hermes/dialogueManager/# -v这种“分层验证”的方法我强烈建议养成习惯先用模拟消息验证agent逻辑再接真实ASR出了问题时能用mosquitto_sub看消息流比盲猜代码高效得多。4. 实际应用场景与系统集成经验4.1 场景一全本地离线语音控光我把自己卧室的灯控系统接到hermes-agent上之后最大的感受是响应速度完全是本地级别的。整个链路由Rhasspy做唤醒和ASRNLU在本地解析然后通过MQTT发布意图agent收到后直接调用灯控模块的HTTP接口。从说出“开灯”到灯亮体感延迟大概在0.5到0.8秒之间比很多依赖云端的智能音箱还要跟手。中间断了外网也不影响因为没有任何一个环节需要访问公网服务。这个场景的架构对你的网络有个硬性要求MQTT broker的地址必须是局域网内固定IP或者使用mDNS主机名这样才能让音箱、树莓派和各设备在同一个网络域里稳定通信。而且建议broker开启简单的用户名密码认证避免局域网里任何设备都能乱发消息。4.2 场景二把agent能力扩展到私有API服务有一次我想让语音助手查一下家里的NAS剩余空间原本以为要写一堆复杂的NAS接口适配结果发现agent本质上只是一个消息转发器完全可以把任意HTTP API包一层语音接口。我在agent里加了一个diskUsageHandle函数收到意图后调用NAS的本地API把返回的JSON拼成一句人话交给TTS播报出来。整个过程不到50行代码不需要任何智能音箱平台审核。这个思路放大之后就是把agent当成一个“语音网关”以后你想控制的设备只要能被本地或私有API驱动就都能接入语音控制。局域网里的监控、下载机、打印服务器、甚至开发板的LED灯珠都可以用同一套机制暴露成意图交给hermes-agent统一调度。自由度远远高于封闭的智能音箱生态。4.3 场景三离线闹钟与日程提醒闹钟类agent是我觉得Hermes协议里最优雅的用法之一。它不需要麦克风连续在线只需要在后台监听定时任务到点后主动发布TTS消息播报“该喝水了”“八点了”。Hermes协议里TTS消息不一定要在会话内发布你可以在任意时刻用hermes/tts/say主题说一句话所有连接在同一siteId下的扬声器都会播放音频。这个场景暴露了一个规划上的细节siteId字段的存在意味着一个Hermes系统可以管理多个房间、多台音箱agent在选择播放设备时要声明siteId。比如“在主卧播报天气”和“在客厅播报闹钟”用的是不同siteId。代码层面就是多传一个字段但架构上必须早早就想清楚设备分组不然后期加房间会很痛苦。4.4 项目代码组织建议如果你和我一样会同时维护好几个agent建议每个agent做成独立Python文件用同一个MQTT连接配置放在一个monitor脚本里统一拉起。这样有三个好处输出日志可以带agent名字前缀排查问题方便单个agent更新时可以单独重启不影响其他功能后续想迁移到Docker或者systemd多进程管理都很容易。我目前在树莓派上就是用一个systemd service跑一个launcher脚本里面的子任务各自独立最坏情况下某个agent崩了会被自动拉起主服务毫发无损。5. 常见问题与排查技巧实录5.1 意图收到了但设备就是不动作这个问题出现频率最高而且绝大多数不是hermes-agent的锅而是槽位匹配不一致。比如NLU解析出的room值是rawValue而你的真实设备表里写的是roomId两者对不上agent自然找不到设备。我的排查习惯是先在agent回调里打印完整的JSON payload看看实际收到的槽位值长什么样再对着日志调映射表。另外注意有些NLU框架返回的slot value是个嵌套对象不是简单字符串处理时要多取一层。5.2 多轮对话卡住TTS不回话如果你用continueSession之后用户回答了问题但agent没反应优先检查intentFilter。举个例子第一轮用户说的是“开灯”你让NLU继续解析turnOnLight意图但TTS追问的时候intentFilter写成了“turnOffLight”那用户说“卧室”就永远走不进你的回调表面上看就是“卡住了”。解决方法是把intentFilter放宽至少把上一轮相关的意图都放进去同时也观察mosquitto_sub日志确认NLU到底发布了哪条消息。5.3 ASR结果一直乱飘agent频繁误触发本地语音识别最头疼的就是误识别。你先用mosquitto_sub监听hermes/asr/textCaptured看看实际转写的文本多半会发现用户没在说话但ASR把环境噪音转成了一句话。这种情况要从声学层面解决调整麦克风增益、开启语音活动检测VAD、提高NLU的置信度阈值。hermes-agent这层能做的事情有限我建议在NLU配置里把confidenceScore低于0.5的意图直接丢弃先挡住一部分误触发。5.4 调试MQTT消息的实用命令行最后分享几个我天天用的MQTT调试命令。要看所有Hermes主题的消息mosquitto_sub -h 127.0.0.1 -t hermes/# -v只看某个类别的消息比如只看TTS播报内容mosquitto_sub -h 127.0.0.1 -t hermes/tts/# -v发布一条自定义意图来测试agentmosquitto_pub -h 127.0.0.1 -t hermes/intent/test:ping -m {sessionId:1,intent:{intentName:test:ping},slots:[]}这组命令看似简单但在排查“消息到底有没有到”“是NLU的问题还是agent的问题”时比从头翻日志高效一个数量级。5.5 梳理一份避坑清单根据我这一个多月的实际使用整理了这份避坑清单每一条都是真金白银换来的教训所有MQTT payload必须用UTF-8编码不要把bytes直接拿去json.loads否则中文槽位会乱码。agent里写的所有日志建议带上sessionId和意图名多个用户同时说话时能区分上下文。TTS回复别用聋哑式的短句至少要带上用户提到的槽位信息比如“已打开客厅的灯”这是体验的底线。Hermes的session有超时机制agent逻辑执行超过几十秒还没回应对话管理器会主动关闭会话如果要做长任务先回复“正在处理”再接异步结果。不要在agent里直接调用阻塞式的设备驱动尤其是那些带睡眠重试的库会卡住整个消息循环最好丢进线程池或消息队列。6. 后续还能怎么玩如果你对hermes-agent这套机制的印象还停留在“接个灯、开个空调”这种level说明它的潜力还没被挖出来。我最近在实验的是把agent和家里的监控告警打通让它在某个服务挂掉的时候主动发消息到音箱用语音提醒我家里的网络异常。逻辑很简单一个脚本每秒轮询几个内外网服务的连通性一旦发现异常就通过hermes/tts/say播报全程不需要任何云端中转。另一个方向是结合情景模式让agent根据时间、人物和传感器上下文自动调整策略比如“工作日早上8点半自动把客厅灯调亮”这个用定时器在agent里就能实现。我个人的体会是像hermes-agent这样的项目最难得的地方在于它把“语音交互”从云端拉回了本地把“智能”的裁判权还给了开发者。当你发现一次开灯操作背后有这么多环节协同工作而且每个环节都能亲手调试和替换的时候那种可控感和成就感不是用几百块买一个智能音箱能比的。最后再分享一个小技巧刚开始接触别追求功能多先把agent最小循环跑通——唤醒、识别、解析、执行、回复每一步用mosquitto_sub确认消息到了哪里走通之后你再往上加东西会发现一切都顺理成章。