Dialtrace:看清语音 AI 电话通话中的时间都花在哪
How HN: Dialtrace – see where the time goes on a voice AI phone call
Dialtrace 是一款零依赖、一条命令即可运行的语音 AI 通话延迟分析工具,把运营商事件与自有 pipeline 阶段放在同一条时间线上。一次美国号码拨打荷兰手机的真实通话显示,接听后到听到第一个音节等待了 640ms,其中 setup 和 ring 占 3,789ms、自有 pipeline 占 1,378ms(85%)。
测量你的语音 AI 电话通话中的静默。
每个语音 AI 延迟基准测试都测量同样的三件事:语音转文本、模型和文本转语音。它们都没有测量电话通话本身。所以团队把 900ms 的流水线调到 600ms,但呼叫者仍然在等待,因为三分之一的等待从来就不在流水线里。
dialtrace 将运营商事件和你自己的流水线阶段放在同一条时间线上,这样你就能看到你实际在为哪一部分付出代价。
在一次从美国号码到荷兰手机的真实通话中,路径中完全没有 AI,呼叫者在接起后等待了 640ms 才听到一个音节。这几乎是亚秒级预算的大部分,在向模型提出任何问题之前就已经花掉了。
零依赖。一条命令。
npx dialtrace demo
dialtrace +31201234567 2026-10-02T13:48:21.133Z
dial sent ├─ 0ms
dial accepted ├── 118ms
call.initiated ├── 121ms
call.answered ├───╳─ 3,910ms ← setup and ring, not separable here
streaming.started ├───╳── 4,024ms
stt.first_partial ├───╳────────── 4,610ms ·
llm.done ├───╳────────────────── 5,180ms ·
speak.started ├───╳───────────────────── 5,402ms ← caller hears you
call.hangup ├───╳────────────────────────────────────────── 7,000ms
╳ = 3,789ms of waiting, not drawn to scale
Where the time went
api round trip 121ms 8% ██
setup and ring 3,789ms — signalling plus a person picking up, not separable
media path 114ms 7% ██
your pipeline 1,378ms 85% ████████████████████████
Answer to first audio 1,492ms
Call duration 7,000ms
Webhook delivery lag 44ms median
Clock skew -12ms ±59ms
Line quality from call_quality_stats on the hangup webhook
inbound MOS 4.21 loss 1.43% jitter var 2.74 491 pkts
outbound MOS 4.48 loss 0.20% 503 pkts
ended normal_clearing · caller · 200
No ringing event, so signalling and ring time cannot be told apart.
The Call Control API does not emit one. TeXML status callbacks do.
First audio out. This is the moment the caller stops hearing silence.
测量一次真实通话
你需要一个 Telnyx API 密钥、一个 Call Control App,以及你账户上的一个号码。
export TELNYX_API_KEY=KEY... export TELNYX_CONNECTION_ID=2345678901234567890 npx dialtrace call \ --to +31201234567 \ --from +31208080808 \ --say "testing one two three" \ --svg call.svg
Telnyx 需要一个公共 URL 来投递 webhook。如果 cloudflared 在你的 PATH 中,会自动为你创建一个,并在结束时拆除。否则传入 --webhook-url https://your-host 并自己将其指向本地端口。
运行结果会保存到 runs/last.json。随时用 npx dialtrace replay runs/last.json 重新渲染它。
把你自己的流水线放到时间线上
运营商事件告诉你通话何时接通。它们无法告诉你呼叫者为什么等待。你的阶段可以。当通话进行时,向本地接收器发送 POST:
curl -s localhost:8787/mark \ -H 'content-type: application/json' \ -d '{"label":"stt.first_partial"}'
在你的 agent 中写三行通常就够了:
const mark = (label) => fetch('http://localhost:8787/mark', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label }), }).catch(() => {}); mark('stt.first_partial'); mark('llm.done'); mark('tts.first_byte');
给任何你想要的东西打标签。标记为 tts.first_byte 或 first_audio 的标签会被视为呼叫者停止听到静默的时刻,与运营商自己的播放事件相同。
它测量什么,以及不测量什么
在向任何人引用这个工具给出的数字之前,值得一读。
从运营商时间戳测量。 每个 Telnyx webhook 都带有 occurred_at。其中两个之间的差值很可靠,因为时钟偏移会抵消:拨号后延迟、振铃时长、接听到第一段音频、通话时长。
这里的拨号后延迟是 call.initiated 到 call.ringing,而不是从你拨号到振铃。从你自己的拨号开始测量会混入 API 往返和你的 TLS 握手,这就是人们最终把自己网络的问题归咎于运营商的原因。API 往返会作为单独一行显示。
在你的时钟上测量。 你的 /mark 调用,以及拨号请求本身。
估算一次,然后沿用。 两个时钟不是同一个时钟。call.initiated 发生在 Telnyx 处理你的 POST /v2/calls 期间,因此该 HTTP 往返的中点是对你这边同一时刻的最佳可用猜测。偏移量会以 clock skew 打印出来,误差范围为往返时间的一半。如果该误差范围比你关心的数字还大,那你就没有那个数字。工具会如实说明,而不是把疑虑四舍五入掉。
从挂断 webhook 读取。 Telnyx 将 call_quality_stats 放在 call.hangup 上,从 CHANNEL_HANGUP_COMPLETE 聚合而来:MOS、抖动方差和数据包计数,按方向分别统计。无需额外启用任何东西,也无需第二次 API 调用。丢包率是推导出来的,因为你得到的是 skip_packet_count 对 packet_count,而不是百分比。
在我去 OpenAPI 规范里查找之前,我并不知道这里有这个。如果你已经在接收 Telnyx webhook,那么这一整段时间里你每次通话都有 MOS。
不测量。 任何需要 RTP 流本身的东西:逐秒的质量而不是最后的一个数字、单向音频检测,或波形。这个工具读取信令和事件,而不是数据包。对于那些,你需要 sngrep、Homer 或 VoIPmonitor,以及一次抓包。
振铃时间是单独报告的,不计入百分比。它通常是通话中最大的数字,而且它不是延迟问题。它是人在伸手去拿手机。把它平均进“通话延迟”里,正是团队最终优化错东西的原因。
Webhook 投递延迟不是通话延迟。它是事件到达你机器所花的时间,包括隧道在内。它不影响通话时间线,后者来自运营商时间戳。它被打印出来,是为了让你看到如果你实时响应 webhook,隧道会给你带来多少成本。
为什么没有 webhook 签名校验
Telnyx 用 telnyx-signature-ed25519 对 webhook 签名,任何对 webhook 采取行动的东西都应该验证它。这个工具不对 webhook 采取行动,它只是给它们打时间戳,而且它在一个一次性隧道上监听一通电话的时长。
真正的原因是,验证会位于响应路径上,而缓慢的回复会变成 Telnyx 重试,这会在你的时间线上显示为重复事件。处理器先回复 200,然后再做其他所有事情。
如果你把它指向一个长期存在的公共 URL,而不是每次运行一个隧道,这种权衡就不再是免费的。任何能访问该端口的人都可以向你的运行中投递事件。每次运行使用一个新隧道,这是默认设置。
拨号后延迟,以及为什么你很可能看不到它
拨号后延迟是你的运营商发出 INVITE 与远端网络开始振铃手机之间的间隔。这个数字告诉你,缓慢的连接是网络问题还是被叫方问题。
Call Control API 不发出振铃事件。一通外呼会经历 call.initiated,然后 call.answered,中间什么都没有。因此信令时间和人伸手去拿手机所花的秒数会作为一个数字到达,没有什么能把它们分开。我查的是 OpenAPI 规范而不是文档:Telnyx 定义的唯一 ringing 回调是 TeXML 的。
这个工具不去猜测这个拆分。它把 setup and ring 作为一个整体块报告,并将其排除在百分比之外,就像振铃时间被排除一样。
如果你需要这个拆分,用 TeXML 驱动通话,并把它的状态回调指向这里。initiated、ringing、answered 和 completed 都会到达那里,当振铃事件出现时,瀑布图会自动拆分。
标志
--to <e164> number to call (required)
--from <e164> caller ID, a number on your account (required)
--connection-id <id> Call Control App id (or TELNYX_CONNECTION_ID)
--api-key <key> Telnyx API key (or TELNYX_API_KEY)
--webhook-url <url> your own public URL, instead of cloudflared
--port <n> local webhook port (default 8787)
--say "<text>" speak this on answer, to mark first audio out
--wait <secs> hang up this long after answer (default 8)
--ring-timeout <secs> give up if nobody answers (default 30)
--svg <path> write the waterfall as an SVG
--json <path> write the raw run (default runs/last.json)
配置首先从标志读取,然后是环境变量,然后是工作目录中的 .env 文件。密钥永远不会写入磁盘,也永远不会完整打印。
其他运营商
时间线模型与运营商无关。只有 src/telnyx.ts 知道 Telnyx,它大约有 80 行 fetch。一个发出带时间戳的通话进度事件的提供商可以在同一接口后面添加。欢迎提交拉取请求。
我为什么构建这个
我是 Telnyx 的一名前向部署工程师。我一周都在与构建语音代理的团队通话,同样的对话不断发生:他们大力调优了自己的流水线,演示仍然感觉慢,而且没有人看过它底下的电话通话。能查看它的工具是运营商工具。它们需要抓包,并且假设你已经会说 SIP。
这是那个的小版本,给那些有 API 密钥和一个问题的人。
MIT。
来源:Hacker News · github.com
