为你的 AI 代理添加语音:一种框架无关的集成模式

发布: (2025年12月30日 GMT+8 04:34)
15 min read
原文: Dev.to

Source: Dev.to

为你的 AI 代理添加语音:框架无关的集成模式

简介

在过去的几年里,文本交互已经成为大多数 AI 代理的默认方式。然而,语音交互能够提供更自然、更直观的用户体验,尤其是在移动设备、智能音箱或无障碍场景中。本文将展示一种 框架无关 的集成模式,帮助你在任何现有的 AI 代理上快速叠加语音能力,而无需对底层业务逻辑进行大幅改动。

为什么要加入语音?

  • 自然性:人类天生擅长口头交流,语音可以降低学习成本。
  • 可达性:对视力受限或手部受限的用户尤为重要。
  • 多任务:用户可以在做其他事情的同时与代理对话(例如开车、烹饪)。

架构概览

+-------------------+      +-------------------+      +-------------------+
|   前端 (Web / App) | ---> |   语音服务层      | ---> |   AI 代理 (LLM)   |
+-------------------+      +-------------------+      +-------------------+
        |                         |                         |
        | 1. 捕获音频              | 2. 语音转文本 (STT)      | 3. 文本处理
        |                         |                         |
        |                         | 4. 文本转语音 (TTS)      |
        |                         |                         |
        +------------------------->+------------------------->+
  1. 捕获音频:使用浏览器或原生 SDK 获取用户的语音输入。
  2. 语音转文本 (STT):将音频流发送到云端或本地的 Speech‑to‑Text 服务。
  3. 文本处理:将转写的文本传递给已有的 AI 代理(例如 ChatGPT、Claude、Llama 等)。
  4. 文本转语音 (TTS):将代理的回复文本转换为音频并返回给前端播放。

步骤详解

步骤 1:捕获音频

在前端使用 MediaRecorder(Web)或对应平台的音频采集 API。关键是把音频 实时分块 发送到后端的 STT 服务。

// 示例:使用 MediaRecorder 捕获音频
navigator.mediaDevices.getUserMedia({ audio: true })
  .then(stream => {
    const recorder = new MediaRecorder(stream);
    recorder.start();
    // 将数据块推送到后端...
  });

注意:如果你使用的是移动端框架(React Native、Flutter 等),请使用对应的音频插件,保持 API 接口一致即可。

步骤 2:语音转文本 (STT)

选择一个 框架无关 的 STT 提供商,例如:

  • Google Cloud Speech‑to‑Text
  • Azure Speech Service
  • OpenAI Whisper (自托管)

只需要把音频流或文件 POST 到相应的 REST 接口,返回的 JSON 中会包含转写文本。

POST https://api.openai.com/v1/audio/transcriptions
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_KEY

--boundary
Content-Disposition: form-data; name="file"; filename="audio.wav"
Content-Type: audio/wav

<binary data>
--boundary--

步骤 3:将文本发送给 AI 代理

这一步保持原有的调用方式不变。只需要把 STT 返回的 transcript 作为 用户消息 发送给 LLM。

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": transcript}
    ]
)
reply_text = response.choices[0].message.content

步骤 4:文本转语音 (TTS)

同样可以选用任意云服务或本地模型:

  • Google Cloud Text‑to‑Speech
  • Azure Speech Service
  • ElevenLabs
  • Coqui TTS (开源)

返回的音频文件(通常是 mp3wav)直接发送回前端。

POST https://api.elevenlabs.io/v1/text-to-speech/voice-id
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "text": "Hello, how can I help you today?",
  "model_id": "eleven_monolingual_v1"
}

步骤 5:在前端播放音频

收到音频后,使用 HTML5 <audio> 或原生播放器播放即可。

const audio = new Audio(urlFromBackend);
audio.play();

完整示例代码

下面提供一个 Node.js + Express 的最小实现,演示从前端接收音频、调用 Whisper、调用 OpenAI ChatGPT、调用 ElevenLabs 并返回音频的完整流程。代码块保持原样,不做翻译

// server.js
const express = require('express');
const multer = require('multer');
const fetch = require('node-fetch');
const FormData = require('form-data');

const upload = multer({ dest: 'uploads/' });
const app = express();
app.use(express.json());

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;

app.post('/voice', upload.single('audio'), async (req, res) => {
  // 1. STT - Whisper
  const audioFile = req.file.path;
  const form = new FormData();
  form.append('file', require('fs').createReadStream(audioFile));
  form.append('model', 'whisper-1');

  const sttResp = await fetch('https://api.openai.com/v1/audio/transcriptions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${OPENAI_API_KEY}` },
    body: form,
  });
  const { text: transcript } = await sttResp.json();

  // 2. LLM
  const chatResp = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: transcript }],
    }),
  });
  const { choices } = await chatResp.json();
  const reply = choices[0].message.content;

  // 3. TTS - ElevenLabs
  const ttsResp = await fetch(
    `https://api.elevenlabs.io/v1/text-to-speech/${process.env.ELEVENLABS_VOICE_ID}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'xi-api-key': ELEVENLABS_API_KEY,
      },
      body: JSON.stringify({ text: reply, model_id: 'eleven_monolingual_v1' }),
    }
  );

  const audioBuffer = await ttsResp.buffer();
  res.set('Content-Type', 'audio/mpeg');
  res.send(audioBuffer);
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

提示:如果你使用的是 FastAPIDjango 或其他后端框架,只需要把上述逻辑迁移到对应的路由处理函数中即可,核心思路保持不变。

关键要点回顾

步骤关键技术可选实现
捕获音频浏览器 MediaRecorder / 原生 SDKWeb、React Native、Flutter
STTWhisper、Google Speech、Azure Speech云端或自托管
LLM 交互OpenAI、Claude、Llama 等保持原有调用方式
TTSElevenLabs、Google TTS、Coqui返回 mp3/wav
播放音频HTML5 <audio> / 原生播放器前端即刻播放

结论

通过上述 框架无关 的四层(捕获 → STT → LLM → TTS)模式,你可以在几乎任何现有的 AI 代理上快速叠加语音功能,而不必重写业务逻辑。只要替换或添加对应的 STT/TTS 服务即可实现高度可定制的语音交互体验。

如果你对特定平台(如 UnityUnreal)或 离线部署 有需求,只需要把相应的 STT/TTS 替换为本地模型,整体架构仍然保持不变。祝你玩得开心,构建出更具沉浸感的 AI 代理!

介绍

如果你最近一直在构建 AI 代理,你可能已经注意到一件有趣的事:大家都在讨论 PydanticAILangChainLlamaIndex,却几乎没有人在谈论如何在不将整个架构耦合到单一语音提供商的情况下添加语音功能。如果你稍微想一想,这就是一个大问题。

我们在 Sayna 正在应对这个确切的挑战,我想分享一些关于为什么抽象模式比你选择的框架或提供商更重要的想法。

Source:

语音集成的真实问题

描述情境,例如:

你有一个使用文本输入输出的 AI 代理——完全没问题。也许你使用 PydanticAI 因为你喜欢类型安全,或者使用 LangChain 因为你的团队已经熟悉它,亦或是因为现有框架不符合你的需求而自行构建了东西。所有这些都运行良好。

但是 有人随后问:“我们能给它加上语音吗?”于是你立刻进入了一个完全不同的问题世界。

一旦开始集成语音,你不仅仅是在添加 TTS 和 STT:你还在引入

  • 延迟要求,
  • 流式处理的复杂性,
  • 供应商特定的 API,以及
  • 全新的一层基础设施关注点。

我接触的大多数开发者都会犯同样的错误:他们挑选一个 TTS 供应商(比如因为音质好而选 ElevenLabs),一个 STT 供应商(也许因为是 OpenAI 的 Whisper),直接把它们接入自己的代理,然后认为工作完成。六个月后他们发现 ElevenLabs 的定价无法支撑他们的规模,或者 Whisper 的延迟对实时对话来说太高,必须重写代码库的大量部分。

这正是我们过去在云供应商身上看到的供应商锁定问题,而现在它正以更快的速度在 AI 服务中重演。

为什么框架无关性很重要

这里有件可能会让你惊讶的事:PydanticAILangChainLlamaIndex 对语音处理都有不同的做法,但它们都没有真正解决语音层面的抽象问题。

  • 它们能够很好地抽象 LLM 调用,但在语音处理方面,你基本上只能自己搞定。
  • LangChain 允许你将组件串联起来:你自行提供 STT(语音转文本)函数、TTS(文本转语音)函数,并将它们连接成顺序链。这很灵活,但抽象的负担落在你身上——每次想更换供应商时,都需要修改链的逻辑。
  • PydanticAI 仍未提供原生语音支持(对此已有未解决的 issue),这意味着开发者需要在其上自行构建定制方案——抽象的责任同样落在你身上。

重点并不是这些框架不好;它们在各自擅长的领域表现出色。关键在于语音是一个独立的层面,把它仅仅当作代理工具箱中的另一个工具,就会忽视更大的全局。

实际需要的抽象模式

当我思考 AI 代理的语音集成时,我看到三个不同的层次:

  1. 你的代理逻辑

    • 这里是 PydanticAI、LangChain 或你自定义方案所在的地方。
    • 它负责思考、工具调用、记忆以及所有智能部分。
    • 它不应该了解任何音频格式、语音合成或转录模型。
  2. 语音抽象层

    • 位于你的代理与实际语音提供商之间。
    • 处理音频流、管理 WebSocket 连接、语音活动检测,最重要的是将各提供商的特定 API 抽象为统一接口。
  3. 语音提供商

    • 实际的 TTS 与 STT 服务:OpenAI、ElevenLabs、Deepgram、Cartesia、AssemblyAI、Google、Amazon Polly … 还有更多。
    • 每个服务都有不同的优势、计费模式、延迟特性以及 API 细节。

关键洞见: 你的代理逻辑应仅与语音抽象层交互,绝不能直接调用提供商。这样,从 ElevenLabs 切换到 Cartesia 只需要更改配置,而不必改写代码。

实际考虑

在你构建几个语音代理之前,有些事情并不明显:

  • 延迟叠加是真实存在的。
    当你将 STT → LLM → TTS 串联时,每毫秒都会累加。用户会在响应时间超过约 500 ms 时察觉到延迟。这意味着你需要在每个阶段都进行 TTS 合成,而不仅仅在 LLM 完成后再合成。你的语音层必须在代理生成完整响应之前就开始 TTS 合成——如果你直接集成各提供商,这是一项非平凡的实现工作。

  • 提供商特性差异极大。

    • 有些 TTS 提供商听起来更自然,但延迟更高。
    • 有些 STT 提供商对口音的适应性更好,却在技术术语上表现欠佳。
    • 有些在高质量音频上表现出色,但在电话线路(8 kHz PSTN 音频与网络音频差别很大)上就会失效。
      能够在不修改代码的情况下切换提供商不仅是便利,更是生产系统的必需。
  • 语音活动检测比想象的更难。
    判断用户何时开始和结束说话、优雅地处理中断以及过滤背景噪音都是已有的解决方案——前提是你使用了合适的工具。从零开始构建这些功能,同时又要开发代理逻辑,极易导致倦怠。

多供应商优势

如果您从一开始就为语音集成设计多供应商支持,您将获得若干可能并不明显的优势:

  • 可以实现成本优化。
    不同供应商的计费模式各不相同:有的按字符计费,有的按分钟计费,还有的提供批量折扣。当您能够轻松切换供应商时,就可以将不同类型的对话路由到最具成本效益的服务。

  • 可靠性得到提升。
    供应商会出现宕机。如果您的语音层支持多个供应商,就可以实现回退逻辑。例如,当 ElevenLabs 出现停机时,您可以自动回退到 Cartesia 或其他 TTS 服务,而不会破坏用户体验。

  • 为未来功能提供灵活性。
    新的能力(例如情感感知 TTS、低延迟流式 STT)可以通过接入新供应商来采用,而无需重写大量堆栈代码。

  • 合规性与地区性要求。
    某些司法辖区要求数据保留在特定的地理边界内。多供应商抽象层让您可以将音频路由到符合当地合规要求的供应商,而无需触及核心代理代码。

结论

将语音视为 AI 架构中的 一等、独立层。构建一个 语音抽象层,使您的代理逻辑免受底层供应商变化的影响。

Back to Blog

相关文章

阅读更多 »