<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Barge-In on Chico's Tech Blog</title><link>https://realtime-ai.chat/tags/barge-in/</link><description>Recent content in Barge-In on Chico's Tech Blog</description><image><title>Chico's Tech Blog</title><url>https://github.com/chicogong.png</url><link>https://github.com/chicogong.png</link></image><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 28 Apr 2026 11:00:00 +0800</lastBuildDate><atom:link href="https://realtime-ai.chat/tags/barge-in/index.xml" rel="self" type="application/rss+xml"/><item><title>实时语音的打断:barge-in 怎么做对</title><link>https://realtime-ai.chat/posts/barge-in-engineering/</link><pubDate>Tue, 28 Apr 2026 11:00:00 +0800</pubDate><guid>https://realtime-ai.chat/posts/barge-in-engineering/</guid><description>用户插话时,语音 Agent 要原子地停播放、取消 TTS、取消 LLM、清状态。这篇拆解打断检测、误打断防护、回声消除与打断后上下文怎么接。</description><content:encoded><![CDATA[<p>约五分之一的语音通话里,用户会在 AI 还没说完时开口插话。</p>
<p>这个数字来自 PolyAI 对生产环境通话的统计,我自己看线上日志,感受也差不多。更值得注意的是后半句:<strong>会打断的用户,往往是意图最强的那批人。</strong> 他们听够了想直接说事,或者发现 AI 理解错了要赶紧纠正。换句话说,打断处理得烂的那一小撮通话,直接决定了你整个产品体验的天花板——因为最在乎、最着急的用户,恰好都撞在这里。</p>
<p>所以&quot;能被打断&quot;不是锦上添花的功能。它和延迟一样,是语音 Agent 的及格线。但和延迟不同,打断的难点不在毫秒,在于它是一个<strong>并发控制问题</strong>:一个事件要同时掐断好几条正在跑的流,还不能掐错。</p>
<h2 id="为什么打断比想象中难">为什么打断比想象中难</h2>
<p>很多人第一次做打断,以为就是&quot;检测到用户说话,调一下 <code>audioPlayer.stop()</code>&quot;。上线一周就会发现到处是坑。</p>
<p>难在三个地方。</p>
<p>第一,<strong>打断是个分布式的取消操作</strong>。用户开口的那一刻,你的系统里可能同时有:扬声器在播第 3 句的音频、TTS 服务还在合成第 5 句、LLM 还在流式生成第 8 句的 token。这三样东西跑在不同进程甚至不同机器上。你要在几十毫秒内把它们<strong>一起</strong>叫停,任何一个漏网,AI 就会出现&quot;我已经打断它了,它怎么还在自说自话&quot;的灵异现象。</p>
<p>第二,<strong>判断&quot;这算不算打断&quot;本身就难</strong>。背景里有人说话、电视开着、用户自己&quot;嗯&quot;&ldquo;对啊&quot;地随口附和——这些都会让一个朴素的 VAD 兴奋地触发打断。误打断比&quot;打断慢了&quot;更伤体验:AI 说到一半被一声咳嗽打断,僵在那里,用户一脸问号。</p>
<p>第三,<strong>打断之后,对话状态是脏的</strong>。AI 那句话只说了一半就被掐了,LLM 的上下文里到底该记成&quot;我说了整句&quot;还是&quot;我只说了半句&rdquo;?记错了,下一轮 AI 要么重复已经说过的内容,要么基于&quot;它以为自己说了但其实没说出口&quot;的信息往下接。</p>
<p>这三件事,后面一件件拆。</p>
<h2 id="怎么知道用户在打断">怎么知道用户在打断</h2>
<p>打断检测的第一关,是 VAD(语音活动检测)。它干一件很窄的事:判断这一小段音频里<strong>有没有人声</strong>。现代 VAD(比如 Silero)能在几十毫秒内给出&quot;有声/无声&quot;的概率,这一层基本算解决了。</p>
<p>但 VAD 只够回答&quot;有没有声音&quot;,回答不了&quot;这声音算不算一次真正的打断&quot;。2026 年成熟的做法,是在 VAD 上面叠一层判断,通常叫<strong>语义化的轮次检测</strong>。</p>
<p>这里要分清两个相关但不同的问题:</p>
<ul>
<li><strong>端点检测(turn detection)</strong>:用户这一轮说完了没?——决定 AI 什么时候开口。</li>
<li><strong>打断检测(barge-in)</strong>:AI 正在说话时,用户这声插话,要不要让 AI 闭嘴?——决定 AI 什么时候停下。</li>
</ul>
<p>它们共用 VAD 这个底座,但上层逻辑不一样。打断检测要额外回答的问题是:这声音是&quot;我要说话了你停下&quot;,还是&quot;嗯哼,我在听&quot;。</p>
<p>主流框架的处理方式,可以摆在一起看:</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>核心信号</th>
          <th>特点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>朴素 VAD</td>
          <td>音频能量 / 人声概率</td>
          <td>最快,但把附和、噪音全当打断</td>
      </tr>
      <tr>
          <td>转写流启发式</td>
          <td>VAD + ASR 部分转写文本</td>
          <td>看用户说出的字是不是&quot;有内容&quot;</td>
      </tr>
      <tr>
          <td>模型化打断判定</td>
          <td>专门小模型读音频+文本</td>
          <td>区分真打断 / 附和 / 噪音,准但有计算开销</td>
      </tr>
  </tbody>
</table>
<p>LiveKit 在 2026 年走的是模型路线:它的<strong>自适应打断</strong>(adaptive interruption)用真实对话音频训练了一个小模型,AI 说话期间检测到人声,不会无脑停,而是让模型先判断&quot;这次该不该让出话权&quot;。Pipecat 的 Smart Turn、Deepgram 的 Flux、VideoSDK 开源的 Namo,思路都类似——<strong>从&quot;听到声音就停&quot;升级到&quot;听懂这是不是打断再停&quot;。</strong></p>
<p>这里有个工程上的取舍:模型判定更准,但它要攒一点点音频和文本才能下判断,这等于给打断响应<strong>加了几十到一两百毫秒延迟</strong>。我的建议是分场景:电话客服这种噪音大、附和多的场景,这点延迟换来的误打断下降很值;而安静环境下的高质量耳麦场景,朴素 VAD 配一个合理阈值往往就够,没必要上重型模型。</p>
<p>说话人识别(speaker diarization)是另一条值得加的信号,尤其当 AI 走的是外放、而不是用户戴耳机的时候。带说话人意识的 VAD 只对&quot;主说话人&quot;的声音触发打断,旁边同事路过聊两句,AI 不会被带跑。代价是要先有一小段主说话人的注册音频,冷启动那几秒会暂时退化成普通 VAD。</p>
<h2 id="打断要原子地做四件事">打断要原子地做四件事</h2>
<p>检测到了真打断,接下来是这篇文章最该记住的一句话:<strong>打断不是&quot;停 TTS&quot;,是四件事必须一起做。</strong></p>
<pre class="mermaid">flowchart TB
  X[检测到真打断] --> A[1 停止音频播放]
  X --> B[2 取消 TTS 合成任务]
  X --> C[3 取消 LLM 流式生成]
  X --> D[4 清空管道缓冲与状态]
  A --> E[管道回到 ready<br/>开始听新输入]
  B --> E
  C --> E
  D --> E
  style X fill:#fde7c2,stroke:#e8b23c
  style E fill:#fde7c2,stroke:#e8b23c
</pre><p>逐件说。</p>
<p><strong>第一件,停止音频播放。</strong> 最直觉的一步,但有个细节:别只停&quot;还没送到扬声器的&quot;,还要清掉<strong>已经在播放缓冲区里排队</strong>的音频。很多框架的播放器有几百毫秒的 jitter buffer,你不主动 flush 它,用户已经开口了,AI 还会从缓冲里&quot;漏&quot;出半秒声音。</p>
<p><strong>第二件,取消还在合成的 TTS 任务。</strong> AI 嘴上播到第 3 句,TTS 服务很可能已经把第 4、第 5 句合成好或正在合成。这些任务必须立刻 cancel,否则它们合成完的音频会涌进刚清空的播放缓冲,等于打断没生效。流式 TTS 一定要支持中途 abort,选型时这是硬指标。</p>
<p><strong>第三件,取消还在生成的 LLM 请求。</strong> 这一件最容易漏,因为 LLM 在后台跑,你&quot;看不见&quot;它。但 AI 说到第 3 句时,LLM 大概率已经流式吐到第 8 句了。不取消,它会一直把 token 生成完——既烧钱,又让这个&quot;已经作废&quot;的回答占着你的会话状态。在 OpenAI、Anthropic 这些流式 API 上,正确做法是 abort 掉那个 HTTP 流;自己部署的推理服务,要确保 cancel 信号能真正中止那一次 forward,而不是只在客户端假装断开。</p>
<p><strong>第四件,清空整条管道的缓冲和状态。</strong> ASR 的部分转写、各级队列、状态机的 flag——全部归零,管道干净地回到&quot;准备听新输入&quot;的状态。</p>
<p>为什么强调&quot;原子&quot;?因为这四件事任何一件慢了或漏了,用户都会立刻察觉。漏了第二件,AI 停顿一下又冒出一句。漏了第三件,你这轮白烧 token,而且下一轮上下文是乱的。理想情况下,这四个取消应该由<strong>同一个打断事件</strong>并发触发、并发完成,而不是串行地&quot;停完播放再去停 TTS 再去停 LLM&quot;——串行会把延迟一段段叠起来。</p>
<p>实操里我会让打断走一个独立的高优先级事件通道,绕开正常的数据流水线,确保它能&quot;插队&quot;执行。否则打断信号自己排在拥堵的管道后面,就荒诞了。</p>
<h2 id="误打断别让噪音掐了-ai-的话">误打断:别让噪音掐了 AI 的话</h2>
<p>把打断做得太灵敏,会走到另一个极端:AI 老是被无关声音掐断。这就是误打断,前面说过,它比&quot;打断慢&quot;更毁体验。</p>
<p>误打断的来源,基本就这几类,得分开治:</p>
<p><strong>第一类,AI 自己的声音绕回来了。</strong> 这是最隐蔽、也最致命的一类。AI 外放的声音被用户的麦克风重新收进去,VAD 一看&quot;有人声&quot;,触发打断——AI 等于被自己说的话打断了,然后陷入&quot;说一句停一句&quot;的死循环。</p>
<p>治它的唯一正解是<strong>回声消除(AEC)</strong>。AEC 知道 AI 正在播什么音频(参考信号),就能从麦克风收到的信号里把这部分减掉,让 VAD 只看到&quot;真正的用户语音&quot;。所以 AEC 不是打断的可选配件,是<strong>前提</strong>。我见过的经验法则是:在调 VAD 阈值、最小语音时长这些参数之前,先确认 AEC 真的把回声压下去了——有团队报告上了带 AEC 的全双工处理后,误打断直接降了三成。次序不能反:回声没压住就调阈值,你只是在和一个会变化的噪声源拉锯。</p>
<p>值得提醒的是:浏览器里 WebRTC 自带 AEC,效果通常够用;但走电话线路(SIP/PSTN)、或者用了某些音频中转,AEC 可能不在你以为的地方,得自己确认链路上到底哪一环在做。</p>
<p><strong>第二类,背景里有别人说话或电视声。</strong> 这一类靠前面说的说话人识别来挡——只认主说话人。挡不住的部分(比如背景人声和主说话人音色接近),就交给模型化的打断判定去兜。</p>
<p><strong>第三类,附和音(backchannel)。</strong> 用户的&quot;嗯&quot;&ldquo;对&quot;&ldquo;哦&rdquo;——这些不是要抢话,是在表示&quot;我在听,你继续&rdquo;。朴素 VAD 完全分不出附和和打断,模型化判定才能。把附和单独识别出来、不触发打断,是这两年体验提升很明显的一块:AI 不会因为你一声&quot;嗯哼&quot;就慌张地停下来反问&quot;您说?&quot;。</p>
<p>调参上有个心智模型:打断检测内部其实有个&quot;置信度&quot;。置信度高(用户说了一串有内容的话)就立即让出话权;置信度低(短促的、像附和的声音)就先压一压、继续观察。把这个判断交给阈值或小模型,而不是&quot;一刀切地一听到声就停&quot;,是朴素方案和生产级方案的分水岭。</p>
<h2 id="打断之后上下文怎么接">打断之后,上下文怎么接</h2>
<p>打断动作做干净了,还剩最后一个、也最容易被忽略的问题:<strong>被打断的那句话,在 LLM 的对话历史里该怎么记。</strong></p>
<p>设想:AI 准备说的完整回答是&quot;您的订单已经发货了,预计明天下午送达,快递单号是 SF1234567890&quot;。它嘴上播到&quot;您的订单已经发货了,预计明天——&ldquo;被用户打断了。</p>
<p>现在 LLM 上下文里这条 assistant 消息,该写什么?</p>
<ul>
<li><strong>写完整回答</strong>:LLM 以为&quot;送达时间和单号我都说过了&rdquo;。下一轮用户问&quot;单号多少&quot;,AI 可能答&quot;我刚才说过了呀&quot;——可它<strong>根本没说出口</strong>。</li>
<li><strong>写空 / 整条丢掉</strong>:LLM 以为这轮自己什么都没说。下一轮可能从头再说一遍&quot;您的订单已经发货了……&quot;,用户已经听过一遍前半句,体验割裂。</li>
</ul>
<p>正确答案是第三个:<strong>把上下文截断到用户实际听到的位置。</strong> 这条 assistant 消息应该记成&quot;您的订单已经发货了,预计明天——&quot;,后面没说出口的部分不进上下文。这样 LLM 的&quot;记忆&quot;和用户的&quot;耳朵&quot;才对得齐,它才知道单号还没讲、送达时间也没讲完。</p>
<p>这件事说起来简单,做起来有个硬骨头:<strong>你得知道用户到底听到了哪个字。</strong> TTS 是流式播放的,被打断时,真正可靠的位置不是&quot;LLM 生成到哪&quot;,也不是&quot;TTS 合成到哪&quot;,而是<strong>音频实际播放到了哪一刻</strong>。理想情况下,TTS 要能给出词级或音素级的时间戳,你拿打断发生的时间戳去对齐,才能精确切到&quot;用户听到的最后一个字&quot;。退一步,按句子边界粗切也比&quot;全记/全不记&quot;强得多。</p>
<p>这不是纸上谈兵。Pipecat 社区里有个被讨论很多的 issue(#2791)就是这个:打断后,已经说出口的半句话没有被写回 LLM 上下文,导致下一轮 LLM 把整个回答重新生成一遍。这说明截断对齐这件事,框架默认不一定帮你做对,得自己确认。</p>
<pre class="mermaid">flowchart LR
  A[LLM 完整回答] --> B[流式 TTS 播放]
  B -->|播到一半| C[用户打断]
  C --> D[取打断时刻<br/>对齐播放时间戳]
  D --> E[上下文截断到<br/>用户实际听到处]
  E --> F[LLM 接住新一轮<br/>知道哪些还没说]
  style C fill:#fde7c2,stroke:#e8b23c
  style E fill:#fde7c2,stroke:#e8b23c
</pre><p>接住之后还有个加分项:把用户的打断<strong>当成信号去理解</strong>,而不只是&quot;换我说了&quot;。用户在 AI 报送达时间时插话,大概率是对前面的信息不满意或要追加条件。一个聪明的 Agent 会把&quot;用户在我说到 X 时打断&quot;这件事本身喂给 LLM,让它意识到 X 可能正是用户要纠正的点。这一步做不做,区别就是&quot;能被打断的机器&quot;和&quot;听得懂你为什么打断的对话者&quot;。</p>
<h2 id="一个落地的优先级">一个落地的优先级</h2>
<p>如果你正在给语音 Agent 加打断,我会建议这个顺序:</p>
<ol>
<li><strong>先保证四件事原子地做对</strong>——停播放、取消 TTS、取消 LLM、清状态,一个都不能漏。这是正确性,漏了就是 bug,不是体验问题。</li>
<li><strong>再把 AEC 压实</strong>——回声不消干净,后面所有的误打断调参都是徒劳。</li>
<li><strong>然后上语义/模型化的打断判定</strong>——把附和、噪音、真打断分开,这一步直接决定&quot;它像不像个会聊天的人&quot;。</li>
<li><strong>最后做上下文截断对齐</strong>——让 LLM 的记忆和用户的耳朵对齐,打断后的对话才接得顺。</li>
</ol>
<p>很多团队卡在第三步,觉得&quot;打断不够智能&quot;,于是拼命换模型、调阈值。但常见的真相是第一步就漏了——LLM 请求压根没被取消,或者第二步 AEC 根本没生效。打断这事,<strong>先把正确性焊死,再谈智能。</strong> 顺序反了,你会在一个有裂缝的地基上反复刷漆。</p>
]]></content:encoded></item></channel></rss>