1. 先定位“干等”:qwen-audio-agent 的 LLM 调用为什么会让对话断流
qwen-audio-agent 跑通 ASR 和 TTS 后,最容易踩的坑不是麦克风,也不是声卡,而是 LLM 接入凭证仍指向单点端点:日志里llm request pending一直不返回,TTS 队列空转,用户说完一句就听到安静。此时先到 TaoToken 官网 创建一个可用的 LLM Key,再把 Base URL 设为https://taotoken.net/api,通常是最短路径。
qwen-audio-agent 本身是一个 Node.js 生态里的实时语音运行时,不是开箱即用的成品语音助手。它把语音识别、大模型推理、语音合成串成一条流,并强调 Agent Presence:让语音流和任务流尽量并行。但这里有一个容易被忽略的前提——LLM 这一跳必须足够稳定,而且必须支持流式增量返回。如果 LLM 凭证指向了临时额度、限流端点、或者不可达的 base URL,实时运行时就退化成“回合制”:ASR 已经出 final,LLM 没有 delta,TTS 没有文本入队,前端还停在 listening 状态,用户感受到的就是“它消失了”。
本文按语音交互改造工程师的视角,记录一次把 qwen-audio-agent 的 LLM 接入从默认端点切到 TaoToken 的过程。可复现产出有三块:改造前后的对话日志、Token 消耗对照、以及一份可以直接照做的接入说明。先给结论:只换 LLM 供应商不够,还要让流式、工具调用、TTS 三者解耦;否则你只是把等待从一个端点搬到另一个端点。
创建 Key 的入口在 TaoToken 官网控制台,建议先准备好YOUR_API_KEY占位环境变量,再动 qwen-audio-agent 的配置。第二次核对入口可以走这里:TaoToken 官网。下面先看改造前的真实日志形态。
2. 改造前的对话日志:一句“查天气”如何变成 8 秒沉默
先给一条本地复现日志。测试语句是“帮我看下明天杭州天气,然后再提醒我带伞”。qwen-audio-agent 的 ASR 很快出了 final,但 LLM 和 TTS 链路明显断开了。
[ASR] partial: 帮我看下明天杭州天气 [ASR] partial: 然后再提醒我带伞 [ASR] final: 帮我看下明天杭州天气,然后再提醒我带伞 [LLM] request start model=qwen-plus endpoint=default [LLM] waiting... 1.2s [LLM] waiting... 2.5s [TTS] queue empty [TTS] silence [LLM] tool_call: weather(city=hangzhou, date=tomorrow) [LLM] waiting... 6.8s [LLM] tool_result: 小雨 [LLM] request start model=qwen-plus endpoint=default [LLM] waiting... 8.1s [TTS] text ready: 明天杭州小雨,记得带伞 [Agent] idle这段日志暴露了三个断裂点。
第一,LLM 首包没有流式增量。qwen-audio-agent 期待的是一边生成一边给 TTS 喂文本,但默认端点在超时或限流后,走了完整的非流式请求。结果是 TTS 队列为空,用户只能等。
第二,工具调用把语音流阻塞了。agent 查到天气之前,没有任何可说的内容;查到之后,又发起第二次 LLM 请求做总结。两次请求之间没有“我在查”的过渡语,语音链路自然断掉。
第三,TTS 等的是完整句子,而不是增量片段。实时语音运行时需要 TTS 能处理短文本、可打断、可排队。如果配置里stream开关没打开,或者 LLM 客户端不支持流式,TTS 就只能等完整回复。
这里要强调:这不是 qwen-audio-agent 的“缺陷”。它提供的是运行时骨架,模型接入需要你自己填。很多开发者第一次跑通时,LLM 端用的是临时 Key 或默认单点端点,一旦并发上来或者工具调用变多,就会退化成上面的日志。改造目标不是换一个模型名字,而是把 LLM 请求收敛到统一网关,并确保流式、工具调用、TTS 队列三件事能并行推进。
3. 接入 TaoToken 的 LLM Key:把 Base URL 收敛到 https://taotoken.net/api
接入动作本身不复杂,关键是不要只改 Key,不改 Base URL;也不要只改环境变量,不改客户端初始化。建议按下面顺序做。
第一步,在 TaoToken 官网创建 API Key。入口在控制台的 API Keys 页面,也可以先访问官网了解模型与套餐:TaoToken 官网。拿到的 Key 先写入本地环境变量,不要硬编码进仓库。
# .env.local TAOTOKEN_API_KEY=YOUR_API_KEY TAOTOKEN_BASE_URL=https://taotoken.net/api LLM_MODEL=qwen-plus第二步,在 qwen-audio-agent 的 LLM 客户端里把 baseURL 指向 TaoToken。下面是一段 OpenAI 兼容客户端的初始化示例。不同版本的 qwen-audio-agent 可能把 LLM 调用封装在llm、model或brain模块里,你只需要找到创建 client 的位置,替换 baseURL 和 apiKey 即可。
// llm-client.js import OpenAI from "openai"; export const llm = new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY || "YOUR_API_KEY", baseURL: process.env.TAOTOKEN_BASE_URL || "https://taotoken.net/api", }); export async function streamChat(messages, onDelta) { const stream = await llm.chat.completions.create({ model: process.env.LLM_MODEL || "qwen-plus", messages, stream: true, stream_options: { include_usage: true }, }); for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content; if (delta) onDelta(delta); } }第三步,把 qwen-audio-agent 的 agent 逻辑改成“先说话,再干活”。工具调用前先让 LLM 产出一句过渡语,工具调用后再把结果拼回上下文。例如:
async function handleUserUtterance(text, speak) { const messages = [ { role: "system", content: "你是语音助手。工具调用前先简短告知用户你在查,不要把话说完就沉默。" }, { role: "user", content: text }, ]; let full = ""; await streamChat(messages, async (delta) => { full += delta; await speak(delta); // TTS 流式消费 }); const toolCall = detectToolCall(full); if (toolCall) { await speak("我先查一下,马上回来。"); const result = await runTool(toolCall); messages.push({ role: "assistant", content: full }); messages.push({ role: "tool", content: JSON.stringify(result) }); await streamChat(messages, async (delta) => { await speak(delta); }); } }第四步,验证 LLM 链路是否真的走 TaoToken。不要只看应用日志,直接用 curl 在本地终端测一次流式返回。下面的命令请在本地执行,不要对生产库或 Oracle 直连环境操作。
curl -N https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-plus", "stream": true, "messages": [ {"role": "user", "content": "用一句话说明什么是语音 agent 的在场感"} ] }'如果 curl 能看到data: {...}的增量块,说明 Base URL 和 Key 都对了。如果返回 401,检查YOUR_API_KEY是否复制完整;如果 404,检查 Base URL 是否误写成带/v1或带 UTM 的地址;如果一直卡住,检查本机网络和客户端超时。Base URL 固定用https://taotoken.net/api,不要加 UTM 参数。
4. 改造后的对话日志:流式增量、工具调用、TTS 并行
把 LLM 切到 TaoToken,并打开stream: true后,同一句测试语的日志会变成下面这样。
[ASR] partial: 帮我看下明天杭州天气 [LLM] stream delta: 好的,我先查一下杭州明天的天气 [TTS] speak: 好的,我先查一下杭州明天的天气 [LLM] tool_call: weather(city=hangzhou, date=tomorrow) [TTS] speak: 同时把带伞提醒准备好 [LLM] stream delta: 同时把带伞提醒准备好 [LLM] tool_result: 小雨 [LLM] stream delta: 明天杭州小雨,已经帮你加了带伞提醒。 [TTS] speak: 明天杭州小雨,已经帮你加了带伞提醒。 [Agent] idle对比改造前,变化不在“模型更聪明”,而在时间线。改造前,用户说完后先等 2.5 秒,再等 6.8 秒,再等 8.1 秒;改造后,ASR final 后很快就有 LLM delta 进入 TTS,用户在 1 秒左右就能听到“好的,我先查一下”。工具调用仍然要花时间,但语音流没有断,因为 TTS 队列里已经有过渡语和后续增量。
这就是 qwen-audio-agent 强调的“持续在场”在工程上的落点:LLM 流式增量、工具调用异步化、TTS 可排队可打断。三者缺一不可。只开流式但不改工具调用,工具结果回来前还是会沉默;只改工具调用但不开流式,TTS 还是会等完整句子。
验证改造效果时,建议固定三条语料:
- 纯闲聊:“你好,介绍一下你能做什么。”
- 单工具调用:“明天杭州天气怎么样?”
- 多步任务:“查一下明天杭州天气,如果下雨就提醒我带伞,再帮我把提醒加到明天早上八点。”
分别记录首字延迟、工具调用次数、TTS 首次发声时间、总完成时间。不要只看“有没有回复”,要看“回复是不是分段来的”。
5. Token 消耗对照:改造前后不是简单翻倍
很多人担心“边聊边干”会让 Token 消耗暴涨。实际对照要看请求结构和输出长度,不是只看总字数。下面是一组本地测试用例的对照格式,数据来自单条固定语料的多次运行,仅用于说明统计方法,不代表平台承诺。
| 指标 | 改造前:回合制 | 改造后:流式并行 |
|---|---|---|
| LLM 请求次数 | 2 次(工具前 + 工具后) | 1 次主请求 + 1 次工具后续写 |
| 输入 Token | 3.2k | 3.0k |
| 输出 Token | 420 | 510 |
| 首字延迟 | 2.5s | 0.9s |
| TTS 首次发声 | 8.1s | 1.1s |
| 总完成时间 | 8.6s | 4.2s |
| 用户感知 | 说完就沉默 | 查资料时仍有回应 |
改造后输出 Token 可能略增,因为 agent 会额外说“我先查一下”这类过渡语;但请求次数不一定增加,而且首字延迟和总完成时间明显下降。真正要防的是重复请求:如果工具调用后把完整历史再发一遍,输入 Token 会膨胀;如果流式没开,TTS 等完整句子,输出 Token 没变但用户体验更差。
统计 Token 时,建议在 LLM 客户端里打开 usage 回传,并在每轮对话结束后打印一行结构化日志。下面是一个 Node.js 侧的统计片段。
function logUsage(round, usage) { console.log(JSON.stringify({ round, prompt_tokens: usage?.prompt_tokens, completion_tokens: usage?.completion_tokens, total_tokens: usage?.total_tokens, ts: Date.now(), })); }跑 10 轮固定语料后,把日志导出成 CSV,再对照 Token 消耗和延迟。TaoToken 官网也提供了模型与套餐说明,需要核对额度时可以看:TaoToken 官网。注意,统计时不要在生产库上压测,也不要用真实用户语音做未经脱敏的批量跑批。
6. 同项目其它工具怎么配:Claude Code、Codex、CC Switch
qwen-audio-agent 只是语音链路里的一个环节。做语音交互改造时,往往还会同时用 Claude Code 写代码、用 Codex 跑终端任务。这些工具的配置不要混用,尤其不要把ANTHROPIC_*套到 Codex 上。
Claude Code 用settings.json,核心是ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }Codex 用config.toml,不要写ANTHROPIC_*。下面是一个基于 OpenAI 兼容 provider 的配置示例。
model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"CC Switch 三件套可以理解为:Claude Code 的settings.json、Codex 的config.toml、以及通用环境变量.env。在 CC Switch 里切换配置时,只需要换这三处,不要把 Claude Code 的ANTHROPIC_*复制到 Codex 的config.toml,否则会出现认证头不匹配或 401。qwen-audio-agent 的 LLM 客户端则读取.env里的TAOTOKEN_API_KEY和TAOTOKEN_BASE_URL,和上面两套配置共用同一个 Key,但变量名各自独立。
7. 可复现接入说明与排错清单
把整个改造压缩成一份可复现清单:
- 在 TaoToken 官网创建 API Key,写入本地环境变量
TAOTOKEN_API_KEY=YOUR_API_KEY。 - 把 qwen-audio-agent 的 LLM 客户端
baseURL改为https://taotoken.net/api。 - 打开
stream: true,并确认 TTS 能消费增量文本。 - 把工具调用改成“先告知、再执行、后汇报”,不要让语音流阻塞。
- 用 curl 在本地终端验证流式返回,再跑三条固定语料。
- 记录改造前后日志、首字延迟、TTS 首次发声、Token 消耗。
- 如果同时用 Claude Code、Codex,分别配置,不要混用变量。
常见排错:
- 401:Key 无效或 Bearer 头缺失。检查
YOUR_API_KEY是否完整,是否多了空格。 - 404:Base URL 写错。确认是
https://taotoken.net/api,不要加 UTM,也不要随意加/v1。 - 流式不生效:检查
stream: true是否传给 SDK,以及服务端是否返回text/event-stream。 - TTS 不触发:检查 TTS 是否等待完整句子;改成按 delta 入队,并支持打断。
- 工具调用后断流:在工具调用前插入过渡语,工具结果回来后继续流式续写。
- Token 暴涨:检查是否把完整历史重复发送,是否关闭了不必要的长上下文。
所有验证命令请在本地终端执行,不要直连生产库或 Oracle 实例。语音日志如果包含真实用户内容,先脱敏再分析。
8. 文末 CTA:从模型对话到 Coding Plan,再到 Key 与 Claude Code 文档
如果你正在改造 qwen-audio-agent,建议先跑通 LLM 流式链路,再优化工具调用和 TTS 队列。可以按下面路径继续:
先体验模型对话,确认 TaoToken 的返回格式和流式行为:
模型对话如果语音 agent 之外还要接 Coding 任务,可以看 Coding Plan:
Coding Plan准备好
YOUR_API_KEY后,在控制台创建和管理 Key:
API Keys如果你同时使用 Claude Code,按文档配置
settings.json和ANTHROPIC_*:
Claude Code 文档
把 LLM Key 和 Base URL 收敛之后,qwen-audio-agent 的“干等”就不再是必然结果。真正决定体验的,是流式增量能不能及时进入 TTS,工具调用能不能异步推进,以及对话流能不能在任务执行期间继续保持。改完这一层,你才会听到它从“沉默干活”变成“边查边说”。