名字里写着 local,默认配置的 LLM 却在云上

2026-08-09

speech-to-speech 的 GitHub 仓库描述只有一行英文:Build local voice agents with open-source models。看到 local 这个词,多数人默认理解成「装完就断网也能用」。

但你照着 README 的 Quickstart 敲三行命令起来的那套流水线,LLM 那一环打的是 OpenAI。

这不是什么阴谋,README 自己把话说得很清楚——它给出了默认配置的等价完整命令,一眼就能看出来。问题在于绝大多数人不会去看那段等价命令,装完对着麦克风说话有反应,就以为一切都在本机跑。这篇就把这套默认值逐段拆开,重点不是复读文档,而是给你几条能自己下判断的动作:怎么确认你这台机器到底连没连外网,以及按你的处境该改哪一个参数。

先把默认值摊开

README 的 Quickstart 是这三行:

pip install speech-to-speech
export OPENAI_API_KEY=...
speech-to-speech serve

中间那行不是装饰。README 明确给出了 speech-to-speech serve 的等价完整命令:

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 的 gpt-5.4-mini。你可以用 --model_name 覆盖,用 --responses_api_base_url 指向其他 OpenAI 兼容服务商或服务,但不改的话,它就是走 OpenAI 默认地址。

哪一段在本地,哪一段在外面

这条流水线是四段级联:VAD → STT → LLM → TTS,每个组件跑在自己的线程里,用队列相连。按默认值对号入座:

环节默认后端跑在哪
VADSilero VAD v5本机,内置
STTparakeet-tdt(CUDA / CPU 取 nvidia/parakeet-tdt-0.6b-v3,Apple Silicon 取 mlx-community/parakeet-tdt-0.6b-v3本机
LLMresponses-api + gpt-5.4-mini远端
TTSqwen3Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice本机

判断一个环节在不在本地,看它选的后端属于哪一类。 responses-apichat-completions 这两个 LLM 后端本身不含推理,它们是 HTTP 客户端:--llm_backend responses-api/v1/responses--llm_backend chat-completions/v1/chat/completions,两者共用同一组 --responses_api_* 连接参数。--responses_api_base_url 省略,就用 OpenAI 默认。真正在本机做推理的 LLM 后端是另外两个:transformers(CUDA / CPU)和 mlx-lm(Apple Silicon)。

作者为什么这么默认

README 自己给了理由:LLM 是流水线里计算最重、延迟最高的组件,一次大模型前向就可能主导端到端响应时间,所以按硬件与延迟预算选后端很重要。把最重的那一段丢给远端,pip install 完配个 key 就能说话,这是发行默认值的取舍。

理解这一点比记住参数更重要:「开箱即用」和「完全本地」在这个项目里就是两套不同的配置,不存在一套通吃。你要哪个,得自己选。

怎么确认你这套到底连没连外网

四个可执行的判定动作,从便宜到彻底:

一、看你有没有传本地 base URL。 命令行里既没有 --responses_api_base_url--llm_backend 又是默认的 responses-api,那就是在打远端。这一条其实已经能定性。

二、-h 看实际生效的默认值。speech-to-speech serve -h 查看当前组合的默认值与专属参数;要看另一种组合,把选择器放在 -h 前面,例如 speech-to-speech serve --stt mlx-audio-whisper -h。整套 CLI 有 131 个参数,只靠记忆对不上。

三、★ 别看你写了什么,看 --llm_backend 的实际取值。 这是最阴的一条:README 说明,CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略;JSON 配置同理,多余的非激活后端键也会被忽略。也就是说,你兴冲冲加了 --llm_device cuda --llm_torch_dtype float16,以为把模型摁在本地显卡上了,可只要 --llm_backend 还是 responses-api,这些参数就只是被警告一句然后照跑。程序不会报错停下——没有红字,不等于你改对了。

四、断网法。 依赖与模型资产都在本地之后,流水线可以断网运行,但 README 有一句关键提醒:不加本地 base URL 覆盖,默认的 responses-api 后端会去调远程服务,离线就断了。所以把网拔了跑一次,是最不容易骗自己的验证。

换成真本地,三条路

路 A:独立起一个 llama.cpp 进程

README 的「Fully Local」一节说,最省事的全本地做法是把 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

注意两个端口别搞反:8080 是 llama.cpp 的口,8765 才是 Realtime 服务自己的口--port 默认 8765,默认服务地址 ws://localhost:8765/v1/realtime)。仓库自带的 docker compose up 起的也正是这两个:一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露 8080 与 8765。--responses_api_api_key 在 llama.cpp 这一栏官方写的就是空字符串;vLLM 那一栏是 http://localhost:8000/v1,key 可省略或填任意字符串。

这条路的好处是 LLM 进程和语音流水线解耦,重启一边不影响另一边。

路 B:进程内本地后端

不想多起一个进程,就让 LLM 在同一个进程里跑:Apple Silicon 用 --llm_backend mlx-lm,CUDA / CPU 用 --llm_backend transformers。这时候前面被忽略的那组参数才真正生效,它们的默认值是:--llm_device cuda--llm_torch_dtype float16--llm_gen_max_new_tokens 1024--llm_gen_temperature 0.0--llm_gen_do_sample False

后两个值得留意:本地后端默认是确定性贪心解码。你如果觉得同一句话回来的答复一模一样、缺少变化,那不是模型坏了,是默认就这么设的。

路 C:macOS 预设

speech-to-speech local --mac-optimal-settings

也可以指定 LLM:

speech-to-speech local \
    --mac-optimal-settings \
    --model_name mlx-community/Qwen3-4B-Instruct-2507-bf16

这个预设做四件事:对支持的模型组件使用 MPS 默认值、STT 设为 Parakeet TDT、LLM 后端设为 MLX LM、TTS 设为 Qwen3-TTS 并使用 mlx-audio、默认取 6bit 的 MLX 变体。

优先级很容易搞反:预设只提供默认值。显式的 --device、组件级设备参数(如 --qwen3_tts_device),以及 --stt--llm_backend--model_name--tts 全部优先于预设。另外,想暴露服务而不启动麦克风客户端时,把这个开关用在 serve 上而不是 local 上——local 是把 servetalk 在同一进程里用环回组合起来的。

还想用云,只是不想用 OpenAI

那就只换连接参数。官方给的服务商对照表:HF Inference Providers 用 https://router.huggingface.co/v1$HF_TOKEN,OpenRouter 用 https://openrouter.ai/api/v1$OPENROUTER_API_KEY,OpenAI 则省略 base URL、用 $OPENAI_API_KEY

什么时候该从默认的 responses-api 切到 chat-completions?README 给了两个具体理由:一是服务商在 Responses 路径上忽略 chat_template_kwargs.enable_thinking,你需要 reasoning_effort 这个旋钮来抑制推理;二是该服务的 Responses 流式工具调用路径不可靠,而它的 Chat Completions 工具调用流式是稳的——README 点名这在某些 vLLM 构建上有用,并指向 issue #312(我们没有读过这个 issue 的正文)。对应的参数是 --responses_api_reasoning_effort none

关掉推理这件事本身就是个权衡:语音对话里模型多想几秒,用户听到的就是干等。

按处境倒推

  • 只想先听个响、手上正好有 key:什么都别改,接受 LLM 在云上。
  • 数据不能出内网:路 A,独立 llama.cpp,--responses_api_base_url 指到本机。
  • Apple Silicon 单机玩:路 C 加 --llm_backend mlx-lm
  • 有 CUDA 机器、不想多起进程:路 B,--llm_backend transformers
  • 要做中文:无论走哪条路,都得先换 STT。默认的 Parakeet TDT 只覆盖 25 种欧洲语言,--language 默认是 en。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
  • 要给多个人用--num_pipelines 默认是 1,也就是默认只承载一个并发会话。这是多用户部署第一个要动的参数。

真要离线,还有一步

断网前先在联网状态下用完全相同的配置启动一次,让它缓存好 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 关掉它。

三个连带的坑

暴露服务这件事得单独说。 serve 默认绑定 127.0.0.1,要暴露到网络必须显式--host 0.0.0.0——官方 help 的原话就是「显式传 0.0.0.0 才会把这个未认证的 API 暴露到网络上」。local 则永远绑环回,并把自带客户端连到 ws://127.0.0.1:<port>/v1/realtime

--enable_llm_proxy 默认 False,开启后服务会把它配置的远端 LLM 也作为普通 OpenAI 兼容端点暴露出来。官方对这条的安全声明必须原样带出:服务端自己不做任何认证,也不做任何限流;只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面。 换个绑定地址、换个端口都不构成安全措施,这里不存在「这样配就安全了」的说法。

老教程跑不通多半是 --mode 这个参数已废弃且很快将停止工作。迁移窗口内 --mode realtime 等价于 serve--mode local 等价于 local,两者都会打印警告;其余所有 mode 取值已被移除,会带着「改用新命令」的提示直接退出。网上不少旧文还在用它。

别把参数默认值当成延迟。 这套 CLI 里散落着一堆毫秒值,它们是流水线自己引入的可配置等待,不含 STT / LLM / TTS 的推理耗时,把它们加起来当成「端到端延迟」是错的。仓库确实提供了对比 MLX 量化变体的 scripts/benchmark_tts.py,但那是给你自己跑的。

最后一句关于口碑:这个仓库在 2026-08-09 的快照里 star 是 11893,README 也提到它作为数千台 Reachy Mini 机器人的对话后端在生产环境运行。这些是事实,但它们推不出延迟、稳定性或者「适合你的场景」的任何结论——那得你自己在自己的机器上跑一遍。

延伸阅读


本文依据 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?报名体系课或加入会员,照着学、照着用。