在实际项目中,将 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。为了本地运行,你有两个主流选择:

  1. 使用在线 API(简单,需网络和费用) :如 OpenAI GPT, Anthropic Claude 等。你只需要一个 API Key。
  2. 本地部署开源模型(复杂,无需网络) :如使用 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 功能测试

按照界面说明进行测试:

  1. 语音指令测试 :点击录音按钮,清晰地说出“现在几点了?”,然后点击“发送语音指令”。观察过程:

    • 界面“识别出的文本”区域应显示识别出的文字。
    • “Viktor 的回复”区域应显示类似“当前系统时间是:2024-01-01 12:34:56”的文本。
    • “Viktor 的语音回复”区域会出现一个音频播放器,点击即可听到语音播报。
    • “对话历史”区域会记录这次完整的交互。
  2. 文字指令测试 :在文本框中输入“计算 125 除以 5 的平方”,点击“发送文字指令”。回复应为“计算结果:125 / 5 ** 2 = 5.0”。

  3. 复杂指令测试 :尝试“查询一下北京和上海的天气,然后告诉我现在伦敦的时间(模拟)”。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 性能与资源优化建议

  1. 模型轻量化 :在开发或资源受限环境,优先使用小模型。ASR 用 whisper-tiny ,LLM 用 7B 甚至更小的模型。
  2. 服务分离 :将 ASR、LLM、TTS 部署为独立的微服务,通过 API 调用。这能提高整体稳定性和可扩展性。
  3. 异步处理 :将耗时的 ASR、LLM 推理、TTS 全部改为异步非阻塞调用,使用 asyncio 和 Gradio 的 gr.AsyncIterableQueue 提升界面响应速度。
  4. 缓存与预热 :对于常用工具的结果(如天气),可以加入缓存机制。在应用启动时预热 ASR 和 TTS 模型。
  5. 上下文管理 :为 ViktorAgent 实现上下文窗口管理,当对话轮数过多时,自动摘要或丢弃最早的历史,以节省 Token 并保持模型关注度。

8. 扩展方向与生产环境考量

当前实现是一个功能完整的原型。要将其转化为一个健壮的“AI 员工”,还需要在以下方面进行深化。

8.1 增强智能体能力

  1. 更多工具 :集成真实的 API,如日历、邮件、Jira、Confluence、企业内部系统等,让 Viktor 真正能处理办公任务。
  2. 复杂任务规划 :当前是简单的单次工具调用循环。对于“先查A,再根据A的结果查B”这类任务,需要增强规划能力,可以考虑采用 ReAct、Plan-and-Execute 等更高级的智能体框架(如 LangChain)。
  3. 记忆与知识库 :为 Viktor 添加长期记忆(向量数据库)和知识库(RAG),使其能基于历史对话和公司文档进行回答。
  4. 多模态能力 :除了语音,是否可以接收图片、文档?可以集成多模态大模型,让 Viktor 能“看”并理解图像和表格中的信息。

8.2 提升语音交互体验

  1. 流式识别与合成 :实现边说边识别的流式 ASR,以及边生成边播放的流式 TTS,减少等待感。
  2. 语音唤醒 :增加唤醒词(如“Hey Viktor”)检测功能,使其像智能音箱一样随时待命。
  3. 声纹识别 :识别不同用户的声音,提供个性化服务。
  4. 情绪识别 :从语音中分析用户情绪,调整回复语气。

8.3 生产环境部署要点

  1. 配置外置化 :将所有配置(模型路径、API密钥、超时时间)移至环境变量或配置中心,避免硬编码。
  2. 日志与监控 :接入结构化日志系统(如 JSON Logger),并添加关键指标监控(如请求延迟、ASR准确率、工具调用成功率)。
  3. 错误处理与降级 :对每一个外部依赖(ASR服务、LLM服务、工具API)设置超时、重试和熔断机制。当某个服务失败时,应有降级方案(例如,LLM服务不可用时,回复固定话术)。
  4. 安全与权限
    • 输入过滤 :对用户输入进行严格的过滤和清洗,防止 Prompt 注入攻击。
    • 工具权限 :为不同用户或角色分配不同的工具调用权限。例如,只有管理员才能执行“重启服务器”工具。
    • 输出审查 :对 LLM 生成的回复内容进行安全审查,过滤不当信息。
  5. 可观测性 :记录每一次交互的完整链路(原始音频、识别文本、LLM请求与响应、工具调用记录、合成音频),便于问题回溯和效果分析。
  6. 版本管理与回滚 :对 Agent 的提示词、工具集、模型版本进行严格的版本控制,确保可以快速回滚到稳定版本。

通过以上步骤,你不仅搭建了一个可交互的语音 AI 智能体原型,更掌握了构建此类应用的核心模块与设计思路。从原型到产品,关键在于持续迭代工具能力、优化交互体验、并构建起支撑稳定运行的基础设施。

更多推荐