问语音助手「巴黎现在天气怎么样」,是两种截然不同的难度。

半双工系统眼里这是一道流水线题:录完音 → 转文字 → LLM 决定调 get_weather → 等 API → 念结果。中间卡两三秒没关系——反正轮到它说话之前,天塌下来用户也只能等。

全双工系统眼里这是一道实时题。之前的系列拆过:全双工模型一直在听、随时会说,时间轴不会为任何人暂停。可工具调用偏偏是个「暂停—等待—恢复」的过程:请求发出去了,答案还没回来,这段真空期模型说什么?用户这时候又插了一句话怎么办?Moshi、OmniFlatten 这批开创性的全双工模型,没有一个碰这个问题——它们能聊天,但不会办事。

NVIDIA 在 2026 年 8 月初放出的 NemotronLabs VoiceChat 11B,给出了第一份开源答案:在全双工模型的输出侧加一条专门的工具通道,把「调用脚本」当成和「说话内容」并行的一路 token 流;等待 API 返回的真空期,则交给一句可配置的 on-hold 消息——「稍等,我帮你查一下」。模型卡的说法是:这是第一个支持工具调用的开源全双工模型。

这篇顺着 NVIDIA-NeMo/Speech 的 nemotron-labs-voicechat 分支把它拆开:先看模型本体的四条通道怎么在 80 毫秒的节拍上共舞,再看工具调用怎么塞进不停走的时间轴,最后看 NVIDIA 怎么把这套东西塞进 Triton 和两个魔改 vLLM 的生产容器——后面这半段在以往的论文式全双工工作里是看不到的。

一、总览:三个部件,一个节拍器

NemotronLabs VoiceChat 是个 11B 的组合体,三个部件全部来自 NVIDIA 自家货架:

  1. 听觉:FastConformer 流式编码器,取自 0.6B 的流式 ASR 模型 Nemotron-Speech-Streaming-En-0.6b,把 16 kHz 用户音频压成低帧率的连续嵌入;
  2. 大脑:Nemotron Nano v2,9B 的 Mamba–Transformer 混合架构 LLM,负责所有「决策」——说什么、什么时候说、要不要调工具;
  3. 嘴巴:一个帧同步的流式 TTS(代码里叫 EAR TTS)加神经 codec,把大脑吐出的文本 token 实时变成 22.05 kHz 的语音。
用户 16 kHzFastConformer流式编码器每帧一个连续嵌入+上一步的三个 token(agent 文本 · 用户转写 · 工具)各自查嵌入后回喂NemotronNano v2 · 9BMamba–Transformer 混合每 80 ms 前向一步只预测文本,不碰音频码agent 文本头用户转写头工具通道头逐帧喂入EAR TTS与 LLM 锁步自回归31 个RVQ 码/帧codec解码器agent 22.05 kHz<TOOLCALL>[{"name": "get_weather", …}]拼出脚本 → 交给外部执行,响应再注入回通道80 ms全局节拍 12.5 Hz:听、想、说、调工具,每 80 ms 各走一步
总览:四条通道踩同一个 80 ms 节拍。用户音频经流式编码器进入 9B LLM,与上一步的三路 token 融合;三个输出头分别吐 agent 文本、用户转写、工具 token;agent 文本逐帧喂给 EAR TTS 出声

真正把三个部件拧成一个系统的,是一个全局节拍器:80 毫秒一步,12.5 Hz。 编码器每 80 ms 产出一帧音频嵌入;LLM 每 80 ms 前向一步;TTS 每 80 ms 产出一帧音频码,恰好解码出 1764 个样本(22050 × 0.08)的波形。听、想、说、调工具,四件事在同一个时钟上各走一格,谁也不等谁——这就是全双工的机械本质。

每一步里,LLM 的输入是四路信息融合成的一个向量:当前帧的用户音频嵌入,加上上一步自己吐出的三个 token(agent 文本、用户转写、工具通道)各自查完嵌入表的向量,默认做加权求和(代码还支持门控融合)。输出侧则是三个并排的头:lm_head 管 agent 说什么,asr_head 管用户在说什么(边听边转写),function_head 管要不要调工具。一进三出,全部对齐在同一根时间轴上。

放回系列的三张地图,它的坐标很清晰:双工决策完全发生在 token 流内部,标准的 L2;输入侧不做离散化、用连续嵌入直进 LLM,和 SALMONN-omni 同路;输出侧则比谁都保守——LLM 只碰文本,一个音频 token 都不碰,声学全部外包给帧同步的 TTS。上篇拆 SALMONN-omni 和 Covo-Audio 时总结过那个正在成型的新共识:智能留在文本域,声学各回各家。 NemotronLabs VoiceChat 是这个共识迄今最彻底的执行者:9B 的大脑里没有一个音频码本的位置,Moshi 那笔「codec 进词表」的智能税,它一分都不交。

这个设计还有一层出身上的意义。它的直系前身是 NVIDIA 2025 年的 SALM-Duplex(arXiv:2505.15670)——那篇论文的卖点就是:用预训练的流式 ASR 编码器接管听觉,就不需要昂贵的语音预训练,任何现成 LLM 都能改装成全双工。VoiceChat 把这条路线放大到 9B 底座,又把输出侧从「LLM 直接出 codec」换成了外挂 TTS。

顺带一提发布形态里一个诚实的细节:这个「端到端模型」其实是拼出来的。仓库里 NemotronVoiceChat 类的 training_step 直接返回 None——组合体本身不训练,大脑(含听觉)和嘴巴各自独立训完,再用 combine_ckpt_conda.sh 把 STT、TTS、RNNT 三份权重合并成一个 HuggingFace checkpoint。两个自回归模型,在文本通道上对接成一个系统。

二、通道即时间线:轮次是「预测」出来的

理解这个模型的钥匙,是想清楚「通道」到底是什么。源码 duplex_stt_model.py 里有一段注释画得极好:

flow:         |---user---||-------assistant--------||-user-|
text channel:  0000000000  1xxxxxxx0000000000000002  000000

agent 文本通道和时间严格对齐:用户说话时,它输出 PAD(0);模型决定开口,先吐一个 BOS(1),跟着吐正文 token;说完吐 EOS(2),回到 PAD。「轮次」不再是对话协议里的一个回合,而是通道上 BOS 与 EOS 之间的一段区间。 什么时候开口、什么时候闭嘴,就是「在哪一帧吐出 BOS/EOS」——轮次切换从一个工程问题(VAD 判停、超时阈值)变成了一个预测问题(下一个 token 是不是 BOS)。

用户转写通道同理,只是记号换成了两个便宜字符:^ 表示用户开口,$ 表示用户说完(直接复用词表里的现成 token,省掉两个特殊 token)。四条通道摆在一起,一次对话长这样:

用户停了半拍 → 模型自己吐 BOS 开口用户插话 → 转写通道冒出 ^用户音频(麦克风)用户转写通道asr_headagent 文本通道lm_headagent 语音EAR TTS“Tell me about the Eiffel Tower”“Shorter please!”^tell me about the eiffel tower$^shorter please$BIt was built in 1889 as the entrance…EBSure…静音码几帧后 agent 通道吐 EOS,TTS 当步换上静音码时间 →(每格 80 ms,图中省略网格)B / E = agent 文本通道的 BOS / EOS  ^ / $ = 转写通道里「用户开口 / 说完」的记号  点线 = PAD(沉默)四条通道同一时钟推进:模型永远在听,也随时可以开口——「轮次」只是通道上 BOS 与 EOS 之间的一段。
四通道时间线:用户问埃菲尔铁塔,停顿半拍后 agent 通道自吐 BOS 开始回答;用户中途插话,转写通道冒出 ^,几帧后 agent 通道吐出 EOS,TTS 当步切静音码

推理循环的核心一步(_step_inference,简化):

# 每 80ms 执行一次
agent_text_emb = self.embed_tokens(gen_text[:, t-1])       # 上一步 agent 说的
user_text_emb  = self.embed_asr_tokens(gen_asr[:, t-1])    # 上一步用户转写
user_audio_emb = audio_embeds[:, t]                        # 当前帧用户音频
function_emb   = self.embed_tokens(gen_function[:, t-1])   # 上一步工具通道

fused = self.fusion_module(agent_text_emb, user_audio_emb,
                           user_text_emb, function_emb)
out = self.llm(inputs_embeds=fused, past_key_values=cache)  # 9B 前向一步

gen_text[:, t]     = sample(out.text_logits)      # agent 下一个字
gen_asr[:, t]      = out.asr_logits.argmax()      # 用户转写下一个字
gen_function[:, t] = out.function_logits.argmax() # 工具通道下一个字

于是全双工的各种行为都成了这个循环的自然推论:

  • 响应快:用户话音刚落,模型看到的音频嵌入已经包含完整语义,下一帧就可以吐 BOS,官方数字是约 450 ms 的轮次响应;
  • 打断:agent 说到一半,音频通道和转写通道同时报告「用户开口了」,模型学过这种局面的正确应对——吐 EOS 让位,官方数字 480 ms;
  • 判停靠语义:「嗯……让我想想」和「说完了」在声学上都是静音,但在 LLM 眼里语义完全不同。模型不依赖外挂 VAD 的静音时长阈值,理论上句中停顿不会被抢话(实际表现后面再说,这里先立个 flag)。

模型自己的预测之外,推理代码里还埋了一套保险丝(force_turn_taking):转写通道连续约 3 秒全是 PAD 且模型迟迟不开口,就强行往 agent 通道写一个 BOS;反过来 agent 说话时转写通道冒出 ^,若一段窗口内还没有 EOS,就强写一个 EOS。生产配置里还有第二套基于 RNNT 解码器的同款守护(下文第五节)。值得注意的是出厂参数把这些门限调得很宽——3 秒量级,而模型卡宣传的响应是 450 ms 量级。快速反应靠模型自己学到的行为,保险丝只兜「模型卡住了」的底。

三、嘴巴:两个自回归模型的锁步舞

大脑只出文本,声音从哪来?来自第二个自回归模型——Duplex EAR TTS,它和 LLM 以完全相同的 12.5 Hz 节拍锁步运行:LLM 每吐一个文本 token,TTS 就把它吃进去,同步产出一帧音频码。

# nemotron_voicechat.py · offline_inference(简化)
for t in range(1, T):
    ans = self.stt_model._step_inference(t, ...)      # 大脑走一步
    code, past_key_values = self.tts_model.infer_codes_one_step(
        current_subword_id=inference_state["gen_text"][:, t],  # 本步文本
        prev_subword_id=inference_state["gen_text"][:, t-1],
        prev_audio_tokens=code,                                # 上一帧音频码
        past_key_values=past_key_values,
    )                                                  # 嘴巴跟一步

这个 TTS 值得单独一节,因为它不是普通 TTS 的流式化,而是为全双工特调的。代码注释交代了出身:架构基于 Audio Flamingo 3(arXiv:2507.08128)提出的流式 TTS,外加三处改造——文本与音频表示的门控融合、改善多 token 单词发音的 subword 感知嵌入,以及一对支持双工交互的定制 BOS/EOS 嵌入,让「说到一半被掐断」成为模型见过的正常局面。

LLM 本步文本 token“wea@@”字符感知编码 CAS看见拼写,发音不糊上一帧 31 个 RVQ 码depthsum 嵌入成一个向量⊕门控融合TTS 主干 × 28 层Gemma3 结构 · 隐宽 1152带 KV cache 流式前向说话人 latent(序列前缀)发布版已烘焙固定音色MoG 头1024 个高斯分量采样连续潜变量 z把 z 逐级贴回 RVQ 码本得到本帧 31 个码(码本各 1024)codec 解码因果,可逐帧出声80 ms 波形(1764 样本)本帧 31 码回喂,成为下一步的「上一帧」若文本通道本步是 EOS:回喂的音频码被强制换成静音帧的码——不等余音播完,下一帧就归于安静。推理时对「有文本条件 / 无条件」双路前向做 classifier-free guidance(scale 0.2),咬字更稳。
EAR TTS 一帧内的流程:本步文本 token 经字符感知编码,与上一帧 31 个 RVQ 码的嵌入门控融合,过 28 层主干后由混合高斯头采样连续潜变量,贴回 RVQ 码本得到本帧 31 个码,codec 增量解码出 80 ms 波形

内部结构几个有意思的点:

  • 主干很小:28 层、隐宽 1152 的 Gemma3 结构 Transformer——嘴巴不需要智能,跟得上节拍就行;
  • 字符感知编码(CAS):文本 token 先过一个字符级编码器再进主干。BPE 把 “weather” 切成什么样是分词器的事,发音却取决于拼写,让 TTS 看见字符能明显减少读错生僻词;
  • 连续潜变量 + RVQ 贴码:每帧的生成不是在 31 个码本上各跑一次分类,而是先由混合高斯头(1024 个分量)采样连续潜变量,再分组「贴」回 RVQ 码本得到 31 个离散码——把逐码本分类换成连续空间的回归加投影,这是它能锁步跑实时的关键之一;
  • CFG 也在场:推理时对「有文本条件/无条件」做双路前向,classifier-free guidance 的 scale 是 0.2,让咬字更稳。

全双工特调的部分藏在一个不起眼的分支里——文本通道一出 EOS,回喂给 TTS 的「上一帧声音」立即被换成静音帧的码:

# duplex_ear_tts.py · infer_codes_one_step
if self.cfg.get('inference_force_speech_silence_on_eos', True):
    prev_audio_tokens = torch.where(
        current_subword_id.unsqueeze(-1) == self.text_eos_id,
        silence_codes,        # EOS 一出现,下一帧起立即归于安静
        prev_audio_tokens,
    )

配合大脑侧「被打断就吐 EOS」的行为,打断的全链路就通了:用户开口 → LLM 吐 EOS → TTS 静音——没有任何一处需要「截断音频流」的外科手术,声音是顺着通道语义自然消失的。

音色方面,TTS 用 3 秒参考音频的 latent 做说话人条件。但发布的 checkpoint 做了一手「voice lock」:预设说话人的 latent 直接烘焙进权重,而把音频参考的投影层重新随机化——你给它任何新的参考音频,得到的都是无意义的条件向量。仓库里那个 _voice_lock 后缀的转换脚本注释写得很直白:只有预烘焙的音色能用。防语音克隆做进了权重而不是服务条款,这个思路值得记一笔。

四、第四条通道:工具调用怎么塞进不停的时间轴

到这里,前三条通道(音频进、agent 文本出、用户转写出)在之前的全双工工作里都能找到影子,第四条才是这个模型的独门菜。

先看训练时怎么教。工具调用在数据里被表示成函数通道上的一段 token:调用脚本是模型要学着「说」的,格式是纯文本的 <TOOLCALL> 包 JSON;三个 Nemotron 词表里的保留 token 划出边界——<SPECIAL_20> 开始调用(代码里叫 SOTC)、<SPECIAL_21> 调用结束(EOTC)、<SPECIAL_22> 响应结束(EOTR)。关键设计是插入式时间轴扩展:函数调用和响应的 token 序列被「插」进时间轴,序列因此变长,插入区间里 agent 文本通道全补 PAD、用户音频通道全补静音。翻译成人话:调工具的那几拍,对话时间被暂停键拉长了,但每条通道依然对齐——模型学到的是「说完 on-hold 消息后闭嘴等待,看到响应注入后再开口」。

推理时的一次完整调用长这样:

注入窗口:响应 token 逐帧插入,时间轴被拉长,其余通道补 PAD / 静音用户音频(麦克风)agent 文本通道→ EAR TTS 发声工具通道function_head外部世界(服务端执行)“What's the weather in Paris?”B“Let me check that for you.”按工具预设的 on-hold 消息EB“Sunny, 18 °C.”<TOOLCALL>[{"name": "get_weather","arguments": {"city": "Paris"}}]</TOOLCALL><TOOL_RESPONSE>[…sunny, 18 °C…]执行 get_weather(city="Paris")服务端内置工具或客户端自带的函数脚本交外部执行响应注入回通道时间 →实线框 = 模型逐帧预测的 token  虚线框 = 从外部注入通道的 token  点线 = PAD(沉默)等待工具返回时用户听到的不是冷场,而是 on-hold 消息;调用与播报在同一时钟上并行推进。工具通道与文本通道共用词表和嵌入,但各挂一个输出头,互不抢字。
工具调用时间线:话音刚落,工具通道拼出 TOOLCALL 脚本,agent 通道同时播报 on-hold 消息;外部执行期间响应 token 被逐帧注入工具通道,其余通道补 PAD 和静音;响应注入完毕,agent 依据结果开口回答

拆开说四个环节:

  1. 工具怎么注册:老老实实写进系统提示词。README 给的默认 Jinja 模板把工具列表(JSON schema)放进 <AVAILABLE_TOOLS> 标签,再附上一大段决策规则——「匹配工具必须调用、常识问题直接回答、都不沾就礼貌拒绝」,甚至包括「参数必须来自用户说出的内容,缺参数要追问,不许瞎猜」;
  2. 调用怎么发出:function_head 在工具通道上逐帧吐出 <TOOLCALL>[{"name": "get_weather", "arguments": {"city": "Paris"}}]</TOOLCALL>。工具通道与文本通道共用词表和嵌入表,各挂一个输出头,说话和调工具互不抢字——这正是「第四条通道」而非「文本里夹工具指令」的意义;
  3. 等待怎么填:每个工具可以配一组 ack_messages(on-hold 消息),服务端检测到调用一发出,就把预设的那句「Sure, let me check the weather for you.」注入 agent 文本通道,TTS 照常发声。用户听到的不是冷场,而是一句自然的过渡;
  4. 响应怎么回来:外部执行完,结果被包成 <TOOL_RESPONSE>[...]</TOOL_RESPONSE> 的 token 序列,逐帧注入工具通道(这也是为什么官方要求响应必须是 ASCII 且「TTS 友好」——它要原样喂进模型,模型多半会照着念)。响应注入完毕,模型回到正常节拍,开口播报结果。

效果如何?模型卡给的数字:BFCL-v3(经 AU Harness)平均 56.1%,其中 simple 58.5%、multiple 62.5%;Full-Duplex-Bench v3 的工具调用项,工具选对率 82.5%,参数准确率 44.2%。参数准确率明显是短板——语音链路里「用户说的城市名」要先被听对、再被拼对,比文本 agent 难一截。官方建议每会话不超过 5 个工具,且工具调用执行期间用户无法打断 agent——全双工在这几拍里暂时退化成了半双工,这是当前实现的诚实边界。

五、上线:把 80 ms 节拍塞进 Triton 和 vLLM

论文式的全双工工作到第四节就该收尾了,但这个仓库最独特的部分才刚开始:一条完整的生产链路。NVIDIA 把整套东西打进一个推理容器,跑在单张 80 GB 卡上(文档给的实际占用约 66 GB),对外是 WebSocket 接口——而且刻意兼容 OpenAI Realtime API 协议:session.update 配工具、input_audio_buffer.append 推音频、response.output_audio.delta 收语音,现成的 Realtime 客户端和 Pipecat 都能直接对接。

客户端(麦克风 ⇄ 扬声器)24 kHz PCM16 · 建议 80 ms 一块 · base64WebSocket /v1/realtime(OpenAI Realtime 协议兼容)推理容器(CUDA + Triton + vLLM,单卡)WebSocket 服务层:会话与事件 · 重采样 24k ⇄ 16k / 22.05k · 内置工具执行 · on-hold 消息注入工具经 session.update 注册,调用结果由 conversation.item.create 回传gRPC · sequence batching(CORRID = 会话号)Triton 推理服务器(Python backend,一次只服务一条会话序列)每个推理步(80 ms 的整数倍)感知编码cache-aware 流式+ 两张 CUDA graph只编码新音频逐帧循环(每帧 80 ms)四通道融合 → LLM 一步(vLLM ①)→ 工具状态机 → 轮次守护(RNNT)→ TTS 一步(vLLM ②)codec 增量解码因果卷积缓存只解新帧,无咔哒声每帧 1764 样本vLLM ①(LLM):bf16 · 显存池 52%embedding 进 · 一帧一 token · ignore_eos,用 abort 收尾vLLM ②(EAR TTS):fp32 · 显存池 18%bf16 会让语音出幻觉,只好全精度单卡部署:要求 ≥ 80 GB 显存(实测约 66 GB)· 全程无权重量化 · 驱动 > 580 · x86_64
推理容器分层:WebSocket 服务层管会话、重采样与工具执行;Triton 的 Python backend 用序列批把流式会话钉在同一实例上;每步内部:带缓存与 CUDA graph 的感知编码 → 逐帧的 LLM/TTS 锁步循环 → codec 增量解码;LLM 与 TTS 各占一个打过补丁的 vLLM 引擎

几个工程决定值得细看。

把「每帧一步」塞进 vLLM 的三板斧。 vLLM 的世界观是「一段 prompt,生成到 EOS 为止」,和 duplex「永远在生成、每步输入都是新的多模态嵌入」的世界观八字不合。适配的办法:

# streaming_llm_engine.py —— 让 vLLM 永不自行收尾
default_sampling = {
    "max_tokens": 100000,     # 设到天上去;结束一律靠显式 abort
    "stop": [], "stop_token_ids": [],
    "ignore_eos": True,
}

一是走自定义输入规格(custom_input_specs),让请求携带的不是 token 而是融合后的 embedding;二是会话开始时用一段假 prompt 开一个「永不结束」的请求,此后每帧调一次 append_request 续上新 embedding——这个 API 是 stock vLLM 没有的,容器启动脚本里那句「patches vLLM」指的就是它;三是断言每步恰好产出一个 token,多一个都算 bug。采样也拆成了两半:LLM 的 top-p、重复惩罚在 vLLM 外面对 logits 做(特殊 token 直通不采样,保证 BOS/EOS 决策不被采样噪声搅局),TTS 则用 skip_sampling 把混合高斯采样整个塞进 vLLM 内部。

精度是道菜谱,不是滑块。 LLM 跑 bf16、占 52% 显存池;EAR TTS 必须 fp32、占 18%——配置注释直说 bf16 会让语音出幻觉。整条链路没有任何权重量化,66 GB 就是这么来的。感知编码器则吃了另一副药:cache-aware 流式编码(每步只编码新增音频)加两张手工捕获的 CUDA graph——为什么是两张?因为首个 chunk 和后续 chunk 的 mel 长度不同,各需要一张静态图。codec 侧用因果卷积缓存做增量解码,注释里的原话是顺便消掉了拼接处的咔哒声。

打断在服务层的最后形态。 生产配置默认用 RNNT 解码器(合并 checkpoint 时塞进来的第三份权重)做轮次守护:它逐帧解码用户音频,连续 N 帧非空白且 agent 正在说话,就往 agent 文本通道当前帧写一个 EOS——和第三节的机制殊途同归,打断永远不是「掐断音频流」,而是「往通道里写一个 EOS,让声音顺着语义消失」。 代码注释里甚至警告:TTS 读取文本 token 的时机必须在轮次守护写入之后,顺序挪早一行,这套机制就静默失效。

工具的等待期是一台小状态机。 服务端内置了九个演示工具(天气、股价、汇率、新闻、GPU 占用……全是朴素的 HTTP 调用,没有 MCP),异步模式下调用一发出,LLM 就脱离 80 ms 节拍在后台全速生成,前台按分层策略播语音:先播 per-tool 的 on-hold 消息,API 慢了再播通用的「还在查」,超时兜底道歉。有个细节能看出调试的血泪:注入的每句 on-hold 消息都要包上 BOS、补至少 17 个 PAD 再加 EOS——不补的话 TTS 的自回归状态停在「话说一半」,会污染工具调用结束后的正式回答。

六、训练配方与体检报告

训练侧的信息散在模型卡和代码里,拼起来大概是:约 55 万小时音频(Fisher、LibriVox、LibriTTS 等真实语音加多套 TTS 合成的对话),文本侧有 Nemotron、UltraChat 等语料;上下文窗口最长 2 分钟音频。得益于 SALM-Duplex 路线——听觉交给预训练好的流式 ASR 编码器——整个系统不需要从头做语音预训练。

代码里能看到不少针对真实世界的加固:背景噪声、房间混响、麦克风脉冲响应、有损编解码四层音频增强;训练时随机往「agent 说话、用户沉默」的段落里叠加用户的附和声(backchannel),教模型别把「嗯嗯」当打断;还有早打断增强,专门制造用户提前插话的局面。

体检报告(模型卡数字):Full-Duplex-Bench 1.0 上轮次切换时延 448 ms、用户打断处理满分 1.0;VoiceBench 在开源全双工模型里排第二。但停顿处理只有 0.153——「嗯……让我想想」还是容易被抢话,第二节立的 flag 没能立住:架构上判停靠语义,不代表行为上真能等你想完。这恰好呼应综述那篇的核心论点:架构可达不等于行为可得,缺的那格永远是数据的格子。 官方局限清单也异常坦诚,摘几条:多轮之后可能退化成胡言乱语;说完一轮后可能自说自话刹不住车;backchannel 处理还不系统;嘈杂或强混响环境、尤其有背景人声时不适用。

七、放回地图

给这个系统在系列坐标系里定个位。

MoshiSALMONN-omniNemotronLabs VoiceChat
听觉输入离散 codec token连续嵌入连续嵌入
LLM 输出文本 + 音频 token文本(含状态 token)三路 token:文本 + 转写 + 工具
发声codec 解码进词表流外挂流式合成器外挂帧同步自回归 TTS
工具调用无无专用通道,TOOLCALL 脚本
生产链路开源 demo论文原型Triton + 双 vLLM 容器

三点观察收尾:

其一,「智能留在文本域」的钟摆继续摆。从 Moshi 的 17 条并行流,到 SALMONN-omni 的连续直连,再到这里「LLM 一个音频 token 都不碰」,声学被推得离大脑越来越远;大脑省下来的容量,这次变现成了 9B 模型的知识和工具调用能力。

其二,全双工开始「agent 化」。此前的全双工竞赛卷的是对话流畅度——响应快不快、打断灵不灵;工具通道的出现把赛道拓宽成「边聊边办事」。56.1% 的 BFCL 和调用期间不可打断说明它离成熟还远,但方向已经立住:语音 agent 不必在「会聊」和「会干」之间二选一。

其三,也是这个发布最被低估的部分:它把全双工的工程账本摊开了。论文会告诉你 448 ms 的时延,不会告诉你 bf16 会让 TTS 出幻觉、vLLM 要打补丁才能每帧续写、on-hold 消息不补 17 个 PAD 会污染下一轮回答。这些细节对复现者和自建者的价值,不比模型权重小。

参考资料