「智能体不说话」是语音开发里最难查的一类问题。可能是 RTC 没连上、ASR 没出字、LLM 超时、TTS 挂了,也可能什么都正常,只是客户端忘了播放远端音轨——五种原因,用户侧的表现一模一样,都是一片安静。
解决办法只有一个:把状态、指标、错误、字幕放到同一条时间线上,一眼看出断在哪。用 agora-agent-client-toolkit@2.9.0 就能做到。
> 开始之前 凭证和客户端都沿用《Next.js 搭建浏览器端语音智能体》,本篇只加事件监听。
一. 事件通道与准备工作
RTC:音频 + 兼容的字幕数据流
RTM:智能体状态、管线指标、模块错误、消息回执
↓
AgoraVoiceAI 2.9.0
↓
事件时间线 / 监控 / 告警
客户端需要 RTC 4.24.6 和 RTM 2.2.4,并且已经用同一个用户 UID/Token 加入了目标频道。
本篇不需要新增凭证,沿用 《Next.js 搭建浏览器端语音智能体》的那几个就行。事件开关是创建智能体时的请求字段,不是密钥。
二. 服务端要开的字段
agent = Agent(
client=client,
advanced_features={"enable_rtm": True},
parameters={
"audio_scenario": "chorus",
"data_channel": "rtm",
"enable_metrics": True,
"enable_error_message": True,
},
).with_stt(FengmingSTT()).with_llm(llm).with_tts(tts)
这几项少一个,客户端就可能只收到部分事件。别用「能听到声音」来代替事件通道的验收。声音正常不代表指标和错误事件通了,等到线上出问题才发现没数据就晚了。
三. 最小客户端代码
import {
AgoraVoiceAI, AgoraVoiceAIEvents, TranscriptHelperMode,
type AgentMetric,
} from "agora-agent-client-toolkit";
type TimelineEvent = {
at: number;
kind: "state" | "metric" | "error" | "transcript";
detail: string;
};
const timeline: TimelineEvent[] = [];
const record = (event: Omit<TimelineEvent, "at">) => {
timeline.push({ at: Date.now(), ...event });
if (timeline.length > 200) timeline.shift();
};
export async function observeAgent(rtcClient: unknown, rtmClient: unknown, channel: string) {
const ai = await AgoraVoiceAI.init({
rtcEngine: rtcClient as never,
rtmConfig: { rtmEngine: rtmClient as never },
renderMode: TranscriptHelperMode.TEXT,
enableLog: true,
});
ai.on(AgoraVoiceAIEvents.TRANSCRIPT_UPDATED, items => {
const latest = [...items].reverse().find(item => item.text);
if (latest) record({ kind: "transcript", detail: `${latest.uid}: ${latest.text}` });
});
ai.on(AgoraVoiceAIEvents.AGENT_STATE_CHANGED, (_, event) => {
record({ kind: "state", detail: String(event.state) });
});
ai.on(AgoraVoiceAIEvents.AGENT_METRICS, (_, metric: AgentMetric) => {
record({ kind: "metric", detail: `${metric.type}/${metric.name}: ${metric.value}ms` });
});
ai.on(AgoraVoiceAIEvents.MESSAGE_ERROR, (uid, error) => {
record({ kind: "error", detail: `${uid}/message/${error.code}: ${error.message}` });
});
ai.on(AgoraVoiceAIEvents.AGENT_ERROR, (uid, error) => {
record({ kind: "error", detail: `${uid}/${error.type}/${error.code}: ${error.message}` });
});
ai.subscribeMessage(channel);
return { ai, timeline };
}
页面卸载或结束对话时,这两行必须执行:
ai.unsubscribe();
ai.destroy();
漏了它们,React Strict Mode 重新挂载后事件会重复注册,表现为一条字幕出现两次、日志重复上报。这类问题在开发环境很容易被当成「玄学」,其实就是实例没销毁。
四. 上生产的观测建议
- 状态转换记全
listening → thinking → speaking,每段停留多久也记下来。哪一段异常变长,问题就在哪一环。 - 延迟按会话和模块统计 p50/p95/p99。只看平均值会漏掉真正影响体验的长尾。
- 错误保留 module/type/code,用户文本、Token、密钥则要脱敏。
- 字幕要区分进行中、已完成和被打断的 turn。别把每条中间字幕都当成一次独立的用户轮次,会把统计数据算飞。
- 服务端保存 agentId/channel 到业务 sessionId 的映射,顺着用户投诉就能查到具体会话。但 Certificate 和模型凭据不能给前端。
五. 运行与验收
npm run typecheck
npm run build
npm run dev
完成两轮对话,并在第二轮主动打断。事件面板上必须同时出现这四类:
- 至少一次智能体状态变化。
- 至少一条带数值的管线指标。
- 用户和智能体双方的字幕。
- 一条人为诱发的错误。做法:在**独立的测试进程**里临时把 MiniMax Key 改成无效值,确认错误面板能拿到 module/code/message。别去动生产环境或
.env.local里的真实凭证。
面板上你会看到状态在持续变化、tts/ttfb 这类指标带着真实数值、双方字幕不断刷新;错误测试进程那边则会收到一条 tts/1000 事件,module、code、message 三项齐全。
六. 故障排查
- 只有字幕,没有状态和指标:检查 RTM 是否在
AgoraVoiceAI.init()之前完成登录,以及服务端的data_channel、enable_metrics有没有配。 - 同一事件重复出现:多半是每次重连都新建了 Toolkit,却没销毁旧实例。
- 错误事件收不到:检查
enable_error_message=true,并且MESSAGE_ERROR和AGENT_ERROR两个事件都要监听。 - 指标时间看着不对:统一用毫秒时间戳,别把事件上报时间当成模块耗时,这两个差得很远。
七. 下一步
客户端这条线通了,还有一条页面关掉也不能丢的线——服务端回调。
《语音智能体的服务端 Webhook:验签、幂等与订阅》