RealtimeAgent 实现该方案,支持:
- 回合检测:支持由模型 API 判断用户何时说完,也支持传入本地 VAD 自行判断
- 打断:支持用户开口即打断当前回复,上下文只保留用户实际听到的部分,也支持由代码主动打断
- 工具调用与用户确认:支持
Toolkit与权限系统,确认过程中语音流不中断 - 文本输入:支持在语音对话中直接发送文字(需模型支持文本输入)
- 断线自动恢复:模型 API 因空闲或超时关闭会话后,用户再次输入时自动重连并恢复会话
- 回合整理:支持合并被停顿切开的句子,过滤没有实际内容的应答
核心概念
语音到语音智能体由三个组件组成:- 音频传输(
TransportBase):负责声音的来源与去向,例如本地声卡或浏览器 - 实时语音模型(
RealtimeModelBase):负责与模型 API 的会话,把协议消息翻译成统一的模型事件 RealtimeAgent:负责在两者之间维护对话回合,处理打断、工具调用与事件产出
其中实时语音模型和音频传输模块支持从基类进行拓展,用以适配新的模型 API,或接入浏览器等其它客户端。
快速开始
首先安装实时语音的可选依赖,其中包含 WebSocket 客户端与本地声卡库:安装实时语音依赖
本地声卡库
sounddevice 依赖 PortAudio。macOS 与 Windows 已随包附带;Debian/Ubuntu 需要先执行 apt install libportaudio2。1
初始化实时语音模型
模型类接收模型名与所属 API 的凭证,模型卡片按名称自动匹配,采样率与上下文限制随之确定。音色、回合检测方式等可调项通过
Parameters 传入。下面的 tab 分别展示四种模型 API 的初始化方式,后续三步与选用哪种模型无关:2
创建智能体
智能体持有模型会话,系统提示在连接模型时一次性发送:
创建智能体
3
创建音频传输
传输决定声音的来源与去向,
LocalAudioTransport 使用本机的麦克风与扬声器。采样率必须与模型一致,因此直接用模型的属性构造,而不是写死数值:创建音频传输
4
运行对话
reply_stream() 借用传输持续泵送音频,并以异步迭代器的形式产出事件。用户说话同样以一次回复的形式产出事件,role 为 "user",因此下面用 reply_id 区分两边,把双方说的话打印到终端:运行对话并打印双方的话
三者分开的好处是客户端断开重连时不丢失模型会话,模型会话超时时也不影响传输。同一个智能体可以在传输更换后再次调用
reply_stream(),对话历史仍在 agent.state 中。
使用智能体
RealtimeAgent 的构造参数如下:
str
必填
智能体名称,会写入智能体消息与事件。
str
必填
系统提示,在连接模型时一次性发送,工具包中技能的说明会附加在后面。
RealtimeModelBase
必填
实时语音模型,支持的模型见上方表格。
Toolkit | None
默认值:"None"
模型可以调用的工具包,工具在智能体侧执行并经过权限检查。
AgentState | None
默认值:"None"
对话历史、权限规则与工具上下文,省略时新建。
TurnAggregator | None
默认值:"None"
回合整理器,负责合并被切开的句子与过滤无内容应答,省略时使用默认配置。
RealtimeAgent 的核心方法如下:
运行并处理事件
reply_stream() 接收一个已经启动的传输,持续把音频送给模型,并以异步迭代器的形式产出事件,直到传输的输入结束。传输由开发者持有,reply_stream() 结束时不会关闭它,同一个智能体可以换一个传输再次运行。
reply_stream() 产出的是与 Agent.reply_stream 相同的智能体事件,因此为文字智能体编写的事件处理逻辑可以直接复用。用户说话也被当作一次回复来报告:开口时发出 role 为 "user" 的 ReplyStartEvent,说完时发出 ReplyEndEvent,转写确定后再以文本块事件送出,因此外层可以用同一套逻辑把用户和智能体的事件各自拼成消息。模型音频则以数据块事件的形式产出:
发送输入
在支持文本输入的模型上,可以用send() 在语音对话中发送文字。文字会先打断当前回复,再作为一轮用户输入交给模型,回复仍以语音播放:
发送文字输入
send() 接收的输入类型与 Agent.reply 对齐:
DashScopeRealtimeModel(Qwen-Omni)的模型 API 不接受文本轮次,其余模型类均支持文本输入,可通过模型类的 supports_text_input 属性判断。打断
用户在回复播放期间开口,智能体会立即停止播放并取消模型侧的回复。传输会报告实际播放到的位置,智能体据此把上下文中的智能体消息截到用户真正听到的部分,避免模型”以为”用户听完了整句话。开发者也可以在代码中主动打断,例如响应界面上的停止按钮:主动打断当前回复
finished_reason 为 interrupted 的 ReplyEndEvent 结束。由于文本增量比音频先到,前端此时已经收到了多于用户实际听到的文字,因此该文本块的 TextBlockEndEvent 会带上 text 字段给出最终文本,用 Msg.append_event 拼消息时会自动覆盖,自行拼接的前端需要用它替换该块的内容。
智能体侧的上下文一定会被截断,模型侧则取决于模型 API 的协议,由模型类的 truncation 属性标明:
调用工具
传入toolkit 后,模型可以在对话中调用工具。工具在智能体侧执行,权限检查、用户确认与普通智能体一致。区别在于实时语音智能体不会暂停:需要确认时,reply_stream() 产出 RequireUserConfirmEvent 后继续产出其它事件,确认结果由开发者在任意时刻通过 send() 送回,与事件流本身解耦:
装配工具并接收确认请求
UserConfirmResultEvent 送回智能体即可,调用位置可以是界面回调、WebSocket 消息处理函数或终端输入。下面的 tab 分别展示两种场景:
- 只有模型卡片标注
supports_tools的模型才会收到工具列表,DashScope 的 Qwen-Omni 系列中仅 qwen3.5 系列支持,其余模型 API 的模型均支持。 - 确认请求超过 5 分钟没有回应,智能体按拒绝处理。
回合检测
回合检测决定”用户什么时候说完了”,只能由一方负责。默认情况下由模型 API 负责,智能体只响应它上报的说话开始与结束;传入vad 参数后改由智能体本地判断,并自动关闭模型 API 侧的回合检测。两种模式的对比如下:
各模型 API 的
turn_detection 取值不同,敏感度、静音时长等参数同样在 Parameters 中:
本地检测需要实现
VADBase:push() 接收传输送来的每一块 PCM16 音频,只在说话开始或结束的那一块返回 SpeechTransition,其余时候返回 None;reset() 在音频流出现间断(例如重连)时清空内部状态。传入 vad 后智能体会把 turn_detection 置为 none,无需手动设置:
接入本地 VAD
TurnAggregator 整理,再写入上下文:
配置回合整理
断线自动恢复
模型 API 都会主动关闭会话,只是触发条件不同:DashScope 的空闲超时约为三分钟,Gemini Live 的音频会话上限约为 15 分钟,OpenAI Realtime 约为 1 小时。智能体把这视为正常情况:记录一条 INFO 日志,保持传输打开,用户下一次说话时自动重连并补发这期间的音频。对话历史保存在agent.state 中,重连时智能体会把此前的对话转写附在系统提示之后发给模型,因此模型能接上之前的话题。
音频传输
音频传输负责”声音从哪里来、到哪里去”,智能体不关心音频来自本地声卡还是浏览器。AgentScope 当前提供以下传输:本地声卡
LocalAudioTransport 支持以下参数:
int
默认值:"16000"
采集采样率,必须等于模型的
input_sample_rate。int
默认值:"24000"
播放采样率,必须等于模型的
output_sample_rate。int | str | None
默认值:"None"
输入设备的编号或名称,省略时使用系统默认设备。
int | str | None
默认值:"None"
输出设备的编号或名称,省略时使用系统默认设备。
int
默认值:"100"
每块上行音频的时长。
int
默认值:"30"
打断时对正在播放的音频施加的淡出时长,避免爆音。
sounddevice 列出设备并按编号指定:
列出音频设备
- 佩戴耳机。 使用外放时麦克风会录到智能体自己的声音,回合检测把它当成用户开口,智能体会打断自己。
LocalAudioTransport不做回声消除。 - 不要让同一个蓝牙耳机同时负责输入和输出。 macOS 会把 AirPods 等设备切换到免提模式,此时经常没有声音。可以把耳机麦克风与内置扬声器搭配使用,例如
LocalAudioTransport(input_device=3, output_device=2)。
自定义传输
接入浏览器或其它音频来源时,继承TransportBase 并实现以下方法:
clear_audio() 返回的播放位置是截断上下文的依据,因此播放进度的统计应尽量靠近扬声器,浏览器中应放在 AudioWorklet 里。