先问大家一个问题:你在公司里做语音助手类项目时,是不是经常碰到下面这两种情况?
- 语音识别出来的文字乱七八糟,带口音、带噪声、带专业名词,直接把后面的大模型带偏。
- 大模型回答得倒是挺好,但语音合成出来像机器人念稿,延迟还高,整个对话体验非常“出戏”。
说白了,语音智能助手并不是“把 STT、LLM、TTS 三个模型拼在一起”就能跑通的。三个独立模型都很强,但串联起来之后,各种问题都来了:上下文怎么传?打断怎么办?角色设定放哪里?回答太长要不要截断?TTS 遇到公式、英文、数字怎么读?
这篇文章我想结合今年实际落地企业级 Voice Agent 的经验,梳理一套能直接拿去用的级联式三明治架构,把STT → Agent/LLM → TTS三段链路拆开讲清楚,并给出可运行的实战代码。内容会覆盖核心概念、环境准备、代码实现、性能瓶颈分析和常见排错思路。无论你是刚接触语音大模型应用,还是已经有业务在跑,这篇文章都值得收藏备用。
1. 什么是 Voice Agent,为什么需要“三明治架构”
1.1 从 Chatbot 到 Voice Agent
先做一个简单的名词区分。
- Chatbot:只能处理文字输入输出,用户打字,它回复文字。
- Voice Agent:能够直接接收语音、理解意图、生成回复,并把回复合成为语音,形成完整的“说话式对话闭环”。
Voice Agent 的核心链路其实只有三条:
- 把语音转成文字(STT,Speech-to-Text)
- 把文字交给大模型或 Agent 框架处理(LLM / Agent)
- 把回复文字转成语音(TTS,Text-to-Speech)
这条链路看起来简单,但实际工程落地时,远比单个模型评测要复杂得多。因为语音交互是实时、双向、有噪声、有打断的,任何一环出问题,整个体验都会崩。
1.2 为什么叫“级联式三明治架构”
所谓“级联”(Cascade),指的是数据流严格按照STT → LLM/Agent → TTS的顺序单向传递,每一级的输出是下一级的输入。
所谓“三明治”,可以从两个层面理解:
- 物理分层:STT 和 TTS 是两片“面包”,负责语音与文本之间的转换;LLM 是中间的“肉”,负责语义理解和决策。
- 逻辑分层:外层是音频处理,内层是文本处理,LLM 不直接接触音频,避免多模态模型带来的复杂部署和延迟问题。
这种架构的优势在于:
- 每一层可以独立选型、独立优化、独立扩容。
- STT、LLM、TTS 可以来自不同厂商,比如 STT 用本地 Whisper,LLM 用云端大模型,TTS 用另一家的合成服务。
- 问题排查简单,哪一环出问题就定位哪一环。
1.3 这套架构解决了语音助手的哪“两大难题”
结合标题和实际项目经验,我认为级联式三明治架构重点解决了两大难题:
难题一:语义理解与上下文管理难
如果只是“语音转文字 + 大模型回复”,没有会话管理和 Agent 设计,大模型记不住前文,回答会非常碎片化。级联式架构把 Agent 放在中间层,让 LLM 不只是“聊天”,而是具备工具调用、知识库检索、状态记忆的能力。
难题二:端到端语音系统的延迟与可控性难
端到端语音大模型虽然听起来很美好,但部署成本高、生成不可控、领域适配难。级联式架构通过把音频处理与文本处理解耦,可以用成熟的 STT/TTS 引擎保障语音质量,用 LLM 保障语义能力,同时在每一层做缓存、打断、流式处理来降低延迟。
2. 核心模块拆解:STT、LLM/Agent、TTS 各自怎么选
2.1 STT:语音转文字
STT 负责把麦克风采集到的音频转成文字。它是整个链路的第一环,识别质量直接决定后面所有环节的效果。
如果你的场景是中文普通话、带口音、有环境噪声,建议优先考虑:
- Whisper(OpenAI 开源):多语言支持好,对噪声鲁棒,但本地部署需要一定的 GPU 资源。如果是 CPU 环境,建议用
small或base模型。 - FunASR(阿里开源):中文效果很好,支持流式识别,适合实时对话场景。
- Paraformer(阿里开源):在中文识别和标点预测方面表现稳定,工业界使用较多。
- 云端 STT:如果允许数据出域,也可以接云厂商的语音识别服务,延迟和并发都有保障,但需要评估成本和数据合规风险。
在级联架构中,STT 有几个关键点需要注意:
- 返回结果需要带标点,否则 LLM 很难断句。
- 最好支持VAD 检测(Voice Activity Detection),也就是检测用户是否开始说话、是否停止说话。
- 需要输出时间戳(Timestamp),方便后续做打断和日志分析。
2.2 LLM / Agent:中间决策层
中间层是整个 Voice Agent 的大脑。它收到 STT 输出的文本后,需要完成以下任务:
- 对话状态维护(多轮记忆)
- 意图识别与槽位提取
- 业务查询 / 工具调用(Function Calling)
- 回复生成(决定“说什么”)
- 回复文本后处理(如增加标点、去掉 Markdown 符号、控制长度)
这里要特别强调,给 TTS 的文本不能直接使用大模型的原始输出。因为大模型经常输出 Markdown、列表、加粗、代码块等格式,TTS 遇到这些符号会读出“星号”“井号”,或者直接读错。所以中间层必须包含一个“文本净化”模块。
线上系统通常使用 Function Calling 或 Agent 框架(如 LangChain、Dify、Coze、自研 Agent Runtime)让 LLM 具备调用内部 API 的能力。比如用户说“帮我查一下明天的会议安排”,LLM 并不需要凭空回答,而是调用日历查询接口,把结果组织成自然语言。
2.3 TTS:文字转语音
TTS 是最后一片“面包”,负责把 LLM 生成的内容朗读出来。它的核心评价指标是:
- 自然度:听起来是否像真人。
- 延迟:首包延迟要低,最好小于 300ms。
- 稳定性:长文本、特殊符号、数字、英文是否稳定发音。
- 情感控制:是否支持快乐、严肃、温柔等语气调节。
现在可选的方案也很多:
- Edge-TTS:免费易用,基于微软语音服务,中英文效果都不错,但依赖网络。
- ChatTTS:开源对话场景 TTS,支持一定的韵律和笑声,适合闲聊类助手。
- CosyVoice(阿里开源):支持零样本声音克隆,可以固定音色。
- 火山引擎 / 阿里云 / 腾讯云 TTS:商用级稳定,延迟低,支持 SSML 标记,适合企业级项目。
在企业级项目中,我更推荐商用 TTS 服务,因为超时、并发、热词定制都有保障。如果是本地离线优先场景,可以用开源 TTS 做私有化部署,但要提前做好音色合成质量和并发压力的测试。
2.4 级联式三明治架构全景图
使用 ASCII 简图表示整个链路:
用户说话音频 | v +----------+ ASR 结果文本 +----------+ 回复文本 +----------+ | STT 模块 | ------------> | LLM/Agent | -------> | TTS 模块 | +----------+ +----------+ +----------+ | | | 音频采集 会话记忆 音量/音色 噪声消除 工具调用 流式播放 端点检测 文本净化 打断处理可以看到,STT 和 TTS 负责“语音到文本”和“文本到语音”的转换,LLM 在中间负责纯粹的文本语义处理。这种分层设计让每一步的职责都清晰明确。
3. 环境准备与项目结构规划
3.1 环境与版本说明
在开始写代码之前,先说明一下本文示例的运行环境。不同的机器、不同版本的依赖库可能会带来差异,所以下面的版本信息请当作“参考基准”,而不是绝对固定值。
操作系统:Ubuntu 22.04 / macOS 14 / Windows 11(WSL2 也可) Python:3.10+ 语音识别:openai-whisper 或 funasr 大模型:OpenAI 兼容接口(也可以换成任意本地 LLM) 语音合成:edge-tts 或 云厂商 TTS SDK如果你使用的是 CPU 环境,可以把 Whisper 模型换成base或small版本;如果你有 NVIDIA GPU(显存 8GB 以上),可以使用medium或large-v3版本。项目的重点在于架构思路和代码组织方式,模型本身可以根据实际资源调整。
安装依赖:
pip install openai-whisper edge-tts openai python-dotenv sounddevice numpy说明:
openai是调用兼容 OpenAI 格式的大模型接口。edge-tts是微软 Edge 浏览器同款语音合成接口的 Python 库,免费且效果不错。sounddevice负责采集麦克风音频;如果只是测试,可以用本地音频文件替代。python-dotenv用来管理 API Key 等环境变量。
3.2 项目结构
下面是我比较推荐的最小项目结构,后续扩展时也方便维护:
voice_agent_project/ ├── .env ├── requirements.txt ├── main.py ├── config.py ├── agent/ │ ├── __init__.py │ ├── stt.py │ ├── llm_agent.py │ ├── tts.py │ └── pipeline.py └── logs/ └── voice_agent.log每个文件职责如下:
main.py:程序入口,调度整个语音对话流程。config.py:读取环境变量,统一管理模型路径、API Key、音色配置。agent/stt.py:语音识别模块,封装 STT 逻辑。agent/llm_agent.py:LLM 对话模块,负责多轮对话和文本后处理。agent/tts.py:语音合成模块,负责把文本变成音频并播放。agent/pipeline.py:三明治架构的核心编排代码,把 STT、LLM、TTS 串起来。logs/:存放运行日志。
3.3 环境变量配置
在项目根目录创建.env文件:
OPENAI_API_KEY=sk-xxx OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_MODEL=gpt-4o-mini TTS_VOICE=zh-CN-XiaoxiaoNeural STT_MODEL=small如果你的大模型是本地部署的(比如 vLLM、Ollama),则把OPENAI_BASE_URL改成对应的地址,例如:
OPENAI_BASE_URL=http://localhost:8000/v14. 实战:基于级联式三明治架构实现 Voice Agent
下面进入核心环节。我们用 Python 逐个模块实现,然后把它们组合成一个完整的语音助手。
4.1 配置模块 config.py
配置文件在项目中起到“总闸”的作用,所有可变的参数都从这里读取,避免在业务代码里散落魔法值。
# 文件路径:voice_agent_project/config.py import os from dotenv import load_dotenv load_dotenv() class Config: # OpenAI / LLM 配置 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") # STT 配置 STT_MODEL = os.getenv("STT_MODEL", "small") # TTS 配置 TTS_VOICE = os.getenv("TTS_VOICE", "zh-CN-XiaoxiaoNeural") # 系统提示词,控制助手的角色定位 SYSTEM_PROMPT = os.getenv( "SYSTEM_PROMPT", "你是一个友好、专业的智能语音助手。请用简洁、口语化的方式回答问题," "不要使用 Markdown 标记,不要使用列表符号,每次回复尽量控制在 80 字以内。", )这里特别说明一下系统提示词的作用。语音场景和文字聊天场景对回复格式的要求完全不同,文字聊天可以输出 Markdown,语音助手必须要求模型输出“适合朗读”的文本。如果你希望助手话少一点,可以在提示词里直接约束“每次回复不超过 2 句话”。
4.2 STT 模块 agent/stt.py
STT 模块接收一个音频文件或音频数据流,返回识别出的文本。为了演示方便,我这里使用本地音频文件作为输入,最终你可以替换成麦克风采集或 WebRTC 音轨。
# 文件路径:voice_agent_project/agent/stt.py import whisper class STTEngine: def __init__(self, model_name: str = "small"): # 加载语音识别模型 self.model = whisper.load_model(model_name) def transcribe(self, audio_path: str) -> str: """ 将语音文件转成文本。 :param audio_path: 音频文件路径,支持 wav/mp3/m4a 等格式 :return: 识别出的文本 """ result = self.model.transcribe(audio_path, language="zh", fp16=False) return result["text"].strip()如果你希望使用 FunASR 来获得更好的中文效果和流式识别能力,可以把transcribe方法内部替换为 FunASR 的调用方式,接口保持不变。这样做的好处是上层应用不需要关心底层用的是哪个 STT 引擎。
4.3 LLM Agent 模块 agent/llm_agent.py
LLM Agent 是中间层,这里我们实现两个核心能力:
- 维护多轮对话历史。
- 对输出文本做净化处理,保证 TTS 能顺利朗读。
# 文件路径:voice_agent_project/agent/llm_agent.py from openai import OpenAI from config import Config class LLMAgent: def __init__(self, system_prompt: str = None): self.client = OpenAI( api_key=Config.OPENAI_API_KEY, base_url=Config.OPENAI_BASE_URL, ) self.model = Config.OPENAI_MODEL self.system_prompt = system_prompt or Config.SYSTEM_PROMPT self.history = [] def reset_history(self): """清空对话历史,开始新一轮会话。""" self.history = [] def add_user_message(self, content: str): self.history.append({"role": "user", "content": content}) def add_assistant_message(self, content: str): self.history.append({"role": "assistant", "content": content}) def get_reply(self, user_text: str) -> str: """ 调用大模型生成回复,并净化文本。 :param user_text: 用户输入文本 :return: 适合 TTS 播放的回复文本 """ self.add_user_message(user_text) messages = [{"role": "system", "content": self.system_prompt}] messages.extend(self.history) response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=0.7, ) reply = response.choices[0].message.content clean_reply = self._clean_text(reply) self.add_assistant_message(reply) return clean_reply @staticmethod def _clean_text(text: str) -> str: """ 对模型输出做净化: 1. 去掉 Markdown 标记 2. 去掉星号、井号、反引号 3. 压缩多余换行 4. 去掉列表序号 """ import re # 去掉 Markdown 图片和链接语法 text = re.sub(r"!\[.*?\]\(.*?\)", "", text) text = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", text) # 去掉 Markdown 标题符号、加粗、斜体、代码块标记 text = re.sub(r"[#>*`_~]", "", text) # 去掉列表序号,如 1. 2. - 等 text = re.sub(r"^\s*(\d+[.、)]|\-)\s*", "", text, flags=re.MULTILINE) # 压缩连续换行 text = re.sub(r"\n{2,}", "\n", text).strip() return text这里有一个工程细节值得注意:_clean_text不仅仅是为了显示美观,更是为了避免 TTS 读出 Markdown 符号。如果你直接让 TTS 读 “你好” 这种文本,可能会出现“加粗你好加粗”这种诡异发音。
4.4 TTS 模块 agent/tts.py
TTS 模块把文本转成音频并播放。这里使用edge-tts作为示例,它支持多种音色,使用简单。
# 文件路径:voice_agent_project/agent/tts.py import asyncio import edge_tts from config import Config class TTSEngine: def __init__(self, voice: str = None): self.voice = voice or Config.TTS_VOICE async def synthesize_to_file(self, text: str, output_path: str): """ 将文本合成为音频文件。 :param text: 需要朗读的文本 :param output_path: 输出的音频文件路径 """ communicate = edge_tts.Communicate(text, self.voice) await communicate.save(output_path) async def synthesize_and_play(self, text: str): """ 将文本合成并播放。 这里直接用 ffplay 播放,也可以替换为 pygame / sounddevice。 """ output_path = "temp_reply.mp3" await self.synthesize_to_file(text, output_path) # 播放音频 process = await asyncio.create_subprocess_exec( "ffplay", "-nodisp", "-autoexit", output_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) await process.wait() def tts_sync(text: str) -> None: """同步包装,方便在普通函数中调用。""" engine = TTSEngine() asyncio.run(engine.synthesize_and_play(text))需要说明的是,edge-tts是一个在线服务,使用前需要保持网络畅通。如果你的项目要求离线或内网部署,可以替换成其他本地 TTS 引擎,只要保持synthesize_and_play(text)这个接口不变即可。
4.5 核心编排模块 agent/pipeline.py
这是整个三明治架构的中枢,它把 STT、LLM、TTS 串成一条完整的处理链。
# 文件路径:voice_agent_project/agent/pipeline.py from agent.llm_agent import LLMAgent from agent.stt import STTEngine from agent.tts import TTSEngine class VoiceAgentPipeline: def __init__(self): self.stt = STTEngine() self.llm = LLMAgent() self.tts = TTSEngine() def handle_audio_file(self, audio_path: str): """ 完整处理一次语音输入: 语音 -> 文本 -> 大模型回复 -> 文本净化 -> 语音输出 """ # Step 1: STT 识别 user_text = self.stt.transcribe(audio_path) print(f"[STT] 识别结果: {user_text}") # Step 2: LLM 推理 reply_text = self.llm.get_reply(user_text) print(f"[LLM] 回复文本: {reply_text}") # Step 3: TTS 合成 self.tts.synthesize_and_play(reply_text) return reply_text为了方便理解,我在代码里加入了[STT]、[LLM]这样的日志前缀。在企业级项目中,这些日志应该改为标准 logging 输出,并把中间过程写入日志文件,方便后续排查。
4.6 程序入口 main.py
现在编写主入口,让整个流程可运行。
# 文件路径:voice_agent_project/main.py import sys from agent.pipeline import VoiceAgentPipeline def main(): if len(sys.argv) < 2: print("用法: python main.py <音频文件路径>") return audio_file = sys.argv[1] pipeline = VoiceAgentPipeline() pipeline.handle_audio_file(audio_file) if __name__ == "__main__": main()运行方式:
python main.py test_audio.wav预期输出类似:
[STT] 识别结果: 你好,请帮我介绍一下你们公司的产品 [LLM] 回复文本: 好的,我们公司主要提供智能语音解决方案,包括语音识别、语音合成和对话式AI服务。请问您想了解哪方面?然后设备会播放合成语音。
4.7 加入多轮对话能力
前面已经实现了基础的“一问一答”,但这还不够。语音助手需要支持多轮对话,比如用户说“再讲详细一点”,系统要能理解“再”指的是上一轮的主题。
下面升级VoiceAgentPipeline,增加一个简单的多轮循环:
# 文件路径:voice_agent_project/agent/pipeline.py from agent.llm_agent import LLMAgent from agent.stt import STTEngine from agent.tts import TTSEngine class VoiceAgentPipeline: def __init__(self): self.stt = STTEngine() self.llm = LLMAgent() self.tts = TTSEngine() self.running = True def handle_audio_file(self, audio_path: str) -> str: user_text = self.stt.transcribe(audio_path) print(f"[STT] 识别结果: {user_text}") # 检测退出指令 if user_text in ["退出", "再见", "结束", "拜拜"]: self.running = False reply_text = "好的,再见!" else: reply_text = self.llm.get_reply(user_text) print(f"[LLM] 回复文本: {reply_text}") self.tts.synthesize_and_play(reply_text) return reply_text def start_conversation(self): """循环对话:每次输入音频文件路径作为模拟语音输入。""" print("开始语音对话,输入音频文件路径继续,输入 q 退出。") while self.running: audio_path = input("请输入音频文件路径(输入 q 退出): ").strip() if audio_path.lower() == "q": break self.handle_audio_file(audio_path) if __name__ == "__main__": pipeline = VoiceAgentPipeline() pipeline.start_conversation()这样,每轮对话都会携带之前的history,大模型能够理解上下文。
4.8 扩展:Function Calling 让 Agent 具备工具能力
真正的企业级 Voice Agent 不能只靠模型“脑补”答案,还要能查数据库、调接口。下面给出一个用 Function Calling 查询天气的示例思路。
# 文件路径:voice_agent_project/agent/llm_agent.py(扩展) def get_reply_with_tools(self, user_text: str) -> str: self.add_user_message(user_text) messages = [{"role": "system", "content": self.system_prompt}] messages.extend(self.history) tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"], }, }, } ] response = self.client.chat.completions.create( model=self.model, messages=messages, tools=tools, tool_choice="auto", ) message = response.choices[0].message if message.tool_calls: # 这里调用本地函数 tool_call = message.tool_calls[0] if tool_call.function.name == "get_weather": import json args = json.loads(tool_call.function.arguments) city = args["city"] weather_result = f"{city}今天晴转多云,温度 18~26 摄氏度" self.add_assistant_message(str(message)) self.add_user_message(str({"weather": weather_result})) # 二次请求让模型生成最终回复 final_response = self.client.chat.completions.create( model=self.model, messages=[{"role": "system", "content": self.system_prompt}] + self.history, ) return self._clean_text(final_response.choices[0].message.content) reply = message.content clean_reply = self._clean_text(reply) self.add_assistant_message(reply) return clean_reply这只是一个最简单的示意,真实项目中需要把工具注册表、参数校验、超时重试、权限校验都完善起来。
5. 如何解决语音助手的两大核心难题
在前面,我们已经从架构上理解了级联式三明治的基本形状。这里再深度展开一下,这套架构到底怎么解决实际业务中的两大难题。
5.1 难题一:语义理解与业务落地的割裂
纯大模型对话和真正的业务之间,有一道很深的鸿沟。用户说“帮我查一下上个月的订单量”,大模型如果不知道你的数据库结构,根本无法回答。级联式三明治架构的解决方式是将中间层升级为Agent 层:
- 大模型负责理解用户意图,并决定“要不要调用工具”。
- 通过 Function Calling,大模型可以查询订单系统、CRM、库存系统。
- Agent 框架维护任务状态,把多次工具调用组合成一个完整答案。
这种模式的业务价值在于:语音助手不再是一个“聊天玩具”,而是真正能完成任务的数字员工。用户对语音助手的信任度来自“能办成事”,而不是“聊得开心”。
5.2 难题二:时延、并发与音频体验的矛盾
语音交互对实时性要求极为严格。根据公开经验数据,端到端延迟大于 2 秒时,用户会明显感到卡顿;大于 5 秒时,用户大概率会打断或放弃。
级联式三明治架构为时延优化提供了多个切入点:
- STT 层流式识别:不必等用户说完再识别,可以边说话边出字,节省等待时间。
- LLM 层流式输出:大模型逐 token 生成,TTS 可以增量合成,实现“边说边生成”。
- TTS 层首包优先:先合成第一句话并播放,后续句子在后台继续合成。
- 缓存机制:高频问题(如“你是谁”“你们公司地址在哪”)可以直接命中缓存,绕过大模型。
下面是这些优化点在代码中的落点说明:
| 阶段 | 优化方式 | 代码落点 |
|---|---|---|
| STT | 开启流式识别 / VAD 端点检测 | transcribe改为流式接口 |
| LLM | 使用流式输出 + 增量提示词 | get_reply改为stream=True |
| TTS | 保存常用回复的合成音频 | synthesize_and_play加缓存判断 |
| 管道 | 并行化音频采集与 LLM 推理 | pipeline使用异步任务 |
5.3 为什么不用端到端语音大模型
现在市面也有一些“语音大模型”,号称直接输入语音输出语音。这类模型确实体验很惊艳,但在企业级场景中,级联式三明治架构仍然更稳妥,原因包括:
- 领域可控性:端到端模型很难控制中间输出,如果用户问“退款政策”,你无法确定模型内部是否遵循了业务知识库。
- 部署成本:端到端模型参数量巨大,私有化部署成本高。
- 工具调用复杂:级联架构中,LLM 可以很方便地调用工具;端到端模型要支持工具调用还需要额外工程开发。
- 语音问题定位难:端到端模型出问题时,你不知道是听懂错了,还是回答错了,还是念错了。级联架构的日志链路清楚,出问题可以直接定位到具体层。
当然,如果未来端到端语音大模型的成本和使用体验提升上来,部分简单场景也可以尝试,但短期内级联式三明治架构仍然是企业落地最稳妥的选择。
6. 常见问题与排查思路
6.1 STT 识别不准
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 中文识别错别字多 | 模型太小 / 领域词缺失 | 换用更大的模型;添加热词表 |
| 噪声大导致识别乱码 | 没有预处理音频 | 加 VAD 和降噪模块 |
| 专业名词识别错误 | 未配置自定义词汇 | 在 STT 中用热词列表强调 |
| 英文和中文混读不准 | 语言参数设置不当 | 设置language="zh"或multilingual |
建议:在 STT 引擎初始化时传入自定义词汇列表,例如公司产品名、行业术语。Whisper 可以通过“上下文提示”实现类似效果。
6.2 LLM 回复不适合朗读
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| TTS 读出 “星号”“井号” | 模型输出带 Markdown | 增加_clean_text净化模块 |
| 回复太长,语音播报超时 | 未限制回复长度 | 系统提示词明确约束字数 |
| 模型输出英文缩写 | prompt 没有约束语言 | 提示词中指定使用中文回复 |
| 多轮回答跑题 | 历史消息没有正确维护 | 检查 history 拼接逻辑 |
建议:把“输出净化”作为一个独立模块来写,并且要有单元测试覆盖。不要相信模型一定不会输出 Markdown,一定要在代码层面兜底。
6.3 TTS 播放卡顿或无声
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 播放无声音 | 系统没有音频输出设备 | 检查声卡 / ffplay 是否安装 |
| 首包延迟高 | TTS 合成慢 | 换用流式 TTS 接口 |
| 长文本合成失败 | 文本长度超限 | 将文本分段合成 |
| 并发播放冲突 | 多次调用播放器 | 增加播放队列 |
建议:在代码中增加一个“播放锁”或“音频队列”,避免多任务并发时互相抢占音频输出设备。
6.4 完整排查 checklist
如果你在运行中遇到问题,可以按下面顺序排查:
- 单独测试 STT:只运行
transcribe,确认识别文本是否正确。 - 单独测试 LLM:直接给
get_reply传文本,确认回复内容是否合理。 - 单独测试 TTS:只合成一句话,确认能正常播放。
- 最后再组合成完整链路,观察哪一环的日志出现异常。
这种“单独验证每一层”的思路,是级联架构最大的工程红利。
7. 最佳实践与工程化建议
7.1 对话管理设计
不要把所有对话状态都放在内存里。企业级系统需要考虑:
- 用 Redis 存储会话上下文,设置过期时间(如 30 分钟无操作自动清理)。
- 给每个会话分配
session_id,方便日志追踪和多轮对话恢复。 - 对敏感对话进行脱敏处理,避免用户隐私进入模型训练。
7.2 提示词与文本后处理
语音场景的提示词比文字聊天要更“啰嗦”一点,一定要把格式约束写清楚。我这里提供一个相对成熟的模板:
你是一个语音助手,正在和用户进行实时对话。 请遵循以下规则: 1. 使用中文口语化回答。 2. 每句话尽量简短,不要超过 30 个字。 3. 不要使用 Markdown、列表、加粗、斜体。 4. 如果不知道答案,直接说“抱歉,我暂时无法回答这个问题”。 5. 涉及数字金额时,使用中文读法,例如“一百二十八元”。7.3 安全边界
即使是语音助手,也必须考虑安全边界:
- 不要允许大模型直接执行高权限操作。比如“删除订单”“转账”这类操作,必须二次确认。
- 工具调用需要鉴权。每个 Function Call 要校验调用者身份和权限。
- 音频数据要加密存储。用户语音属于敏感数据,日志中不要明文记录完整音频内容。
- 设置超时与重试。调用第三方模型服务时,建议默认设置超时时间为 5~10 秒,并做指数退避重试。
7.4 日志与监控
三明治架构的每一层都要输出结构化日志。
- STT 层:记录音频时长、识别文本、识别置信度。
- LLM 层:记录 prompt 长度、模型名称、首 token 延迟、总 token 数。
- TTS 层:记录合成文本长度、首包延迟、播放时长。
- 管道层:记录总耗时和各阶段耗时占比。
当用户反馈“助手变笨了”时,第一件事就是看日志:到底是 STT 没听清,还是 LLM 答错,还是 TTS 没播出来。这一步能节省大量排查时间。
7.5 性能优化分级
不同业务对延迟的要求不同,优化优先级也不同。
- 初级优化:模型降级、文本净化、回复长度限制。
- 中级优化:流式 STT、流式 LLM、TTS 增量播报。
- 高级优化:端到端延迟预算系统、自动扩缩容、地域级边缘部署。
建议先从初级优化开始,因为它投入最小、收益最明显。不要一上来就追求高难度的流式改造。
8. 总结与下一步学习方向
这篇文章从企业级 Voice Agent 的落地视角,把级联式三明治架构(STT → LLM/Agent → TTS)完整拆解了一遍。核心内容总结如下:
- 级联式三明治架构是当前企业落地语音助手最稳妥的方案,分层清晰、可插拔、易排错。
- STT 负责“听清”,LLM/Agent 负责“听懂”和“办成事”,TTS 负责“说好”。
- 文本净化是语音助手非常容易被忽略但又极其重要的一步。
- LLM 不只用来聊天,更重要的是通过 Function Calling 完成真实业务任务。
- 每一层都可以独立优化和替换,这也是架构最大的价值。
接下来你可以继续深入学习的方向包括:
- 流式 STT 与 VAD 端点检测,让助手的响应更快。
- Function Calling 完整实战,让语音助手接入真实业务系统。
- RAG 知识库增强,让助手能回答私有领域问题。
- 音色定制与情感合成,提升产品体验。
- 语音打断(Barge-in),实现“用户说话时助手立刻停止播放”。
语音 Agent 并不是简单的模型拼接,而是一部需要精心调校的“机器”。希望这篇文章能帮你把最难的两块骨头啃下来。如果你在实际运行中遇到问题,欢迎在评论区留言交流;觉得有用的话,也可以收藏备用,后续项目需要时再翻出来对照着配置。