Loxinity 通话里 AI 反应慢半拍——瓶颈在采集端还是服务端?

文章导读
判断 Loxinity 通话里 AI「慢半拍」卡在采集端还是服务端,不需要先拿到服务端日志。可行做法是:在客户端把「用户说完」到「AI 声音出来」这一整段,切成采集、上传、等待、播放四段并各自打一个时间戳;先单独测本地录音回放是否本身就慢,再用同一台设备、同一网络重复几次,看滞后稳定集中在哪一段。只要打点位置准确,就能把问题缩小到「客户端本地处理」「上行带宽」「服务端返回前」这三类之一,而不是凭感
📋 目录
  1. 壹 先测本地录音回放的时间差,确认采集端没有额外延迟
  2. 贰 用代码在说话结束与音频开始播放两处打点
  3. 叁 把打点结果拆成上传、等待、播放三段看
  4. 肆 换 Wi-Fi 与有线、换不同分辨率各复测一次
  5. 伍 在拿不到服务端数据时,明确结论只能到哪一步
A A

判断 Loxinity 通话里 AI「慢半拍」卡在采集端还是服务端,不需要先拿到服务端日志。可行做法是:在客户端把「用户说完」到「AI 声音出来」这一整段,切成采集、上传、等待、播放四段并各自打一个时间戳;先单独测本地录音回放是否本身就慢,再用同一台设备、同一网络重复几次,看滞后稳定集中在哪一段。只要打点位置准确,就能把问题缩小到「客户端本地处理」「上行带宽」「服务端返回前」这三类之一,而不是凭感觉换网络。

先别猜服务端。用 performance.now() 在本地录音停止、上行发完、首个音频包到达、音频真正起播四个位置打点,把端到端拆成上传、等待、播放三段。本地录音回放时间差正常、且滞后稳定落在等待段时,只能说明延迟发生在上行发完之后;要区分网络往返、服务端排队与推理耗时,仍需网关日志或服务端时间戳补齐,客户端打点单独不足以下最终结论。

先测本地录音回放的时间差,确认采集端没有额外延迟

这一步的目的是先把设备和浏览器的处理开销排除掉。如果本机从「停止录音」到「听到自己刚才那段录音开始播放」就已经明显拖沓,那么后面所有的端到端测量都会被这个底噪污染,测出来的结论不可信。

粗略计时方法不需要专业设备:在页面上放一个「录 3 秒—立即回放」的小按钮,手动点停止后立刻点播放,同时用手机秒表或另一个屏幕上的计时器记录「点击停止」到「听到声音」的主观间隔;更稳一点的做法是在录音停止事件里记 performance.now(),在 audio 元素的 playing 事件里再记一次,两个值相减。这里不要指望毫秒级准确,看的是量级:是几十毫秒的感觉,还是肉眼可辨的停顿。

对照时至少换两类设备:一台是平时用得最顺的笔记本电脑(有线网络、外接或不外接麦克风各测一次),一台是平时觉得「卡」的手机。同一段操作在两类设备上差别很大时,问题更可能在特定设备的麦克风驱动、浏览器省电策略或后台标签页限流,而不是 Loxinity 服务端。

明显偏慢时的常见来源通常集中在几处:页面在后台标签页被浏览器降频;麦克风采样率与 AudioContext 不一致导致的协商开销;开了回声消除、降噪、自动增益,这些处理本身有固定延迟;以及录音格式需要转码后再播放。可以逐项关掉再测一次,看本地时间差是否回落。这一步只做排除,不做优化承诺。

用代码在说话结束与音频开始播放两处打点

本地回放正常之后,再在真实通话链路里打点。下面是一段通用骨架,不绑定具体 SDK,接入时把 recorder、上行发送处、音频数据回调、audio 元素替换成 Loxinity 页面上实际的对象即可。目的是拿到可比较的分段时间,而不是采集全量日志。

const trace = {};
const now = () => performance.now();

// A. 说话结束:以本地录音停止为准,作为整条链路起点
recorder.addEventListener('stop', () => {
  trace.t0 = now();
  console.log('[trace] t0 采集结束', trace.t0.toFixed(1));
});

// B. 上行发完:最后一段音频数据交给发送侧之后
//    WebSocket 放在最后一个 chunk 的 send 之后;HTTP 上传放在 fetch 拿到响应头之前
function onUpstreamDone() {
  if (trace.t1) return;
  trace.t1 = now();
  console.log('[trace] t1 上行发完', trace.t1.toFixed(1));
}

// C. 首个响应音频数据到达前端(不是请求发出,是真正收到音频字节)
function onFirstAudioChunk() {
  if (trace.t2) return;
  trace.t2 = now();
  console.log('[trace] t2 首包到达', trace.t2.toFixed(1));
}

// D. 音频真正出声:playing 事件,或 AudioContext 起播时记录
playbackAudio.addEventListener('playing', () => {
  trace.t3 = now();
  const row = {
    设备: navigator.userAgent,
    上传段ms: Math.round(trace.t1 - trace.t0),
    等待段ms: Math.round(trace.t2 - trace.t1),
    播放段ms: Math.round(trace.t3 - trace.t2),
    端到端ms: Math.round(trace.t3 - trace.t0)
  };
  console.log('[trace] 汇总', JSON.stringify(row));
});

打点是否真的触发,要一个个确认:先只说一句话,看控制台是否按 t0、t1、t2、t3 的顺序打印四个值。如果 t1 缺失,说明上行发完的回调位置放错了,或者发送是流式的、根本没有「发完」这个明确时刻,那就把 t1 改成「最后一个音频 chunk 送入发送队列」的时刻。t2 缺失通常是音频走了别的通道(例如直接由 WebRTC 轨道播放),需要在对应轨道或解码回调处补。

导出成表格:把每次测量 push 进数组,测完后在控制台执行 copy(JSON.stringify(arr)),粘贴到表格软件里按行展开,字段保持「时间、设备、网络、上传段、等待段、播放段、端到端」。字段固定下来,后面换网络复测才有可比性。

把打点结果拆成上传、等待、播放三段看

三段的边界这样定义,避免各人理解不一:上传段是 t0 到 t1,即本地录音停止到上行数据交付发送侧;等待段是 t1 到 t2,即上行发完到首个响应音频包返回;播放段是 t2 到 t3,即首包到达到真正出声。三段相加应接近端到端值,差得多说明中间还有没打到的环节。

Loxinity 通话里 AI 反应慢半拍——瓶颈在采集端还是服务端?

某一段是否异常,不建议套用固定阈值,而是用同一台设备、同一网络下的自身数据做基线:重复测 5 到 10 次,取每段的中位数,并看最小值与最大值之间的跨度。如果某段的中位数明显高于它自己的最小值,且这一段的波动主导了端到端波动,那这一段就是主要嫌疑;如果三段都在小幅波动、端到端却整体偏大,则更可能是持续存在的固定开销,而不是偶发抖动。

次数上,单次数据不要用来下结论。同一网络下先跑 5 次看稳定性,再决定要不要加测;说话时长和内容尽量保持一致,否则上传段会因为音频长度不同而不可比。

换 Wi-Fi 与有线、换不同分辨率各复测一次

换网络是为了看滞后是否随带宽和链路质量变化。复测时必须固定的变量包括:同一台设备、同一浏览器版本、同一个麦克风、同样的说话时长与语速、是否外放、是否开回声消除,以及尽量相同的时段(避开同一网络的下载高峰)。每次只改一个变量,改两个以上就无法归因。

数据量与滞后的对应关系,主要看两件事:把音频采样率或码率调高、或者把视频通话分辨率调高之后,上传段是否随之明显变大。上传段跟着数据量走,说明上行带宽或链路质量是主要约束;上传段几乎不动、而等待段明显拉长,则更可能发生在服务端排队、模型推理或下行返回环节。播放段变长通常与客户端解码、设备性能或音频上下文被挂起有关,与带宽关系较弱。

避免过度归因:换到有线后变快,不等于「Wi-Fi 有问题」,中间还隔着路由器负载、信号强度、公司网络策略、DNS 解析等环节;换分辨率后变慢,也不等于「服务端算不过来」,因为数据量变大本身就会拉长上传段。写结论时用「在本次对照中,改变 X 后某一段随之变化」这样的表述,不要写成因果定论。

在拿不到服务端数据时,明确结论只能到哪一步

客户端打点能下的结论有明确上限。可以这样写:「在当前设备与网络条件下,端到端耗时里较大比例落在等待段,滞后发生在上行数据发完之后、首个音频包返回之前;上传段与播放段占比很小。这一段耗时可能来自网络往返、服务端排队或推理,客户端无法进一步区分。」这样的表述既给出了位置,也没有越界替服务端下判断。

要往下走,需要补充的信息通常包括:服务端或网关侧记录的请求接收时间与首个响应时间、所用模型或接口的首包耗时、客户端到服务端的网络往返测量结果、以及同一时段是否有并发请求排队。这些信息任意一项到手,就能把等待段再切成网络和服务端两截。

下次复测要保留的记录:完整的控制台 trace 行、导出后的表格文件、设备与浏览器标识、网络类型(有线、Wi-Fi 频段)、当次音频参数、说话内容与时长、以及复测的绝对时间点,方便和服务端日志按时间对齐。记录不齐,下一次还是只能重复这一次的推断。