
为什么 AI 转写总是听错技术术语(以及我们是怎么修复的)
一次线上会话把 "what is the pointer in C++" 听成了 "what is the point in life"。这篇文章记录了从那份转写稿到 GeekBye v2.0.11 的完整取证过程 — keyterm 偏置、一个会掐断连接的时序竞争,以及我们自己的修复反而帮了倒忙的那一天。
7 月 2 日,我们跑了一次测试会话,对着 GeekBye 大声问了一个简单的问题:"What is the pointer in C++?"(C++ 里的指针是什么?)
实时转写给出了一个诗意的回答:
[23:16:37] You: Tell me, what is the point in life? [23:16:52] You: Handy Plus. [23:17:02] You: What the pointer in Plus Plus? [23:17:09] You: C.
"pointer in C++"(C++ 里的指针)就这样变成了 "point in life"(人生的意义)。同一场会话的健康指标讲完了故事的另一半:163 秒内 3 次转写连接断开,以及转写稿里一个 51 秒的空洞。还有一条后来被证明最关键的线索:我们的会话后恢复流程 — 它会重新转写本地保存的音频来填补空缺 — 几乎把这句话认对了:"a pointer in plus, plus? What the pointer in plus, plus C++."
音频本身没有任何问题。只是实时模型没有任何理由预期会出现 C++。
这就是 GeekBye v2.0.11 的故事,由真实的转写稿和生产日志讲述。
为什么语音模型会听错你的词汇
语音识别是一个预测问题。面对模糊的音频,模型会挑出最可能的词 — 而对一个通用模型来说,"point in life" 远比 "pointer in C++" 更像一句常见的话。凡是见过会议转写把 Kubernetes 写成 "cube and eddies" 的工程师,都遇到过这种失败。
解药不是更好的麦克风,而是 keyterm 偏置:在会话开始前告诉模型,哪些平时罕见的词对你来说是高频词。我们的语音服务商每个会话最多支持 50 个偏置词条。尴尬的部分来了:这些词条的传输管道在我们的技术栈里是端到端齐全的 — 客户端、后端、服务商 — 但从来没有任何东西往里面填过值。每一场会话都在零领域帮助的情况下裸奔。
修复 1:你的 profile 变成模型的词汇表
GeekBye 本来就知道你的领域 — 它就写在你激活的 profile 里。v2.0.11 会从 profile 的名称和描述中派生偏置 keyterms:带符号的词条(C++、Node.js)、缩写(SQL、AWS)、驼峰命名(TypeScript、PostgreSQL),以及专有名词。一个提到你技术栈的 profile,现在会让那个技术栈变成意料之中,而不是稀奇古怪。
修复让一切变得更糟的那一天
我们的第一版把每个首字母大写的词都当成了专有名词。在一个内部测试构建上(它从未到达任何客户手中),一份用散文写成的 profile 给模型发去了这样一份偏置列表:
Senior, Writing, Direct, For, Includes, Write, Role, Intent…
把语音模型往 "For" 这个词上偏置,比完全不偏置还要糟。在紧接着的测试会话里,清清楚楚说了好几遍的 "speak" 一词,回来时变成了 "Clicky"、"Hey, Vicky" 和 "Peter Paderty"。这个教训花了我们一个下午:只用有区分度的词条做偏置。首字母大写的词现在只有出现在句中时才算数(那才是真正的专有名词信号);markdown 标题里每个词都大写,永远不参与。同一份 profile 现在派生出的正是 LinkedIn, AI, CEO, MCP — 而验证会话在多语言、快速切换的音频下连续正确转写了 199 秒,189 个转写片段,零错误。
修复 2:一直在掐断连接的竞争
keyterms 解释了那些听错的词,但解释不了那三次断连。
这条线索通向了一个更隐蔽的地方。我们的服务商会根据自己的语音活动检测(VAD),在静音大约 1 秒后自行 commit(定稿)转写。我们的客户端也会在静音 250 毫秒后发送一个保险 commit,用来冲刷悬着的半句话。而服务商"我已经 commit 过了"的确认,要 1 到 3 秒才能传回来。拿这三个数字算一下:只要服务商先 commit,我们的保险 commit 就会打在一个几乎已经清空的缓冲区上 — 而服务商对此的回应可不是礼貌的拒绝。它直接掐断了连接。 每一次说话停顿都是一次抛硬币。
v2.0.11 针对这个问题上了两道防线:
- 在应用里: 当一条已 commit 的转写到达时,客户端现在知道服务商的缓冲区刚刚被冲刷过,于是跳过多余的保险 commit。
- 同一天,在我们的后端: 位于应用和服务商之间的代理会精确镜像服务商的音频记账 — 它以零延迟看到每一个音频帧和每一条 commit 确认 — 于是干脆拒绝转发任何服务商必然会拒绝的 commit。这一道防线一次性保护所有客户端版本,包括还没更新的用户。
不到一小时,我们就在生产环境看到它生效了。守卫拦截了两个只携带 178ms 和 256ms 缓冲音频的注定失败的 commit — 在那天之前,每一个都意味着一次板上钉钉的断连,和某人会议笔记里的一段空白。当天下午一场 60 分钟的连续会话记录了 5 次拦截和零断连。而在修复之前,就在同一天早上,一位真实用户为了对抗的正是这个 bug,在 6 分钟里重启了 5 次录音。
顺路搭车的两个小修复
AI 洞察现在会等到有实质内容再动。 那些早期的乱码片段,过去会喂进 GeekBye 的实时建议卡片,让它信心满满地从一个听错的 C++ 问题里生成 "Defining Life's Ultimate Purpose"(定义人生的终极意义)这样的话题。建议现在会等到会话积累起真正的对话体量再出现。
恢复出来的文字会挂上正确的说话人。 那个正确转写出我们 C++ 问题的恢复流程,当时把它归给了 "Them"(对方)。本地保存的音频时间线现在会记录当时是谁在说话,所以恢复出的片段能正确归属到 You 或 Them。
记分板
| 指标(实测,非估算) | 修复前 | v2.0.11 + 后端守卫之后 |
|---|---|---|
| 测试会话中的连接断开 | 163 秒内 3 次 | 0 |
| 最长的转写空洞 | 51 秒 | 验证中最差间隙约 6 秒 |
| "pointer in C++" | "point in life" | 识别正确,词汇已偏置 |
| 到达服务商的注定失败的 commit | 全部 | 0(在后端被拦截) |
如果你也在实时语音 API 上做产品
这次发布可以带走的三条通用经验:
- 给偏置功能喂料。 如果你的 STT 服务商支持 keyterms / 短语提示,填入一份小而有区分度的词汇表是你能拿到的最便宜的准确率提升 — 而填入常见词则是准确率损失。
- 永远不要在网络往返的劣势一侧,和服务商自己的状态机赛跑。 我们的客户端赢不了一场 250ms 对 3 秒的信息竞赛。守卫应该放在两路信号汇合的地方 — 对我们来说,就是后端代理。
- 发布前用真实构建做验证。 keyterms 的回归之所以被抓住,是因为 GeekBye 的每一个发布版本在出货前都会以签名并公证的构建形态,对着生产环境做测试。那个坏版本只在一台内部机器上存在了几个小时,从未出现在你的 Mac 上。
GeekBye v2.0.11 现已上线 — 如果你在用 v2,自动更新已经把它送到你手上了。想了解这次发布所依赖的可靠性基础工作,请看为什么你的 AI 笔记助手在糟糕的 Wi-Fi 下会停摆和 GeekBye v2 有什么新变化。想了解实时转写的日常用法,可以从 GeekBye 的实时转写开始。


