LLM 后端选型:托管、自托管、进程内三条路

2026-08-09

先说一个容易让人愣住的事实: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-apichat-completions + --responses_api_base_url 指向本机你自己起的 vLLM / llama.cpp 进程
服务商 API同样是 responses-apichat-completions别人的机房

注意中间那一格:自托管和调服务商用的是同一对后端,区别只在 --responses_api_base_url 指向哪里。这两个后端也共用同一组 --responses_api_* 连接参数。README 给的对照表是这样的:

服务商 / 服务--responses_api_base_url--responses_api_api_key
OpenAI省略,用 OpenAI 默认$OPENAI_API_KEY
HF Inference Providershttps://router.huggingface.co/v1$HF_TOKEN
OpenRouterhttps://openrouter.ai/api/v1$OPENROUTER_API_KEY
vLLMhttp://localhost:8000/v1省略或任意字符串
llama.cpphttp://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 服务和实时服务,暴露 80808765 两个端口。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.0local 则永远绑环回,并把自带客户端连到 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 给了两个改用后者的具体理由:

  1. 服务商在 Responses 路径上忽略 chat_template_kwargs.enable_thinking,你需要一个 reasoning_effort 旋钮来抑制推理;
  2. 该服务的 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 的实际输出为准。

延伸阅读


本文依据 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 原文为准,本文不构成法律意见。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。