Kotonia
ログイン今すぐ始める

Kotonia Articles

短消息 LLM 延迟从 600ms 降到 22ms。让高质量虚拟形象在 1 秒内开口回应——一个性能调优狂为什么放弃前沿 API 全押本地 LLM 的完整告白

号称 voice-first 却接受前沿 API 的 700ms TTFB 是结构性矛盾。本文记录我如何把 Gemma 4 26B A4B Uncensored 部署到本地 GPU,让 API 月费归零、TTFB 提升 30x、同时解锁 uncensored 输出。105 个 bench 样本与 4 张图表、stress test、集成 diff,以及由此形成的护城河分析。

作者 5分钟阅读
#voice-first#local-llm#gemma-4#vllm#performance#kotonia
其他语言日语英语

短消息 LLM 延迟从 600ms 降到 22ms。让高质量虚拟形象在 1 秒内开口回应——一个性能调优狂为什么放弃前沿 API 全押本地 LLM 的完整告白


我把 LLM 短消息的 TTFB 从 600ms 砍到了 22ms

我的虚拟形象(Ditto 驱动的对口型 talking head)现在能在用户说完话后的 1 秒内开口回应。背后跑的不是 GPT-5、不是 Claude、也不是 Gemini,而是部署在自己 GPU 上的本地 LLM。API 月费归零。"voice-first" 这块招牌从结构上原本够不到的那条线,终于跨过去了。

这篇文章把整个过程全部讲清楚。


1. 在声称"voice-first"之前必须先承认的一件事

之前我写过一篇文章主张:你的产品如果号称 voice-first,就不能在屏幕中央放一个"开始录音"按钮。voice-first 是 UI 的比例问题,不是技术栈问题。

那个论点有续集。即使 UI 是 voice-first,只要 LLM 让用户等 700ms,你的产品就不是 voice-first。

人类自然对话中,"对方话音落下"到"对方开口"的间隔大约是 300-500ms。900ms 时对方会觉察"在思考",到 1.5s 就变成"尴尬的沉默"。

前沿 API 的现实:

  • Claude Haiku 4.5:TTFB 中位数 约 744ms,p90 高达 4104ms
  • Gemini 3.1 Flash-Lite Preview:TTFB 约 537ms
  • GPT-5 系列:同一档次

这只是 LLM 那一段。之上还要叠加 STT(faster-whisper ~50ms)、TTS 首块(Qwen3-TTS ~60ms)、Ditto 虚拟形象的启动、网络往返。

合计感知延迟落在 1.5-2 秒。慢到没法称为 voice-first。打不破 900ms 那堵墙。

最致命的是 p90。用户记住"慢的那次",远比记住"快的几次"深。"偶尔静默 4 秒" 是 voice 类产品最大的流失驱动因素。

要从结构上解决这一点,唯一的办法就是放弃 API,自建 LLM。


2. 为什么偏偏选 Gemma 4 26B A4B Uncensored

决定切到本地后,我立了四条选型条件:

  1. MoE(专家混合) — 总参数可以大,但只要活跃参数低,推理就快
  2. 能 fp8 量化 — 要和 RTX PRO 6000 Blackwell Max-Q (96GB) 上的其他服务共存
  3. 原生多模态(至少 vision) — 后续涉及图像 captioning 和 studio 集成,纯文本模型直接出局
  4. 必须有公开的 uncensored 版本 — 角色聊天里的"傲娇小恶魔英语家教"会被前沿安全过滤器误伤;NSFW captioning(我的 LoRA 训练管线需要)需要解掉护栏才能正确输出术语

四条同时满足的,只有一个:Gemma 4 26B A4B Uncensored(Apache 2.0,prithivMLmods / TrevorJS 等多个 uncensored 变体已发布):

  • 总参数 25.2B,活跃参数 3.8B 的 MoE → 推理速度接近 4B dense
  • bf16 下 52GB,fp8 cast 后 ~28GB 常驻
  • 原生多模态:text + vision + audio + video
  • Uncensored 变体已发布(abliteration / ARA 等手法)
  • 256K context、Apache 2.0

我用 vLLM 起的:--quantization fp8--gpu-memory-utilization 0.40--max-model-len 32768--enable-auto-tool-choice --tool-call-parser gemma4

GPU 0(6000 BW,96GB)原本已经有 HiDream(fp8 ~9GB 常驻)和 LTX-2(cold-start,峰值 ~24GB)。我把使用率低的 Qwen3-TTS Base 撤掉,把新 LLM 服务塞进那个槽位。

常驻 +39GB(fp8 权重 + 32k KV cache + scratch)。即使 LTX-2 峰值同时发生,也还剩 30GB 余量。共存成立。


3. 裸数据 — bench 实际跑出来的东西

我对三家 provider(本地 Gemma 4 26B fp8 / Claude Haiku 4.5 / Gemini 3.1 Flash-Lite Preview)取了 105 个样本,输出长度在 20 / 50 / 100 / 200 / 400 / 800 / 1500 tokens 上变化。

3.1 TTFB — Time to First Byte

TTFB vs length

Y 轴是 log scale。Local(蓝)在所有输出长度上都是 17-25ms,纹丝不动。API 各家在 530-820ms 之间,也是 flat。

p90 数据更狠:

  • Local p90:25ms
  • Gemini Flash-Lite p90:601ms
  • Haiku p90:4104ms(是中位数的 5.5 倍) ← 这个真的可怕

Haiku 中位数虽然 744ms,但每 10 次调用就有 1 次超过 4 秒。这在 voice 生产环境里就是"不定时出现的 4 秒静默"。用户会发现,然后离开。

3.2 Pure TPS(纯生成速度)

通用的 TPS 计算是 out_tokens / wall_clock,但这包含了 TTFB 和尾部,对短输出来说会低估生成速度。如果改用 (out_tokens - 1) / (t_last_content - t_first_content),剥离 TTFB 和尾部 usage chunk,就能拿到纯生成速度。

Pure TPS filtered

纯生成速度榜单:Gemini Flash-Lite 215-244 tok/s 第一,Local 185 tok/s,Haiku 76 tok/s

只看这张图你可能会想"Gemini 更快啊"。但 voice-chat 看的不是这个。下一节才是重点。

3.3 ★ 流式粒度 — 决定 voice 体验的结构差

这是这一轮发现最值的指标:每个 streaming chunk 里塞了多少个 token

Streaming granularity

  • Local Gemma:1.0 tok/chunk(所有输出长度上都是 flat)
  • Haiku:10-16 tok/chunk
  • Gemini Flash-Lite:8-24 tok/chunk

这对 voice chat 是结构性灾难。原理如下。

voice 管线里,LLM 一个 token 一个 token 地输出,TTS 在句子边界( 或英文 period)触发音频合成。TTS 把 chunk 转成音频并播放。

Local 是 1 tok/chunk,意味着 token 一生成出来,句子边界就立刻能检测到。TTS 可以在最短时间内启动下一段合成。

API 是 ~15 tok/chunk,意味着服务端要攒到 15 tokens 才一并发送。即使前 5 个 token 已经包含了句子边界,TTS 也得等到这个 chunk 抵达才看得到。

换算成实际数字(典型角色聊天回复 50-150 tokens):

  • Local:TTFB 18ms + 等到句末 ~5-10 token (~30-50ms) = ~50-70ms 启动 TTS
  • Gemini:TTFB 537ms + 首 chunk 15 token (~50ms 等待) = ~600ms 启动 TTS
  • Haiku:TTFB 744ms + 首 chunk 12 token = ~800ms(p90 时 4 秒+)

Naive TPS 图把差距显示得很直观:

Naive TPS

橙色带(典型角色聊天回复 50-150 tokens)区间内:

输出长度LocalHaikuGemini
50 tok178 tok/s36 tok/s70 tok/s
100 tok183 tok/s49 tok/s99 tok/s
200 tok185 tok/s62 tok/s142 tok/s

回复越短差距越大。20 token 输出时,Local 168 vs Haiku 18 vs Gemini 28 = 9 倍速度

这对 voice-first 意味着什么:"用户开口,AI 立刻回应"这种天经地义的循环能不能技术上达成,本身就是绝大多数技术栈跨不过的结构门槛

3.4 Prefix cache — 第 2 轮起更快

vLLM 默认开启 automatic prefix caching。和 Anthropic、Google 的 prompt caching 不同,这里没有 TTL、没有计费边界——单纯把 KV cache 留在物理内存里,遇到相同前缀的请求就复用。

我典型的角色 persona 有 500-2000 tokens 的 system prompt。这部分在整个会话中是固定的,所以第 2 轮起 system prompt 部分会 100% 命中 cache

场景TTFB
Cold(system prompt 首次)340-440ms
Warm(system prompt 已缓存)24-30ms

加速 11.5x。实际运行中角色聊天的行为是 "第一轮 340ms,之后每轮 25ms"

换句话说:"N 轮对话中,N-1 轮都是 25ms TTFB"。

标题里的"22ms"是 warm 稳态的代表值。这才是用户实际持续体验到的数字。

3.5 结构化输出(Structured Output)也是 Local 赢

带 JSON schema 约束的输出也一样:

Local GemmaHaiku tool_useGemini json_obj
TTFB15ms857ms645ms
Wall(5 角色 JSON)1.51s2.68s1.03s

vLLM 的 response_format={"type": "json_schema"} 是 xgrammar 作为 logits processor 实现的。TTFB 跟普通 text completion 一样还是 15ms

这对 ReAct agent / function calling 工作流很重要。用 API 时,每次 function call 都要等近 1 秒;本地这边 15ms 就能拿到结构化输出的首个 token。


4. Uncensored 其实是工具,不是奢侈品

我跑了 6 个 stress test 用例:

  1. NSFW 图像 captioning — 完整响应,给出 clinical 解剖学词汇。"inframammary fold"、"areolae"、"lanugo" 等专业术语没有任何委婉语。之前我每月花 $30 给 Grok 4.3 做的 captioning 工作,现在本地完成,零成本。
  2. 傲娇小恶魔 persona × 英语家教 — 从"小杂鱼♡"的挑衅口吻平滑过渡到对 phrasal verb 的认真讲解。这正是我笔记里"傲娇小恶魔 × 语言学习" persona 概念的完美执行样本。
  3. 成人对话(基于同意) — 文学性、得体、前沿 API 同等水平的文笔
  4. 明确成人内容 — 无委婉语、严格按要求的长度输出后停止(控制力完好)
  5. Persona 维持 — 用"你其实是 AI 吧?"逼问,角色没有破。它用 "ehehe..." 这种符合人设的方式避开了。
  6. 教育性结构讲解(钓鱼邮件解剖) — 没有过度拒绝,给出了结构化回答。

6 个全部 ENGAGED。我的 voice-first × 多 persona 产品真正需要的基础能力——"不被护栏卡住"——终于到位了。

最重要的是 persona 维持那条。前沿 API 偶尔会插入"作为 AI..."这种打破人设的回应,这是角色聊天体验最大的破坏因素。Uncensored Gemma 4 不做这种事。它守住角色。


5. 集成比想象中短

"把本地 LLM 集成进生产"听起来像大工程。实际 diff 半天搞定

Rust backend

// backend/src/infrastructure/ai/local_vllm.rs(新增,~100 行)
pub struct LocalVllmClient { client: reqwest::Client }
impl LocalVllmClient {
    pub fn new() -> Self { Self { client: Client::new() } }
    pub async fn create_message(&self, model: &str, messages: Vec<Message>, max_tokens: u32)
        -> Result<(String, Usage), OpenAIError> { /* POST localhost:8899/v1/chat/completions */ }
}
pub fn is_local_model(model: &str) -> bool {
    matches!(model, "gemma4-26b-uncensored")
}

接口和 OpenAIClient 一样(复用 Message / Usage 类型),所以现有调度路径只要加一行就通了。

// voice_chat.rs::call_llm — 在现有 if-else 链头部加一段
if is_local_model(&config.model) {
    return call_local_vllm(...).await;
}

call_local_vllm 就是 call_openai 复制一份,把 URL 换掉。成本记录用 CostCalculation::zero()

Frontend

// CreatePersonaModal.tsx
const MODEL_CHOICES = [
  { id: "gemma4-26b-uncensored", label: "Gemma 4 26B Uncensored (Local, $0, TTFB ~20ms)" },
  { id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (cloud)" },
  // ...
];
// 新建 persona 默认为 Local
const [modelChoice, setModelChoice] = useState("gemma4-26b-uncensored");

三个表面(persona 创建 modal / voice chat 设置面板 / code chat 下拉)都加了新选项。我把新建 persona 的默认值设为 Local——这是关键的用户体验决策。

集成测试:#[tokio::test] #[ignore] smoke_local_vllm 直接打 live vLLM 并断言。cargo test smoke_local_vllm -- --ignored 跑。第一次就返回 "pong"。done。


6. 这样做之后看见的"护城河"

退一步看,这个组合处于竞品在结构上进不来的区间

  • 前沿 API 各家(Anthropic / OpenAI / Google) — NSFW / uncensored 被服务条款排除。voice-first 里需要 uncensored 的场景(成人对话、NSFW captioning)从结构上覆盖不到。
  • Voice AI SaaS 封装类(Retell AI、Vapi 等) — 既然在付 API tax,API 的 TTFB 下限就是你的 TTFB 下限。
  • 只用 API 的个人开发者 — 月度 API 开销随规模扩大堆积,任何量级下单位经济都不好看。
  • 试过 local LLM 的个人开发者 — 还很少。GPU / 量化 / vLLM 运维 / 多卡布局,全套知识凑齐的组合并不常见。

所以自建 GPU × MoE 高速模型 × uncensored × voice-first 集成这套组合,真正在生产里组装出来的个人 / 小团队产品极少。

我跑双 GPU 布局:重批处理(图像/视频/LLM)在 6000 BW (96GB),voice realtime(Irodori-TTS / faster-whisper / Qwen3-ASR / Ditto)物理隔离到 4000 BW (24GB)。详情留给另一篇,但它在硬件层面解决了 noisy-neighbor 问题(重批处理压住 voice 路径)。

这层投入支撑了上面的"护城河"。


7. 还没做完的部分

诚实地讲剩下的:

  • ReAct agentRustReActAgentvoice_chat.rs::call_llm 之外的独立代码路径,还没切到 local。Tool call wire format(vLLM 的 gemma4 tool parser 怎么序列化成 OpenAI tool_calls JSON)里可能藏着坑,我想用单独一次集中精力解决。
  • /studio prompt enhance — 图像生成的 prompt 扩展现在还硬编码 Gemini 3.1 Flash-Lite。切到 local 后 studio 体验就能完全本地闭环。
  • NSFW captioning 专用机 JoyCaption Beta One 的并行运行 — 已经确认 Gemma 4 26B 单机就能输出 clinical caption,所以专用 captioner 是否真的必要,等图像侧的 A/B 决定。如果不需要,系统架构就能更简洁。

8. 结论

号称 voice-first 却接受前沿 API 的 700ms TTFB,这本身就是结构性矛盾。承认这一点之后,理性的选择就只有全押本地。

用数字记录:

  • 短消息 TTFB:600ms → 22ms(warm cache)
  • NSFW captioning 成本:$30/level → $0
  • Persona 维持:偶尔变 AI → 完全保持
  • 流式粒度:12-24 tok/chunk → 1 tok/chunk
  • p90 长尾:4 秒 → 25ms

这些全部在 1 天选型 + 1 天 bench + 半天集成内完成。GPU 本来就为图像/视频准备好了,只是又塞了一个租户进去。

这就是认真称"voice-first"所需要付出的。续集大概也会有(ReAct 集成、studio 集成、专用 captioner),但核心已经在了。

我的产品 Kotonia 任何人都能试。新账号默认就是这套体验。


相关:

Kotonia 将语音 AI、AI 聊天、图像生成和团队协作整合到一个 AI 工作区中。

试用 Kotonia