这是一个在跑步过程中用语音陪伴跑者的 AI 教练。它接入运动手表的心率、 配速、步频、位置,判断该不该开口、说什么、怎么说, 然后把话说出来 —— 全程不需要跑者看屏幕。
产品的难点不在"能说话",而在克制: 一次多余的提醒会打乱呼吸节奏。所以它的默认动作是沉默, 只有判断"这句话对接下来三十秒有帮助"时才开口。
点开任意一块直接看详情,不用按顺序读。
启动控制台.bat)
不是所有信号都值得开口。每一种触发都配了优先级、冷却时间、有效窗口和通道:
| 触发 | 什么时候 | 优先级 | 冷却 | 有效窗口 | 走哪条路 |
|---|---|---|---|---|---|
| 身体异常 | 用户描述胸痛、头晕、呼吸困难 | 最高 | 0s | 5s | 端侧 |
| 心率超区间 | 超过上限持续 20 秒 | 最高 | 90s | 15s | 端侧 |
| 配速偏离 | 偏离目标超过 5% 持续 30 秒 | 高 | 120s | 20s | 云侧 |
| 偏离路线 | 偏出规划路线 | 高 | 180s | 20s | 云侧 |
| 整公里播报 | 每完成一公里 | 普通 | 0s | 8s | 端侧 |
| 段落切换 | 进入热身 / 匀速 / 间歇 | 普通 | 0s | 5s | 端侧 |
| 里程碑 | 完成计划、最后十公里倒计时 | 普通 | 0s | 20s | 云侧 |
| 用户提问 | 跑者主动开口 | 普通 | 0s | 8s | 云侧 |
内部用工程语言描述事件,对外要表达的是「教练现在想干什么」。这一层把两者对上, 并且每一句都带理由码,事后可查"为什么说这句"。
| 触发 | 意图 | 紧急度 | 真实话术样例 | 理由码 |
|---|---|---|---|---|
| 身体异常 | RECOVER | high | 「停下来,别跑了。需要就医。」 | SYMPTOM_REPORTED |
| 心率超区间 | WARN | high | 「慢一点,心率超了。」 | HR_ZONE_4 · HR_182 |
| 配速偏离 | CORRECT | medium | 「快一点,配速掉了。」 | PACE_TOO_SLOW · TARGET_310S |
| 偏离路线 | CORRECT | medium | 「转回来,偏出路线了。」 | OFF_ROUTE |
| 整公里播报 | CONFIRM | low | 「五公里,二十三分四十三秒。」 | KM_MILESTONE |
| 段落切换 | GUIDE | low | 「进入匀速段,稳住节奏。」 | SEGMENT_CHANGED |
| 里程碑 | ENCOURAGE | low | 「还剩四百米,稳住。」 | GOAL_NEAR |
| 用户提问 | GUIDE | low | 「每公里五分十秒,心率别过一百七十三。」 | USER_ASKED |
| 训练结束 | SUMMARY | low | 跑后复盘(见「记忆」板块) | WORKOUT_END |
同一套阈值不可能对所有训练都对。跑者的典型配速和最高心率是已知的, 教练按今天做什么课自动推算目标 —— 这就是「目标从哪来」的答案。
| 今天的课 | 目标配速 | 心率上限 | 用途 |
|---|---|---|---|
| 恢复跑 | 6:14 | 135 | 比轻松跑还慢,目的就是别累着 |
| 轻松跑 | 5:48 | 145 | 能边跑边说话的强度 |
| 长距离 | 5:54 | 155 | 比轻松跑略慢,重点是把时间撑住 |
| 节奏跑 | 5:06 | 175 | 能说短句但说不成段,练乳酸阈值 |
| 间歇 | 4:43 | 190 | 短时间高强度,中间有恢复 |
| 比赛 | 4:37 | 190 | 目标赛事,全力以赴 |
| 规则 | 普通提示 | 安全提示 |
|---|---|---|
| 占用每 5 分钟 3 次的额度 | 占用 | 不占用 |
| 重置 45 秒最小间隔 | 重置 | 不重置 |
| 因"重复"被抑制 | 会 | 永不 |
第三条经历过一次反复。规格要求「不重复最近两次相同意图」, 但直接照做会让心率提醒在第三次时彻底沉默 —— 那是安全问题。 最终方案是:有备用说法的换一句话继续说,没备用说法的才抑制。
同一场跑,心率持续超标,教练连说四次是这样的:
「慢一点,心率超了。」
「降速,心率太高。」
「慢一点,心率超了。」
而「掉速纠正」只有一句话,没有备用说法,所以连着第三次时会不再说 —— 因为它冷却 120 秒,能连说三次说明已经说了四分钟以上,第三次确实是唠叨。
唯一判断标准是一句话:「这句话,在此刻,对用户接下来的三十秒有帮助吗?」 没把握,就不说。
同一个人在不同日子需要不同的教练:比赛日要冷静专业的,恢复跑要温和陪伴的, 冲成绩要有人在旁边推的。做成可切换的参数集合,而不是写死三套话术。
| 人格 | 像谁 | 语气 | 直接度 | 激励占比 | 单句上限 | 称呼名字 | 反馈频率 |
|---|---|---|---|---|---|---|---|
| 温和陪伴 | 熟人陪跑 | warm | 低 | 0.40 | 8 秒 | 是 | 低 |
| 专业纠错 | 体能教练 | neutral | 高 | 0.10 | 6 秒 | 否 | 中 |
| 激励挑战 | 比赛日教练 | strict | 高 | 0.55 | 8 秒 | 是 | 高 |
同一个音色、同一句话,两种处理方式效果完全不同。下面两组可以当场对比:
| 类型 | 韵律处理 | 为什么 |
|---|---|---|
| 纠错 | 指令段稳、理由段加快 | 人要抓住的是开头那几个字 |
| 播报 | 平稳,像报数 | 只是给事实,不带情绪 |
| 激励 | 先快推、最后放慢上扬 | 尾部上扬才有"推一把"的感觉 |
| 安全 | 最慢、最低 | 最不容置疑的语气 |
每个字都会被念出来,所以按「被听到」而不是「被读到」来写:
| 规则 | 值 | 原因 |
|---|---|---|
| 单句长度 | ≤ 20 字,最多两句 | 跑动中注意力有限 |
| 发音时长 | 3 ~ 8 秒 | 用真实语音标定过:每字约 333 毫秒 |
| 阿拉伯数字 | 禁止 | 语音合成会念错,必须写成「五分二十秒」 |
| markdown / 括号 / 列表 | 禁止 | 会被逐字念出来 |
| 指令位置 | 纠错必须指令在前 | 说「慢一点,心率超了」,不说反过来 |
| 书面连接词 | 禁止 | 「首先其次」「总结一下」「作为 AI」一律不用 |
| 标点 | 只用逗号和句号 | 句号的位置就是真正的停顿 |
| 引擎 | 冷启动 | 缓存命中 | 需要网络 | 用在哪 |
|---|---|---|---|---|
| 在线神经语音 | 4.3 ~ 7.9 秒 | 57 毫秒 | 是 | 普通话术(预生成+缓存) |
| 离线本机引擎 | 0.78 ~ 1.46 秒 | — | 否 | 安全提示 / 断网场景 |
代价:离线引擎只支持语速和音量,不支持音高, 所以韵律工程里的音高维度在离线路径上会丢失。这个损失如实记录在案。
云希(男·年轻亲和)、云健(男·有精神)、云扬(男·沉稳)、 云夏(男·少年)、晓晓(女·温柔)、晓伊(女·活泼)。 演示控制台里可以随时切换试听。
| 记忆层 | 记什么 | 从哪来 | 影响什么 |
|---|---|---|---|
| 跑者档案 | 典型配速、观测最高心率、历史最好成绩 | 手表数据长期统计 | 所有阈值的地基 |
| 训练计划 | 今天做什么课、目标配速、目标心率、训练周期阶段 | 训练安排 | 「目标」的唯一来源 |
| 反馈效果 | 哪种反馈对这个跑者真的有效 | 每场跑后的复盘统计 | 下一场优先用有效的那一类 |
| 历史流水 | 每次训练的时间、距离、心率、配速分布 | 手表数据 | 趋势判断、里程碑对比 |
所以目标必须来自训练计划:跑者的典型配速乘以课型系数。 这样轻松跑的目标就是轻松跑的配速,间歇就是间歇的配速。
规格要求「第二次训练能读取第一次的记忆,并且策略有变化」。实测五步是这样:
| 步骤 | 发生什么 | 实测数据 |
|---|---|---|
| 1 · 计划给目标 | 今天是节奏跑 | 目标配速 5:06 · 心率上限 175 |
| 2 · 第一场 | 心率爬到 190、步频从 186 掉到 160 | t=271s 心率警告 · t=309s 掉速纠正 |
| 3 · 跑后复盘 | 汇总这一场的表现 | 静默比 83.3% · 疲劳度 0.793(高) |
| 4 · 写回记忆 | 结论存进计划记忆 | 「上次疲劳度 0.79,建议下次降级为轻松跑」 |
| 5 · 第二场读到 | 课型自动降级 | 节奏跑 → 轻松跑 · 心率上限 175 → 145 |
不是看单帧心率,而是看趋势。三个信号加权合成一个 0~1 的数, 全部可以从一秒级数据算出来:
实测那一场:心率负荷 0.931、心率漂移 0.125、步频下滑 0.072, 合成 0.793,判定为「高,优先保安全」。
full(原始,只在本机分析用)、
blur(默认,坐标模糊到约 1.1 公里)、
strip(对外分享,GPS 整列删除而不是降精度)。
这一条规格里没有写,但实际必须做 —— 是核对规格时我们自己补上的。
| 层级 | 来源 | 质量 | 生效条件 |
|---|---|---|---|
| 第 1 级 | 真实大模型生成 | 最好,贴合当下语境 | 网络正常 + 通过守门 |
| 第 2 级 | 本地话术缓存 | 好,是上次真生成过的句子 | 断网,但这类触发以前说过 |
| 第 3 级 | 云侧确定性模板 | 中,固定句式但内容正确 | 有网络、大模型不可用 |
| 第 4 级 | 端侧预置模板 | 够用,永远可用 | 完全断网 |
| 状态 | 触发次数 | 产出合规话术 | 可用率 |
|---|---|---|---|
| 正常 | 154 | 154 | 100.00% |
| 云端全停 | 154 | 154 | 100.00% |
| 触发 | 正常通道 | 断网时仍然产出 |
|---|---|---|
| 身体异常 | 端侧 | 14 / 14 |
| 心率超区间 | 端侧 | 14 / 14 |
| 配速偏离 | 云侧 | 30 / 30 |
| 整公里播报 | 端侧 | 104 / 104 |
| 里程碑 | 云侧 | 6 / 6 |
定义说明:这个数度量的是「断网时还能不能产出一句合规的话」,不含出声环节。 出声能力由离线语音引擎单独验证 —— 它不需要网络,冷启动 0.78~1.46 秒。
任何一句话,无论来自模板还是大模型,都必须过这套检查,不过就重写(最多三次):
| 检查项 | 不通过会怎样 |
|---|---|
| 含阿拉伯数字 | 打回 —— 语音会念错 |
| 含 markdown / 括号 / 列表符号 | 打回 —— 会被逐字念出来 |
| 超过 20 字 / 超过两句 | 打回 |
| 发音时长超出 3~8 秒 | 打回(安全类不设下限) |
| 纠错类没有把指令放开头 | 打回 |
| 命中书面连接词(首先/其次/作为AI) | 打回 |
大模型写完一句话,先过上面这套检查。不过就打回重写,并把它错在哪告诉它。 连试三次都不行,就降级用预置安全句 —— 绝不让一句不合规的话被念出来。
实测守门拦截效果(用八个故意违规的假模型做压测):
| 指标 | 结果 |
|---|---|
| 违规检出率 | 8 / 8 = 100% |
| 合规样本误报 | 0 |
| 真大模型的拒绝率 | 11 次调用中 9 次被打回 |
跑者在心率已经顶到上限时问「还能再快点吗」,教练不能顺着他说。 实测行为(这是回归测试里的一条):
「再快点行吗?」
「不行,心率已经到顶了。」
逻辑是:心率上限是训练学约束,不是商量项。 人格可以温和、可以强硬,但不能因为用户想要就放开。
实测:在没有传状态数据的情况下问它「现在心率是不是太高了」,它的回答是:
「心率我这边看不到,按感觉放慢一点。」
它没有编一个数字,而是直说看不到。 这条在回归里也有守(C18/C20 一组用例专门盯"没数据就不许说")。
| 项 | 值 |
|---|---|
| 原始文件 | 1,914 个运动手表导出文件 |
| 识别出跑步 | 112 次(其中 100 次数据完整可用) |
| 总时长 / 总距离 | 26.2 小时 / 292.8 公里 |
| 时间跨度 | 18 个月 |
| 观测最高心率 | 204(用于推算个人心率区间) |
| 配速分布 | 10/50/90 分位:4:21 / 5:22 / 6:12 |
| 命令 | 产出 |
|---|---|
python run_eval.py | 27 条回归测试 |
python tools/run_metrics.py | 运动指标 + 系统指标 + 四条件对照 |
python tools/measure_latency.py | 四个延迟口径 + 断网可用率 |
python tools/survey.py --verify | 统计实现自检(对教科书临界值) |
所有报告的抬头都带一行指纹,说明这份数字是哪套代码、哪套阈值、哪个模型产出的。 用内容哈希而不是提交号 —— 因为改了阈值但没提交,也必须能看出来。
实测:心率上限从 170 改成 180,config 指纹立刻变。回归里有一条专门守这个。
| 指标 | 值 | 说明 |
|---|---|---|
| 回归测试 | 27 / 27 | 每条都对应一个产品约束 |
| 话术库合规 | 43 / 43 | 时长 3.03~5.36 秒,中位 4.03 |
| 静默比 | 83.3% ~ 99.3% | 大部分时间不说话 |
| 决策延迟 | 0 毫秒 | 纯本地规则,不联网 |
| 大模型延迟 | 1.4 秒(预算 0.9 秒) | 超出,需换更快模型或调预算 |
| 语音冷启动 | 4.3 ~ 7.9 秒 | 所以必须预生成 + 缓存 |
| 语音缓存命中 | 57 毫秒 | 实际使用路径 |
| 断网可用率 | 100% | 154 次触发全部产出合规话术 |
| 条件 | 开口次数/场 | 静默比 | 重复率 |
|---|---|---|---|
| A 无 Agent | 0 | 100% | 0.000 |
| B 固定播报 | 8.09 | 9.66% | 0.843 |
| C 状态触发 + 模板 | 14.32 | 55.76% | 0.413 |
| D 状态触发 + 个性化 | 14.32 | 54.01% | 0.413 |
结论:固定播报是台复读机。它每公里说同一句话,重复率 0.843; 加上状态判断后降到 0.413,只有一半。这是"什么时候说"这套设计最直接的证据。
因为模拟器和真设备走的是同一个端口、同一套协议:
没有"模拟专用通道"这种东西。所以演示能跑通,就意味着真设备能跑通 —— 换的只是谁在生产数据。
| 方向 | 接口 | 做什么 |
|---|---|---|
| 上行 推 | POST /api/session/ingest | 每秒把心率、配速、步频、距离报上来 |
| 下行 拉 | GET /api/session/outbox | 拉取该播的话术,播完回传 out_seq |
seq,重连重发同一批不会重复处理。
缺号也不阻塞 —— 少一帧心率没影响,卡住整条链路影响很大。out_seq,重连后从那之后继续拉,
不会重复播报。
打开 用户端,
数据来源选「真设备」 —— 页面会停止自己造数据,
变成一个被动显示器,就等你从 POST /api/session/ingest 推数据进来。
这是对接真设备最快的验证方式。
| 想验证什么 | 数据来源选 |
|---|---|
| 看看完整流程长什么样 | 情景模拟(7 种情景) |
| 跑真实的历史数据 | 回放历史(22 个真实跑步文件) |
| 验证设备接入链路 | 真设备(等你推数据) |
samples 上报seq,重发时沿用同一个 seq(这才是去重的意义)docs/DEVICE_API.md。机器可读版:GET /api/protocol。
| 环节 | 实测 | 对设备的含义 |
|---|---|---|
| 决策计算 | 0 毫秒 | 纯本地规则,与网络无关 |
| 话术生成(大模型) | 1.4 秒 | 超 0.9 秒预算,需换模型或调预算 |
| 语音合成·在线冷启动 | 4.3~7.9 秒 | 实时用不了,必须预生成 |
| 语音合成·离线本机 | 0.78~1.46 秒 | 安全提示走这条 |
| 排队等自然间隙 | 刻意等 | 这是设计,不该算进延迟预算 |
| 能力 | 状态 | 证据 |
|---|---|---|
| 状态感知 | 完成 | 11 个状态字段,缺传感器降置信度而不编造 |
| 决策与克制 | 完成 | 四道关卡 + 安全类特权,27 条回归在守 |
| 意图分类 | 完成 | 8 触发 → 7 意图,每句带理由码 |
| 话术与守门 | 完成 | 违规检出 100%,真模型打回后自动降级 |
| 语音(在线+离线) | 完成 | 韵律合成,离线引擎断网可用 |
| 四类记忆 | 完成 | 跨场次闭环实测通过(第二场读到第一场) |
| 降级与容错 | 完成 | 四级降级链,断网可用率 100% |
| 实测与追溯 | 完成 | 指标/延迟报告 + 版本指纹 |
| 实时演示 | 完成 | 网页控制台真连 agent,可当场操作 |
| 项目 | 内容 |
|---|---|
| 设计 | 被试内:同一个人跑四场,四种条件用拉丁方平衡顺序 |
| 样本量 | 8 人(最少 6),共 32 场 |
| 问卷 | 四维度 × 3 题:时机 / 内容 / 感受 / 信任 |
| 统计 | Friedman + Wilcoxon 事后比较 + Bonferroni 校正 + 效应量 |
| 数据表 | 已生成,条件顺序预填好,跑完一场填一次分 |
| 局限 | 影响 |
|---|---|
| 单跑者数据(N=1) | 阈值只对这个人可信;换人必须重新标定 |
| 只有一秒级信号 | 没有逐拍心搏、呼吸率、原始加速度 —— 做不到相位级检测 |
| 被动数据无反事实 | 证不了"教练让人跑得更好",只能证"系统按设计工作" |
| 无主观指标 | 打扰度、舒适度都没有真人评分,全靠客观代理量 |
| 端侧是模拟的 | 真实产品要跑在手表或耳机芯片上,这里是算法层 |
实时演示控制台 →
(需本机运行 启动控制台.bat)