语音智能体实战:级联式三明治架构搭建VoiceAgent全教程
在语音交互项目里,真正难的不是单独跑通一个 ASR 模型或接上一个对话大模型,而是把“听到声音、听懂语义、做出决策、返回语音”这条完整链路串起来。这篇教程围绕 VoiceAgent 的代码实战展开,核心是级联式三明治架构:上下两层是音频相关的输入输出能力,中间夹着大模型与智能体决策层。全文不依赖某个具体的云平台,以本地可复现的方式实现一个最小可运行的语音智能体,从环境准备、代码实现、运行验证到问题排查逐步展开。
适用人群是已经了解 Python 基础、想动手做语音 Agent 项目的开发者。学完后,你会得到一套包含录音、语音识别、大模型决策、工具调用、语音合成与播放的完整项目骨架,并且知道每一层失败时应该从哪里查起。
1. 先理解 VoiceAgent 的级联式三明治架构
很多教程一上来就贴代码,结果读者改完配置依然跑不通。原因是只关注了某个模型,没有理解整个系统如何分层、数据和错误如何流动。VoiceAgent 这个名字听起来复杂,拆开之后其实只有三层。
1.1 三明治架构的三层划分
所谓“三明治架构”,指的是语音交互系统由三层构成,中间一层是决策核心,上下两层是语音能力层。用一张通俗的图来记忆:两片面包是音频相关技术,中间的肉和蔬菜是大模型智能体。
第一层是感知输入层,负责把物理声音变成文本。这个阶段主要涉及麦克风录音、音频降噪、端点检测、语音识别 ASR。它的输出是一段用户文本。
第二层是智能决策层,也就是三明治中间的“馅”。它接收文本,通过大模型理解用户意图,判断是否需要调用工具,结合上下文生成最终回复文本。这一层是 VoiceAgent 的价值所在,因为单纯的语音识别和文本问答已经被解决得很成熟,难的是把“语音识别出来的话”变成“系统能执行的动作”。
第三层是执行输出层,负责把回复文本变成声音并播放出来,同时执行工具侧的真实动作。TTS 合成、音频播放、调用业务系统 API、下发控制指令都在这一层完成。
| 层位置 | 职责 | 典型技术组件 | 输出 |
|---|---|---|---|
| 上片:感知输入层 | 录音、降噪、识别 | 麦克风、faster-whisper、VAD | 用户文本 |
| 中间:智能决策层 | 意图理解、任务规划、工具调用 | 大模型、Agent Core、工具注册表 | 回复文本、动作指令 |
| 下片:执行输出层 | 语音合成、播放、业务执行 | edge-tts、pyttsx3、业务 API | 音频、动作结果 |
1.2 为什么用级联而不是端到端
端到端语音大模型是理想形态:声音进去,声音出来,中间不做文本中转。但现阶段在工程落地中,级联式架构仍然更可控。
级联的意思是处理流程严格自上而下单向传递:录音完成后做 ASR,ASR 输出文本交给大模型,大模型输出文本交给 TTS。每一级的输出是下一级的输入。
这种设计的几个实际好处:
- 每一层可以独立测试和替换。ASR 效果不好只需要换 ASR,大模型决策有问题只需要改提示词和工具定义,不需要重写整条链路。
- 每一层都能留日志。文本中间结果可以直接打印,出问题时很快能判断是识别错、理解错还是合成错。
- 工具调用更容易落地。大模型输出结构化 JSON 后,由 Agent 层负责解析和执行,不需要让语音层感知业务细节。
- 延迟虽然比端到端高,但对于大多数语音助手、工单录入、智能客服辅助场景,几百毫秒到一两秒的延迟是可接受的。
1.3 级联链路中的关键数据对象
级联式架构要稳定运行,必须定义几个统一的数据对象,避免每一层各写各的格式。
项目中主要使用三个数据对象:
-
AudioChunk:录音产生的音频数据,携带采样率、声道数、时长。 -
UserMessage:经过 ASR 后的用户文本,附带置信度、识别耗时。 -
AgentReply:智能体最终的回复文本,携带是否调用过工具、链路耗时、日志标识。
代码中不一定要定义成复杂的类,可以用简单字典,但字段名必须统一。下面给出一个建议的字段约定:
# 全链路统一的数据结构约定
audio_input = {
"data": bytes, # PCM 音频数据
"sample_rate": 16000, # 采样率
"duration": 3.2, # 音频时长,单位秒
}
user_message = {
"text": "今天深圳天气怎么样",
"asr_cost_ms": 1200,
"confidence": 0.98,
}
agent_reply = {
"text": "深圳今天多云,气温 28 度,出门建议带伞。",
"action": [
{"tool": "get_weather", "args": {"city": "深圳"}}
],
"agent_cost_ms": 800,
}
有了这套约定,pipeline 层只需要按照顺序把它们串起来,每一层内部怎么实现不会被外部感知。
注意:级联架构最怕“中间结果不可见”。建议从项目第一天就把每一层的中间文本和耗时打印到日志里,否则后期排查问题时只能靠猜。
2. 环境准备:先把依赖和模型准备到能跑通的状态
不少项目写完了代码却跑不起来,原因往往不是代码本身,而是环境版本不一致、模型没有下载、服务没有启动。这一节先把环境固定下来。
2.1 运行环境和依赖清单
推荐在 Linux 或 macOS 上运行,Windows 也可以,但需要注意麦克风设备和
sounddevice
的驱动兼容性。Python 版本建议 3.10 或 3.11,避免部分音频库在 3.12 下出现编译问题。
创建虚拟环境:
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
依赖文件
requirements.txt
如下:
sounddevice==0.4.6
numpy>=1.24,<2.0
faster-whisper>=1.0.0
openai>=1.30.0
edge-tts>=6.1.0
python-dotenv>=1.0.0
soundfile>=0.12.1
requests>=2.31.0
安装命令:
pip install -r requirements.txt
这里说明一下每个依赖的用途:
-
sounddevice:负责读麦克风音频和播放音频,底层基于 PortAudio。 -
faster-whisper:使用 CTranslate2 加速的 Whisper 版本,在 CPU 上也能跑。 -
openai:Python SDK 用来调用 OpenAI 兼容的大模型接口,本地 Ollama 和云端服务都兼容。 -
edge-tts:微软 Edge 的在线 TTS 服务,合成中文语音效果好,且不需要申请额外密钥。 -
python-dotenv:读取.env文件管理密钥和配置。
2.2 准备 ASR 模型
faster-whisper 在第一次调用时自动下载模型。为了减少运行时的不确定性,建议提前手动下载一次。
from faster_whisper import WhisperModel
# 下载并加载 small 模型到内存
model = WhisperModel("small", device="cpu", compute_type="int8")
print("ASR model loaded")
# 测试一段中文转写
segments, info = model.transcribe("test.wav", language="zh")
print(info.language, info.language_probability)
如果你本机没有
test.wav
文件,可以先随便录制一段音频再跑。
small
模型在 CPU 上的速度基本可用,显存足够时可以换
medium
,识别准确率更高,但速度会变慢。
2.3 准备大模型推理服务
在本地环境,推荐使用 Ollama 部署一个 OpenAI 兼容接口。这种方式不需要申请云上密钥,也不涉及网络策略问题,适合作为学习环境。
安装 Ollama 后,先拉取模型:
ollama pull qwen2.5:7b
启动服务:
ollama serve
确认接口可用:
curl http://localhost:11434/v1/models
看到返回模型列表,说明大模型服务已经就绪。Ollama 提供的 API 兼容 OpenAI 格式,因此项目中直接使用
OpenAI
客户端,把
base_url
指向
http://localhost:11434/v1
即可。
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # 本地服务不校验密钥,但字段不能为空
)
resp = client.chat.completions.create(
model="qwen2.5:7b",
messages=[{"role": "user", "content": "你好"}],
)
print(resp.choices[0].message.content)
如果你使用的是云端模型服务,把
base_url
和
api_key
换成你自己的配置即可。教程后面所有代码不依赖具体模型厂商。
2.4 确认麦克风和音频设备
录音环节最常遇到的问题是“程序跑起来但没有声音进去”。运行下面这段脚本,查看默认输入设备:
python -c "import sounddevice as sd; print(sd.query_devices()); print(sd.default.device)"
重点确认默认输入设备不是
None
,采样率支持 16000 Hz。如果系统有多个麦克风,需要在后续 Recorder 类中显式指定设备编号。
3. 项目结构与配置
环境准备好之后,开始搭建项目目录。代码量不大,但结构要清晰,方便后续扩展成多轮对话或接入新的 TTS 引擎。
3.1 目录结构
voice_agent/
├── .env.example
├── requirements.txt
├── config.py
├── pipeline.py
├── main.py
├── audio/
│ ├── __init__.py
│ ├── recorder.py
│ └── player.py
├── asr/
│ ├── __init__.py
│ └── transcriber.py
├── agent/
│ ├── __init__.py
│ ├── llm_client.py
│ ├── tools.py
│ └── core.py
├── tts/
│ ├── __init__.py
│ └── synthesizer.py
├── temp/
└── output/
temp
目录存放每次录音的临时音频文件,
output
目录存放合成后的语音文件,二者都可以在运行时自动创建。
3.2 全局配置 config.py
配置文件集中管理音频参数、模型参数和接口地址。把参数抽出来而不是散落在各个模块里,后期调参时只需要改一个文件。
# config.py
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent
TEMP_DIR = BASE_DIR / "temp"
OUTPUT_DIR = BASE_DIR / "output"
TEMP_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
# 音频采集参数
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 1024
DEFAULT_RECORD_SECONDS = 5
# ASR 参数
ASR_MODEL_SIZE = "small"
ASR_DEVICE = "cpu"
ASR_COMPUTE_TYPE = "int8"
# 大模型参数
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "http://localhost:11434/v1")
LLM_API_KEY = os.getenv("LLM_API_KEY", "ollama")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen2.5:7b")
LLM_TEMPERATURE = 0.2
LLM_TIMEOUT = 30
# TTS 参数
TTS_ENGINE = "edge"
TTS_VOICE = "zh-CN-XiaoxiaoNeural"
TTS_RATE = 0
TTS_VOLUME = 100
3.3 环境变量 .env.example
.env.example
提供给团队其他成员参考,实际运行时复制一份为
.env
再填入自己的值。
# .env.example
LLM_BASE_URL=http://localhost:11434/v1
LLM_API_KEY=ollama
LLM_MODEL=qwen2.5:7b
如果是远程大模型服务,只需要修改这些值,其余业务代码不用动。这正是配置外置化的价值,也是从学习环境过渡到生产环境的第一步。
4. 逐层实现代码
下面按三层架构逐层写代码。每段代码都会说明解决的问题和需要注意的关键点。
4.1 语音采集层:Recorder
Recorder 的核心任务是把麦克风输入保存为 WAV 文件,并且尽量控制时长。学习阶段不引入复杂的 VAD 模型,采用“固定时长 + 静音判断”的简化方案。
# audio/recorder.py
import time
import wave
import numpy as np
import sounddevice as sd
from config import SAMPLE_RATE, CHANNELS, CHUNK
class Recorder:
def __init__(self, sample_rate=SAMPLE_RATE, channels=CHANNELS):
self.sample_rate = sample_rate
self.channels = channels
def record(self, max_seconds=5, silence_threshold=0.02, silence_seconds=1.0):
"""录音:在 max_seconds 内如果持续静音超过 silence_seconds 则提前结束。
返回音频文件路径和实际录制时长。"""
frames = []
start = time.time()
last_voice_time = time.time()
def callback(indata, frame_count, time_info, status):
frames.append(indata.copy())
with sd.InputStream(
samplerate=self.sample_rate,
channels=self.channels,
dtype="float32",
callback=callback,
):
while time.time() - start < max_seconds:
if frames:
last_frame = frames[-1]
volume = np.sqrt(np.mean(last_frame ** 2))
if volume > silence_threshold:
last_voice_time = time.time()
if time.time() - last_voice_time > silence_seconds and frames:
break
time.sleep(0.05)
if not frames:
raise RuntimeError("没有采集到音频数据")
audio_data = np.concatenate(frames, axis=0)
duration = len(audio_data) / self.sample_rate
path = f"temp/record_{int(time.time() * 1000)}.wav"
with wave.open(path, "wb") as wf:
wf.setnchannels(self.channels)
wf.setsampwidth(2)
wf.setframerate(self.sample_rate)
wf.writeframes((audio_data * 32767).astype(np.int16).tobytes())
return path, duration
这个 Recorder 采用流式回调收集音频,在每次回调中计算音量,用音量判断用户是否还在说话。注意这里把 float32 转成了 int16 再写入 WAV,因为 faster-whisper 对 16 位 PCM 支持最稳定。
4.2 ASR 层:语音转文本
ASR 层封装 faster-whisper,外部只需要传入音频路径,返回文本结果。加了一层轻量容错,识别为空时直接抛出异常,让上层走兜底逻辑。
# asr/transcriber.py
from faster_whisper import WhisperModel
from config import ASR_MODEL_SIZE, ASR_DEVICE, ASR_COMPUTE_TYPE
class Transcriber:
def __init__(self, model_size=ASR_MODEL_SIZE, device=ASR_DEVICE, compute_type=ASR_COMPUTE_TYPE):
self.model = WhisperModel(model_size, device=device, compute_type=compute_type)
def transcribe(self, audio_path, language="zh"):
segments, info = self.model.transcribe(
audio_path,
language=language,
vad_filter=True,
beam_size=5,
)
text = "".join(seg.text for seg in segments).strip()
if not text:
raise RuntimeError("ASR 识别结果为空")
return text, info.language_probability
这里打开
vad_filter
后,Whisper 会自动过滤掉较长的静音片段,对录音首尾的环境噪声不敏感。
language="zh"
固定中文,避免中英文混输时识别混乱。
4.3 中间决策层:大模型客户端
大模型客户端是对 OpenAI SDK 的薄封装。为什么要再包一层?因为后续可能需要在请求里统一注入超时、重试、日志,放在一个类里管理最方便。
# agent/llm_client.py
from openai import OpenAI
from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, LLM_TIMEOUT
class LLMClient:
def __init__(self, base_url=LLM_BASE_URL, api_key=LLM_API_KEY, model=LLM_MODEL, timeout=LLM_TIMEOUT):
self.client = OpenAI(base_url=base_url, api_key=api_key, timeout=timeout)
self.model = model
def chat(self, messages, tools=None):
kwargs = {
"model": self.model,
"messages": messages,
"temperature": 0.2,
}
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
resp = self.client.chat.completions.create(**kwargs)
return resp.choices[0].message
temperature
设为 0.2 是因为智能体场景希望输出稳定,不希望同一个问题每次回复都不同。
4.4 工具注册与执行
工具是智能体真正落地的关键。这里定义两个示例工具:获取时间、查询天气。查询天气用模拟数据,实际项目中可以替换成真实天气 API。
# agent/tools.py
import datetime
import json
WEATHER_MOCK = {
"深圳": {"temperature": 28, "condition": "多云", "wind": "东南风3级"},
"广州": {"temperature": 30, "condition": "阵雨", "wind": "南风2级"},
"北京": {"temperature": 22, "condition": "晴", "wind": "北风3级"},
}
def get_current_time():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def get_weather(city: str):
if city in WEATHER_MOCK:
data = WEATHER_MOCK[city]
return json.dumps({"city": city, **data}, ensure_ascii=False)
return json.dumps({"city": city, "error": "暂不支持该城市"}, ensure_ascii=False)
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "获取当前日期和时间",
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的实时天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,例如 深圳"},
},
"required": ["city"],
},
},
},
]
def execute_tool(name: str, arguments: str):
args = json.loads(arguments) if arguments else {}
if name == "get_current_time":
return get_current_time()
if name == "get_weather":
return get_weather(args.get("city", ""))
return json.dumps({"error": f"unknown tool: {name}"})
工具的 schema 必须严格写清楚参数类型和必填项,否则大模型在生成 tool call 时容易漏参数。实际项目中工具函数应该做参数校验,不能直接相信大模型生成的 JSON。
4.5 智能体核心:Agent Core
Agent Core 的职责是维护消息列表、判断是否调用工具、把工具结果回填给模型。
# agent/core.py
import json
from agent.llm_client import LLMClient
from agent.tools import TOOL_SCHEMAS, execute_tool
SYSTEM_PROMPT = """你是 VoiceAgent 的决策核心,运行在一个语音助手系统中。
用户说话会先经过语音识别变成文本,你需要根据用户意图执行工具或直接回答。
规则:
1. 用户询问时间、天气等可以通过工具获取的信息时,必须先调用工具。
2. 工具返回结果后,用一句简洁自然的中文回答用户。
3. 不要编造工具返回结果。
4. 如果不能确定,请直接说明不知道。
"""
class AgentCore:
def __init__(self):
self.llm = LLMClient()
self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
def run(self, user_text: str) -> str:
self.messages.append({"role": "user", "content": user_text})
for _ in range(3):
msg = self.llm.chat(self.messages, tools=TOOL_SCHEMAS)
if msg.tool_calls:
self.messages.append(msg)
for tool_call in msg.tool_calls:
result = execute_tool(
tool_call.function.name,
tool_call.function.arguments,
)
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
else:
reply = msg.content or "抱歉,我没有理解你的意思。"
self.messages.append({"role": "assistant", "content": reply})
return reply
return "抱歉,多次尝试后仍然无法处理你的请求。"
循环上限设为 3,是为了防止大模型陷入“调用工具-再调用工具”的死循环。每一次循环要么执行工具,要么返回最终回复。多轮对话中消息列表会不断增长,后续可以考虑裁剪历史消息。
注意:
self.messages.append(msg)这一行不能漏。如果模型返回了 tool_calls,但没有把原始消息回填到对话列表,下一次请求就会丢失工具调用上下文,导致模型重新生成相同的工具调用,形成死循环。
4.6 TTS 层:文本转语音
TTS 层优先使用 edge-tts,原因是它合成的中文语音自然度高,不需要申请密钥,接口简单。
# tts/synthesizer.py
import edge_tts
import asyncio
from config import TTS_VOICE, TTS_RATE, TTS_VOLUME
class Synthesizer:
def __init__(self, voice=TTS_VOICE):
self.voice = voice
def synthesize(self, text: str, output_path: str) -> str:
rate = f"{TTS_RATE:+d}%"
volume = f"{TTS_VOLUME:+d}%"
async def _run():
communicate = edge_tts.Communicate(
text,
self.voice,
rate=rate,
volume=volume,
)
await communicate.save(output_path)
asyncio.run(_run())
return output_path
这里
asyncio.run
每次调用都会创建新的事件循环。在简单脚本中够用,但如果后续集成 Web 框架或流式服务,需要改成复用循环。
播放环节使用 sounddevice 播放 WAV 文件:
# audio/player.py
import soundfile as sf
import sounddevice as sd
def play_wav(path: str):
data, sr = sf.read(path)
sd.play(data, sr)
sd.wait()
4.7 串联 pipeline:把三层级联起来
pipeline 是整条链路的编排者,负责把录音、识别、决策、合成、播放串起来,同时打印每一层的耗时。
# pipeline.py
import time
from audio.recorder import Recorder
from audio.player import play_wav
from asr.transcriber import Transcriber
from agent.core import AgentCore
from tts.synthesizer import Synthesizer
from config import OUTPUT_DIR
class VoicePipeline:
def __init__(self):
self.recorder = Recorder()
self.transcriber = Transcriber()
self.agent = AgentCore()
self.synthesizer = Synthesizer()
def run_once(self, max_seconds=5):
t0 = time.time()
print("[VOICE] 开始录音,请说话...")
audio_path, duration = self.recorder.record(max_seconds)
print(f"[VOICE] 录音结束,实际时长 {duration:.1f} 秒")
t1 = time.time()
text, confidence = self.transcriber.transcribe(audio_path)
print(f"[ASR] 识别结果: {text} (置信度 {confidence:.2f})")
t2 = time.time()
reply = self.agent.run(text)
print(f"[AGENT] 最终回复: {reply}")
t3 = time.time()
output_path = str(OUTPUT_DIR / f"reply_{int(time.time() * 1000)}.mp3")
self.synthesizer.synthesize(reply, output_path)
print(f"[TTS] 语音合成完成: {output_path}")
t4 = time.time()
play_wav(output_path)
print(f"[STATS] 录音={t1 - t0:.2f}s ASR={t2 - t1:.2f}s "
f"AGENT={t3 - t2:.2f}s TTS+播放={t4 - t3:.2f}s")
4.8 入口 main.py
入口脚本采用回车触发的形式,每按一次回车开始一轮录音,输入
exit
退出。这样在终端里操作最直观。
# main.py
from pipeline import VoicePipeline
def main():
pipeline = VoicePipeline()
print("VoiceAgent 已启动。按回车开始录音,输入 exit 退出。")
while True:
cmd = input("> ").strip().lower()
if cmd == "exit":
break
try:
pipeline.run_once(max_seconds=5)
except Exception as e:
print(f"[ERROR] {e}")
if __name__ == "__main__":
main()
到这里,最小的 VoiceAgent 已经完整了。接下来需要实际运行并验证。
5. 运行与验证
代码写完后不要直接开始加功能,先把链路跑通,确认每一层都能输出预期结果。
5.1 端到端运行的预期日志
启动程序后,按回车并说一句话,例如“深圳今天天气怎么样”。正常情况下会看到类似下面的日志:
VoiceAgent 已启动。按回车开始录音,输入 exit 退出。
>
[VOICE] 开始录音,请说话...
[VOICE] 录音结束,实际时长 2.8 秒
[ASR] 识别结果: 深圳今天天气怎么样 (置信度 0.98)
[AGENT] 最终回复: 深圳今天多云,气温 28 度,东南风 3 级。
[TTS] 语音合成完成: output/reply_1750000000000.mp3
[STATS] 录音=0.80s ASR=1.50s AGENT=0.60s TTS+播放=0.70s
先看 ASR 一行的文本是否准确,再看 Agent 的回复是否合理,最后确认能听到合成的语音。三步都通过,说明链路正常。
5.2 用文本直接测试 Agent,减少变量
如果端到端跑不通,先绕过录音和 TTS,单独测试 Agent。这种方式能快速判断问题在哪一层。
# test_agent.py
from agent.core import AgentCore
agent = AgentCore()
while True:
text = input("请输入文本: ")
if text == "exit":
break
reply = agent.run(text)
print("回复:", reply)
用这个脚本测试以下输入:
- “现在几点了”
- “北京天气怎么样”
- “你好,你是谁”
- “帮我查一下上海的天气”
5.3 衡量链路耗时和各层开销
通过
[STATS]
行可以判断系统瓶颈:
| 层 | 耗时范围 | 主要影响因素 |
|---|---|---|
| 录音 | 0.5 - 1.5 秒 | 静音检测触发时机、用户语速 |
| ASR | 0.5 - 3 秒 | 模型大小、CPU 还是 GPU |
| Agent | 0.5 - 5 秒 | 模型推理速度、工具调用轮数 |
| TTS + 播放 | 0.3 - 1 秒 | 网络延迟(edge-tts 在线合成) |
如果 ASR 耗时过高,把
ASR_MODEL_SIZE
从
small
改成
tiny
测试效果。如果 Agent 耗时过高,换更小的模型,或者把
temperature
调低以减少采样波动。
6. 常见问题排查
级联链路出错时,遵循“从输入到输出”的顺序排查:先确认音频录上了,再确认识别文本,再确认大模型回复,最后看 TTS。不要一开始就去改模型参数。
6.1 ASR 层问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 安装 faster-whisper 失败 | Python 版本过高或缺少 C++ 编译环境 |
python --version
,查看报错日志
| 使用 Python 3.10 或 3.11,重新创建虚拟环境 |
| 识别结果为空 | 麦克风没声音、录音太短、说话音量低 | 打印录音文件时长和音量 | 延长录音时间,提高说话音量,检查麦克风设备 |
| 中文识别出英文或乱码 |
没有指定
language="zh"
| 查看 transcribe 调用参数 |
显式传入
language="zh"
|
| CPU 推理非常慢 | 模型过大或未启用 int8 |
查看
compute_type
|
改用
small
+
int8
,或换 GPU
|
6.2 Agent 层问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 连接大模型失败 | Ollama 服务未启动或端口不对 |
curl http://localhost:11434/v1/models
|
执行
ollama serve
|
| 提示 model not found | 模型名字与本地不一致 |
ollama list
查看模型名
|
修改
LLM_MODEL
|
| 工具调用参数一直是空 | 工具 schema 缺少必填字段或描述不清晰 |
打印
tool_call.function.arguments
|
在 schema 中把必填字段标注为
required
|
| 回复内容与工具结果无关 | 系统提示词约束不足 | 查看完整 messages 日志 | 在提示词中明确“必须根据工具返回结果回答” |
| 循环调用工具不退出 | 消息列表缺少 assistant 原始回填 |
检查
self.messages.append(msg)
| 调用工具后立即回填 |
最危险的问题是“吞异常”。不要在 Agent 层写一个空
except
然后返回固定文案,那会让问题被掩盖。至少要打印
traceback
,或者把异常信息组装到回复里。
6.3 TTS 层问题
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| edge-tts 合成失败 | 网络无法访问在线服务 | 检查网络、查看报错信息 | 切换到 pyttsx3 离线引擎 |
| 合成成功但播放无声音 | 播放设备错误或音量过低 | 用播放器直接打开 mp3 文件 | 检查系统输出设备 |
| 播放报错 samplerate mismatch | WAV 采样率与播放设备不匹配 | 打印音频文件参数 | 统一用 16000 Hz 或让播放器自动重采样 |
6.4 麦克风问题
录音没有声音前,先确认系统级麦克风是可用的。可以用系统录音工具录一段,再播放确认。然后在 Python 里运行:
import sounddevice as sd
print(sd.query_devices())
print(sd.default.device)
如果默认输入设备不对,在
Recorder
构造时传入
device
参数,或者直接在
sounddevice.InputStream
中指定
device
。
排查顺序建议:系统麦克风 → Python 设备识别 → 录音文件波形 → 静音阈值 → ASR 输入。不要跳过任何一步,否则容易在错误层级浪费时间。
7. 生产环境最佳实践与扩展方向
学习环境跑通只是开始。要把 VoiceAgent 应用到真实业务中,还需要考虑多轮对话、工具真实化、延迟优化、可观测性和安全边界。
7.1 从单轮到多轮对话
当前 AgentCore 的消息列表只存在于一次进程内,且没有做历史裁剪。生产环境要支持多轮,需要:
-
在消息列表中加入
session_id维度,按照会话区分上下文。 - 设置最大历史轮数,例如保留最近 10 轮,超出后丢弃最老的非系统消息。
- 对话持久化到 Redis 或数据库,服务重启后可以恢复上下文。
- 增加敏感信息过滤,避免把身份证号、手机号等明文发送给模型服务。
7.2 把模拟工具升级为真实系统调用
工具函数在前端表现为一个 Python 函数,但在生产环境中它可能就是一次 API 调用、一条数据库查询或一个控制指令。升级时要重点考虑:
- 工具调用超时和重试策略,避免大模型等待业务接口太久。
- 工具权限校验,不是每个用户都能调用所有工具。
- 返回值长度控制,工具返回内容过多会占用大模型上下文。
- 幂等性,防止语音误触发导致重复下单。
7.3 延迟优化和流式交互
当前实现是完整的“录音结束后再处理”,用户体验是说完话要等两三秒。优化方向包括:
- 使用 VAD 检测说话起始点,用户开口就开始识别,而不是等整句结束。
- ASR 开启流式识别,边说话边出文本。
- TTS 改为流式播放,第一段语音合成完成后立即播放,不用等整句合成结束。
- 如果使用远程大模型,可以启用流式输出,首 token 延迟会明显缩短。
这些优化要以“链路可观测”为前提。建议全链路加入
trace_id
,每次请求都生成一个唯一 ID,日志中带上这个 ID,这样每一层耗时一目了然。
7.4 安全与可观测性
语音智能体涉及音频数据和个人信息,生产环境不能按学习环境的方式处理。
- 音频文件不能长期保留,处理完应及时删除或脱敏保存。
- 大模型提示词要约束“不能泄露系统提示词”“不能执行非授权工具”。
- 对工具调用结果做 JSON Schema 校验,遇到非法结构要重试或降级,而不是直接拼接到回复里。
- 记录每个用户会话的识别文本和工具调用记录,便于审计和问题追溯。
- 设置全链路超时,避免录音、ASR、Agent、TTS 某一步卡死后整个服务不可用。
7.5 学习路线建议
如果你是新手,建议按下面这个顺序推进:
- 跑通本文的单轮闭环,不要加额外功能。
- 替换 ASR 模型大小,观察识别准确率和耗时变化。
- 新增一个自定义工具,例如“查询今日待办”“计算器”,理解工具 schema 和大模型输出的关系。
- 加入多轮对话和上下文裁剪。
- 把 TTS 替换成离线引擎,对比在线和离线的效果差异。
- 引入流式识别和流式合成,优化交互体验。
每一步都做一次“日志验证”,而不是只验证“能启动”。
本文的核心技术判断是:VoiceAgent 的落地点不是大模型本身,而是级联链路里每一层之间的数据契约和异常处理机制。把录音、ASR、Agent、TTS 解耦成独立模块,每层都能独立测试和替换,整个项目才具备从 demo 演进到生产系统的可能。下一步,你可以先加一个真实业务工具,再逐步引入流式交互,让语音智能体真正解决实际问题。
更多推荐

所有评论(0)