LLM 后端选型:托管、自托管、进程内三条路
先说一个容易让人愣住的事实:speech-to-speech 这个项目的官方描述是 “Build local voice agents with open-source models”,但你按 README 的 Quickstart 装完、export OPENAI_API_KEY=... 然后 speech-to-speech serve,跑起来的那条流水线里,STT 和 TTS 在本地,LLM 打的是 OpenAI。
这不是我们的推测,README 自己把默认配置的等价完整命令写出来了:
speech-to-speech serve \
--thresh 0.6 \
--stt parakeet-tdt \
--llm_backend responses-api \
--tts qwen3 \
--qwen3_tts_model_name Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--qwen3_tts_speaker Aiden \
--qwen3_tts_language auto \
--qwen3_tts_backend ggml \
--qwen3_tts_non_streaming_mode True \
--qwen3_tts_mlx_quantization 6bit \
--model_name gpt-5.4-mini \
--chat_size 30 \
--responses_api_stream \
--enable_live_transcription
--llm_backend responses-api 加上 --model_name gpt-5.4-mini,走的是 OpenAI 的 Responses API。想全本地,得自己动手换。这篇就讲怎么换、以及在你这台机器、你这个场景下该换成哪一种。
三条路分别是什么
README 把 LLM 后端归成三类,对应的其实是两组 --llm_backend 取值:
| 路线 | --llm_backend 取值 | 模型跑在哪 |
|---|---|---|
| 进程内本地推理 | transformers(CUDA / CPU)、mlx-lm(Apple Silicon) | 和 VAD/STT/TTS 同一个进程 |
| 自托管服务 | responses-api 或 chat-completions + --responses_api_base_url 指向本机 | 你自己起的 vLLM / llama.cpp 进程 |
| 服务商 API | 同样是 responses-api 或 chat-completions | 别人的机房 |
注意中间那一格:自托管和调服务商用的是同一对后端,区别只在 --responses_api_base_url 指向哪里。这两个后端也共用同一组 --responses_api_* 连接参数。README 给的对照表是这样的:
| 服务商 / 服务 | --responses_api_base_url | --responses_api_api_key |
|---|---|---|
| OpenAI | 省略,用 OpenAI 默认 | $OPENAI_API_KEY |
| HF Inference Providers | https://router.huggingface.co/v1 | $HF_TOKEN |
| OpenRouter | https://openrouter.ai/api/v1 | $OPENROUTER_API_KEY |
| vLLM | http://localhost:8000/v1 | 省略或任意字符串 |
| llama.cpp | http://127.0.0.1:8080/v1 | 空字符串 |
表里最后两行是「自托管」这条路的全部秘密:把 base URL 改成本机地址,key 给空串或随便一个字符串。协议这层没有任何特殊处理。
至于为什么这个选择值得单独花时间——README 的定性判断是:LLM 是整条流水线里计算最重、延迟最高的那个组件,一次前向就可能主导端到端响应时间,所以按硬件和延迟预算选后端很重要。这是文档观点,我们没有跑过任何一次推理,下面也不会给出任何延迟数字。
从你的处境倒推
与其比三条路的优劣,不如按顺序回答四个问题。
问题一:你在什么平台上
这个问题决定「进程内」这条路对你是否存在。README 的说法很直接:Apple Silicon 用 --llm_backend mlx-lm,CUDA / CPU 用 --llm_backend transformers。没有第三种进程内后端。
macOS 上还有一个省事的入口:
speech-to-speech local --mac-optimal-settings
这个预设做四件事:对支持的组件用 MPS 默认值、STT 设为 Parakeet TDT、LLM 后端设为 MLX LM、TTS 设为 Qwen3-TTS 并用 mlx-audio、默认取 6bit 的 MLX 变体。可以带指定模型:
speech-to-speech local \
--mac-optimal-settings \
--model_name mlx-community/Qwen3-4B-Instruct-2507-bf16
这里有个优先级规则很容易搞反:预设只提供默认值。显式的 --device、组件级设备参数(如 --qwen3_tts_device),以及 --stt、--llm_backend、--model_name、--tts,全部优先于预设。所以你写了预设又写了 --llm_backend,听你的。另外,如果你只想暴露服务、不想同时启动麦克风客户端,这个预设要加在 serve 上而不是 local 上。
Windows 侧要留意的是进程内后端的默认设备:--llm_device 默认就是 "cuda",--llm_torch_dtype 默认 "float16"。没有 N 卡的机器走 transformers 这条路,这两个参数是你首先要面对的。
问题二:要不要中文
这个问题的答案不在 LLM 那一环。 很多人在这里绕了远路:换了一圈 LLM 后端发现中文还是不行,其实卡点在 STT。
README 的多语言矩阵写得很清楚:语言覆盖取决于你选的 STT 与 TTS,而不是流水线本身。而默认的 Parakeet TDT 只覆盖 25 种欧洲语言。要做中文语音代理,必须换 STT——Whisper 系(Transformers 里的 Whisper、Apple Silicon 上的 Lightning Whisper MLX,选择器写作 --stt whisper-mlx、以及要装 faster-whisper extra 的 Faster Whisper、macOS 上内置的 MLX Audio Whisper mlx-audio-whisper)或者 Paraformer(走 paraformer extra,--paraformer_stt_model_name 默认是 paraformer-zh)。各后端具体的 --stt 取值以 speech-to-speech serve -h 的实际输出为准。TTS 这边,默认的 Qwen3-TTS 是多语言的,--qwen3_tts_language 默认 auto;ChatTTS 覆盖英文与中文。
另外 --language 默认是 en,不是 auto。README 给的中文示例是这一条:
speech-to-speech serve \
--stt whisper-mlx \
--stt_model_name large-v3 \
--language zh \
--llm_backend mlx-lm \
--model_name mlx-community/Qwen3-4B-Instruct-2507-bf16
想让它自己判断语种就用 --language auto,STT 会检测每次说话的语言并转给 LLM。还可以加 --enable_lang_prompt(默认 False),它会追加一句 “Please reply to my message in …” 的指令;README 给的判断依据是:大模型通常能从上下文推断语言,但对小模型来说这条显式指令可能有帮助。
所以中文场景下,LLM 后端三条路都能用,只是你得先把 STT 那一环换掉。
问题三:联不联网
如果答案是「不联」,那服务商 API 这条路直接出局,剩下自托管和进程内。README 的 Offline Operation 一节有一条特别值得抄在便签上的提醒:不加本地 base URL 覆盖,默认的 responses-api 后端会去调远程服务——断网就断了。
离线跑的正确姿势是:先在联网状态下用完全相同的配置启动一次,让它把 STT、LLM、TTS、Silero VAD、NLTK 和 Smart Turn 需要的资源都缓存下来,然后:
HF_HUB_OFFLINE=1 speech-to-speech serve \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key ""
Smart Turn 用的是单独的 ONNX 检查点:已缓存的检查点配合 HF_HUB_OFFLINE=1 可用;想要显式、不依赖缓存的配置,传 --smart_turn_model_path /path/to/smart-turn-v3.2-cpu.onnx;检查点拿不到就传 --no_smart_turn 关掉它。
README 说最省事的全本地方案是把 LLM 放到独立的 llama.cpp 进程,两个终端:
# 终端 1:llama.cpp 提供 Gemma 4
llama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full
# 终端 2:speech-to-speech 指向这个本地服务
speech-to-speech serve \
--stt parakeet-tdt \
--llm_backend responses-api \
--tts qwen3 \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key "" \
--responses_api_stream \
--enable_live_transcription
这里能看出「自托管服务」相对「进程内」的一个结构性好处:LLM 崩了、要换模型、要单独给它调显存策略,都不用动语音流水线那个进程。仓库自带的 docker compose up 走的也是这个形状——compose 文件会启动一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露 8080 与 8765 两个端口。8765 是 Realtime 服务的默认端口(--port 默认值),8080 是 llama.cpp 那一侧,别记反。
问题四:是不是要进生产
进生产的话,有两个默认值必须先看一眼,它们跟后端选型是绑在一起的。
第一个是 --num_pipelines,默认是 1。它的 help 写得很明白:这是池子里隔离流水线实例的数量,一个 uvicorn 服务监听 --port,把每个进来的客户端路由到下一个空闲流水线,每个流水线有自己的 VAD/STT/LM/TTS handler 和对话状态;最大并发 WebSocket 会话数就等于 num_pipelines,再多的连接会被拒绝。也就是说默认配置只服务一个人。这个数字一往上抬,进程内后端就意味着同一台机器上多份模型状态——这时候把 LLM 挪到独立的自托管服务,是更容易算清楚账的做法。
第二个是绑定地址。serve 默认绑 127.0.0.1,要暴露到网络必须显式传 --host 0.0.0.0;local 则永远绑环回,并把自带客户端连到 ws://127.0.0.1:<port>/v1/realtime。
如果你还打算开 --enable_llm_proxy(默认 False,开启后实时服务会把它配置的远端 LLM 也作为普通 OpenAI 兼容端点暴露出来,供客户端跑摘要、起标题一类侧边任务),README 的安全声明必须原样带出来:服务端自己不做任何认证,也不做任何限流,只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面。这里不存在「照着配一遍就安全了」的说法,网关和访问控制得你自己负责。
responses-api 还是 chat-completions
选定了路线,还剩一个叉路口。默认是 responses-api(打 /v1/responses),chat-completions 打 /v1/chat/completions。README 给了两个改用后者的具体理由:
- 服务商在 Responses 路径上忽略
chat_template_kwargs.enable_thinking,你需要一个reasoning_effort旋钮来抑制推理; - 该服务的 Responses 流式工具调用路径不可靠,而它的 Chat Completions 工具调用流式是稳的。README 点名这在某些 vLLM 构建上有用,并给出 issue #312 的链接(这个 issue 的正文我们没有读过,不展开)。
对应的参数是 --responses_api_reasoning_effort(默认 None),传 none 用来在「chat-template 标志无效」的服务商上关掉推理。README 给的示例连注释都写明了动机是压低语音延迟:
# 通过 HF router 在 Cerebras 上跑 Gemma 4 31B,关闭推理
speech-to-speech serve \
--stt parakeet-tdt \
--llm_backend chat-completions \
--tts qwen3 \
--model_name "google/gemma-4-31B-it:cerebras" \
--responses_api_base_url "https://router.huggingface.co/v1" \
--responses_api_api_key "$HF_TOKEN" \
--responses_api_reasoning_effort none \
--responses_api_stream
这个权衡在语音场景下比在聊天框里尖锐得多:模型多想几秒,文本界面上是转圈,语音里就是纯粹的干等。顺带一提,--responses_api_disable_thinking 默认就是 True,走的是 chat_template_kwargs.enable_thinking=false 那条路;reasoning_effort 是给这条路不生效的服务商准备的。
还有一种情况必须用 chat-completions:跳过 STT 的直接音频输入模式。
speech-to-speech serve \
--stt none \
--llm_backend chat-completions \
--model_name "YOUR_AUDIO_CAPABLE_MODEL" \
--responses_api_base_url "https://provider.example/v1" \
--responses_api_api_key "$PROVIDER_API_KEY"
--stt none --llm_backend chat-completions 会把每个 VAD 切好的完整音频段直接送给支持音频输入的模型。README 列了四条硬约束:responses-api 不支持这个模式(原因是一个模型可能通过 /v1/chat/completions 接受音频却不支持 /v1/responses,举的例子是 OpenAI 的 gpt-audio-1.5);必须显式设 --model_name 为接受音频的模型,默认的 gpt-5.4-mini 接受文本与图像、不接受音频;启用前去查服务商的模型文档与端点支持;不同服务表示输入音频的方式不同,--responses_api_audio_content_type 默认 input_audio(内嵌 WAV base64),另一个取值是 audio_url(base64 data URL)。
这些维度我们不比
横向对比到这里就该收手了,因为再往下就没有依据了。
- 延迟:README 只给了一句定性判断(LLM 是延迟大头、关推理有助于压延迟),没有任何数字。参数里那些毫秒值是流水线自己引入的可配置等待,不含 STT/LLM/TTS 的推理耗时,把它们相加得出「端到端多少毫秒」是错的。
- 显存 / 内存:我们手上关于体量的硬事实只有模型名字里自带的参数量(比如
nvidia/parakeet-tdt-0.6b-v3的 0.6B、Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice的 1.7B)。由参数量和精度算出来的是权重体积的算术下限,实际占用必然更高——还有激活值、KV cache、音频缓冲和框架开销,不同后端差异很大。 - 音质、识别准确率、跑分:仓库确实提供了
scripts/benchmark_tts.py用来对比 MLX 量化变体,但我们没有跑过它,一个结论都给不出来。
这三样恰恰是选型时最想知道的,所以说白比说满重要:这几点官方没给数据,你得在自己机器上量。
换完之后怎么验收
三个容易在这一步栽跟头的地方。
一是参数写错不报错。README 明说:CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略,JSON 配置同理。所以你把 --llm_backend 换成了 mlx-lm,却还留着一堆 --responses_api_*,程序不会拦你,只会警告后照跑——然后你对着「为什么 base URL 没生效」查半天。换后端时顺手清掉不属于新后端的参数。
二是看对 -h 的输出。speech-to-speech serve -h 显示的是当前这套组合的默认值与专属参数。想看另一种组合,得把选择器放在 -h 前面:speech-to-speech serve --stt mlx-audio-whisper -h。
三是别照着老教程写 --mode。这个参数已经废弃且很快将停止工作。迁移窗口内,--mode realtime 等价于 serve、--mode local 等价于 local,两者都会打印警告;其余所有 mode 取值已被移除,会带着「改用新命令」的提示直接退出。网上大量旧文用的还是 --mode,跑不通多半是这个原因。
最后补一句进程内后端的默认行为,免得你换过去之后觉得回答”太死板”:本地 LLM 的 --llm_gen_temperature 默认 0.0、--llm_gen_do_sample 默认 False,也就是确定性贪心解码;--llm_gen_max_new_tokens 默认 1024。这跟走 API 时的服务商默认值不是一回事,换路线的时候记得一起看。
以上命令均为 README 与参数导出的原样引用,未逐项实测,以官方文档与 speech-to-speech serve -h 的实际输出为准。
延伸阅读
- 默认绑 127.0.0.1 是有道理的:改成 0.0.0.0 之前想清楚
- 断网也能跑:HF_HUB_OFFLINE=1 之前要先做的事
--mac-optimal-settings做了什么,又被什么覆盖
本文依据 speech-to-speech 官方仓库(github.com/huggingface/speech-to-speech)的 README、
src/speech_to_speech/arguments_classes/ 下的参数定义与 Realtime Engine 架构文档整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装、部署或调用过该服务,文中毫秒值均为参数默认值而非实测延迟。
参数与默认值随版本变动,请以 speech-to-speech serve -h 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。许可条款请以官方 LICENSE 原文为准,本文不构成法律意见。