1. 项目概述:一个能听会做的语音AI助手

最近我花了不少时间,捣鼓了一个挺有意思的东西:一个完全由语音控制的AI智能体。简单来说,你对着它说话,它不仅能听懂,还能根据你的指令去执行具体的任务,比如创建一个文件、写一段代码、总结一段文字,或者就是简单地和你聊聊天。整个过程的流水线,从语音输入到最终结果,都会在一个简洁的网页界面上清晰地展示出来。这个项目的核心目标,就是想探索如何将前沿的AI能力(语音识别、大语言模型)与本地化的工具执行结合起来,打造一个既智能又实用的交互式应用。

对于开发者或者对自动化工具感兴趣的朋友来说,这个项目提供了一个很好的样板。它展示了如何将不同的API服务(语音转文本、大模型推理)串联成一个协同工作的“智能体”,并赋予它执行本地操作的能力。整个技术栈基于Python,用Streamlit快速搭建了交互界面,核心的AI能力则分别交给了Groq的语音识别API和OpenRouter的大模型API。选择云端API而非完全本地部署,主要是出于降低使用门槛和提升可移植性的考虑,毕竟不是每个人的电脑都适合跑大型模型。

2. 核心思路与技术选型解析

2.1 为什么选择“云端API + 本地执行”的架构?

在项目启动前,我面临一个关键抉择:是全部在本地部署,还是部分依赖云端服务?完全本地化的方案听起来很酷,比如使用开源的Whisper模型进行语音识别,再搭配Ollama来运行本地大语言模型。这样做的优势是数据完全不出本地,隐私性好,且没有网络延迟。但深入一想,问题也不少。

首先,本地语音识别(如Whisper)虽然强大,但往往需要依赖FFmpeg等额外的音视频处理库,在不同操作系统上的安装和配置可能成为新手的第一道门槛。其次,本地大语言模型对硬件(尤其是GPU显存)有较高要求,模型的下载、加载和推理速度在普通电脑上可能不尽如人意。更重要的是,这个项目的初衷之一是“易于复现和部署”,我希望其他开发者能最简单地把项目跑起来,而不是花半天时间在环境配置上。

因此,我最终采用了“云端API处理智能部分,本地程序处理执行部分”的混合架构。具体来说:

  • 语音转文本 (STT) :使用 Groq 的语音识别 API 。Groq 以其LPU(语言处理单元)硬件闻名,提供了极快的推理速度,能将语音实时、准确地转为文字,且API调用简单稳定。
  • 意图理解与内容生成 (LLM) :使用 OpenRouter API 。OpenRouter 是一个聚合了众多主流大模型(如Claude、GPT、Llama等)的平台,它提供了统一的接口。我主要用它来完成两项核心任务:一是分析用户文本,判断其意图(是想创建文件、写代码、总结还是聊天);二是根据意图,生成相应的内容(如代码片段、文本总结、对话回复)。
  • 本地执行与界面 :使用 Python Streamlit 。Streamlit 能让我用纯Python脚本快速构建出包含录音、文件上传、按钮和结果显示的Web应用。AI理解用户指令后,具体的文件创建、代码写入等操作,则由Python在本地执行。

这个架构的优势非常明显: 部署极其简单 。用户只需要准备Python环境、安装几个库、配置好API密钥,就能一键运行。它牺牲了极致的离线能力,换来了无与伦比的易用性和稳定性,非常适合作为原型验证或轻量级工具。

2.2 系统安全边界设计:把“笼子”先画好

让一个AI程序能在你的电脑上执行“创建文件”、“写入代码”这样的操作,听起来有点吓人。安全是首要考虑。我的核心原则是: 严格限制AI能操作的文件系统范围

为此,我在项目根目录下定义了一个名为 output/ 的专用文件夹。所有由AI智能体生成的文件,无论是Python脚本、文本笔记还是其他任何内容, 都只能被创建和写入到这个 output/ 文件夹及其子目录中 。程序逻辑上会进行强制约束,任何试图向 output/ 之外路径进行写入的操作都会被拦截或重定向。

这就好比给AI划了一个“沙箱”或“工作区”。无论它内部如何理解指令、生成内容,其所有对本地系统的写操作,都被限制在这个安全的围栏之内。这样一来,完全避免了它意外覆盖或删除用户重要系统文件的风险。在Web界面上,我也会明确告知用户所有生成物均在此目录下,让整个过程透明可控。

3. 核心模块拆解与实现细节

3.1 语音输入与转写模块

语音输入是整个流程的起点。我通过Streamlit提供了两种方式: 实时录音 音频文件上传 。Streamlit的 st.audio_input 组件可以直接在浏览器中调用麦克风进行录制,生成一个临时音频文件(通常是 .webm .wav 格式)。文件上传则使用 st.file_uploader ,支持常见的音频格式。

拿到音频文件后,下一步就是调用 Groq 的语音识别 API。这里有一个细节:Groq API 通常直接接收音频文件数据进行处理。我们需要用Python正确读取文件内容,并以二进制格式发送。我使用了 httpx 库进行异步请求,因为网络I/O是主要的耗时环节,异步能防止界面卡死。

import httpx
import os

async def transcribe_audio_with_groq(audio_file_path):
    """
    使用Groq API将音频文件转写成文字
    """
    api_key = os.getenv("GROQ_API_KEY")
    url = "https://api.groq.com/openai/v1/audio/transcriptions"

    headers = {
        "Authorization": f"Bearer {api_key}",
    }

    with open(audio_file_path, "rb") as audio_file:
        files = {
            "file": (os.path.basename(audio_file_path), audio_file, "audio/wav") # 注意根据实际格式调整MIME类型
        }
        data = {
            "model": "whisper-large-v3" # 指定Groq支持的语音识别模型
        }

        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(url, headers=headers, files=files, data=data)
            response.raise_for_status()
            result = response.json()
            transcribed_text = result.get("text", "").strip()
            return transcribed_text

注意: 临时音频文件在处理完毕后应立即删除,以释放磁盘空间。同时,要处理各种可能的异常,如网络超时、API额度不足、音频格式不支持等,并在界面上给出友好的错误提示。

3.2 意图识别与路由中枢

拿到转写后的文本,比如“帮我创建一个叫utils.py的文件,里面写一个计算斐波那契数列的函数”,AI需要先理解用户到底想干什么。这就是 意图识别(Intent Classification) 的任务。

我并没有训练一个专门的分类模型,而是利用了大语言模型(LLM)强大的零样本(zero-shot)理解能力。通过设计一个精准的 系统提示词(System Prompt) ,我让OpenRouter的模型充当了这个分类器。

这个提示词需要明确以下几点:

  1. 定义任务 :告诉模型你是一个意图分类器。
  2. 列出所有可能的意图类别 :我定义了四类: create_file (创建文件)、 write_code (写入代码)、 summarize_text (总结文本)、 general_chat (通用聊天)。
  3. 给出清晰的分类规则和例子 :这是最关键的部分,直接决定了分类的准确率。最初我的规则比较模糊,导致“创建并写入代码”的指令被错误地归类为 create_file 。后来我细化了规则:
    • create_file :仅当指令明确要求创建一个 空文件 或指定了文件名但 未提及文件具体内容 时。
    • write_code :当指令要求 在文件中写入、添加、生成或修改代码/特定内容 时,无论是否同时要求创建新文件。
    • summarize_text :当指令中出现“总结”、“概括”、“摘要”等关键词,且上下文提供了待总结的文本时。
    • general_chat :以上都不符合的普通对话、问答。
  4. 要求固定格式输出 :为了便于程序解析,我要求模型 必须 以JSON格式输出,且只包含 intent reason (简要说明分类理由)两个字段。
import json
import os

async def classify_intent_with_openrouter(user_text):
    """
    使用OpenRouter API进行意图分类
    """
    api_key = os.getenv("OPENROUTER_API_KEY")
    url = "https://openrouter.ai/api/v1/chat/completions"

    system_prompt = """
    你是一个意图分类器。请根据用户输入,判断其意图属于以下四类中的哪一类:
    1. create_file: 用户只想创建一个新的空文件,或指定了文件名但未提及文件具体内容。例如:“创建一个config.json文件”、“在output文件夹里新建一个log.txt”。
    2. write_code: 用户要求向文件中写入代码或特定文本内容。无论是否同时要求创建新文件。例如:“在utils.py里写一个排序函数”、“创建一个script.py,内容是用Flask启动一个web服务器”。
    3. summarize_text: 用户要求对一段文本进行总结或概括。例如:“总结一下上面这段话”、“把这段会议纪要浓缩成要点”。
    4. general_chat: 用户在进行普通对话、提问或请求帮助,不涉及上述具体操作。例如:“你好”、“Python里怎么连接数据库?”。

    请严格遵循以下规则:
    - 如果指令涉及“创建文件并写入内容”,优先归类为 `write_code`。
    - 只有在明确只创建空文件时,才用 `create_file`。
    - 如果用户提供了需要总结的文本,则归类为 `summarize_text`。

    你的输出必须是合法的JSON对象,且只包含两个键:`intent` (取上述四个值之一) 和 `reason` (用一句话简要说明分类理由)。
    """

    payload = {
        "model": "meta-llama/llama-3.1-8b-instruct:free", # 示例模型,可根据需要和预算更换
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_text}
        ],
        "response_format": {"type": "json_object"} # 强制要求JSON输出
    }

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }

    async with httpx.AsyncClient(timeout=60.0) as client:
        response = await client.post(url, json=payload, headers=headers)
        response.raise_for_status()
        result = response.json()
        # 解析模型返回的JSON内容
        content_str = result["choices"][0]["message"]["content"]
        try:
            intent_data = json.loads(content_str)
            return intent_data.get("intent"), intent_data.get("reason")
        except json.JSONDecodeError:
            # 如果模型没有返回合法JSON,则降级处理
            return "general_chat", "无法解析意图,转入通用聊天模式。"

3.3 动作执行器:从意图到结果

识别出意图后,就进入了执行阶段。这是一个典型的“路由器(Router)”模式,根据不同的 intent 值,调用相应的执行函数。

1. create_file 执行器 这个相对简单。需要从用户指令中提取出目标文件名和路径(确保路径在 output/ 下)。可以使用简单的关键词匹配或再次借助LLM进行少量信息提取。创建文件时,使用 open(file_path, 'w') 即可,创建空文件实际上就是打开后立即关闭。

2. write_code 执行器 这是最复杂的部分。它需要完成两个子任务:

  • 内容生成 :再次调用OpenRouter API,但这次是代码生成或文本编写模式。需要设计新的提示词,例如:“你是一个资深Python程序员,请根据以下要求生成代码:[用户指令]。只输出代码本身,无需任何解释。”
  • 文件写入 :将生成的内容写入到指定文件。这里要特别注意路径安全校验,确保最终写入路径位于 output/ 目录内。可以使用 os.path.abspath os.path.commonprefix 进行检查。

3. summarize_text 执行器 用户指令中可能包含了待总结的文本,或者需要结合上下文(例如之前对话中用户粘贴的文本)。执行器需要提取出待总结的文本内容,然后调用OpenRouter的总结功能。提示词可以设计为:“请用简洁的语言总结以下文本的核心内容:[待总结文本]”。

4. general_chat 执行器 这是最通用的模式,直接将用户输入和历史对话上下文(如果有)发送给OpenRouter的聊天模型,获取一个自然、友好的回复。

所有执行器的结果,无论是创建成功的确认信息、生成的代码内容、总结的文本还是聊天回复,都会被统一收集,准备返回给前端界面展示。

3.4 用户界面与流程展示

Streamlit 让界面构建变得异常简单。我的应用界面布局如下:

  1. 标题与说明 :清晰介绍应用功能和安全边界(所有文件仅生成于output/目录)。
  2. 语音输入区 :并排放置 st.audio_input (录音)和 st.file_uploader (上传)组件。
  3. 控制按钮 :一个“开始处理”按钮,用于触发整个流程。
  4. 流水线展示区 :这是UI的核心,我使用 st.expander 或简单的标题加文本框,分步骤展示:
    • 步骤1:转写文本 :显示从Groq API返回的原始文字。
    • 步骤2:识别意图 :显示分类结果( intent )和模型给出的理由( reason )。
    • 步骤3:执行动作 :显示正在执行哪个动作,例如“正在创建文件:output/utils.py”。
    • 步骤4:最终结果 :展示执行后的输出。如果是写代码,可以高亮显示代码块;如果是总结,显示总结文本;如果是聊天,显示回复。同时,提供生成文件的路径链接(使用 st.markdown 生成可点击的链接)。

整个流程是线性的、同步的(用 asyncio 管理异步调用),用户点击按钮后,界面会按顺序更新每一步的结果,给人一种清晰的“流水线”可视化体验。

4. 关键技术挑战与解决方案实录

4.1 意图分类的模糊边界问题

如前所述,意图分类是第一个拦路虎。最初的简单提示词导致了许多误判。例如,“给我写一个快速排序的Python文件”这种指令,模型有时会分类为 create_file ,因为它看到了“文件”这个词。

我的解决方案是进行“提示词工程”迭代:

  1. 明确优先级 :在系统提示词中明确指出,当“创建文件”和“写入内容”同时出现时, 优先归类为 write_code 。这更符合用户的实际意图——用户要的不是空文件,而是带有内容的文件。
  2. 提供对比示例 :在提示词中加入正反例。
    • 正例( write_code ):“创建一个叫 hello.py 的文件,打印‘Hello World’。”
    • 反例( create_file ):“在output目录下新建一个空的 data.json 文件。”
  3. 要求说明理由 :强制模型输出 reason 字段,这不仅能帮助我调试,有时模型在组织理由的过程中,会进行更严谨的逻辑推理,从而提升分类准确率。

经过3-5轮的提示词调整和测试,分类准确率得到了显著提升。这让我深刻体会到,在大模型应用中, 提示词不是魔法咒语,而是精确的工程说明书 。你需要像调试代码一样,不断调整提示词的措辞、结构和示例。

4.2 安全性与错误处理强化

1. 路径遍历攻击防护: 尽管我们将基础目录限制在 output/ ,但用户指令中仍可能包含类似 ../../../etc/passwd 的恶意路径。因此,在执行任何文件操作前,必须进行规范化( os.path.normpath )和绝对路径检查,确保目标路径的公共前缀( os.path.commonprefix )确实是 output/ 目录的绝对路径。

import os

def is_safe_path(target_path, base_directory):
    """检查目标路径是否在基础目录内"""
    # 转换为绝对路径
    base_dir = os.path.abspath(base_directory)
    target_dir = os.path.abspath(os.path.join(base_dir, target_path))

    # 检查目标路径是否以基础目录开头
    return os.path.commonprefix([base_dir, target_dir]) == base_dir

# 使用示例
requested_file = “../../敏感文件.txt” # 模拟恶意输入
safe_base = “./output”
if not is_safe_path(requested_file, safe_base):
    st.error(“文件路径不安全,操作被拒绝。”)
    return

2. API密钥管理与错误处理: 使用 python-dotenv .env 文件加载 GROQ_API_KEY OPENROUTER_API_KEY 。务必在 .gitignore 中加入 .env ,防止密钥意外提交到代码仓库。对于API调用,必须用 try-except 块包裹,处理网络异常、认证失败、额度不足、服务器错误等各种情况,并在UI上给出明确而非崩溃的提示。

3. 音频文件处理: 用户上传的音频文件格式五花八门。虽然Groq API支持常见格式,但最好在前端或后端进行初步校验。可以使用 librosa pydub 库尝试加载文件,如果失败则提前报错,避免将无法处理的文件发送给API。

4.3 性能与用户体验优化

1. 异步编程: 语音识别和LLM调用都是网络I/O密集型操作,同步等待会导致Streamlit界面“假死”。使用 asyncio httpx.AsyncClient 将这两个主要耗时节改为异步并发执行,可以大幅缩短总等待时间。在Streamlit中,需要配合 asyncio.run() 或专门的异步支持来运行这些协程。

2. 流式输出与中间状态: 对于耗时较长的操作(如生成一大段代码),可以考虑使用Streamlit的 st.status 组件或动态更新的文本区域,向用户展示“正在转写...”、“正在生成代码...”等中间状态,提升交互感。

3. 会话状态管理: 利用 st.session_state 来存储对话历史、上一次的识别结果等。这样用户可以在不刷新页面的情况下进行多轮交互,例如基于上一轮生成的文件继续提问,让智能体更像一个连贯的助手。

5. 项目复盘与扩展思考

5.1 从“调用API”到“构建智能体”的认知转变

这个项目看似是几个API的拼接,但实际做下来,我最大的收获是对“智能体(Agent)”有了更落地的理解。它远不止是调用一个模型那么简单。核心工作在于 流程编排 安全管控

你需要设计一个稳健的管道(Pipeline),让数据(语音->文本->意图->指令->结果)在其中顺畅、准确地流动。每一个环节都可能出错:语音转写可能有误、意图可能识别偏差、生成的代码可能有语法错误。因此,必须在每个环节加入校验、错误处理和回退机制(例如,意图识别失败时,默认转入聊天模式询问用户澄清)。

此外,智能体需要有“记忆”和“状态”。简单的实现可以用 session_state 存储几轮对话历史,让LLM在生成内容时能有上下文。更复杂的还可以为智能体设计“工具(Tools)”集合,除了文件操作,未来还可以集成查询网络、执行系统命令(需极度谨慎)、调用其他API等,让它的能力圈不断扩大。

5.2 可扩展性与未来方向

目前这个项目是一个功能完整的最小可行产品(MVP)。在此基础上,有很多可以扩展的方向:

  • 多模态输入 :除了语音,是否可以支持直接上传图片、截图,让AI识别图片中的需求并执行?例如,截图一个网页,让AI根据截图写前端代码。
  • 更复杂的工具集 :集成 subprocess 在安全沙箱中运行生成的代码并返回结果;集成 requests 库让AI能获取网络信息;集成数据库操作等。
  • 本地模型集成 :为追求隐私和离线的用户,提供可配置的选项。例如,检测到本地安装了Ollama,则优先使用本地模型,否则回退到OpenRouter。这可以通过一个配置层来实现。
  • 意图识别升级 :当前基于提示词的零样本分类虽然方便,但在复杂指令下仍有局限。可以收集一批指令样本,进行微调(fine-tuning),得到一个更专、更准的小型分类模型,降低成本并提升速度。
  • 部署与分享 :Streamlit应用可以非常方便地部署到Streamlit Community Cloud、Hugging Face Spaces或任何支持Python的云服务器上,生成一个公开链接,让其他人也能直接体验你的语音AI助手。

5.3 给复现者的几点实操建议

如果你也想动手实现一个类似的语音AI智能体,以下是我踩过坑后的经验:

  1. 从最简单的流程开始 :先别急着做UI或处理语音。先用脚本实现最核心的链路:一段硬编码的文本 -> 调用OpenRouter识别意图 -> 根据意图打印不同结果。确保这个逻辑跑通。
  2. 分而治之,逐个测试 :语音识别、意图分类、代码生成、文件操作,每个模块单独写测试脚本,确保其独立工作正常,再组合起来。
  3. 重视提示词设计 :把提示词当作重要的配置文件来管理。可以把它放在单独的 prompts.py 文件或甚至JSON/YAML配置里,方便修改和版本管理。多测试不同场景下的指令,反复迭代提示词。
  4. 安全第一 :在早期就引入路径安全检查、API密钥管理机制。不要等到功能都实现了再补,容易遗漏。
  5. 利用Streamlit的快速迭代能力 :Streamlit的魅力在于修改代码后保存,页面实时刷新。充分利用这一点,快速调整UI布局和交互逻辑。

构建这样一个项目,最有成就感的一刻,莫过于对着麦克风说一句话,看着网页上流水线一步步执行,最终在本地文件夹里生成一个完全符合你要求的文件。它把看似遥远的AI能力,变成了一个听得懂、做得快的个人助手。这个过程本身,就是对当前AI应用开发模式一次绝佳的实践。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐