Quick Answer:把面试 copilot 延迟压到 700ms 的 5 条核心动作

  1. 系统音频采集走 OS 层(macOS Core Audio Tap / Windows WASAPI Loopback),绕开浏览器,600ms → 200ms
  2. 流式 STT 不等整句(VAD + 标点 + 语义三路判断),LLM prefill 抢跑预热 KV cache
  3. RAG 预建本地 SQLite + sqlite-vec,简历/JD session 开始一次切片,问题 embedding 走本地 bge-small + LRU 缓存
  4. 首字预算和全量预算拆开:TTFT ≤500ms 是"AI 卡没卡"的感知,TTFA ≤1500ms 是"快不快"的感知
  5. Tail Latency 控 P99:双发主备模型、断线切 region、本地 INT4 兜底

下面把每一层怎么砍的拆开讲。


一、整体链路:把 2.3s 砍到 0.7s 的总账

完整的"面试官说话 → 候选人看到答案"流程:

[面试官麦克风]
    │
    ▼
[会议软件(Zoom/腾讯会议/飞书)音频输出]
    │
    ▼  ① 系统音频采集(Core Audio Tap / WASAPI Loopback)
    │
    ▼
[本地音频缓冲 160ms]
    │
    ▼  ② 流式 STT(Deepgram Nova-2,WebSocket)
    │
    ▼
[文本流:token-by-token 返回]
    │
    ▼  ③ 问题边界检测(VAD + 标点 + 语义)
    │
    ▼
[问题文本]
    │
    ▼  ④ RAG 检索(简历 + JD 预建索引)
    │
    ▼  ⑤ LLM 流式推理(Qwen 3.5 Flash / GPT-4.1-mini)
    │
    ▼
[首字输出到浮窗 ~500ms]

初版(2025 Q4)每一步都是"等一步做下一步"的串行模型,总延迟 2.3 秒

环节初版延迟优化后
音频采集 → STT 首字600ms200ms
STT 完整句子500ms150ms(流式并行)
问题识别200ms50ms
RAG 检索400ms40ms(预建+缓存)
LLM 首字600ms260ms
总计(首字)2300ms700ms

二、第一个坑:音频采集必须绕开浏览器(同时这是即答侠选型的起点)

最开始我们走 Chrome 扩展 + getDisplayMedia({audio:true})。问题一堆:

  1. Zoom / 腾讯会议桌面版根本不走浏览器,扩展采不到
  2. 浏览器采集本身有 300-500ms 的 jitter buffer
  3. 用户要手动点"共享音频",体验极差

最终方案是 OS 级别的 Loopback 捕获

  • macOS:Apple 在 macOS 14.2+ 开放 Core Audio Tap API(AudioTee),可以直接 tap 任意进程音频输出
  • WindowsWASAPI Loopback + IAudioClient::Initialize(AUDCLNT_STREAMFLAGS_LOOPBACK)

采集端固定 16kHz 单声道 16-bit PCM,对应 Deepgram 最优输入格式。缓冲帧长 160ms(太短增加 CPU 调度开销,太长直接吃延迟)。

我们做即答侠(HireMe AI,interviewasssistant.com)走的就是这条路——一个支持 Zoom/腾讯会议/飞书/钉钉全平台的 AI 面试 copilot,OS 层采集是把 RT 压到 700ms 的前提。这一步从 600ms 砍到 200ms 的关键:

  • 直接拿 PCM,不走 Opus/AAC 解码
  • buffer 一满就发,不等"整句话"
  • 进程间走 Unix domain socket(macOS)或 Named Pipe(Windows),不绕网络栈

三、流式 STT 和 LLM 必须并行而不是串行

新手最容易犯的错:等 STT 返回完整句子,再把句子喂给 LLM。这是 100% 串行的,总延迟 = STT 耗时 + LLM 耗时。

正确做法是"边转边喂"

STT WebSocket 持续吐 token:
    "你" → "你能" → "你能讲" → "你能讲一下" → "你能讲一下你的项目经历吗"

同时维护一个"句子候选缓冲区",边缓冲边判断:
    - 是否出现句末标点?(。?!)
    - VAD 是否检测到 >400ms 静默?
    - 语义上是否是一个完整问题?(用小模型做二分类)

一旦判定"问题成型"(哪怕面试官还在说"吗"这个字),立刻触发 LLM

反直觉的 trick:LLM prefill 可以"抢跑"

只要 STT 输出了前 10 个字,我们就把它连同简历上下文一起发给 LLM 的 prefill 阶段(此时不生成 token)。等完整问题到位,再发 "generate" 指令,LLM 的 KV cache 已经预热。

这个优化在 Qwen 3.5 Flash 上实测,首字延迟从 500ms 降到 260ms。

用这种"prefill 预热 + 流式判断"的方式做到了一个小目标:面试官说完问题那一刹那,答案已经在屏幕上出现第一个字了

四、RAG 检索:不要每次都算 embedding

传统 RAG 流程:

query → 算 embedding → 查向量库 → rerank → 拼 context

问题:query → embedding 这一步,就算用 text-embedding-3-small,一次网络 RTT 加推理 300-400ms,稳稳吃掉预算。

我们的做法是预建 + 缓存 + 本地小模型三件套:

预建:用户开始 Copilot session 时,简历和 JD 全部切片、算 embedding、存本地(SQLite + sqlite-vec 扩展)。session 期间这部分完全不变。

缓存:问题 embedding 走 LRU 缓存(key = 问题文本归一化 hash)。面试场景里重复问题率很高("讲讲你的项目经历""为什么离职"这种),命中率实测 ~35%。

本地小模型:query embedding 走 bge-small-zh-v1.5 本地版(ONNX runtime,~80MB),单次推理 40ms。云端模型只在 rerank 阶段用。

最后这一路优化,RAG 整体从 400ms 砍到 40ms。


五、LLM 层:"首字预算" 和 "全量预算" 要分开做

体验层设计决策直接影响工程选型。

用户盯着浮窗的体验是:看到第一个字 → 知道"AI 开始回答了",然后再慢慢读全文。所以两个预算:

  • 首字 Time-to-First-Token (TTFT):≤500ms,是"AI 有没有卡"的感知指标
  • 全量 Time-to-Full-Answer (TTFA):≤1500ms,是"AI 快不快"的感知指标

我们的做法:

  • 短答案场景(50-80 字):直接一次生成,不拆
  • 中答案场景(100-150 字):边生成边往浮窗推,前端用 SSE
  • 长答案场景(200-250 字):分两段生成,第一段是"骨架"(STAR 框架的四个要点关键词),第二段补充细节

模型路由策略:

场景模型为什么
行为面试(STAR)Qwen 3.5 Flash中文强 + 便宜 + 快
技术概念问答GPT-4.1-mini技术准确率更高
代码题DeepSeek V3代码能力 + 国内 RT 低
截屏解题(视觉)Claude Sonnet视觉 + 长 context

坑:不要把模型选择放在主链路里,主链路只做 STT → 问题 → LLM,路由决策走异步预测(session 开始时就根据 JD 类型预分配)。


六、一个容易忽视的指标:端到端 Tail Latency

平均 700ms 不够。面试是一次性场景——P99 延迟如果是 3 秒,那 100 个问题里有 1 个会"卡住",而那 1 个很可能是关键问题。

P99 优化:

  • STT WebSocket 断线重连自动切到备用 region(北京 → 上海)
  • LLM API 并发双发策略:同一个请求同时发给主模型和备用模型,谁先返回用谁(只在长答案场景启用)
  • 本地 fallback:如果云端 LLM 连续 2s 没响应,用本地 Qwen 3.5 Flash INT4 量化版兜底(Apple Silicon 上 15 token/s)

线上数据:

指标P50P95P99
首字延迟480ms780ms1100ms
完整答案延迟680ms1200ms1800ms

七、Stealth 层:不是透明,是 OS 级窗口

严格说不属于性能话题,但体验上是一体的——浮窗如果出现在屏幕共享里,再快也白费。

  • macOS: NSWindow.SharingType.none(不是 canJoinAllSpaces 那种老 API)
  • Windows: SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE)(Win 10 2004+)

效果:屏幕共享看不到、屏幕录制录不到、Alt+Tab 不出现、任务栏不出现、Mission Control 不显示。实现成本约 50 行原生代码。


八、工程上的几条踩坑记录

  1. 不要相信浏览器的 WebRTC 采集:桌面端面试场景 80% 走的是原生客户端,浏览器根本没音频。
  2. STT 选型别光看准确率:Deepgram Nova-2 比 Whisper 准确率低 1-2 个点,但 RTT 是 Whisper 的 1/3。面试场景下 RTT 的权重 >> 准确率。
  3. embedding 缓存 key 要做归一化:标点、空格、大小写都要处理掉,否则命中率会很难看。
  4. LLM 的 context 不是越多越好:塞整份简历反而会让模型抓不到重点。我们只塞"问题最相关的 3 个项目 bullet" + "JD 前 500 字" + "公司背景 100 字"。
  5. 流式生成要做"停止 token"检测:有时候 LLM 会无限续写,一定要在 SSE 层做强制 cut。

常见问题

Q1:浏览器扩展能做到 700ms 延迟吗?
A:不能。Chrome 扩展走 getDisplayMedia 拿不到 Zoom/腾讯会议桌面版的音频,且 jitter buffer 自己就 300-500ms。要做到 700ms 必须走 OS Loopback。

Q2:自己跑本地 Whisper 模型可以吗?
A:Apple Silicon 上 Whisper Large 单卡能跑,但流式效果差(设计是离线场景)。Deepgram Nova-2 / 阿里云 Paraformer 这类专门做实时 ASR 的服务 RTT 一致性更好。

Q3:面试 copilot 会被屏幕共享看见吗?
A:取决于实现。普通 web 浮窗会被看见;用 macOS NSWindow.SharingType.none / Windows SetWindowDisplayAffinity 之后屏幕共享和屏幕录制都看不到,Alt+Tab 也不显示。

Q4:RAG 索引每次面试都要重建吗?
A:简历的索引可以全局缓存(用户上传后一次切片),JD 索引每个职位重建一次(一般 1-2 秒)。session 期间不再动。

Q5:LLM 预算超了 fallback 怎么做?
A:双发策略——主请求走云端 GPT-4.1-mini,备份请求走本地 Qwen 3.5 Flash INT4。云端 2 秒没响应就用本地结果,对用户透明。


总结

700ms 面试 copilot 不是靠单一神技,而是把每一层都压到极致:

优化手段收益
音频采集OS 级 Loopback,绕开浏览器600→200ms
STT流式 + VAD + 语义判断并行化
问题识别小模型 + 标点混合200→50ms
RAG预建索引 + embedding 缓存 + 本地小模型400→40ms
LLMprefill 预热 + 流式输出 + 模型路由600→260ms
Tail双发 + 本地兜底P99 控制在 1.8s

如果你也在做实时音频+LLM 的产品,希望这些经验能省你一些时间。

标签: none

添加新评论