skip to content
tlj 的工程笔记
← 项目

语音日历

比赛

一句话记日程的语音日历。Azure 语音转文字 + 中文时间解析 + 冲突检测,端到端把口语转成结构化日程。

时间
2026-05
角色
独立开发(后端 / 时间解析 / 部署)
成果
三天内完成全部计划 PR,96 个单元测试全绿,端到端可演示

七牛云一次实训营的赛题。目标是做一个语音日历:用户口述”明天下午三点三刻提醒我开会”,系统将其转成结构化日程并入库,同时检测与已有安排的冲突。

整条链路

一句口语要变成一条可用的日程,需要走完这几步:

  1. 语音转文字:由 Azure 语音服务完成。这是项目的核心能力,因此开发第一步就是验证 key 有效、token 端点可达,确认这条链路没有问题再继续。
  2. 意图识别:判断用户是新增、查询还是修改日程,由 LLM 完成。
  3. 时间解析:把”明天下午三点三刻”解析成具体的 datetime。这一块没有使用现成库,而是自己实现了规则引擎,也是问题最集中的部分,单独写了一篇复盘(三点三刻被解析成 15:15 )。
  4. 冲突检测与入库:判断新日程是否与已有安排冲突,冲突时主动建议调整时间。

几个关键决策

LLM 采用双后端加自动 fallback。 意图识别与槽位抽取以 DeepSeek 为主(成本低、中文表现好),Azure 上的模型作为备份,任意一侧不可用时自动切换,两侧均使用 JSON mode 以保证输出可解析。比赛环境的网络与额度都不稳定,单点依赖会把可用性押在一个不可控因素上。

开发机与运行机严格分离。 开发机内存只有 3.8G 且已在使用 swap,同时还有其他项目共用一台机器。因此约定:本机只用于写代码、commit、push 和纯 mock 的单元测试;任何真实服务(uvicorn、前端 dev server、连接真实 LLM 的联调)一律在 push 之后到云端 VM 运行。这条约定来自下面这个问题。

小步多 PR,main 始终可运行。 每个 PR 只完成一件事,从项目骨架、LLM 抽象层、时间解析、数据模型、冲突检测到 API 编排,逐步推进。好处是任何时刻 main 都是绿的、可演示的。

一个有代表性的问题

验证后端能否启动时,我在本机起了 uvicorn,用 curl 请求 /api/health,返回的却不是这个服务的健康检查——是同机另一个项目占用了该端口。我的 uvicorn 实际上已经因端口冲突退出,但 curl 请求落到了相邻服务上,表面看起来”正常”,几乎造成误判。

之后做了两处调整:健康检查接口的返回中加入 service 字段,便于一眼确认请求落到的是不是本服务;部署时使用独立端口和独立的 compose project,不与其他项目共享。“开发机不运行常驻服务”这条约定也是由此确立的。

结果

三天内按计划排定的 PR 全部提前完成,96 个单元测试全绿,命令行端到端可完整运行”语音文本 → 意图 → 时间 → 入库 → 冲突提醒”。其中冲突检测的演示效果最直观:尝试新增一个会议时,系统会返回类似”该时段与产品评审会冲突,是否改到 16 点”的建议。