基于Mycroft AI的香港巴士实时查询技能开发实战
2026/6/10 8:24:16 网站建设 项目流程

1. 项目概述:一个为智能音箱打造的香港巴士到站时间查询技能

如果你和我一样,是个住在香港或者经常往返香港的“数码生活家”,同时又是个公共交通重度依赖者,那你肯定对等巴士这件事深有体会。站在巴士站,看着站牌上密密麻麻的线路,心里最想问的就是:“下一班车到底什么时候来?” 官方App当然能查,但每次都要掏出手机、解锁、打开App、输入线路或站名,这一套流程下来,总觉得不够“丝滑”。尤其是在双手提着东西,或者只是在家随口一问的时候。

tomfong/hk-bus-eta-skill这个项目,就是为了解决这个“最后一米”的体验问题而生的。它是一个为Mycroft AI这类开源、注重隐私的智能语音助手平台开发的“技能”(Skill)。简单来说,你只需要对着你的智能音箱(比如安装了Mycroft的Mark 1、Picroft,或者是在树莓派上自建的)说一句:“Hey Mycroft, 下一班102巴士什么时候到?” 它就能用语音告诉你实时的到站时间。

这个项目的核心价值,在于它将开放的公共交通数据与本地化、隐私友好的智能家居生态进行了桥接。它没有依赖Google Assistant、Amazon Alexa或小爱同学这些大厂平台,而是选择了开源的Mycroft,这意味着你的查询数据不会被上传到商业公司的云端进行分析。对于注重数据隐私,又希望享受智能家居便利的用户来说,这是一个非常“Geek”且实用的解决方案。它解决的不仅仅是“查时间”这个功能需求,更是一种对可控、透明数字化生活的追求。

2. 技能的整体架构与设计思路拆解

要理解这个技能是如何工作的,我们需要把它拆解成几个核心部分,就像理解一个智能产品的内部构造一样。

2.1 核心数据源:香港实时公共交通数据接口

任何巴士到站查询功能,根基都在于数据。香港的公共交通数据开放程度相当高,运输署及几家主要的巴士运营商(如城巴、新巴、九巴、龙运等)都提供了面向开发者的实时到站数据API。这个技能的核心,就是对接这些官方或社区维护的API。

通常,这类API的工作流程是:

  1. 输入:提供巴士路线编号(如“102”)和巴士站编号(如“NA29”)。
  2. 处理:服务器根据这些标识,查询数据库中的实时车辆GPS位置、班次时刻表、交通状况等。
  3. 输出:返回一个结构化的数据,通常是JSON格式,里面包含了未来几班车的预计到站时间(以分钟计,如“3”, “8”, “15”)和车牌号码等信息。

这个技能扮演的角色,就是一个“翻译官”和“调度员”。它需要把用户模糊的自然语言(“下一班102巴士”),精准地“翻译”成API能理解的参数(路线route=102,车站stop_id=NA29),然后去调用对应的接口,拿到数据后再“翻译”成人类能听懂的句子(“下一班102巴士预计3分钟后到达”)。

2.2 Mycroft技能框架解析

Mycroft的技能框架是构建这个项目的基石。一个标准的Mycroft技能通常包含以下关键文件:

  • __init__.py: 技能的入口文件,负责初始化技能类,注册意图处理函数。
  • intent.json(或使用intent目录下的.intent文件): 定义“意图”(Intent)。意图是理解用户想干什么的核心。比如,我们定义一个BusETAIntent。在这个文件里,我们会列出许多能触发这个意图的例句,例如:
    • When is the next {route} bus
    • What's the ETA for bus {route}
    • 下一班{route}巴士几时到
    • 查询{route}路线巴士这里的{route}是一个“槽位”(Slot),用于捕获用户话语中的动态信息,即具体的巴士线路。
  • .voc文件: 定义一些关键词语(Vocabulary),比如“巴士”、“分钟”、“到达”等词的多种说法,帮助Mycroft的语音识别引擎(如Precise)更好地理解特定领域的词汇。
  • requirements.txt: 列出项目所需的Python第三方库,比如用于网络请求的requests,用于处理时间的pytz等。
  • settings.json: 定义技能的可配置选项。对于这个巴士查询技能,一个非常重要的设置可能就是“默认巴士站”。用户可以预先在Mycroft的Web配置界面(home.mycroft.ai)或技能设置页面里,设置自己最常用的巴士站ID。这样,当用户只说“下一班102巴士”时,技能就能自动使用这个默认车站进行查询,无需每次都说全车站信息。

2.3 设计中的关键考量:容错与用户体验

在设计之初,就需要考虑各种边界情况和糟糕的网络环境,这直接决定了技能的可用性。

  1. 模糊匹配与纠错:用户可能会说“102号巴士”、“102路车”,甚至口误说成“120”。技能是否需要集成一个简单的线路名称纠错或模糊匹配算法?或者至少对输入进行基本的清洗(去除“号”、“路”、“巴士”等字眼)?
  2. 多数据源回退:香港的巴士数据可能来自多个API端点(例如,一个社区维护的聚合API作为主源,官方API作为备用)。当主数据源不可用或返回错误时,技能能否自动、无缝地切换到备用源?这需要优雅的错误处理和重试逻辑。
  3. 响应格式优化:API返回的时间可能是“3”、“8”、“15”。技能在组织语音回复时,是简单地说“3分钟,8分钟,15分钟”,还是更人性化地处理为“最快的一班大约3分钟后到,之后还有两班分别在8分钟和15分钟后”?后者显然体验更好。
  4. 离线与缓存:虽然实时数据无法缓存太久,但对于线路和车站的静态映射关系(如“弥敦道某某大厦站”对应哪个stop_id),是可以进行一次查询后缓存在本地的,避免每次都要查询静态信息,加快响应速度。

3. 核心功能实现与代码细节剖析

让我们深入到代码层面,看看一个典型的查询意图是如何被处理的。这里我会结合常见的实现模式进行讲解,你可以将其视为一份“代码导读”。

3.1 意图处理器的实现逻辑

__init__.py中,我们会定义一个技能类,并包含一个处理巴士查询意图的方法。

from mycroft import MycroftSkill, intent_handler from .bus_api_client import BusAPIClient # 假设我们有一个封装好的API客户端 class HKBusETASkill(MycroftSkill): def __init__(self): super().__init__(name="HKBusETASkill") # 初始化API客户端,可能会从设置中读取API密钥或端点 self.api_client = BusAPIClient() # 从技能设置中加载用户配置的默认车站 self.default_stop = self.settings.get('default_bus_stop', '') @intent_handler('BusETAIntent') def handle_bus_eta_intent(self, message): # 1. 从用户话语中提取“槽位”信息 route = message.data.get('route') # 注意:用户可能没有说出车站,所以我们需要处理车站信息 stop = message.data.get('stop', self.default_stop) # 2. 验证输入 if not route: self.speak_dialog('error.no.route') # 播放预设的语音对话“请告诉我您要查询的巴士线路。” return if not stop: self.speak_dialog('error.no.stop') # 播放预设的语音对话“请设置默认车站或说出车站名称。” return # 3. 显示等待反馈(例如,让Mycroft的眼睛转一圈) self.gui.show_animated_progress() # 如果有屏幕的话 # 4. 调用API获取数据 try: eta_data = self.api_client.get_eta(route, stop) except Exception as e: self.log.error(f"查询巴士ETA失败: {e}") self.speak_dialog('error.api.failed') return # 5. 处理并播报结果 if eta_data and eta_data['etas']: # 假设eta_data['etas']是一个分钟数的列表,如 [3, 8, 15] next_bus = eta_data['etas'][0] dialog_data = {'route': route, 'minutes': next_bus} self.speak_dialog('bus.eta.info', data=dialog_data) # 对应的`bus.eta.info.dialog`文件内容可能是:“{route}巴士最快的一班将在{minutes}分钟后到达。” else: self.speak_dialog('info.no.bus') # “目前没有查询到即将到站的巴士。” def initialize(self): # 技能初始化时,可以检查必要配置或预热数据 self.log.info("HK Bus ETA Skill 初始化完成。")

关键点解析

  • @intent_handler装饰器将这个方法与intent.json中定义的BusETAIntent绑定。
  • 消息处理是异步的,要确保任何网络请求都不会阻塞Mycroft的主事件循环。
  • speak_dialog()方法用于播放.dialog文件中的语句,支持变量替换,是实现多语言支持的关键。
  • 错误处理至关重要。网络超时、API格式变化、无效输入都必须有对应的用户友好提示。

3.2 API客户端封装的艺术

将API调用单独封装成一个类(如BusAPIClient)是良好的实践。这个类负责:

  • 构建请求:添加必要的请求头(如User-Agent)、参数。
  • 处理认证:如果API需要密钥,在这里管理。
  • 解析响应:将原始的JSON响应,解析成技能内部易于处理的Python数据结构。
  • 异常处理:统一处理网络异常(requests.exceptions.RequestException)、HTTP错误状态码(如404, 503)、以及API返回的业务逻辑错误。
  • 缓存策略:可以实现一个简单的内存缓存(如使用functools.lru_cache),对静态信息或短时间内的相同查询进行缓存,减轻API压力,提升响应速度。
# bus_api_client.py 示例片段 import requests from functools import lru_cache from typing import List, Optional class BusAPIClient: BASE_URL = "https://api.example.com/hk-bus/v1" # 示例URL def __init__(self, api_key=None): self.session = requests.Session() if api_key: self.session.headers.update({'Authorization': f'Bearer {api_key}'}) @lru_cache(maxsize=128, ttl=300) # 缓存最多128条,有效期300秒(5分钟) def get_stop_id_by_name(self, stop_name: str) -> Optional[str]: """根据车站名称查询车站ID(带缓存)""" # ... 调用查询车站的API ... pass def get_eta(self, route: str, stop_id: str) -> dict: """获取实时到站时间""" url = f"{self.BASE_URL}/eta" params = {'route': route, 'stop': stop_id} try: resp = self.session.get(url, params=params, timeout=10) resp.raise_for_status() # 如果状态码不是200,抛出HTTPError data = resp.json() # 假设API返回格式为 {'status': 'success', 'data': {'etas': [...]}} if data.get('status') == 'success': return data['data'] else: raise ValueError(f"API返回错误: {data.get('message')}") except requests.exceptions.Timeout: raise Exception("查询超时,请检查网络连接。") except requests.exceptions.RequestException as e: raise Exception(f"网络请求失败: {e}")

3.3 多语言与本地化支持

香港是一个中英文双语环境。一个好的技能应该能根据Mycroft的系统语言设置,自动用中文或英文回复。Mycroft通过.dialog.voc文件体系原生支持这一点。

  • 你会有bus.eta.info.dialog文件,里面包含:
    en-us: The next {route} bus will arrive in {minutes} minutes. zh-hk: 下一班{route}號巴士將於{minutes}分鐘後到達。
  • 同样,error.no.route.dialog文件:
    en-us: Please tell me which bus route you want to check. zh-hk: 請告訴我你想查詢哪一條巴士路線。

Mycroft会根据当前语言环境自动选择对应的语句进行播报。这要求开发者在编写所有语音反馈时,都需要准备至少中英两套文案。

4. 部署、配置与调试全流程

开发完成后,将技能部署到Mycroft设备上,并让用户能够轻松配置,是项目从代码变成可用产品的关键一步。

4.1 技能安装与部署

对于用户来说,最简单的安装方式是通过Mycroft Skills Marketplace。开发者需要将代码提交到官方的技能仓库(如GitHub),并按照规范创建skill.json等元数据文件,提交拉取请求(PR)进行审核。通过后,用户就可以在设备的Web界面或语音命令(“Hey Mycroft, install Hong Kong bus ETA skill”)直接安装。

对于开发者调试,则常用手动安装

  1. 将技能文件夹克隆或复制到Mycroft的技能目录下(通常是~/mycroft-core/skills/)。
  2. 重启Mycroft服务(mycroft-start restart)。
  3. Mycroft会自动检测新技能并加载。

4.2 用户配置引导

一个“开箱即用”但“可深度定制”的技能配置非常重要。

  1. 首次运行引导:技能加载后,可以主动播报:“我是香港巴士到站查询技能,要开始使用,请先为您设置一个常用的默认巴士站。您可以通过说‘配置我的巴士技能’,或访问Web设置页面来完成。”
  2. Web设置界面:在settings.json中定义default_bus_stop字段后,Mycroft会自动在home.mycroft.ai的个人技能设置页面上生成一个文本框。我们需要在技能的settingsmeta.yamlsettingsmeta.json文件中,对这个字段进行更详细的描述,甚至提供下拉菜单(如果可能预置热门车站列表)。
    # settingsmeta.yaml 片段 skillMetadata: sections: - name: "options" label: "巴士设置" fields: - name: "default_bus_stop" type: "text" label: "默认巴士站编号" value: "" placeholder: "例如:NA29" description: "在此输入您最常使用的巴士站编号。查询时若未指定车站,将使用此默认站。"
  3. 语音配置:实现一个SettingBusStopIntent,允许用户通过语音交互来设置或更改默认车站。这比让用户去记复杂的站号要友好得多。例如,用户说:“设置默认车站为弥敦道某某大厦”,技能可以调用车站搜索API,找到匹配的车站ID并保存。

4.3 开发调试与日志排查

调试语音技能有其特殊性,因为涉及语音识别(STT)、意图解析(NLP)、技能逻辑、语音合成(TTS)多个环节。

  • 查看日志mycroft-cli clienttail -f /var/log/mycroft/skills.log是查看技能日志最直接的方式。所有self.log.info()/error()的输出都在这里。
  • 意图调试:在Mycroft的CLI界面中,你可以直接输入文本命令来模拟语音输入,绕过语音识别,直接测试意图解析和技能逻辑,这非常高效。
  • 模拟对话:使用mycroft-skill-testrunner等工具,可以编写自动化测试用例,模拟用户说出一句话,并断言技能会给出怎样的回应,确保代码更新不会破坏原有功能。
  • 网络请求模拟:在开发初期,可以使用responsespytest-mock库来模拟API的返回,避免在测试时频繁调用真实API,也便于构造各种边界情况(如无数据、错误响应)进行测试。

注意:在真实设备上测试时,务必注意网络环境。很多家庭网络对某些API端点可能存在访问不畅的问题,需要考虑使用代理或选择更稳定的数据源。

5. 进阶优化与扩展可能性

一个基础可用的技能只是起点,要让其变得出色,还需要考虑以下进阶方向。

5.1 性能与稳定性优化

  1. 异步化改造:最初的技能可能使用同步的requests.get()。在高并发或网络慢的情况下,这会阻塞整个技能系统。可以将其改造为使用aiohttp等异步HTTP库,使网络请求变为非阻塞操作,提升Mycroft系统的整体响应度。
  2. 指数退避重试:对于暂时性的网络故障,实现一个带有指数退避机制的重试逻辑,比简单的立即重试更加健壮。
  3. 健康检查与降级:技能可以定期(如每30分钟)调用一个简单的API健康检查接口。如果连续多次失败,则自动进入“降级模式”,在用户查询时给出“实时服务暂不可用,请参考巴士站时刻表”的提示,而不是一个生硬的错误。

5.2 功能扩展设想

  1. 多线路与到站提醒:用户可以说“查询102和106的到站时间”,技能同时查询并播报。更进一步,可以实现主动提醒:“Hey Mycroft, 当102巴士还有5分钟到站时提醒我。” 这需要技能在后台运行定时任务。
  2. 基于位置的自动车站识别:如果设备具有GPS功能(如某些便携式Mycroft设备),技能可以获取当前位置,自动查找附近的巴士站,无需用户设置默认站。例如:“Hey Mycroft, 离我最近的巴士站有哪些车快到了?”
  3. 路况与特别消息:整合交通消息(如改道、延误),在查询结果后附加播报:“另外提醒您,因道路施工,102线路部分班次可能有所延误。”
  4. 支持更多交通类型:将技能扩展为“香港公共交通查询技能”,除了巴士,再加入地铁(MTR)下一班车时间、渡轮班次查询等功能。

5.3 社区维护与协作

开源项目的生命力在于社区。作为开发者,你可以:

  • 编写清晰的中英文README.md,说明功能、安装、配置方法。
  • 在代码仓库中使用Issues模板,引导用户清晰地提交Bug报告或功能请求。
  • 建立CONTRIBUTING.md文档,说明代码风格、提交规范,鼓励其他开发者参与。
  • 将数据源抽象成可插拔的“Provider”。不同的巴士公司API可能不同,可以设计一个DataProvider基类,然后为“CitybusNWFBProvider”、“KMBProvider”等编写具体实现。这样,社区贡献者可以轻松地添加对新数据源的支持,而无需改动核心逻辑。

6. 实战避坑指南与常见问题

在我实际开发和维护类似技能的过程中,踩过不少坑,这里总结一下,希望能帮你绕开这些弯路。

6.1 数据源的选择与维护

  • 坑1:API不稳定或突然关闭。香港一些公共交通API是社区志愿者维护的,可能因各种原因(成本、政策)停止服务。
    • 对策永远不要只依赖单一数据源。在技能设计初期,就应考虑抽象层和备用源。定期检查数据源的可用性,并在文档中明确告知用户技能依赖的第三方服务。
  • 坑2:数据格式变更。API升级版本时,返回的JSON结构可能发生变化。
    • 对策:在API客户端解析响应时,使用.get()方法安全地访问字典键值,并设置合理的默认值。如果可能,为API响应编写JSON Schema进行验证。在日志中记录完整的原始响应,便于出错时排查。
  • 坑3:请求频率限制。免费API通常有调用次数限制。
    • 对策:严格遵守API的使用条款。在技能中实现请求限流和缓存。对于用户可能频繁查询的相同路线和车站,短期缓存(如1-2分钟)结果可以大幅减少不必要的API调用。

6.2 Mycroft技能开发特定问题

  • 坑4:意图匹配失败。用户说的话千奇百怪,你的.intent文件不可能覆盖所有情况。
    • 对策:充分利用Mycroft的“Padatious”意图解析器的训练功能。收集真实的用户查询日志(需匿名化并征得同意),不断补充和优化你的意图示例句子。对于确实无法匹配的,可以设置一个回退意图(Fallback Intent),给出友好的引导,例如:“我没听清您要查哪路巴士,请再说一遍线路编号好吗?”
  • 坑5:技能设置不同步。用户在Web界面修改了设置,但技能运行时读取的仍是旧值。
    • 对策:Mycroft会在设置变更时,向技能发送一个settings.change消息。你的技能需要监听这个消息,并在处理程序中更新内存中的配置变量。
    def initialize(self): # ... 其他初始化 ... self.add_event('skill.hk-bus-eta.settings.change', self.handle_settings_change) def handle_settings_change(self, message): self.default_stop = self.settings.get('default_bus_stop', '') self.log.info(f"默认车站设置已更新为: {self.default_stop}")
  • 坑6:长耗时操作阻塞系统。如果你的API调用很慢,又没用异步,会导致Mycroft在等待期间无法响应其他任何命令,甚至触发超时。
    • 对策:如前所述,使用异步请求。如果必须用同步代码,确保设置合理的超时时间,并在进行长时间操作前,调用self.set_context()来保持对话上下文,或者使用self.gui.show_animated_progress()给用户一个视觉反馈,表明系统正在工作。

6.3 用户体验细节

  • 坑7:播报信息过载。如果API返回未来5班车的时间,全部念出来会很长。
    • 对策:默认只播报最快到达的1-2班车。可以在设置中增加一个选项,让用户选择播报的班次数量。或者说:“下一班102巴士3分钟后到,需要我告诉您后面几班的时间吗?” 实现一个简单的对话交互。
  • 坑8:数字播报不自然。TTS引擎可能会把“102”读成“一百零二”,而不是“一零二”。
    • 对策:在组织回复文本时,对线路编号进行特殊处理。例如,将route从数字102转换为字符串"一零二"或直接保留为"102"(有些TTS能正确读)。这需要针对不同的TTS引擎(如mimic, google)进行测试和适配。
  • 坑9:无网络环境下的处理。设备离线时,技能完全无法工作。
    • 对策:在技能启动或查询前,检查网络连接。如果离线,可以播报:“当前无法连接网络,无法查询实时到站信息。” 如果技能缓存了静态时刻表(对于班次固定的线路),甚至可以提供一个粗略的参考时间。

开发这样一个技能,从技术上看是API集成和语音交互逻辑的实现,但从产品角度看,它关乎如何在一个注重隐私的开源平台上,打磨一个真正方便、可靠、懂用户的日常工具。每一次查询成功的背后,都是对数据源稳定性、代码健壮性和交互设计细节的反复考量。当你对着音箱问出问题,并立刻得到准确回答时,那种“科技服务于生活”的满足感,正是驱动这类开源项目不断完善的动力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询