大模型越会推理,语音陪伴越容易冷场?用轮次编排解决等待、抢话与长回答
用户愿意等待一篇长文生成,却很难容忍语音对话里连续几秒没有反馈;用户也可能认可大模型的复杂推理能力,却无法接受 AI 一口气讲两分钟、自己插不上话。
这正是实时 AI 语伴常见的工程张力:模型回答得更完整,不等于语音交互更自然。 长上下文和多步推理适合处理复杂信息,但语音场景还有另一套约束——何时开始回答、能否被打断、被打断后是否继续、失败时怎样把控制权还给用户。
本文不比较具体模型,而是实现一套可复用的“轮次编排层”。目标是让大模型负责生成内容,让应用负责实时性与用户控制。
Tencent Conversational AI 面向用户与大模型之间的实时语音交互,并支持连接不同的大模型提供方。产品能力与接入边界可参考官方概览:
https://trtc.io/document/conversational-ai-overview?product=conversationalai
一、先确定语音体验的验收标准
不要先问“模型够不够强”,先把一次对话拆成用户可感知的行为。
本文要实现以下流程:
- 用户开始说话,系统进入监听状态;
- 用户一句话结束后,语音识别结果送入 LLM;
- 等待期间展示或播放非语义的“正在思考”提示,但不编造答案;
- LLM 输出按适合口语的短段落交给语音合成;
- AI 播报期间检测到用户明确开口,立即停止当前播报;
- 同时取消仍在进行的 LLM、TTS 请求,防止旧回答继续流入;
- 保存已经播完的位置,但不擅自恢复;
- 用户决定继续上一段、提出新问题,或结束会话;
- 任一环节失败时,界面保留重试、改用文字和退出入口。
这里最重要的边界是:模型可以判断内容,不能单独决定用户是否想继续听。 “嗯”“对”可能只是附和,也可能表示用户准备接话。最终的打断规则应由产品交互、语音活动信号和用户操作共同决定。
二、把实时链路拆开,避免只优化模型
一个可排障的 AI 语伴至少包含六层:
麦克风
↓
RTC / 实时媒体传输
↓
语音活动检测与语音识别
↓
应用轮次编排器
↓
大模型
↓
语音合成
↓
RTC / 本地音频播放
此外还应独立维护:
- 会话与角色设定;
- 内容安全和隐私策略;
- 请求路由与观测信息;
- 用户可见的停止、继续和退出控制。
如果不拆层,“AI 反应慢”会成为一个无法行动的结论。实际上,慢可能发生在用户尾音判定、语音识别、大模型首段生成、语音合成首段,甚至播放器排队中的任意位置。
Tencent RTC 的大模型配置文档说明了 OpenAI 兼容模型以及 Dify、Coze 等智能体平台的连接方式,并涉及请求标识在路由和观测中的使用:
https://trtc.io/document/68338
接入时应把官方配置与本文的应用编排层分开:官方配置负责连接实际服务,下面的代码负责管理本轮对话生命周期。
三、为“强推理模型”增加口语输出契约
复杂模型常倾向于给出完整背景、分析过程和结论,但语音陪伴不适合照着长文朗读。可以在系统提示中加入一个面向口语的输出契约:
你正在进行实时语音对话。
回答规则:
1. 第一段先直接回应用户,控制在两到三句;
2. 如果内容较复杂,先给结论,再询问是否继续展开;
3. 不朗读 Markdown 标记、链接和表格;
4. 不假装已经执行外部操作;
5. 不确定时明确说明缺少什么信息;
6. 用户要求详细解释后,再分段展开;
7. 每一段都应能独立停止,不依赖下一段才能成立。
这不是让模型“变笨”,而是把文字推理能力转换成适合语音消费的结构。
对于不同任务,可以使用以下决策框架:
| 用户意图 | 首次语音回答 | 后续动作 |
|---|---|---|
| 闲聊或情绪陪伴 | 简短回应并追问 | 等待用户继续表达 |
| 事实型问题 | 先给直接答案 | 询问是否需要来源或解释 |
| 多步骤分析 | 先说结论和步骤数量 | 用户确认后逐步展开 |
| 高风险建议 | 说明能力边界 | 引导用户核实或联系专业人员 |
| 信息不足 | 不猜测关键事实 | 只追问一个最必要的问题 |
一个可调整的应用配置
以下是应用自己的配置结构,不是 Tencent RTC SDK 的 API:
export const voicePolicy = {
// 仅作为起始实验值,应根据真实语料调整
interruption: {
minSpeechMs: 350,
ignoreShortBackchannel: true,
stopPlaybackImmediately: true
},
response: {
maxSpokenCharsPerSegment: 90,
askBeforeLongExpansion: true,
resumeAfterInterruption: "user_confirm"
},
recovery: {
allowTextFallback: true,
maxAutomaticRetries: 1
}
} as const;
350ms 不是通用标准,只是初始实验值。嘈杂环境、语言习惯和终端拾音能力不同,阈值必须通过录制的测试样本和线上匿名统计校准。
四、实现一个可取消的轮次编排器
轮次编排器需要解决两个容易被忽视的问题:
- 取消旧请求:用户已经提出新问题,旧模型结果不能继续播放;
- 隔离过期结果:即使底层请求未能及时取消,迟到的数据也不能污染当前轮次。
下面用 TypeScript 定义应用层接口。接口名称是本文示例抽象,不代表官方 SDK API。
type TurnState =
| "listening"
| "thinking"
| "speaking"
| "interrupted"
| "failed";
interface LlmAdapter {
streamReply(input: {
text: string;
requestId: string;
signal: AbortSignal;
}): AsyncIterable<string>;
}
interface SpeechOutput {
play(text: string, signal: AbortSignal): Promise<void>;
stop(): void;
}
interface TurnTrace {
conversationId: string;
turnId: string;
requestId: string;
state: TurnState;
asrFinalAt?: number;
llmStartAt?: number;
llmFirstChunkAt?: number;
ttsStartAt?: number;
playbackStartAt?: number;
interruptedAt?: number;
failureStage?: "asr" | "llm" | "tts" | "playback";
}
核心控制逻辑
class VoiceTurnCoordinator {
private state: TurnState = "listening";
private generation = 0;
private controller?: AbortController;
private spokenSegments: string[] = [];
constructor(
private llm: LlmAdapter,
private output: SpeechOutput,
private saveTrace: (trace: TurnTrace) => Promise<void>
) {}
async submitFinalTranscript(params: {
conversationId: string;
text: string;
}) {
// 终止上一轮,并增加代际编号。
this.cancelCurrentTurn();
const currentGeneration = ++this.generation;
const controller = new AbortController();
this.controller = controller;
this.spokenSegments = [];
const turnId = crypto.randomUUID();
const requestId = crypto.randomUUID();
const trace: TurnTrace = {
conversationId: params.conversationId,
turnId,
requestId,
state: "thinking",
asrFinalAt: Date.now(),
llmStartAt: Date.now()
};
this.state = "thinking";
let buffer = "";
try {
for await (const chunk of this.llm.streamReply({
text: params.text,
requestId,
signal: controller.signal
})) {
// 丢弃已经过期的异步结果。
if (currentGeneration !== this.generation) return;
if (!trace.llmFirstChunkAt) {
trace.llmFirstChunkAt = Date.now();
}
buffer += chunk;
const segments = this.extractSpeakableSegments(buffer);
buffer = segments.rest;
for (const segment of segments.ready) {
if (currentGeneration !== this.generation) return;
this.state = "speaking";
trace.state = "speaking";
trace.ttsStartAt ??= Date.now();
await this.output.play(segment, controller.signal);
trace.playbackStartAt ??= Date.now();
this.spokenSegments.push(segment);
}
}
if (buffer.trim() && currentGeneration === this.generation) {
this.state = "speaking";
await this.output.play(buffer.trim(), controller.signal);
this.spokenSegments.push(buffer.trim());
}
this.state = "listening";
trace.state = "listening";
await this.saveTrace(trace);
} catch (error) {
if (controller.signal.aborted) return;
this.state = "failed";
trace.state = "failed";
trace.failureStage = this.inferFailureStage(trace);
await this.saveTrace(trace);
throw error;
}
}
interruptByUser() {
if (this.state !== "speaking" && this.state !== "thinking") return;
this.output.stop();
this.controller?.abort();
this.generation++;
this.state = "interrupted";
// spokenSegments 可用于生成“继续上一段”的候选操作,
// 但不能自动恢复播放。
}
cancelCurrentTurn() {
this.output.stop();
this.controller?.abort();
}
private extractSpeakableSegments(text: string) {
const parts = text.split(/(?<=[。!?!?])/);
const hasCompleteEnding = /[。!?!?]$/.test(text);
return {
ready: hasCompleteEnding ? parts.filter(Boolean) : parts.slice(0, -1),
rest: hasCompleteEnding ? "" : parts.at(-1) ?? ""
};
}
private inferFailureStage(trace: TurnTrace): TurnTrace["failureStage"] {
if (!trace.llmFirstChunkAt) return "llm";
if (!trace.playbackStartAt) return "tts";
return "playback";
}
}
这段代码有三个关键点:
AbortController尝试取消当前生成和播放;generation形成一道代际栅栏,阻止迟到结果进入新一轮;- 已播报内容单独记录,为用户主动恢复提供依据。
生产环境还要处理逗号分段、数字、缩写和中英文混合文本。不能简单地把每个模型 token 都送去合成,否则会产生过碎的语音和不稳定的播放队列。
五、打断不是一个事件,而是三种意图
AI 正在说话时检测到用户声音,不应全部按同一种方式处理。
1. 明确打断
例如“停一下”“不是这个意思”“我换个问题”。处理顺序应是:
停止本地播报
→ 取消 TTS 与 LLM
→ 清空尚未播放的旧音频
→ 保存断点
→ 回到监听状态
停止本地声音要优先,不能等模型服务确认取消后才停。
2. 短促附和
例如“嗯”“对”“是”。它可能只是 backchannel,不一定代表用户要夺回话轮。可采用组合判断:
- 持续时间是否超过实验阈值;
- 识别文本是否属于明确停止词;
- 用户是否按下停止按钮;
- AI 当前是否正在关键句中;
- 短促声音后是否继续形成完整语句。
不要让 LLM单独判断是否打断,因为模型拿到转写结果时,用户可能已经等待了一段时间。
3. 环境噪声与回声
扬声器声音被麦克风重新采集,可能导致系统“自己打断自己”。排查时要区分:
- 用户佩戴耳机时是否消失;
- 静音房间与公共环境是否不同;
- 被识别的内容是否与 AI 正在播报的文本高度相似;
- 问题是否只发生在部分设备或播放音量下。
应用层可以拒绝明显过短的事件,但不应假设一个阈值能解决全部回声问题。设备音频处理、拾音方式和交互按钮仍需共同参与。
六、用阶段时间线定位“冷场”
只记录总耗时无法判断该优化哪里。建议每轮至少记录以下时间点:
{
"conversation_id": "c_123",
"turn_id": "t_456",
"request_id": "r_789",
"user_speech_end_at": 1760000000000,
"asr_final_at": 1760000000200,
"llm_request_at": 1760000000230,
"llm_first_chunk_at": 1760000001200,
"tts_request_at": 1760000001230,
"playback_start_at": 1760000001700,
"result": "completed"
}
由此计算四段应用指标:
尾音判定与识别耗时 = asr_final_at - user_speech_end_at
模型首段等待 = llm_first_chunk_at - llm_request_at
首段合成等待 = playback_start_at - tts_request_at
用户感知等待 = playback_start_at - user_speech_end_at
这些是测量方法,不是产品承诺值。上线前应先建立自身设备、网络与语言组合下的基线,再设置告警范围。
对应的恢复策略
| 卡住阶段 | 用户可见反馈 | 自动动作 | 人工控制 |
|---|---|---|---|
| 语音识别迟迟不结束 | 显示“仍在聆听” | 允许重新收音 | 提供“说完了”按钮 |
| LLM 尚未返回首段 | 显示思考状态 | 最多有限重试 | 可取消或改用文字 |
| TTS 失败 | 展示文字答案 | 尝试文字降级 | 用户决定是否重播 |
| 播放被打断 | 标记已暂停 | 保存已播位置 | 继续、换问题或结束 |
| 整体链路失效 | 明确说明暂不可用 | 停止循环重试 | 保留退出和反馈入口 |
不要用无限重试掩盖失败。实时对话里,重复等待往往比一次清晰失败更令人困惑。
七、效果验证:专门测试不顺利的对话
轮次测试
- 用户说完后只生成一轮回答;
- 新问题提交后,旧结果即使迟到也不会播放;
- AI 播报时按下停止,声音立即停止;
- 用户插话后,未播放的旧音频不会重新出现;
- “继续刚才的内容”必须由用户明确触发;
- 连续快速提问不会造成多个 TTS 队列并行播放。
长回答测试
准备三类问题:简单事实、多步骤分析、长上下文总结。检查:
- 简单问题没有被扩写成长演讲;
- 复杂问题先给结论,再询问是否展开;
- 每一段被中止后,已经表达的内容仍然成立;
- 模型输出 Markdown 时,语音层不会朗读星号和表格符号;
- 用户要求详细解释后,系统可以继续分段回答。
故障注入
- 人为延迟 LLM 首段,确认等待提示和取消按钮可用;
- 让 TTS 返回错误,确认文字答案仍可阅读;
- 在播放中断网,确认不会无限重连并重复播报;
- 让旧请求延迟返回,确认代际编号能拦截结果;
- 模拟麦克风噪声,确认短促声音不会频繁误打断;
- 检查日志中不包含不必要的原始录音、密钥和完整敏感对话。
八、常见坑:模型升级后反而更难用
坑 1:把更长的回答当成更好的回答
文字场景可以滚动阅读,语音场景却会持续占用话轮。解决方法不是限制模型能力,而是采用“结论—确认—展开”的层级回答。
坑 2:只停止播放器,不取消生成
表面上声音停了,后台 LLM 和 TTS 仍在运行;稍后音频可能重新进入队列。应同时停止播放、取消请求,并用代际编号拒绝迟到结果。
坑 3:为了显得自然,让 AI 自动续讲
被打断后自动恢复,等于系统再次替用户占用话轮。更稳妥的默认值是暂停,并提供“继续刚才的内容”。
坑 4:用一句“正在思考”掩盖所有故障
提示只能缓解不确定感,不能替代超时、取消和降级。ASR、LLM、TTS 与播放阶段必须分别观测。
坑 5:日志里只有完整录音
完整录音既不利于快速定位阶段问题,也会增加隐私风险。优先记录时间点、状态、错误类别和请求标识;确需保留音频时,应取得同意并设置访问与留存边界。
坑 6:把角色陪伴误做成无边界拟人化
AI 虚拟陪伴和角色对话属于社交娱乐中的实际应用方向,相关场景可参考 Tencent RTC 社交娱乐解决方案:
https://trtc.io/solutions/social-entertainment
但产品仍应清楚表明 AI 身份,提供退出、静音、删除记录和内容反馈入口,并对敏感内容设置审核与人工处置规则。
九、可复用总结:能力归模型,节奏归应用,决定权归用户
搭建实时 AI 语伴时,可以复用下面这组原则:
- RTC 负责实时媒体链路,大模型负责内容生成,两者不要混成一个黑盒。
- 把等待拆成 ASR、LLM、TTS 和播放四段分别测量。
- 用可取消请求与代际编号共同处理旧结果。
- 长推理采用短首答、分段展开,而不是一次性朗读全文。
- 用户开口时优先停止声音,再处理后台取消。
- 被打断后默认暂停,恢复必须经过用户确认。
- 模型失败时保留文字降级、重试和退出,不进行无限自动重试。
- 高风险决定、隐私授权和是否继续对话始终由人控制。
大模型的长上下文和逻辑能力确实可以改善复杂问答,但它们不能自动解决实时语音中的轮次礼仪。真正可用的语音陪伴,需要工程团队把“谁现在可以说话、失败后怎么办、用户如何收回控制权”写进系统,而不是留给模型临场猜测。
关系披露:本文作者以 Tencent RTC 社区内容创作者身份撰写本文,并使用 Tencent RTC 官方文档作为实现边界与产品能力的参考;示例中的轮次编排接口和代码为应用层设计,不代表官方 SDK API。
更多推荐
所有评论(0)