网页加载两秒,用户会等;语音对话沉默两秒,用户会以为断线了,然后开始「喂?喂?」。
这是语音场景特有的问题,解法是两个小配置:填充语在模型思考期间说一句「稍等,我看一下」,告诉用户系统还在干活;优雅退出让智能体把告别说完再离开频道,而不是话说一半突然消失。
两个功能加起来代码没几行,但对「像不像人」的影响,比换个更贵的模型明显得多。
配好之后你会注意到:模型思考的那一两秒不再是空白,而挂断前那句告别也能完整说完。
> 开始之前 浏览器端直接用《Next.js 搭建浏览器端语音智能体》里的客户端,先备好它。
一. 架构与准备工作
用户说完 ─→ 凤鸣 ASR ─→ DeepSeek 生成
└──── 等待期间播放中文填充语
DeepSeek 回复 ─→ MiniMax TTS ─→ 正常答案
停止会话 ─→ 等待告别播报 ─→ 智能体离开频道
二. 环境变量
AGORA_APP_ID=REPLACE_WITH_SHENGWANG_APP_ID
AGORA_APP_CERTIFICATE=REPLACE_WITH_PRIMARY_CERTIFICATE
DEEPSEEK_API_KEY=REPLACE_WITH_DEEPSEEK_KEY
MINIMAX_API_KEY=REPLACE_WITH_MINIMAX_KEY
FILLER_PHRASES=这个问题很好,我想一下。|好的,稍等我一下。|明白,我正在整理。
AGENT_FAREWELL=感谢你的交流,我们下次再见。
GRACEFUL_EXIT_TIMEOUT_SECONDS=8
三. 配置中文填充语与退出
import os
DEFAULT_PHRASES = [
"这个问题很好,我想一下。",
"好的,稍等我一下。",
"明白,我正在整理。",
"让我确认一下。",
]
def phrases_from_env() -> list[str]:
raw = os.getenv("FILLER_PHRASES", "")
phrases = [item.strip() for item in raw.split("|") if item.strip()]
return phrases or DEFAULT_PHRASES
def filler_words() -> dict:
return {
"enable": True,
"content": {
"mode": "static",
"static_config": {
"phrases": phrases_from_env(),
"selection_rule": "shuffle",
},
},
}
def farewell_config() -> dict:
timeout = int(os.getenv("GRACEFUL_EXIT_TIMEOUT_SECONDS", "8"))
return {
"graceful_enabled": True,
"graceful_timeout_seconds": max(1, min(timeout, 30)),
}
当前版本用的是静态填充语,通过 shuffle 避免每次都说同一句——同一句话听三遍,用户就会开始注意到这是个机器人。
填充语要短、要自然,而且绝不能伪造进度。「稍等一下」「我想想」都可以,但「已经查到了」「订单已确认」这种绝对不行——话说出口的时候你根本还没查,用户后面听到不一致的答案会直接失去信任。
四. 接入智能体
import os
from agora_agent import Area, AsyncAgora
from agora_agent.agentkit import Agent
from agora_agent.cn import DeepSeekLLM, FengmingSTT, MiniMaxTTS
client = AsyncAgora(
area=Area.CN,
app_id=os.environ["AGORA_APP_ID"],
app_certificate=os.environ["AGORA_APP_CERTIFICATE"],
)
farewell = os.getenv("AGENT_FAREWELL", "感谢你的交流,我们下次再见。")
llm = DeepSeekLLM(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/chat/completions",
model="deepseek-v4-flash",
system_messages=[{"role": "system", "content": (
"你是简洁自然的中文语音助手。回答控制在两句话内。"
f"用户告别时请回复:{farewell}"
)}],
greeting_message="你好!今天有什么可以帮你?",
failure_message="抱歉,服务暂时繁忙,请稍后再试。",
max_history=20,
max_tokens=256,
params={"thinking": {"type": "disabled"}},
)
tts = MiniMaxTTS(
key=os.environ["MINIMAX_API_KEY"], model="speech-01-turbo",
voice_id="female-shaonv", sample_rate=16000, language_boost="Chinese",
)
agent = (Agent(
client=client,
filler_words=filler_words(),
turn_detection={"language": "zh-CN"},
advanced_features={"enable_rtm": True},
parameters={
"audio_scenario": "chorus",
"data_channel": "rtm",
"enable_metrics": True,
"enable_error_message": True,
"farewell_config": farewell_config(),
},
).with_stt(FengmingSTT()).with_llm(llm).with_tts(tts))
正常用 session.start() 和 session.stop() 即可。
客户端这边要配合一下。停止时别急着销毁浏览器的 RTC/RTM,要等后端 /stopAgent 返回之后再释放。抢在前面关掉,告别语就播不完——那优雅退出也就白配了。
五. 运行与验证
cd code/filler-exit
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env.local
uvicorn app:app --host 0.0.0.0 --port 8000
用 《Next.js 搭建浏览器端语音智能体》的客户端连上这个后端,问一个需要模型组织一下的问题,比如「用三点总结一次线上活动的复盘方法」。
预期表现:
- 模型首包到达前,可能会播一条中文填充语。
- 填充语不会和正式回答重复或叠在一起。
- 你说「再见」时,智能体会说告别语。
- 点结束后,服务端会等优雅退出走完再释放会话。
六. 故障排查
- 没听到填充语:模型响应快的时候系统会判断不需要播,这不算失败。想验证触发逻辑,用一个故意加延迟的测试服务。
- 每轮都播,听着烦:短问题场景下少用填充语,单条控制在一秒左右,并监控实际触发率。填充语的价值在于「偶尔出现」,一直出现就变成噪音了。
- 告别语被截断:客户端别在后端 stop 返回前就关掉远端音轨;也可以适当调高 graceful timeout,但别超过业务能接受的等待时间。
七. 下一步
填充语和打断是同一件事的两面:一个填空白,一个管抢话。
《语音智能体打断实战:VAD、关键词与不可打断》