在本教程中,您将构建一个可用于生产环境的语音代理架构:该架构包括一个通过WebRTC(Web实时通信)传输音频的浏览器客户端、一个用于生成短期会话令牌的后端服务、以及一个能够安全地协调语音处理及相关工具运行的代理运行时系统;此外,该架构还能为后续的工作流程生成必要的后调用处理结果。

本文刻意保持中立立场,不偏袒任何特定供应商。您可以使用任何支持WebRTC技术(无论是直接支持还是通过SFU选路单元实现)以及服务器端令牌生成功能的人工智能语音平台来实施这些设计模式。我们的目标是帮助您构建出一个在安全性、可观测性及可用性方面都符合生产环境要求的语音代理架构。

声明:本文仅反映作者的个人观点和经验,并不代表我的雇主或文中提到的任何供应商的观点。

目录

  • 您将构建什么

  • 如何避免语音代理在生产环境中出现的常见故障

  • 如何为实时语音代理设计延迟控制方案

  • 中立供应商视角下的生产环境语音代理架构

    • 生产环境准备检查清单

    • 对话过程中,用户能感受到某种“节奏感”,而这种节奏感在很大程度上是由延迟决定的。

      以下是一些实用的经验法则:

      • 当延迟低于约200毫秒时,用户会感觉响应非常迅速

      • 当延迟在300至500毫秒之间时,系统仍能被认为具备良好的响应能力

      • 当延迟超过约700毫秒时,用户就会觉得使用体验很糟糕

      你的端到端延迟实际上是由麦克风采集时间、网络传输时间、语音转文本处理时间、模型推理耗时、辅助工具执行时间、文本转语音功能耗时以及播放缓冲时间共同叠加而成的。必须为这些环节设定合理的阈值,否则即使系统在技术上没有问题,用户也会觉得它不够智能。

      如何设计一个适用于生产环境的语音助手架构(厂商中立型设计)

      可投入生产的语音助手架构示意图,包括Web客户端、令牌服务、WebRTC实时传输层、语音助手运行时模块、辅助工具层以及通话后处理功能。

      一个可扩展的语音代理架构通常包含以下层次:

      1. Web客户端:负责麦克风音频采集、音频播放以及用户界面状态的显示

      2. 令牌服务:用于生成短期有效的会话令牌(敏感信息始终存储在服务器端)

      3. 实时通信层:包含WebRTC媒体流及数据传输通道

      4. 代理运行时模块:负责文本转语音、逻辑推理等功能,以及各工具组件的协调运行

      5. 工具层:用于执行需要安全控制的外部操作

      6. 通话后处理模块:在会话结束后生成总结信息及结构化输出结果

      这种层次划分能够明确各功能模块之间的故障隔离范围与信任边界。

      步骤0:创建项目目录

      首先创建一个新的项目文件夹:

      mkdir voice-agent-app
      cd voice-agent-app
      npm init -y
      npm pkg set type=module
      npm pkg set scripts.start="node server.js"
      

      然后安装所需的依赖包:

      npm install express dotenv
      

      接下来建立文件结构:

      voice-agent-app/
      ├── server.js
      ├── .env
      └── public/
          ├── index.html
          └── client.js
      

      最后添加一个.env配置文件:

      VOICEPLATFORM_URL=https://your-provider.example
      VOICE_platform_API_KEY=your_api_key_here
      

      现在你可以开始实现系统的各个部分了。

      步骤2:将敏感信息存储在服务器端

      安全信任边界示意图,显示浏览器属于不可信区域,而后端工具属于可信区域,敏感信息保存在服务器端。

      请将所有API密钥视为重要的敏感信息来处理:

      • 将其存储在环境变量或专门的秘密管理工具中

      • 如果这些密钥被泄露,应及时更换

      • 绝不要将它们嵌入浏览器或移动应用中

      • 避免记录这些敏感信息(必要时仅记录其简短的后缀部分)

      • 即使供应商支持CORS功能,浏览器也不适合作为存储长期有效凭证的安全场所。

        步骤3:构建后端令牌服务端点

        你的后端系统应该做到以下几点:

        • 对用户进行身份验证

        • 使用平台提供的API生成短期会话令牌

        • 仅返回客户端所需的信息(包括URL、令牌以及有效期)

        创建server.js文件(使用Node.js和Express框架)

        import express from "express";
        import dotenv from "dotenv";
        import path from "path";
        import { fileURLToPath } from "url";
        
        dotenv.config();
        
        const app = express();
        app.use(express.json());
        
        // 从/public目录提供Web客户端服务
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = path.dirname(__filename);
        app.use(express.static(path.join(__dirname, "public)));
        
        const VOICEPLATFORM_URL = process.env.VOICE_platform_URL;
        const VOICE PLATFORM_API_KEY = process.env.VOICE_PLATFORM_API_KEY;
        
        app.post("/api/voice-token", async (req, res) => {
          res.setHeader("Cache-Control", "no-store");
        
          try {
            if (!VOICEPlatform_URL || !VOICEPLATFORM_API_KEY) {
              return res.status(500).json({
                error: "配置文件中缺少VOICE_platform_URL或VOICEPLATFORM_API_KEY",
              });
            }
        
            // 在生成令牌之前,需要对请求者进行身份验证
        
            const r = await fetch(`${VOICE PLATFORM_URL}/api/v1/token`, {
              method: "POST",
              headers: {
                "X-API-Key": VOICEPlatform_API_KEY,
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ participant_name: "Web User" }),
            });
        
            if (!r.ok) {
              const detail = await r.text().catch(() => "");
              return res.status(r.status).json({ error: "令牌请求失败,详细原因如下:", detail });
            }
        
            const data = await r.json();
        
            res.json({
              rtc_url: data.rtc_url || data.livekit_url,
              token: data.token,
              expires_in: dataexpires_in,
            });
          } catch (err) {
            res.status(500).json({ error: "生成令牌时出现错误" });
          }
        });
        
        app.listen(3000, () => console.log("服务器已启动,访问地址为http://localhost:3000");
        

        运行服务器

        npm start

        然后打开以下地址:http://localhost:3000

        这段代码的运作原理

        • 您从环境变量中获取认证信息,因此这些秘密数据永远不会进入浏览器。

        • 端点/api/voice-token会调用语音平台的令牌API。

        • 系统仅返回rtc_urltoken以及有效期信息。

        • 浏览器根本看不到API密钥。

        • 如果服务提供方返回错误信息,系统会返回一个结构化的错误响应。

        生产环境注意事项

        • 对接口/api/voice-token实施速率限制(用于控制成本及防止滥用)。

        • 记录令牌生成的延迟时间以及错误率。

        • 将令牌的有效期设置得较短,并妥善处理刷新或重新连接的操作。

        • 仅返回必要的字段信息。

        步骤3:通过Web客户端进行连接(使用WebRTC与SFU技术)

        在这一步中,您需要构建一个简单的Web用户界面,该界面能够:

        • 向后端请求一个有效期较短的令牌。

        • 连接到实时WebRTC房间(通常通过SFU中继实现)。

        • 播放代理方的音频文件。

        • 捕获并发送麦克风采集的音频数据。

        创建文件public/index.html

        <!doctype html>
        <html>
          语音代理演示
          语音代理演示
        
            
            
        
            

        >空闲状态