1. 电话 AI 不冷场的关键:把 LLM 请求切到 TaoToken
如果你正在用 qwen-audio-agent 做电话 AI,最怕的体验不是识别不准,而是 LLM 请求一慢,TTS 就没词可播,电话那头只剩呼吸声。TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_intro)这里先给结论:把 LLM 请求客户端的 Base URL 统一指向 https://taotoken.net/api,Key 用 YOUR_API_KEY 从控制台获取,qwen-audio-agent 的实时链路会更容易稳定在“有话可说”的状态。qwen-audio-agent 是通义千问音频团队开源的实时语音运行时,它提出的 Agent Presence 概念,本质上是在解决语音对话和任务执行割裂的问题:agent 查资料、调工具、跑任务时,不能从对话里消失。对电话机器人开发者来说,这个能力底座很关键,因为电话场景没有“等待动画”,用户只能靠声音判断你有没有在听。一旦 LLM 请求阻塞超过一两秒,TTS 就接不上话,冷场立刻发生。把 LLM 出口统一到 TaoToken 后,你可以在一个 Base URL 下管理模型调用、Key 权限和 Token 统计,再配合 qwen-audio-agent 的流式链路,把“边聊边干”落到电话线路里。下面这份笔记不是泛泛的 AI 开源推荐,而是从电话机器人开发者的视角,拆解 qwen-audio-agent 的 LLM 请求初始化、冷场日志、Token 统计和排障清单,全部围绕一个可复现目标:让电话 AI 不冷场。
2. qwen-audio-agent 的实时运行时里,冷场发生在哪一段
qwen-audio-agent 的核心是一条实时语音运行时链路:音频输入经过 ASR 变成文本,文本进入 LLM 做意图理解与决策,如果涉及工具调用就执行任务,任务结果再回到 LLM 继续生成回复,最后交给 TTS 合成语音。理想情况下,这条链路全程流式,用户说完一句话,agent 能一边处理任务一边保持语音反馈。但实际做电话机器人时,冷场往往发生在三个位置。
第一个位置是 LLM 请求客户端初始化。很多示例代码直接把 LLM 地址写死在业务逻辑里,或者用默认的 OpenAI 兼容地址。一旦网络波动、区域限制或 Key 额度异常,请求会卡在连接阶段,ASR 已经出了 final 文本,TTS 却迟迟拿不到首 token。电话用户听到的就是突然安静。
第二个位置是工具调用与 LLM 的衔接。qwen-audio-agent 支持 agent 在查资料、调工具、跑任务时继续对话,但前提是 LLM 请求不能因为工具调用而完全断流。如果工具执行耗时较长,而 LLM 客户端又没有设置合理的超时、重试和流式续写,语音流就会断。用户会感觉 agent “掉线了”,而不是“在忙”。
第三个位置是 TTS 的断流处理。即使 LLM 首 token 回来了,如果后续 chunk 间隔过长,TTS 也可能播完上一句后无话可说。电话线路对静音很敏感,超过一定时长的静音会被用户理解为挂断或故障。
所以,解决电话 AI 冷场,不是单纯换一个模型,而是要把 LLM 请求客户端初始化成可观测、可切换、可统计的通道。TaoToken 在这里扮演的是统一 LLM 出口的角色:Base URL 固定为 https://taotoken.net/api,Key 从官网控制台获取,模型调用、Token 消耗和错误码都在同一套体系里。这样你在复现冷场问题时,才能区分是 ASR 慢、工具慢,还是 LLM 请求本身慢。
3. 初始化 LLM 请求客户端:从官网取 Key,Base URL 指向 TaoToken
这一节给出可复制的初始化步骤。假设你已经在 Node.js 环境里跑 qwen-audio-agent,并且准备把 LLM 请求从默认地址切到 TaoToken。
第一步,访问 TaoToken 官网获取 API Key。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_key ,在控制台里创建 Key。创建时建议按项目命名,例如phone-audio-agent-dev,方便后续按电话机器人项目统计 Token。Key 只在创建时显示一次,保存到本地环境变量,不要硬编码进仓库。
第二步,安装依赖。qwen-audio-agent 本身是 npm 包,JavaScript 项目可以直接引入。LLM 请求客户端我们使用 OpenAI 兼容 SDK,因为 TaoToken 的 Base URL 兼容主流 Chat Completions 调用方式。
npm init -y npm install qwen-audio-agent openai dotenv第三步,创建.env文件,把 Key 和 Base URL 写进去。注意 Base URL 不加 UTM,统一为https://taotoken.net/api。
TAOTOKEN_API_KEY=YOUR_API_KEY TAOTOKEN_BASE_URL=https://taotoken.net/api第四步,初始化 LLM 客户端。下面这段代码可以直接运行,用于验证 Key 和 Base URL 是否连通。
// llm-client.js import OpenAI from "openai"; import dotenv from "dotenv"; dotenv.config(); const llmClient = new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY || "YOUR_API_KEY", baseURL: process.env.TAOTOKEN_BASE_URL || "https://taotoken.net/api", timeout: 15000, maxRetries: 2, }); export async function chatOnce(messages) { const completion = await llmClient.chat.completions.create({ model: "qwen-plus", messages, temperature: 0.6, stream: false, }); return completion.choices[0]?.message?.content ?? ""; } export async function streamReply(messages, onDelta) { const stream = await llmClient.chat.completions.create({ model: "qwen-plus", messages, temperature: 0.6, stream: true, stream_options: { include_usage: true }, }); let usage = null; for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content; if (delta) { onDelta(delta); } if (chunk.usage) { usage = chunk.usage; } } return usage; }第五步,在 qwen-audio-agent 的 LLM 适配层里,把原来的请求客户端替换成上面的llmClient。如果你的项目里是直接new OpenAI,只需要改apiKey和baseURL两个字段。如果项目里有自定义的 LLM adapter,就把 adapter 内部的请求地址指向process.env.TAOTOKEN_BASE_URL,把鉴权头换成Authorization: Bearer YOUR_API_KEY。这样 qwen-audio-agent 在查资料、调工具、跑任务时,所有 LLM 请求都会经过 TaoToken 出口。
第六步,跑一次最小验证。准备一段电话场景的 messages,调用streamReply,观察首 token 时间。
import { streamReply } from "./llm-client.js"; const messages = [ { role: "system", content: "你是一个电话客服助手,回答要简短、口语化,避免长段落。" }, { role: "user", content: "帮我查一下我的订单到哪了,订单号是 A12345。" }, ]; const usage = await streamReply(messages, (delta) => { process.stdout.write(delta); }); console.log("\nToken usage:", usage);如果控制台能流式打印出内容,并且最后能看到prompt_tokens、completion_tokens、total_tokens,说明 LLM 请求客户端已经指向 TaoToken,后续就可以把它接入 qwen-audio-agent 的实时语音链路。
4. 电话对话日志:用事件流复现“冷场前后”
要证明电话 AI 不冷场,不能只靠感觉,需要可复现的对话日志。建议在 qwen-audio-agent 的运行时里埋点,把每个关键事件写成 JSON Lines,一行一个事件。这样既能做冷场前后对照,也能统计 LLM Token。
推荐记录这些字段:
call_id:电话会话 ID,用来串起一通电话。turn_id:对话轮次,用户每说一句算一轮。event:事件名,例如asr_final、llm_request_start、llm_first_token、tool_call_start、tool_call_end、llm_stream_end、tts_start、tts_end。timestamp:毫秒时间戳。text:ASR 文本或 LLM 输出片段。usage:LLM Token 统计,只在llm_stream_end里出现。error:错误码或异常信息。
下面是一个可运行的日志写入模块。
// call-logger.js import fs from "fs"; const logFile = "./phone-call.log"; export function logEvent(event) { const line = JSON.stringify({ ...event, timestamp: event.timestamp ?? Date.now(), }); fs.appendFileSync(logFile, line + "\n", "utf8"); } export function logAsrFinal(callId, turnId, text) { logEvent({ call_id: callId, turn_id: turnId, event: "asr_final", text }); } export function logLlmStart(callId, turnId, model) { logEvent({ call_id: callId, turn_id: turnId, event: "llm_request_start", model }); } export function logLlmFirstToken(callId, turnId) { logEvent({ call_id: callId, turn_id: turnId, event: "llm_first_token" }); } export function logLlmEnd(callId, turnId, usage) { logEvent({ call_id: callId, turn_id: turnId, event: "llm_stream_end", usage }); } export function logTtsStart(callId, turnId) { logEvent({ call_id: callId, turn_id: turnId, event: "tts_start" }); } export function logTtsEnd(callId, turnId) { logEvent({ call_id: callId, turn_id: turnId, event: "tts_end" }); }在 qwen-audio-agent 的 LLM 调用处,把上面的埋点加进去。例如:
import { logLlmStart, logLlmFirstToken, logLlmEnd } from "./call-logger.js"; import { streamReply } from "./llm-client.js"; let firstTokenLogged = false; logLlmStart(callId, turnId, "qwen-plus"); const usage = await streamReply(messages, (delta) => { if (!firstTokenLogged) { logLlmFirstToken(callId, turnId); firstTokenLogged = true; } // 这里把 delta 推给 TTS 流 ttsStream.push(delta); }); logLlmEnd(callId, turnId, usage);有了日志之后,你可以写一个分析脚本,计算每一轮从asr_final到llm_first_token的延迟,以及从llm_first_token到tts_start的延迟。这两个指标直接对应“用户说完到 AI 开口”的等待时间。如果这个时间超过电话场景可接受范围,就需要继续排查。
// analyze-cold-start.js import fs from "fs"; const lines = fs.readFileSync("phone-call.log", "utf8").trim().split("\n"); const events = lines.map((line) => JSON.parse(line)); const turns = new Map(); for (const event of events) { const key = `${event.call_id}:${event.turn_id}`; if (!turns.has(key)) { turns.set(key, {}); } const turn = turns.get(key); if (event.event === "asr_final") turn.asrFinal = event.timestamp; if (event.event === "llm_first_token") turn.llmFirstToken = event.timestamp; if (event.event === "tts_start") turn.ttsStart = event.timestamp; if (event.event === "llm_stream_end") turn.usage = event.usage; } for (const [key, turn] of turns.entries()) { const waitLlm = turn.llmFirstToken && turn.asrFinal ? turn.llmFirstToken - turn.asrFinal : null; const waitTts = turn.ttsStart && turn.llmFirstToken ? turn.ttsStart - turn.llmFirstToken : null; console.log(key, { waitLlmMs: waitLlm, waitTtsMs: waitTts, totalTokens: turn.usage?.total_tokens ?? 0, }); }跑完这个脚本,你会得到每轮电话对话的等待时间和 Token 消耗。冷场前后对照就有了数据基础,而不是靠主观感受。
5. LLM Token 统计与冷场对照:把“在场感”变成可观测指标
电话 AI 的“在场感”听起来很玄,但落到工程上就是几个可观测指标:首 token 延迟、TTS 衔接间隔、每轮 Token 消耗、错误率。qwen-audio-agent 的 Agent Presence 强调 agent 在查资料、调工具时依然能保持对话,这意味着 LLM 请求不能是一次性的,而应该是流式、可续写、可统计的。
下面是一张冷场前后对照表。表中的数值来自一次本地电话机器人压测的日志示例,实际数值会随网络、模型和工具耗时变化。重点不是具体数字,而是指标本身。
| 观测指标 | 冷场前:默认 LLM 出口 | 切到 TaoToken 后:统一 Base URL |
|---|---|---|
| ASR final 到 LLM 首 token | 波动大,偶发超时 | 连接稳定,首 token 可统计 |
| 工具调用期间语音流 | 容易断流,用户以为掉线 | 可继续流式输出,保持在场 |
| TTS 衔接 | 首 token 慢时出现静音 | 首 token 回来自动推 TTS |
| Token 统计 | 分散在各处,难汇总 | usage字段统一返回 |
| 错误排查 | 只看业务日志,无请求级信息 | 可按 Key、模型、错误码定位 |
| 模型切换 | 改代码、改地址、重新部署 | 改环境变量或控制台配置 |
要把这些指标固定下来,建议在每通电话结束后汇总一次:
// summarize-call.js import fs from "fs"; const callId = process.argv[2]; if (!callId) { console.error("用法: node summarize-call.js CALL_ID"); process.exit(1); } const lines = fs.readFileSync("phone-call.log", "utf8").trim().split("\n"); const events = lines .map((line) => JSON.parse(line)) .filter((event) => event.call_id === callId); let totalPromptTokens = 0; let totalCompletionTokens = 0; let totalTokens = 0; let firstTokenDelays = []; let ttsGaps = []; const turnMap = new Map(); for (const event of events) { const key = event.turn_id; if (!turnMap.has(key)) turnMap.set(key, {}); const turn = turnMap.get(key); if (event.event === "asr_final") turn.asrFinal = event.timestamp; if (event.event === "llm_first_token") turn.llmFirstToken = event.timestamp; if (event.event === "tts_start") turn.ttsStart = event.timestamp; if (event.event === "llm_stream_end" && event.usage) { totalPromptTokens += event.usage.prompt_tokens ?? 0; totalCompletionTokens += event.usage.completion_tokens ?? 0; totalTokens += event.usage.total_tokens ?? 0; } } for (const turn of turnMap.values()) { if (turn.asrFinal && turn.llmFirstToken) { firstTokenDelays.push(turn.llmFirstToken - turn.asrFinal); } if (turn.llmFirstToken && turn.ttsStart) { ttsGaps.push(turn.ttsStart - turn.llmFirstToken); } } const avg = (arr) => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0; console.log({ callId, turns: turnMap.size, totalPromptTokens, totalCompletionTokens, totalTokens, avgFirstTokenDelayMs: avg(firstTokenDelays), avgTtsGapMs: avg(ttsGaps), });这个脚本输出的是单通电话的 Token 汇总和平均延迟。你可以把它接到每日报表里,观察电话机器人在不同时间段的冷场率。如果某段时间首 token 延迟明显上升,就可以回到 TaoToken 控制台检查 Key 调用量、模型响应和错误码,而不是盲目重试。
对于电话机器人开发者来说,Token 统计还有一个实际价值:估算每通电话的成本。语音场景的 LLM 请求往往比纯文本短,但轮次多,工具调用还会产生额外的上下文。按total_tokens汇总后,你可以更准确地把 LLM 成本摊到每通电话、每个坐席、每个客户身上。
6. 电话机器人排障清单:401、429、超时与 TTS 断流
把 LLM 请求指向 TaoToken 后,大多数冷场问题会变成可定位的请求问题。下面这份排障清单按错误码和现象分类,适合电话机器人开发者直接对照。
401 Unauthorized:Key 无效或没有正确带上鉴权头。检查.env里的TAOTOKEN_API_KEY是否等于YOUR_API_KEY占位符之外的真实 Key,检查代码里是否把apiKey传给了 OpenAI 客户端。如果你在 qwen-audio-agent 的 adapter 里手写 HTTP 请求,确认请求头是Authorization: Bearer YOUR_API_KEY。
429 Too Many Requests:调用频率超限或并发过高。电话机器人经常在高峰期同时处理多通电话,如果每通电话都独立并发请求 LLM,容易触发限流。解决方案是在业务层加队列和并发上限,把 LLM 请求按优先级排队。qwen-audio-agent 的实时链路允许 agent 在任务执行期间保持对话,但并不意味着可以无限并发。建议在网关层统计并发数,超过阈值时先让 TTS 播报“我正在查询,请稍等”,保持在场感。
超时:连接超时或首 token 超时。检查timeout设置,电话场景建议首 token 超时不超过 3 秒,整体请求超时不超过 15 秒。如果超时频繁,先用stream: false做一次短请求验证连通性,再回到流式模式。确认 Base URL 是https://taotoken.net/api,不要带多余路径。
TTS 断流:LLM 流式 chunk 间隔过长。流式响应不是每个 chunk 都立即到达,如果模型在思考或工具调用,chunk 间隔可能拉长。解决办法有两类:一类是在 qwen-audio-agent 里插入“填充语音”,例如“我看一下”“稍等,我帮你查”;另一类是把工具调用改为异步,先让 LLM 给出简短过渡句,再在任务完成后继续流式输出。TaoToken 的流式接口会返回 usage 信息,方便你判断是首 token 慢还是后续 chunk 慢。
模型不可用:检查模型名是否写错。电话机器人常用轻量模型做意图理解,用更强模型做复杂任务。建议在代码里把模型名做成环境变量,例如TAOTOKEN_LLM_MODEL=qwen-plus,这样切换模型不需要改业务代码。
工具调用超时:工具本身慢,不要误判为 LLM 慢。在日志里记录tool_call_start和tool_call_end,把工具耗时和 LLM 耗时分开。qwen-audio-agent 的价值在于任务执行期间对话不中断,但如果工具本身耗时超过 10 秒,再好的运行时也难保证体验。此时应该优化工具接口,或者把长任务拆成多个短任务,边执行边汇报。
日志缺失:如果电话出问题但日志里只有 ASR 和 TTS,没有 LLM 事件,说明 LLM 请求没有走到埋点。检查 qwen-audio-agent 的 LLM adapter 是否真的调用了你替换后的客户端。很多项目会保留默认示例代码,导致实际请求仍然走旧地址。搜索代码里的baseURL和apiKey,确认只有一处出口指向https://taotoken.net/api。
7. 本地开发辅助:Claude Code 与 Codex 统一走 TaoToken
电话机器人项目通常不只是 qwen-audio-agent 一个仓库,还包括 ASR/TTS 适配、工具网关、日志平台和测试脚本。开发这些模块时,你可能同时用 Claude Code 和 Codex 做代码辅助。为了避免多个工具各自管理 Key,可以把它们也统一指向 TaoToken。注意:Claude Code 用 Anthropic 系环境变量,Codex 用config.toml,两者不要混用。
Claude Code 的settings.json示例,路径通常是~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-3-5-sonnet-latest" } }Codex 的config.toml示例,路径通常是~/.codex/config.toml:
model = "gpt-5" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"如果你用 CC Switch 管理多个配置,可以把“三件套”填清楚:供应商 Base URL、API Key、Model 名。Base URL 统一填https://taotoken.net/api,API Key 填从 TaoToken 控制台创建的 Key,Model 按项目需要选择。这样切换开发辅助工具时,不需要在多个配置文件里重复找 Key。创建 Key 和查看用量可以走 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_dev_aux ,控制台里能按 Key 区分电话机器人项目和本地开发辅助的调用量。
需要再次强调:Claude Code 的ANTHROPIC_*变量只用于 Claude Code 系工具,不要套到 Codex 的config.toml里。Codex 的供应商配置是独立的,用base_url和env_key指定。把两个工具的配置分开,排障时才不会互相干扰。
8. 从模型对话到 Coding Plan:把电话 AI 的 LLM 出口固定下来
做到这里,你已经完成了电话 AI 不冷场的关键一步:qwen-audio-agent 的 LLM 请求不再散落在业务代码里,而是统一指向https://taotoken.net/api,Key 从 TaoToken 控制台获取,Token 统计和错误码有统一出口。接下来建议按这个顺序继续验证和扩展。
第一步,先用模型对话验证基础连通性。打开 https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_chat ,输入一段电话客服场景的对话,确认模型能正常返回。这个步骤不需要写代码,适合快速确认 Key 和模型是否可用。
第二步,如果你要长期跑电话机器人项目,可以了解 Coding Plan。打开 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_coding_plan ,看是否有适合你团队调用量的方案。电话机器人的 LLM 调用量和坐席数、通话时长强相关,提前规划比事后临时加 Key 更稳。
第三步,创建项目专用 Key。打开 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_api_keys ,按项目创建 Key,例如phone-audio-agent-prod和phone-audio-agent-test。生产 Key 只放在生产环境,测试 Key 用于本地复现冷场日志。Key 占位符统一写成YOUR_API_KEY,不要提交到 Git。
第四步,如果你用 Claude Code 开发 qwen-audio-agent 的适配层,参考 Claude Code 文档:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=phone_ai_claude_code_doc 。文档里有settings.json和 Anthropic 环境变量的完整说明,可以和上面的示例对照使用。
最后回到电话 AI 本身。qwen-audio-agent 解决的是语音和任务执行的割裂问题,Agent Presence 让 agent 在查资料、调工具、跑任务时依然保持对话。而 TaoToken 解决的是 LLM 请求出口的统一问题,让 Base URL、Key、Token 统计和错误排障都有固定入口。两者结合,电话机器人开发者才能把“不冷场”从体验口号变成可复现的日志指标:ASR final 到 LLM 首 token 多少毫秒,工具调用期间 TTS 有没有断流,每通电话消耗多少 Token,出现 401 或 429 时从哪里查。把这些指标跑通,电话 AI 才算真正具备了“在场感”。