实时语音模型最难处理的矛盾,不是“能不能听懂”,而是如何在用户仍然期待即时反馈时完成推理、调用工具并自然开口。Qwen-Audio-3.0-Realtime 的发布正面回应了这个问题:公告将升级集中在推理能力、Agent 工具调用、共情对话和双工交互流畅度四条主线上,并提供偏重推理的 Plus 版本与偏重速度的 Flash 版本。
这意味着开发者面对的不再只是一个语音转文字接口,而是一条持续接收音频、判断说话轮次、执行任务并流式返回语音的实时链路。
“实时”是整条链路的预算
端到端响应时间通常由音频采集、网络传输、模型首包、工具执行和音频播放共同决定。模型响应很快,并不代表用户一定觉得快:如果客户端积攒数秒音频才上传,或者工具接口需要两秒才能返回,交互仍会明显停顿。
可以把一次响应拆成以下指标:
| 指标 | 含义 | 建议观察方式 |
|---|---|---|
| 音频上行延迟 | 采集到服务端收到音频的时间 | 在音频块中记录客户端时间戳 |
| 首事件延迟 | 发出音频到收到首个模型事件 | 网关统一打点 |
| 首音频延迟 | 用户停顿到扬声器开始播放 | 客户端播放回调 |
| 工具耗时 | 模型请求工具到工具返回 | 按工具名称统计 P50/P95 |
| 打断恢复时间 | 用户插话到旧音频停止 | 播放器和会话事件联合记录 |
工程上应优先使用较小的音频块持续上传,并让文本、工具调用和音频结果都采用流式事件。音频块过大会增加等待时间,过小则会提高网络包、序列化和调度开销,需要根据移动网络与桌面网络分别压测。
Plus 和 Flash 不必二选一
公告给出的两个版本有清晰侧重:Plus 强调更强推理,Flash 强调更快速度。实际系统可以按照任务风险和复杂度路由,而不是让全部会话固定使用同一版本。
客服中的欢迎语、意图确认、订单号复述等步骤通常更看重速度;退款判断、课程讲解、多约束行程规划则更需要推理能力。可以这样实践一个简单路由器:
from dataclasses import dataclass
@dataclass
class Turn:
text: str
tool_required: bool = False
high_risk: bool = False
def choose_model(turn: Turn) -> str:
complex_markers = ("比较", "分析", "为什么", "制定计划", "分步骤")
needs_reasoning = any(word in turn.text for word in complex_markers)
if turn.high_risk or turn.tool_required or needs_reasoning:
return "qwen-audio-3.0-realtime-plus"
return "qwen-audio-3.0-realtime-flash"
if __name__ == "__main__":
turns = [
Turn("帮我查一下订单状态", tool_required=True),
Turn("你好,请介绍一下课程"),
Turn("比较两个退款方案的影响", high_risk=True),
]
for turn in turns:
print(choose_model(turn), turn.text)
这里的模型标识仅用于展示路由结构,应替换成实际服务提供的模型名称。更成熟的路由还应加入用户等级、预算、上下文长度和降级策略,并记录路由原因,避免线上问题无法追踪。
双工交互不是同时收发那么简单
自然的实时对话必须允许用户在模型说话时插话。客户端检测到用户开始说话后,应立即停止当前播放、清空尚未播放的音频,并向服务端发送取消当前响应的事件。否则模型虽然支持双工,用户仍会听到两个声音重叠。
由于公告摘要没有给出正式接口协议,下面是一个可改造的 WebSocket 客户端骨架,事件名和地址均为工程假设,不代表官方 API。运行前安装依赖,并把 REALTIME_URL、API_KEY、模型名和消息结构替换为正式文档中的定义:
python -m pip install websockets
export REALTIME_URL='wss://your-realtime-endpoint.example/v1/audio'
export API_KEY='replace-me'
python realtime_client.py
# realtime_client.py
import asyncio
import base64
import json
import os
import sys
import websockets
CHUNK_BYTES = 3200 # 示例:按实际采样率和服务端限制调整
async def send_audio(ws, pcm_path: str) -> None:
with open(pcm_path, "rb") as audio:
while chunk := audio.read(CHUNK_BYTES):
await ws.send(json.dumps({
"type": "input_audio.append",
"audio": base64.b64encode(chunk).decode("ascii"),
}))
await asyncio.sleep(0.1)
await ws.send(json.dumps({"type": "input_audio.commit"}))
async def receive_events(ws) -> None:
async for raw in ws:
event = json.loads(raw)
event_type = event.get("type")
if event_type == "response.text.delta":
print(event.get("text", ""), end="", flush=True)
elif event_type == "response.audio.delta":
audio = base64.b64decode(event["audio"])
sys.stdout.buffer.write(audio)
sys.stdout.buffer.flush()
elif event_type == "response.tool_call":
print("\n工具请求:", event, file=sys.stderr)
elif event_type == "error":
raise RuntimeError(event)
async def main() -> None:
url = os.environ["REALTIME_URL"]
api_key = os.environ["API_KEY"]
async with websockets.connect(
url,
additional_headers={"Authorization": f"Bearer {api_key}"},
) as ws:
await ws.send(json.dumps({
"type": "session.update",
"model": "qwen-audio-3.0-realtime-flash",
"instructions": "回答简洁;执行外部操作前先复述并确认。",
"audio_format": "pcm16",
}))
await asyncio.gather(
send_audio(ws, "question.pcm"),
receive_events(ws),
)
if __name__ == "__main__":
asyncio.run(main())
这个骨架刻意把发送和接收放进两个并发协程,以体现全双工连接。生产环境还要补上麦克风采集、扬声器播放队列、断线重连、心跳、会话恢复和响应取消,不能直接把二进制音频写到标准输出当作播放器。
工具调用要把确认、幂等和超时放在模型之外
语音 Agent 可以查订单、预约课程或修改服务,但模型发出工具请求并不等于操作可以无条件执行。涉及付款、退款、删除和身份信息修改时,服务端应校验参数,并要求用户明确确认。
建议为每次工具执行设置:
- 唯一幂等键,防止重连或重复事件造成二次下单。
- 明确超时,超时后让模型说明仍在处理中,而不是持续沉默。
- 参数白名单与权限检查,不能只依赖提示词约束。
- 可审计日志,保存会话 ID、工具名、参数摘要、确认状态和结果。
- 语音回读,高风险操作执行前复述金额、对象和影响范围。
共情表达同样存在边界。客服和教育场景可以让模型识别用户的焦虑或困惑,并调整语气,但不应把语言上的共情包装成专业医疗、心理或法律判断。
上线前按场景验收
接入这类模型时,版本选择只是开始。团队应使用真实噪声、口音、弱网和频繁插话构建测试集,分别记录首音频延迟、任务成功率、错误工具调用率和打断成功率。
一个稳妥的采用顺序是:先让 Flash 承担低风险、高频、短回答任务,再把复杂问答路由到 Plus;工具调用从只读查询开始,确认审计链路稳定后再开放写操作。这样既能利用实时交互带来的体验改善,也能把成本、延迟和业务风险控制在可观测范围内。