从零构建语音交互AI智能体:基于大模型与工具调用的实战指南
在实际项目中,将 AI 大模型能力与语音交互结合,构建一个能理解指令、执行任务的“AI 员工”,正从概念走向落地。这类应用的核心挑战在于如何将非结构化的语音指令,转化为结构化的任务流程,并调用合适的工具或 API 来完成。本文将以一个名为“Viktor”的 AI 员工项目为例,拆解其背后的技术架构与实现路径。我们将从零开始,构建一个能通过语音指令指挥的简易 AI 智能体,涵盖语音识别、大模型意图理解、任务规划与执行、以及语音合成的完整闭环。
本文适合对 AI 应用开发、智能体(Agent)架构感兴趣的开发者。你将了解到如何利用现有开源工具和框架,快速搭建一个可交互的语音 AI 原型。我们将使用 Python 作为主要开发语言,结合 Gradio 构建 Web 交互界面,并集成语音处理与大模型能力。最终,你将获得一个可以通过麦克风输入指令、由 AI 解析并执行简单任务(如查询信息、计算、控制设备模拟)、并通过语音播报结果的演示系统。
1. 理解 AI 智能体(Agent)与语音交互的核心链路
在开始编码之前,必须理清从“用户说话”到“AI 执行并反馈”的整个技术链路。这不仅仅是调用几个 API,而是涉及多个模块的协同工作。
1.1 什么是 AI 智能体(Agent)?
通俗地讲,AI 智能体是一个能感知环境、自主决策并执行动作以达成目标的程序。在本文语境下,我们的智能体“Viktor”就是一个软件程序,它接收用户的语音指令作为输入,通过内部“思考”(大模型推理),决定需要调用哪些“技能”(工具函数),最终执行并给出语音反馈。
其核心工作流程可以概括为: 感知 -> 规划 -> 行动 -> 观察 -> 循环 。对于 Viktor 来说:
- 感知 :通过语音识别(ASR)将用户语音转为文本。
- 规划 :大模型分析文本,理解用户意图,并拆解为可执行步骤(例如:“查询天气” -> 步骤1: 提取城市名;步骤2: 调用天气查询API)。
- 行动 :执行规划好的步骤,调用对应的工具函数(如网络请求、本地计算)。
- 观察 :获取工具执行的结果(如 API 返回的 JSON 数据)。
- 循环 :将观察结果反馈给大模型,由大模型判断任务是否完成,若未完成则继续规划下一步行动,直至任务完成并生成最终回复文本。
1.2 语音交互的技术栈分解
一个完整的语音指挥 AI 系统,通常包含以下技术组件:
| 组件 | 功能 | 常见技术选型(开源/本地化) | 在 Viktor 项目中的角色 |
|---|---|---|---|
| 语音识别 (ASR) | 将用户语音转换为文本。 | Whisper, FunASR, Qwen-Audio | 接收麦克风输入,输出指令文本。 |
| 大语言模型 (LLM) | 理解指令、规划任务、生成回复。 | Qwen, Llama, ChatGLM, 或通过 API 调用(如 OpenAI) | Viktor 的“大脑”,负责意图理解和任务规划。 |
| 智能体框架 | 管理工具调用、控制任务执行流程。 | LangChain, LlamaIndex, Dify, 或自研框架 | 封装 LLM 与工具,实现规划-行动循环。 |
| 工具集 (Tools) | 执行具体任务的能力,如计算、查询、控制。 | Python 函数、HTTP 客户端、数据库驱动等 | Viktor 的“双手”,执行 LLM 规划出的具体动作。 |
| 语音合成 (TTS) | 将回复文本转换为语音播报。 | VITS, Edge-TTS, pyttsx3 | 将 Viktor 的文本回复转为语音输出。 |
| 交互界面 (UI) | 提供录音、播放、文本展示的入口。 | Gradio, Streamlit, 自定义 Web | 为用户提供启动录音、查看交互历史的界面。 |
在接下来的实现中,我们将采用一个轻量级组合: Gradio(界面) + 本地部署的 Qwen-Chat 模型(LLM) + 自研简单 Agent 逻辑 + pyttsx3(TTS) 。ASR 部分为了简化,我们先用 Gradio 的麦克风输入组件获取音频文件,再使用一个离线 ASR 模型进行处理。这个组合足以在单台开发机上跑通全流程,理解核心机制。
2. 环境准备与项目初始化
我们将在一个干净的 Python 虚拟环境中进行开发,以避免依赖冲突。
2.1 创建项目目录与虚拟环境
首先,创建项目目录并进入。
mkdir project_deskless_viktor && cd project_deskless_viktor
使用 venv 创建 Python 虚拟环境并激活。
# Linux/macOS
python3 -m venv venv
source venv/bin/activate
# Windows
python -m venv venv
venv\Scripts\activate
激活后,命令行提示符前应显示 (venv) ,表示已进入虚拟环境。
2.2 安装核心依赖
创建 requirements.txt 文件,列出项目所需的主要库。我们将分步安装,先确保基础环境。
# 基础与界面
gradio>=4.0.0
# 大模型调用 (以 OpenAI API 格式兼容的本地模型为例,我们使用 `openai` 包)
openai>=1.0.0
# 语音合成 (离线,跨平台)
pyttsx3>=2.90
# 音频处理
pydub>=0.25.1
# 网络请求
requests>=2.28.0
# 环境变量管理
python-dotenv>=1.0.0
执行安装命令:
pip install -r requirements.txt
注意:
openai包版本 1.x 与之前的 0.x 版本 API 有重大变化。本文基于 1.x 版本编写。如果你连接的是本地部署的兼容 OpenAI API 的模型服务(如 FastChat, vLLM, OpenLLM 等),需要使用此版本。
2.3 准备大模型服务
Viktor 的“大脑”需要一个 LLM。为了本地运行,你有两个主流选择:
- 使用在线 API(简单,需网络和费用) :如 OpenAI GPT, Anthropic Claude 等。你只需要一个 API Key。
- 本地部署开源模型(复杂,无需网络) :如使用
ollama运行 Qwen2.5,或使用vllm部署一个模型服务。
为了教程的完整性和可复现性,我们假设你在本地部署了一个兼容 OpenAI API 格式的模型服务。例如,使用 ollama 拉取并运行 qwen2.5:7b 模型,并启用其 OpenAI 兼容接口:
# 安装 ollama (请参考官网)
# 拉取模型
ollama pull qwen2.5:7b
# 以 OpenAI 兼容模式运行,指定端口
OLLAMA_HOST=0.0.0.0:11434 ollama serve
# 另开一个终端,创建自定义模型
ollama create viktor -f ./Modelfile
其中 Modelfile 内容为:
FROM qwen2.5:7b
PARAMETER temperature 0.7
SYSTEM “你是一个名为Viktor的AI助手,擅长将复杂任务拆解为步骤并执行。”
这样,你就拥有了一个运行在 http://localhost:11434 的,兼容 OpenAI API 的模型端点。其 API 路径与 OpenAI 一致(如 /v1/chat/completions )。
请根据你的实际情况,记录下模型的 Base URL 和 API Key (如果本地部署且未设置鉴权,API Key 可填 ollama 或任意非空字符串)。
3. 构建 Viktor 的核心智能体引擎
智能体引擎是 Viktor 的核心,它负责串联 ASR 文本、调用 LLM 进行规划、管理工具执行并生成最终回复。
3.1 定义工具(Viktor 的技能)
首先,在项目根目录创建 tools.py 文件,定义 Viktor 可以使用的工具。每个工具都是一个 Python 函数,并附上清晰的文档字符串,供 LLM 理解其功能。
# tools.py
import requests
import json
import math
from datetime import datetime
def get_current_time(location: str = None) -> str:
"""
获取当前时间。如果提供了地点参数,则返回该地点的当前时间(模拟)。
参数:
location (str, optional): 城市名,例如 "Beijing", "New York"。默认为 None,返回服务器时间。
返回:
str: 格式化的时间字符串。
"""
now = datetime.now()
if location:
# 这里简化处理,实际应调用时区API
return f"模拟 {location} 的当前时间是:{now.strftime('%Y-%m-%d %H:%M:%S')}"
else:
return f"当前系统时间是:{now.strftime('%Y-%m-%d %H:%M:%S')}"
def calculator(expression: str) -> str:
"""
执行数学计算。支持加减乘除、乘方和简单函数。
参数:
expression (str): 数学表达式,例如 "3 + 5 * 2", "sqrt(16)"。
返回:
str: 计算结果或错误信息。
"""
# 安全警告:在生产环境中,直接eval是危险的,这里仅用于演示。
# 应使用更安全的表达式解析库,如 `asteval`。
try:
# 为表达式添加一些常用的数学函数和常量
allowed_names = {
k: v for k, v in math.__dict__.items() if not k.startswith("_")
}
allowed_names.update({"abs": abs, "round": round})
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"计算结果:{expression} = {result}"
except Exception as e:
return f"计算错误:无法解析表达式 '{expression}'。错误详情:{e}"
def search_web(query: str) -> str:
"""
模拟网络搜索,返回模拟结果。在实际项目中,应替换为真正的搜索引擎API。
参数:
query (str): 搜索关键词。
返回:
str: 模拟的搜索结果摘要。
"""
# 模拟一个简单的搜索返回
mock_results = [
f"关于 '{query}' 的百科摘要:这是一个模拟结果,关键词涉及相关领域。",
f"最新关于 '{query}' 的新闻:模拟新闻内容,展示了该主题的最新动态。",
f"技术论坛关于 '{query}' 的讨论:用户分享了使用经验和解决方案。"
]
return "\n".join(mock_results)
def get_weather(city: str) -> str:
"""
模拟获取城市天气信息。实际应调用如 OpenWeatherMap 等天气API。
参数:
city (str): 城市名称,例如 "北京"。
返回:
str: 模拟的天气信息。
"""
# 模拟数据
weather_data = {
"北京": "晴,15°C 到 25°C,西北风2级。",
"上海": "多云,18°C 到 28°C,东南风3级。",
"广州": "阵雨,22°C 到 30°C,南风1级。",
"深圳": "雷阵雨,23°C 到 31°C,西南风2级。"
}
forecast = weather_data.get(city, "抱歉,未找到该城市的天气信息。")
return f"{city}的天气:{forecast}"
# 工具列表,用于注册到智能体
TOOLS = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "获取指定地点或服务器的当前时间。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名,例如 Beijing, New York。如果不提供,则返回服务器时间。",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "执行数学计算,支持加减乘除、乘方和sqrt等函数。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,例如 '3 + 5 * 2', 'sqrt(16)'。",
}
},
"required": ["expression"],
},
},
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "根据查询词进行网络搜索,返回信息摘要。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词。",
}
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如 '北京'。",
}
},
"required": ["city"],
},
},
}
]
# 工具名称到实际函数的映射
TOOL_FUNCTION_MAP = {
"get_current_time": get_current_time,
"calculator": calculator,
"search_web": search_web,
"get_weather": get_weather,
}
3.2 实现智能体执行引擎
接下来,创建 agent_engine.py ,实现智能体的核心循环。我们将使用 OpenAI 格式的客户端,并实现简单的函数调用逻辑。
# agent_engine.py
import os
from openai import OpenAI
from dotenv import load_dotenv
from tools import TOOLS, TOOL_FUNCTION_MAP
# 加载环境变量
load_dotenv()
class ViktorAgent:
def __init__(self):
# 初始化 OpenAI 客户端,指向本地模型服务
self.client = OpenAI(
base_url=os.getenv("LLM_BASE_URL", "http://localhost:11434/v1"), # 你的本地模型服务地址
api_key=os.getenv("LLM_API_KEY", "ollama") # 本地服务可能不需要key,但需非空
)
self.model = os.getenv("LLM_MODEL", "qwen2.5:7b") # 模型名称
self.conversation_history = [] # 维护对话历史
def process_user_input(self, user_input: str) -> str:
"""
处理用户输入(文本),通过LLM进行规划并执行工具调用,返回最终回复文本。
参数:
user_input (str): 用户输入的文本指令。
返回:
str: AI 助手的最终回复文本。
"""
# 将用户输入加入历史
self.conversation_history.append({"role": "user", "content": user_input})
# 准备发送给LLM的消息,包含历史对话和工具定义
messages = self.conversation_history.copy()
# 如果是第一次对话或需要,可以加入系统提示词
if len(self.conversation_history) == 1:
system_prompt = {
"role": "system",
"content": "你是一个名为Viktor的AI助手。你可以使用工具来帮助用户。请根据用户需求,决定是否需要调用工具。如果需要,请严格按照工具定义的JSON格式回复。如果不需要,直接给出自然语言回答。"
}
messages.insert(0, system_prompt)
max_iterations = 5 # 防止无限循环
final_response = ""
for i in range(max_iterations):
# 调用LLM,传入工具定义
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=TOOLS,
tool_choice="auto", # 让模型自行决定是否调用工具
)
message = response.choices[0].message
# 将模型的回复加入消息列表,用于后续上下文
messages.append(message)
# 检查模型是否决定调用工具
if message.tool_calls:
# 处理每个工具调用(通常一次只调用一个)
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f"[Viktor 正在执行] 工具: {function_name}, 参数: {function_args}")
# 执行工具函数
function_to_call = TOOL_FUNCTION_MAP.get(function_name)
if function_to_call:
try:
tool_result = function_to_call(**function_args)
except Exception as e:
tool_result = f"工具执行出错: {e}"
else:
tool_result = f"错误:未知工具 '{function_name}'"
# 将工具执行结果作为一条新消息加入对话
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(tool_result), # 结果必须是字符串
"name": function_name
})
# 继续循环,让模型根据工具结果生成回复
else:
# 模型没有调用工具,直接给出了最终回复
final_response = message.content
# 将助手的最终回复也加入历史记录
self.conversation_history.append({"role": "assistant", "content": final_response})
break # 退出循环
if i == max_iterations - 1:
final_response = "抱歉,任务处理似乎进入了循环,未能完成。"
break
return final_response
def clear_history(self):
"""清空对话历史"""
self.conversation_history.clear()
这个 ViktorAgent 类封装了与 LLM 的交互和工具调用循环。关键点在于 tool_choice=”auto” 让模型自行决定是否调用工具,以及通过 tool 角色消息将执行结果反馈给模型。
3.3 配置环境变量
在项目根目录创建 .env 文件,用于安全地存储配置。请根据你的本地模型服务实际情况修改。
# .env
# 本地 Ollama 服务(默认)
LLM_BASE_URL=http://localhost:11434/v1
LLM_API_KEY=ollama
LLM_MODEL=viktor # 或你使用的模型名,如 qwen2.5:7b
# 如果使用 OpenAI 官方 API,则像下面这样配置(需付费)
# LLM_BASE_URL=https://api.openai.com/v1
# LLM_API_KEY=sk-your-openai-api-key-here
# LLM_MODEL=gpt-3.5-turbo
4. 集成语音识别与合成模块
现在,我们将为 Viktor 添加“耳朵”(ASR)和“嘴巴”(TTS)。
4.1 实现语音识别(ASR)
为了简化,我们使用一个轻量级的离线 ASR 模型。这里以 funasr 或 whisper 为例。由于 whisper 安装和模型下载可能较慢,我们提供一个备选方案:使用 gradio 的客户端音频提交,并假设后端有一个处理函数。在实际中,你可以选择任一方案。
创建 speech_module.py 文件:
# speech_module.py
import whisper
import tempfile
import os
class SpeechRecognizer:
def __init__(self, model_size="base"):
"""
初始化 Whisper 语音识别模型。
参数:
model_size (str): 模型大小,可选 "tiny", "base", "small", "medium", "large"。
越小越快,但精度越低。
"""
# 首次运行会下载模型,请确保网络通畅
print(f"正在加载 Whisper {model_size} 模型...")
self.model = whisper.load_model(model_size)
print("模型加载完毕。")
def transcribe_audio_file(self, audio_file_path: str) -> str:
"""
将音频文件转录为文本。
参数:
audio_file_path (str): 音频文件路径(支持 wav, mp3, m4a 等)。
返回:
str: 识别出的文本。
"""
if not os.path.exists(audio_file_path):
return f"错误:音频文件不存在 {audio_file_path}"
# 使用 Whisper 进行转录
result = self.model.transcribe(audio_file_path, language="zh", fp16=False) # fp16=False 兼容更多环境
return result["text"].strip()
# 全局识别器实例,避免重复加载模型
_recognizer = None
def get_recognizer():
global _recognizer
if _recognizer is None:
_recognizer = SpeechRecognizer(model_size="base") # 可根据性能调整
return _recognizer
def audio_to_text(audio_file_path: str) -> str:
"""对外提供的音频转文本接口"""
recognizer = get_recognizer()
return recognizer.transcribe_audio_file(audio_file_path)
注意:
whisper库及其依赖(如ffmpeg)可能需要额外安装。你可以通过pip install openai-whisper安装,并确保系统有ffmpeg。如果环境受限,可以先使用一个模拟函数返回文本,优先保证主流程跑通。
4.2 实现语音合成(TTS)
我们使用 pyttsx3 ,这是一个离线的、跨平台的文本转语音库。
# speech_module.py (续)
import pyttsx3
import threading
class SpeechSynthesizer:
def __init__(self):
self.engine = pyttsx3.init()
# 设置语速和音量(可选)
self.engine.setProperty('rate', 180) # 语速,默认200
self.engine.setProperty('volume', 0.9) # 音量 0.0-1.0
# 获取并选择语音(可选,取决于系统)
voices = self.engine.getProperty('voices')
# 尝试选择中文语音(如果存在)
for voice in voices:
if 'chinese' in voice.name.lower() or 'zh' in voice.id.lower():
self.engine.setProperty('voice', voice.id)
break
def text_to_speech(self, text: str, save_path: str = None):
"""
将文本转换为语音,可播放或保存为文件。
参数:
text (str): 要合成的文本。
save_path (str, optional): 如果提供,则保存为音频文件,否则直接播放。
"""
if save_path:
self.engine.save_to_file(text, save_path)
self.engine.runAndWait()
else:
self.engine.say(text)
self.engine.runAndWait()
# 全局合成器实例
_synthesizer = None
def get_synthesizer():
global _synthesizer
if _synthesizer is None:
_synthesizer = SpeechSynthesizer()
return _synthesizer
def text_to_audio(text: str, save_path: str = None):
"""对外提供的文本转语音接口"""
synthesizer = get_synthesizer()
# 在新线程中运行,避免阻塞主线程(特别是Gradio界面)
def _run():
synthesizer.text_to_speech(text, save_path)
thread = threading.Thread(target=_run)
thread.start()
thread.join() # 等待播放/保存完成
5. 使用 Gradio 构建一体化交互界面
最后,我们将所有模块整合到一个 Gradio 界面中,提供录音、文字交互、语音播报和记录展示功能。
创建 app.py 作为应用入口:
# app.py
import gradio as gr
import tempfile
import os
from agent_engine import ViktorAgent
from speech_module import audio_to_text, text_to_audio
# 初始化智能体
agent = ViktorAgent()
def process_audio_input(audio_input):
"""
处理音频输入:识别 -> 智能体处理 -> 合成语音。
参数:
audio_input: Gradio 麦克风组件返回的元组 (采样率, 音频数据) 或文件路径。
返回:
tuple: (识别文本, 助手回复文本, 临时语音文件路径)
"""
# 1. 保存音频到临时文件
if audio_input is None:
return "未接收到音频", "", None
# Gradio Audio 组件返回格式可能是 (sample_rate, audio_data) 或文件路径
if isinstance(audio_input, tuple):
sample_rate, audio_data = audio_input
# 这里需要将 numpy 数组保存为wav文件,为简化,我们假设使用文件路径输入
# 在实际完整实现中,需要处理此格式转换。本例中我们假设前端已处理好。
return "请使用‘录音’按钮录制音频并上传文件", "", None
else:
# audio_input 是临时文件路径
audio_path = audio_input
# 2. 语音识别
print(f"[ASR] 正在识别音频: {audio_path}")
user_text = audio_to_text(audio_path)
print(f"[ASR 结果] {user_text}")
if not user_text or len(user_text.strip()) == 0:
user_text = "(未能识别出有效语音)"
# 3. 智能体处理
print(f"[LLM] 处理用户指令: {user_text}")
assistant_text = agent.process_user_input(user_text)
print(f"[LLM 回复] {assistant_text}")
# 4. 语音合成
print(f"[TTS] 正在合成回复语音...")
# 创建临时文件保存语音
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmpfile:
speech_path = tmpfile.name
text_to_audio(assistant_text, save_path=speech_path)
print(f"[TTS] 语音已保存至: {speech_path}")
return user_text, assistant_text, speech_path
def process_text_input(user_text):
"""处理文本输入(备用方式)"""
print(f"[LLM] 处理用户文本指令: {user_text}")
assistant_text = agent.process_user_input(user_text)
print(f"[LLM 回复] {assistant_text}")
# 语音合成
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmpfile:
speech_path = tmpfile.name
text_to_audio(assistant_text, save_path=speech_path)
return assistant_text, speech_path
def clear_conversation():
"""清空对话历史"""
agent.clear_history()
return None, None, None, None # 清空所有界面组件
# 构建 Gradio 界面
with gr.Blocks(title="Project Deskless: AI 员工 Viktor") as demo:
gr.Markdown("# 🎤 语音指挥 AI 员工 Viktor")
gr.Markdown("通过语音或文字向 Viktor 下达指令,它可以查询信息、计算、报时等。")
with gr.Row():
with gr.Column(scale=1):
audio_input = gr.Audio(
sources="microphone",
type="filepath",
label="录音指令",
interactive=True
)
audio_submit_btn = gr.Button("发送语音指令", variant="primary")
gr.Markdown("---")
text_input = gr.Textbox(
label="或直接输入文字指令",
placeholder="例如:北京今天天气怎么样?",
lines=3
)
text_submit_btn = gr.Button("发送文字指令", variant="secondary")
clear_btn = gr.Button("清空对话", variant="stop")
with gr.Column(scale=2):
# 对话记录显示
chat_history = gr.Chatbot(label="对话历史", height=400)
# 状态信息显示
recognized_text = gr.Textbox(label="识别出的文本", interactive=False)
response_text = gr.Textbox(label="Viktor 的回复", interactive=False, lines=4)
# 语音播放组件
audio_output = gr.Audio(label="Viktor 的语音回复", interactive=False, type="filepath")
# 绑定事件
audio_submit_btn.click(
fn=process_audio_input,
inputs=[audio_input],
outputs=[recognized_text, response_text, audio_output]
).then(
# 更新聊天记录
lambda ut, rt, **kwargs: [(ut, rt)],
inputs=[recognized_text, response_text],
outputs=[chat_history]
)
text_submit_btn.click(
fn=process_text_input,
inputs=[text_input],
outputs=[response_text, audio_output]
).then(
lambda ut, rt, **kwargs: [(ut, rt)],
inputs=[text_input, response_text],
outputs=[chat_history]
)
clear_btn.click(
fn=clear_conversation,
inputs=[],
outputs=[chat_history, recognized_text, response_text, audio_output]
)
gr.Markdown("### 使用说明")
gr.Markdown("""
1. **语音指令**:点击‘录音指令’区域的红色按钮开始录音,完成后点击‘发送语音指令’。
2. **文字指令**:在下方文本框中输入指令,点击‘发送文字指令’。
3. **Viktor 的技能**:报时、计算、模拟搜索、模拟查询天气。
4. **清空对话**:点击‘清空对话’按钮重置 Viktor 的记忆。
""")
# 启动应用
if __name__ == "__main__":
# 先预加载模型,避免首次请求等待过久
print("正在预加载语音识别模型...(首次运行需要下载,请耐心等待)")
from speech_module import get_recognizer
_ = get_recognizer() # 触发加载
print("预加载完成,启动 Web 界面...")
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
6. 运行验证与结果分析
6.1 启动应用
确保你的本地 LLM 服务(如 Ollama)正在运行,并且环境变量配置正确。
在项目根目录下,运行:
python app.py
Gradio 会启动一个本地 Web 服务器。在终端中,你会看到类似以下的输出:
正在预加载语音识别模型...(首次运行需要下载,请耐心等待)
正在加载 Whisper base 模型...
模型加载完毕。
预加载完成,启动 Web 界面...
Running on local URL: http://0.0.0.0:7860
Running on public URL: https://xxxxxx.gradio.live
在浏览器中打开 http://localhost:7860 ,你将看到 Viktor 的交互界面。
6.2 功能测试
按照界面说明进行测试:
-
语音指令测试 :点击录音按钮,清晰地说出“现在几点了?”,然后点击“发送语音指令”。观察过程:
- 界面“识别出的文本”区域应显示识别出的文字。
- “Viktor 的回复”区域应显示类似“当前系统时间是:2024-01-01 12:34:56”的文本。
- “Viktor 的语音回复”区域会出现一个音频播放器,点击即可听到语音播报。
- “对话历史”区域会记录这次完整的交互。
-
文字指令测试 :在文本框中输入“计算 125 除以 5 的平方”,点击“发送文字指令”。回复应为“计算结果:125 / 5 ** 2 = 5.0”。
-
复杂指令测试 :尝试“查询一下北京和上海的天气,然后告诉我现在伦敦的时间(模拟)”。Viktor 应该能理解这是一个复合指令,并依次调用
get_weather和get_current_time工具,最终整合成一个连贯的回复。
6.3 关键日志分析
在运行应用的终端里,你可以看到详细的处理日志,这是排查问题的重要依据。
[ASR] 正在识别音频: /tmp/tmp12345.wav
[ASR 结果] 现在几点了
[LLM] 处理用户指令: 现在几点了
[Viktor 正在执行] 工具: get_current_time, 参数: {}
[LLM 回复] 当前系统时间是:2024-01-01 12:34:56
[TTS] 正在合成回复语音...
[TTS] 语音已保存至: /tmp/tmp67890.wav
通过日志,你可以清晰地看到语音识别结果、LLM 决定调用的工具及其参数、工具执行结果以及最终的回复文本。如果任何一步出错,日志将是首要的排查点。
7. 常见问题排查与优化
在实际部署和运行中,你可能会遇到以下问题。
7.1 语音识别相关
| 问题现象 | 可能原因 | 检查与解决方式 |
|---|---|---|
| 识别结果为空或乱码 | 1. 音频格式不支持。 2. 录音质量差(环境嘈杂、音量过低)。 3. Whisper 模型未正确加载。 |
1. 确保使用 gradio.Audio 组件,它已处理格式转换。 2. 在安静环境下清晰录音,检查麦克风是否正常。 3. 查看启动日志,确认 Whisper base 模型 加载成功。首次运行需联网下载。 |
| 识别速度非常慢 | 1. 使用的是 large 模型。 2. CPU 性能不足。 |
1. 在 speech_module.py 的 SpeechRecognizer 初始化时,将 model_size 改为 ”tiny” 或 ”base” 。 2. 考虑使用 GPU 加速(需安装对应版本的 PyTorch 和 CUDA)。 |
| 中文识别不准 | 1. 默认语言设置问题。 | 1. 在 transcribe_audio_file 方法中,已设置 language=”zh” 。确保你的 Whisper 版本支持中文。 |
7.2 大模型与智能体相关
| 问题现象 | 可能原因 | 检查与解决方式 |
|---|---|---|
报错 openai.APIConnectionError |
1. LLM 服务地址 ( LLM_BASE_URL ) 错误或服务未启动。 2. 网络不通。 |
1. 检查 .env 文件中的 LLM_BASE_URL ,确认 Ollama 等服务正在运行 ( ollama serve )。 2. 使用 curl http://localhost:11434/v1/models 测试端点是否可达。 |
| 模型不调用工具,直接回复“我无法操作” | 1. 系统提示词 ( system_prompt ) 未强调工具使用。 2. 工具描述 ( description ) 不够清晰。 3. 模型能力不足。 |
1. 强化 agent_engine.py 中的 system_prompt ,明确指示其可以使用工具。 2. 检查 tools.py 中每个工具的 description ,确保其清晰描述了功能和参数。 3. 尝试更换更强大的模型。 |
| 工具调用参数解析错误 | 1. 模型生成的参数 JSON 格式错误。 2. 参数类型与函数定义不匹配。 |
1. 查看日志中 [Viktor 正在执行] 后的参数,检查是否为合法 JSON。 2. 确保工具函数参数有合理的默认值或类型转换。在 agent_engine.py 的 tool_result 获取处添加更详细的异常捕获和日志。 |
| 对话历史混乱,导致后续回复异常 | 1. 历史消息管理出错。 2. 上下文过长。 |
1. 检查 agent_engine.py 中 conversation_history 的添加逻辑,确保 user 和 assistant 角色交替正确。 2. 实现历史消息截断或总结功能,防止超出模型上下文长度。 |
7.3 语音合成与界面相关
| 问题现象 | 可能原因 | 检查与解决方式 |
|---|---|---|
| 没有声音输出 | 1. pyttsx3 未找到可用的语音引擎。 2. 音频文件保存路径权限问题。 3. Gradio 音频组件无法播放生成的文件。 |
1. 在 SpeechSynthesizer 初始化后,打印 self.engine.getProperty(‘voice’) 检查可用语音。 2. 检查临时文件目录是否有写入权限。 3. 确认 audio_output 组件的 type 设置为 ”filepath” 。 |
| 界面卡死或无响应 | 1. ASR 或 TTS 在主线程中运行,阻塞了 Gradio。 2. LLM 调用超时。 |
1. 我们已经将 TTS 放在线程中运行。ASR 部分如果使用大模型也可能阻塞,考虑使用 gr.Queue 或异步函数。 2. 为 client.chat.completions.create 设置 timeout 参数。 |
| 清空对话后历史残留 | clear_conversation 函数未正确清理所有状态。 |
确保 clear_conversation 函数返回的值与界面组件的输出顺序一一对应,并且重置了 agent.conversation_history 。 |
7.4 性能与资源优化建议
- 模型轻量化 :在开发或资源受限环境,优先使用小模型。ASR 用
whisper-tiny,LLM 用 7B 甚至更小的模型。 - 服务分离 :将 ASR、LLM、TTS 部署为独立的微服务,通过 API 调用。这能提高整体稳定性和可扩展性。
- 异步处理 :将耗时的 ASR、LLM 推理、TTS 全部改为异步非阻塞调用,使用
asyncio和 Gradio 的gr.AsyncIterableQueue提升界面响应速度。 - 缓存与预热 :对于常用工具的结果(如天气),可以加入缓存机制。在应用启动时预热 ASR 和 TTS 模型。
- 上下文管理 :为
ViktorAgent实现上下文窗口管理,当对话轮数过多时,自动摘要或丢弃最早的历史,以节省 Token 并保持模型关注度。
8. 扩展方向与生产环境考量
当前实现是一个功能完整的原型。要将其转化为一个健壮的“AI 员工”,还需要在以下方面进行深化。
8.1 增强智能体能力
- 更多工具 :集成真实的 API,如日历、邮件、Jira、Confluence、企业内部系统等,让 Viktor 真正能处理办公任务。
- 复杂任务规划 :当前是简单的单次工具调用循环。对于“先查A,再根据A的结果查B”这类任务,需要增强规划能力,可以考虑采用 ReAct、Plan-and-Execute 等更高级的智能体框架(如 LangChain)。
- 记忆与知识库 :为 Viktor 添加长期记忆(向量数据库)和知识库(RAG),使其能基于历史对话和公司文档进行回答。
- 多模态能力 :除了语音,是否可以接收图片、文档?可以集成多模态大模型,让 Viktor 能“看”并理解图像和表格中的信息。
8.2 提升语音交互体验
- 流式识别与合成 :实现边说边识别的流式 ASR,以及边生成边播放的流式 TTS,减少等待感。
- 语音唤醒 :增加唤醒词(如“Hey Viktor”)检测功能,使其像智能音箱一样随时待命。
- 声纹识别 :识别不同用户的声音,提供个性化服务。
- 情绪识别 :从语音中分析用户情绪,调整回复语气。
8.3 生产环境部署要点
- 配置外置化 :将所有配置(模型路径、API密钥、超时时间)移至环境变量或配置中心,避免硬编码。
- 日志与监控 :接入结构化日志系统(如 JSON Logger),并添加关键指标监控(如请求延迟、ASR准确率、工具调用成功率)。
- 错误处理与降级 :对每一个外部依赖(ASR服务、LLM服务、工具API)设置超时、重试和熔断机制。当某个服务失败时,应有降级方案(例如,LLM服务不可用时,回复固定话术)。
- 安全与权限 :
- 输入过滤 :对用户输入进行严格的过滤和清洗,防止 Prompt 注入攻击。
- 工具权限 :为不同用户或角色分配不同的工具调用权限。例如,只有管理员才能执行“重启服务器”工具。
- 输出审查 :对 LLM 生成的回复内容进行安全审查,过滤不当信息。
- 可观测性 :记录每一次交互的完整链路(原始音频、识别文本、LLM请求与响应、工具调用记录、合成音频),便于问题回溯和效果分析。
- 版本管理与回滚 :对 Agent 的提示词、工具集、模型版本进行严格的版本控制,确保可以快速回滚到稳定版本。
通过以上步骤,你不仅搭建了一个可交互的语音 AI 智能体原型,更掌握了构建此类应用的核心模块与设计思路。从原型到产品,关键在于持续迭代工具能力、优化交互体验、并构建起支撑稳定运行的基础设施。
更多推荐



所有评论(0)