七牛云实训营的题目一。用户口述”明天下午三点三刻提醒我开会”,系统转成结构化日程并入库,还能检测冲突、多轮澄清。一句口语要走完”语音转文字 → 意图识别 → 时间解析 → 冲突检测 → 入库 → 前端联动”才算数。
中文时间解析:推翻 BRIEF,自己写规则引擎
比赛的 BRIEF 预设方案是”dateparser 为主 + 规则补丁”。我实测后把它推翻了——dateparser 1.x 对中文口语几乎全废,“明天下午三点""三天后”全返回 None。所以我改成自写规则层为主,dateparser 降为边角兜底,LLM 出 ISO 作最后兜底的三层架构,参考日固定在 2026-05-29 周五保证可复现。
这块是踩坑最多、也最长本事的地方,记两个具体的:
“三点三刻”被解析成 15:15
。 正确是 15:45
(一刻 = 15 分钟,三刻 = 45)。根因在正则:分钟匹配的交替分支里 刻 排在 三刻 前面,正则把”三刻”的”三”当成分钟数字吃了、“刻”当成一刻 = 15。修法是把”刻”当成会改写数字含义的独立单位,按”N 刻 = N×15”解析。
“大后天”被算成 +2 天。 因为相对天字典遍历时,“后天”是”大后天”的子串、先命中了。修法是按 key 长度降序匹配,先试最长的”大后天”。这个防御后来在序数匹配上又复用了一次——“第一”不会误命中”第十一”。
还做了符合直觉的滚动:今天周五说”周一上午九点”,本周一已过就滚下周一;“早上八点”已过就滚明天。这些不是算法难点,但少一个用户就会觉得”这玩意儿不懂人话”。
LLM 双后端 + 自动 fallback
意图识别用 DeepSeek 默认、Azure 备用,统一接口、走 JSON mode(强制 response_format=json_object 保证可解析)。自动 fallback 是真的:主后端抛任何异常(网络/限流/余额)就切下一个重试,两个都挂才报错。测试用注入式 FakeLLM 全程脱网,关键用例就叫 test_fallback_to_second_backend_on_error——主后端抛”限流”、第二后端兜底成功。比赛环境网络和额度都不稳,单点依赖等于把可用性押在一个不可控因素上。
冲突检测与多轮澄清
冲突判定用半开区间 [s,e),相邻不算冲突(14:00-15:00
和 15:00-16:00
不冲突);冲突时按 15 分钟步长向后扫第一个落在工作时段的空档。真实 demo:“安排下周三场论文复习每次两小时避开已有的会”→ 系统排出三个时段问”确认添加吗”→“确认”→ 事件从 5 个变 8 个。
多轮澄清解决一个真问题:原来”要删哪个?“问完,用户答”第一个”会被当成新指令。补了一个确定性的指代消解——依次按末位(“最后”)、序数(“第一个”)、时段(“下午那个”)、标题关键词去定位,选不出唯一才再问。前端的待定状态做成了联合类型,澄清和”add 冲突待确认”两条路分开走。
三个真实的坑
端口冲突(三项目共机)。 本机起 uvicorn 验证时,curl /health 返回的健康检查不是我这个服务的——8099 端口被同机另一个项目占了,我的 uvicorn 其实因端口冲突 Exit 1,curl 命中了旧服务,差点骗过我。之后健康检查接口的返回里加了 service 字段一眼区分,并定了规矩:共享机验证前先 ss -ltnp 确认端口空闲,真实服务一律 push 后到 VM 跑。
穷举 fuzz 抓出 4 个真 500。 并发 fuzz 79 条请求,状态码分布里有 4 个真实 500:对一个超大整数 id 发 DELETE/PATCH,触发 OverflowError: Python int too large to convert to SQLite INTEGER(SQLAlchemy 不校验 path 整数范围);给指代消解和计划确认接口塞 {"candidates":[null,null]},触发 NoneType 不可下标。这些 happy path 永远测不出来,得靠对边界的暴力构造。
余额误读虚惊。 一次冒烟后脚本打印”余额 0.00”,跟冒烟前的 ¥9.53 矛盾——一次调用不可能烧光。查原始 JSON 发现 DeepSeek 的 balance_infos 数组顺序变了,脚本按固定下标取到了 USD 的 0.00,CNY 其实没变。教训:第三方返回的数组顺序不保证,要按字段(currency)取,不能硬下标。
一个我觉得对的判断:.ics 不需要 LLM
后期加日历订阅导出(.ics)。这是纯确定性的标准 iCalendar 生成,不该为它烧 DeepSeek 余额,也不该把任何密钥喂给不可信的第三方中转。识别出”哪些事不需要大模型”,和识别出”哪些事需要”一样重要。
小结
三天里按计划的 PR 全部提前做完,22 个 PR、60 个 commit、后端 107 + 创新阶段扩到 153 个测试全绿,命令行端到端跑通”语音 → 意图 → 时间 → 入库 → 冲突提醒”。这个项目我最有体会的是中文时间解析那块——口语里全是”看着规整、实际全是例外”的边角,以及一堆只有真接了外部接口、真做了穷举测试才会撞上的坑。