图片来源:latent space
Z highlights
-
OpenAI的实时API支持低延迟、双向音频流,使得多模态AI应用(如语音对话Agent)得以实现。它通过WebSocket连接管理对话状态,并提供短语结束检测和语音活动检测(VAD)功能,大大简化了实时语音应用的开发。 -
语音到语音的延迟目标是500-800毫秒,但实际实现中延迟可能会受到网络状况和音频处理的影响。OpenAI实时API提供了高效的推理性能,但开发者需关注因素如音频压缩和网络丢包对延迟的影响,并在生产环境中使用WebRTC以降低延迟。 -
实时API支持函数调用和上下文管理,简化了多轮对话中的对话历史管理。API自动缓存上下文,并支持长时间对话,减少了成本。开发者可以通过API内置事件方便地管理语音输入、函数调用和音频转录。
前言
今天我们受邀在OpenAI DevDay新加坡活动上演讲,在演讲中展示了如何构建一个编码语音AI
Agent。自从在DevDay SF发布以来,我们一直在利用Realtime API构建各种创意,并从Daily.co的联合创始人Kwindla Hultman Kramer那里获得了很多宝贵的经验和建议。Daily.co自从语音实时技术流行之前就已涉足这一领域。
因此,作为我们演讲的一部分,我们发布了这篇由OpenAI团队审核过的客座文章,积极分享他在构建Pipecat时的经验教训。Pipecat是由Daily启动的开源项目,现在已发展成为一个完全中立的Realtime API框架,现有的非Daily用户比Daily用户还要多。
但在此之前,我们想分享一些我们在使用原生Realtime API(没有框架、没有外部依赖)时学到的技巧,尤其是在为DevDay新加坡活动做准备时。OpenAI的标准参考应用程序内置了许多功能,因此我们尽可能去除了一些内容,但仍专注于VAD(语音活动检测)和函数调用,创建了一个simple-realtime-console演示。
图片来源:latent space
从实际操作角度来看,语音活动检测(VAD)仍然有时会出现bug,而且大多数情况下,你会希望在不完美的环境中展示语音应用(毕竟安静的房间是很少见的)。因此,我们建议始终设置“静音”和“强制回复”按钮,正如我们在演示中所展示的那样。这个演示还展示了简单的模式,用于添加和插入记忆,以及显示双方的转录文本。
01从Pipelines到Omni-model
在我的大部分职业生涯中,我一直从事人际对话的网络基础设施工作——构建低延迟媒体流、视频通话和大数据协作环境等工具。GPT-4在2023年3月时是一个仅支持文本的模型。但实际上,给GPT-4添加语音模式相对容易。我将一个语音转文本系统连接起来,将语音输入转换为文本提示。然后,我将GPT-4的输出传递给一个文本转语音的音频生成器。
图片来源:DevDay Realtime API Talk
这种多模型pipeline的方法并不新颖。我们在拨打电话客服热线时与之互动的“自然语言处理”系统就类似于这种方式。新颖之处在于,pipeline核心的GPT-4大型语言模型。你可以与GPT-4进行真实的对话。
显然,老旧的自然语言处理系统已经过时。但也有一些明显的挑战。
•延迟表现不佳。GPT-4开始生成回应需要大约一秒钟的时间。语音转文本(STT)和文本转语音(TTS)模型又增加了另一到两秒的延迟。
•有时GPT-4会偏离主题。如何尽量减少和检测这一点似乎是一个相当大的问题。
•一些经典的自然语言处理问题仍然存在,比如短语结束检测(判断何时LLM应当作出回应)和中断处理。
•虽然GPT-4在对话方面表现不错,但与现有后端系统的整合方式却不够成熟。
•现有的TTS模型的语音输出质量显得明显机械。
自GPT-4发布以来的20个月里,AI技术的进步令人瞩目。当前版本的GPT-4在遵循提示、保持任务一致性和避免幻觉方面做得很好。函数调用和结构化数据输出也变得可靠,模型响应速度较快。而且我们现在有了快速、价格合理且高质量的TTS模型。
GPT-4的最新能力是原生的音频输入和输出。升级版的GPT-4o现在拥有自己的声音和耳朵!
02 Realtime API
10月1日,OpenAI发布了一个低延迟的多模态API,利用了GPT-4o非常强大的“语音到语音”功能。这个新的“实时API”能够管理对话状态,实现短语结束检测(轮次检测),提供双向音频流,并支持用户中断LLM的输出。通过这个API,最简单的处理pipeline看起来是这样的:
图片来源:Singapore DevDay!
我一直在帮助客户、朋友和我合作的开源项目的人员,帮助他们快速上手OpenAI的实时API。
使用这个新API与使用OpenAI的HTTP推理API完全不同。新的实时API是有状态的。它在一个长连接的WebSocket基础上定义了一个双向事件协议。
最近,我写下了我对实时API的笔记,讨论了它的优点,认为仍需要改进的地方,以及如何有效使用它。这个笔记的读者是AI工程师——那些正在构建利用GPT-4o和其他LLM的工具的人。也许你因为觉得这项技术有趣而在进行这些工作,或者你正在构建商业化的应用。不管怎样,我希望能提供一些有用的背景信息、代码示例的指引,以及一些具体的细节,能帮你节省时间。
在需要代码的地方,我会链接到Pipecat的源代码和示例,然而这些经验教训不仅限于Pipecat。Pipecat是一个开源、与供应商无关的Python框架,用于实时、多模态AIAgent和应用。Pipecat支持GPT-4o和GPT-4o实时功能,并兼容40多种AI API和服务,支持多种网络传输选项(WebSockets、WebRTC、HTTP、SIP、PSTN/拨入/拨出)。Pipecat还提供了一个大型核心库,包含上下文管理、内容审核、用户状态管理、事件处理、脚本执行等多种功能模块,是构建语音(和视频)Agent的基础。
完整的Pipecat OpenAI实时API集成代码如下:
https://github.com/pipecat-ai/pipecat/tree/main/src/pipecat/services/openai_realtime_beta
下面的链接里放了一个使用实时API的单文件示例语音机器人:
https://github.com/pipecat-ai/pipecat/blob/main/examples/foundational/19-openai-realtime-beta.py
03 OpenAI实时API的架构
对话语音是OpenAI实时API支持的核心应用场景。一个对话语音API需要:
•管理多个用户和LLM轮次之间的对话状态
•确定用户何时停止讲话(并期望LLM给出回应)
•处理用户中断LLM输出的情况
•用户语音的文本转录、函数调用以及LLM上下文的操作也是许多用例中非常重要的部分。
OpenAI实时API通过定义一组事件来支持这些功能,这些事件通过一个长期保持的WebSocket连接进行发送和接收。该API有9个客户端事件(客户端发送给服务器的事件)和28个服务器事件(服务器发送给客户端的事件)。所有37个事件的Pydantic事件定义可以在这里找到:
图片来源:GitHub
这个事件结构非常不错。一个最简的Python命令行客户端大约只有75行代码,包括所有的导入和asyncio的样板代码!
图片来源:X
音频作为Base64编码的片段通过input_audio_buffer.append和audio.delta事件发送和接收。
目前,API支持未压缩的16位、24kHz音频和压缩的G.711音频。G.711仅在电话通信场景中使用;与其他更现代的编解码器选项相比,音频质量相对较差。
未压缩的16位、24kHz音频的比特率为384千比特每秒(kbps)。Base64编码的开销将名义比特率推高到大约500 kbps。但使用permessage-deflate标准压缩后,比特率将回落到300-400kbps。
300 kbps是比一般通过WebSocket连接发送的媒体流更大的数据流,如果你关心实时延迟的话。我们稍后会讨论延迟,关于WebSocket的内容会在后面的部分进一步展开。
04延迟
人类在正常对话中期望快速的回应。500毫秒的响应时间是典型的,较长的停顿会让对话显得不自然。
如果你正在构建对话式AI应用程序,800毫秒的语音到语音延迟是一个不错的目标,尽管在今天的LLM中要始终如一地达到这一点是很困难的。
OpenAI实时API提供了非常好的推理延迟。我们通常看到位于美国的客户端从API收到的首字节时间大约是500毫秒。如果我们期望总的语音到语音延迟为800毫秒,那么剩下的大约300毫秒就要用于音频处理和短语结束检测。在完美条件下,这几乎是可行的。
手动测量语音到语音的延迟是比较简单的。只需录制对话,将录音加载到音频编辑器中,查看音频波形,并测量从用户说话结束到LLM说话开始的时间。如果你为生产环境构建对话式语音应用,偶尔手动检查一下延迟数据是值得的。测试时如果加上模拟的网络丢包和抖动,会更好!
因为部分延迟发生在操作系统内部,所以程序化地测量真实的语音到语音延迟是具有挑战性的。所以大多数可观察性工具只测量推理的首字节时间。这是一个合理的语音到语音延迟的Agent,但请注意,这个测量并不包括短语结束检测的时间(见下一节)。
许多“微小”的因素可能会以出乎意料的方式影响延迟。例如,蓝牙音频设备可能会增加几百毫秒的延迟。有关在Web浏览器中运行的语音到语音应用中所有可能影响延迟的因素的更多详细信息,可以参见以下推文、这篇文章和这场AI.Engineer的讲座:https://youtu.be/dRQHikOrH2A
05短语结束检测(轮次检测)和中断处理
在对话中,人们会轮流发言。语音AI应用中的轮次切换有两个组成部分:
1.应用需要决定人类何时说完话并期待回应。这被称为短语结束检测或轮次检测。大多数对话式语音应用尝试实现自动轮次检测。但也有一些应用使用“按键讲话”(push to talk)界面。
2.如果人类打断LLM,通常LLM的语音输出应该立即停止。对于某些应用,这需要是可配置的;有时,即使用户开始说话,LLM也应该完成它的语音输出。
OpenAI实时API内置了自动短语结束检测和中断处理。这些功能通过服务器端的语音活动检测(VAD)来实现。自动轮次检测是默认启用的,但可以随时禁用。
这里是OpenAI关于轮次检测的概述文档:
图片来源:Open AI
有几个VAD(语音活动检测)参数是可以配置的。其中最重要的参数是silence_duration_ms。这是VAD在用户停止讲话后会等待的时间,直到发出input_audio_buffer.speech_stopped事件并开始推理。
OpenAI维护了一个服务器端的音频缓冲区,应用程序将音频帧流式传输到该缓冲区(通过发送input_audio_buffer.append事件)。在自动轮次检测模式下,应用程序可以持续发送音频,并依赖OpenAI服务器端的VAD来确定用户何时开始和停止讲话。
当用户停止讲话时,多个API事件会被触发,LLM开始生成回应。当用户开始讲话时,任何正在进行的回应会被取消,音频输出会被刷新。
这是一个简单的过程(即不需要客户端代码),而且效果很好。然而,有三种情况可能会导致应用程序选择关闭OpenAI的轮次检测:
1.如果应用程序不希望允许中断,则需要禁用自动轮次检测。
2.对于“按键讲话”风格的用户界面,应用程序需要手动管理音频缓冲区并触发LLM的回应。
3.应用程序开发人员可能希望使用不同的短语结束检测实现。
如果禁用了OpenAI的自动轮次检测,客户端需要在用户讲话结束时发送两个实时API事件:input_audio_buffer.commit 和response.create。
当用户开始讲话时,调用这两个事件的Pipecat代码如下:
图片来源:GitHub
OpenAI的VAD似乎对背景噪音比Pipecat的默认短语结束检测实现更敏感。Pipecat使用输入音频能量的平滑滑动平均值,以自动调节相对于背景噪音的音量水平。它还会忽略音频中的短暂波动,即使这些波动的语音置信度较高,同时支持可选的主讲人分离和其他高级音频处理功能。
OpenAI的silence_duration_ms参数默认为500毫秒(Pipecat称该参数为stop_secs)。这是一个在过长的对话响应时间和LLM回应过快、打断用户未完成的思路之间的良好折衷。对于一些应用场景,更长的沉默时长更为理想。例如,在工作面试场合,给面试者更多的时间思考回答通常会提供更好的体验。在这种情况下,800毫秒甚至1秒钟是理想的。
使用标准VAD时,我们通常不建议将设置低于500毫秒,除非是语音AI的演示!有一些技术可以通过补充标准VAD以上下文感知的短语结束检测,或者进行推测性(贪婪型)推理,或两者结合,从而获得更快的响应时间。这些技术超出了本文的讨论范围,但如果你对此感兴趣,Pipecat的Discord社区是一个不错的地方。
Pipecat的基础VAD实现代码在这里:
图片来源:GitHub
06管理上下文
多轮对话是一系列用户输入和LLM回应的过程。
LLM本身是无状态的,因此每次有用户输入时,都需要将所有相关的对话历史发送给LLM。如果你之前构建过对话式LLM应用(无论是文本还是语音),你一定熟悉如何追踪对话历史,并使用这些历史记录来创建一个不断增加的“上下文”,然后不断地将这个上下文发送给LLM。
OpenAI实时API为你处理了对话管理。这带来了两个非常大的好处:代码更简洁和延迟更低。
代码更简洁是因为你不需要在应用中追踪对话历史。
延迟更低有几个原因。你不需要每次都重新发送一个庞大的上下文,这样可以节省一些网络开销。此外,当前的音频输入可以直接流式传输到OpenAI服务器,以便在推理请求时立即可用。最后,OpenAI还可以实现内部优化,例如上下文缓存。这些都是巨大的优势!
需要注意两个限制:最大上下文长度为128,000个tokens,单次会话的最大时间为15分钟。在语音对话中,你不太可能遇到token限制。语音每分钟大约使用800个tokens。然而,15分钟的限制对于某些应用来说可能会是一个约束。
目前,通过OpenAI实时API无法检索对话上下文、将“助手”音频消息加载到上下文中,或可靠地加载多条消息历史。有关测试用例,请参见此仓库:
图片来源:GitHub
不过,确实可以实现持久对话和长对话。你需要将对话历史保存为文本,然后在重新开始对话时,将完整的对话历史(以及适当的提示)作为新对话的第一条消息发送。
这是一个保存和重新加载对话的示例语音机器人:
https://github.com/pipecat-ai/pipecat/blob/main/examples/foundational/20b-persistent-context-openai-realtime.py
以下是使用与OpenAI HTTP API相同的消息列表格式初始化对话的Pipecat代码:
图片来源:GitHub
关于上下文管理,还有一些其他的事项值得提及。
LLM的音频生成速度比语音输出速度要快。OpenAI会将服务器端生成的LLM回应尽快添加到对话上下文中。但语音是以较慢的速率播放的。如果用户打断了LLM的回应,用户只会听到LLM回应的一部分。在大多数情况下,你会希望对话历史中只包含用户实际听到的那部分LLM回应。
你需要发送一个conversation.item.truncate事件,强制服务器端的上下文与用户听到的音频时长匹配。请注意,无论你是否使用自动轮次检测(server_vad),都需要执行此操作。
以下是计算用户听到的音频时长并调用conversation.item.truncate的Pipecat代码:
图片来源:GitHub
对于许多用例,用户输入和LLM输出的转录都非常重要。保存对话以便用户以后可以返回查看就是一个例子。许多企业级用例需要对话记录,用于内容审核、后处理需求或合规性要求。
OpenAI实时API始终提供LLM输出的转录。输入转录默认是关闭的,但可以通过在配置会话时设置input_audio_transcription字段来启用。
输出转录是由LLM原生生成的,与音频输出非常接近。输入转录则由一个独立的模型生成,并不总是与模型“听到”的内容完全匹配。这对于某些用例可能会是一个问题。如果转录数据包含语言字段,那将会更有用。
目前没有办法将输出转录与语音时序对齐。这使得在用户打断时难以截断文本输出,也难以构建如逐字精确的流式文本字幕。
输入音频转录也可能会滞后于模型输出几秒钟。如果你需要使用转录进行内容审核,可能需要使用自己的转录模型,并将短语结束检测的触发条件设置为转录完成或内容审核本身。
最后,请注意,Realtime API消息的内容格式与OpenAI HTTP API格式不同。以下是将HTTP API格式转换为Realtime API格式的Pipecat代码:
图片来源:GitHub
07函数调用
在OpenAI实时API中,函数调用工作得相当顺畅(就像所有GPT-4系列模型一样)。与对话消息的格式类似,工具的格式与OpenAI HTTP API略有不同。以下是HTTP API工具列表(仅包含一个条目):
Plain Text |
转换为实时API中的工具列表如下:
Plain Text |
函数调用事件可以通过两种方式从API中获取:
1.通过流式事件response.function_call_arguments.delta 和function_call_arguments.done。
2.作为response.done 事件的一部分。
如果你是从HTTP API迁移过来的,并希望尽可能保留现有的代码结构,流式事件可能会很有用。但实时API使得从response.done事件中提取函数调用结构变得非常简单。流式事件对于函数调用并不是特别有用——在调用函数之前,你需要完整的函数调用结构——并且从流式响应片段中组装函数调用数据,在使用HTTP API时一直是一个小麻烦。
以下是从response.done事件的输出字段中提取函数调用信息的Pipecat代码:
https://github.com/pipecat-ai/pipecat/blob/main/src/pipecat/services/openai_realtime_beta/openai.py#L469
08成本
对于对话式AI应用,成本通常会随着会话长度的增加而呈指数增长。大多数对话式AI应用和API在每个轮次的推理请求中都会使用完整的对话历史,OpenAI实时API也不例外。
然而,OpenAI会自动缓存并重用发送到实时API的输入tokens,缓存的音频tokens比非缓存tokens便宜80%。这对于降低长时间对话的成本非常有帮助。典型的对话成本:
OpenAI不会为沉默(或接近沉默)的音频输入生成tokens(即没有语音或显著背景噪音的音频)。上述成本估算假设70%的时间是讲话时间。如果讲话时间比例较低,将有助于降低成本。
可以合理假设,OpenAI将很快并且频繁地大幅降低实时API的成本。然而,目前,如果你的应用对成本敏感,可能需要每隔几轮重新编写对话上下文,将音频消息替换为文本消息(也可以使用总结技术,进一步减少输入token的数量)。
以下是一个成本计算器电子表格,你可以复制、使用并调整其中的假设:
https://docs.google.com/spreadsheets/d/1EL-mjqlmj4ehug8BjmgmAFm9uFZtZXY9N9EvqLm8Ebc/edit?usp=sharing
09 WebSockets和WebRTC
OpenAI实时API使用WebSockets作为网络传输方式。WebSockets非常适合服务器到服务器的用例、延迟不是主要关注点的用例,并且非常适合原型开发和一般的黑客实验。但对于客户端与服务器之间的实时媒体连接,WebSockets不应在生产环境中使用。
如果你正在构建一个使用实时API的浏览器或原生移动应用,并且对话延迟对你的应用很重要,那么你应该使用WebRTC连接来发送和接收应用到服务器的音频。然后,你可以直接从服务器使用实时API。
WebSockets在向最终用户设备传输实时媒体时存在的主要问题有:
•WebSockets是基于TCP构建的,因此音频流会受到队头阻塞的影响,并且即使数据包延迟到无法用于播放,它们也会自动尝试重新发送。
•WebRTC使用的Opus音频编解码器与WebRTC的带宽估计和数据包时序(拥塞控制)逻辑紧密耦合,使得WebRTC音频流对广泛的现实网络行为具有弹性,而这些行为会导致WebSocket连接积累延迟。
•Opus音频编解码器具有非常好的前向错误修正,使音频流对较高的丢包率具有弹性。(但是,这只有在你的网络传输能够丢弃晚到的数据包,并且不会进行队头阻塞时才有效。)
•通过WebRTC发送和接收的音频会自动进行时间戳处理,因此播放和打断逻辑非常简单。而使用WebSockets时,处理所有角落案例会比较困难。
•WebRTC包括用于详细的性能和媒体质量统计的挂钩。一个好的WebRTC平台将为你提供详细的仪表板和分析,用于音频和视频的汇总统计以及单个会话的统计。这种可观察性是WebSockets中难以实现的,甚至几乎不可能实现。
•WebSocket的重连逻辑非常难以实现得足够健壮。你将不得不构建一个ping/ack框架(或者完全测试并理解你使用的WebSocket库所提供的框架)。不同平台上的TCP超时和连接事件行为不同。
•最后,今天好的WebRTC实现都带有非常好的回声消除、噪声降低和自动增益控制。你可能需要想办法将这些音频处理功能融入一个使用WebSockets的应用中。
此外,无论底层网络协议是什么,长距离的公共互联网连接对于延迟和实时媒体的可靠性都是有问题的。因此,如果你的终端用户距离OpenAI的服务器较远,重要的是尽可能将用户连接到距离他们最近的媒体路由器。在这个“边缘”连接之后,你可以使用更高效的骨干路由。一些好的WebRTC平台会自动为你完成这个过程。
如果你对用于发送媒体的网络协议感兴趣,这里有一个关于RTMP、HLS和WebRTC的技术概述:
https://www.daily.co/blog/video-live-streaming/
关于WebRTC边缘和网格路由的深入分析,这里有一篇关于Daily全球WebRTC基础设施的长文:
https://www.daily.co/blog/global-mesh-network/
10回声消除和音频处理
几乎所有非电话类的对话式语音应用都需要回声消除和其他音频处理功能。
Chrome、Safari和Edge都通过Media Capture和Streams API提供了非常好的回声消除功能。我们强烈建议不要将Firefox作为实时语音开发和测试的主要浏览器。Firefox的回声消除和一般音频流管理存在不少问题,且远远落后于其他浏览器,这样你将花费大量时间处理一些其他浏览器用户永远不会遇到的问题。建议先在Chrome和Safari上使功能正常运作,再决定是否投入时间将Firefox调试到相同的水平。
如上所述,浏览器WebRTC实现和本地SDK都包括了良好的回声消除、背景噪声减少和自动增益控制,并且默认会启用这些功能。
需要注意的是,回声消除必须在客户端设备上进行,其他类型的音频处理则可以在服务器端进行。比如,Pipecat就包含了与Krisp非常优秀的商业噪声减少和说话者隔离模型的集成。
11关于API设计的离题话题
每个API都是一个工程制品,受到软件设计和开发中所有约束和权衡的影响。
优秀的API力求清晰地表达它们试图实现的目标,为其预期用户和使用场景提供合适粒度的抽象,使简单的事情变得简单,复杂的事情变得可能。
将OpenAI Realtime API集成到Pipecat中非常有趣。我们喜欢比较两种支持对话语音AI的方式。总体而言,OpenAI的事件与Pipecat的框架类型非常匹配。对于我们这些在过去一年多一直在Pipecat进化过程中工作的开发者来说,OpenAI Realtime API定义的问题领域是熟悉的。
然而,OpenAI Realtime API的架构与Pipecat有很大不同。我猜测这种差异部分是出于OpenAI希望使用最广泛可访问的构造的考虑,部分则是因为项目范围的不同。
Realtime API的事件架构几乎可以轻松地融入到任何语言或框架中。发送事件只需将一些JSON(或类似格式)推送到网络中,接收事件只需从读取循环中分发到相应的函数。
相比之下,Pipecat采用的是数据流架构,受到像GStreamer这样的媒体处理框架多年工作的影响。Pipecat的设计强调可组合性、顺序处理,并将实时行为表示为一等抽象。
Realtime API中的核心构建块是“事件”。而Pipecat中的核心构建块是“帧处理器”。在Pipecat中,语音到语音的处理循环可能看起来是这样的:
Plain Text |
随着语音转文本和文本转语音的帧处理器的加入,这种语音到语音的结构可以与任何LLM一起使用。
Plain Text |
这是来自一个生产环境中Pipecat应用的更复杂的处理pipeline,包含了多种客户端命令和事件的帧处理器、通过webhooks实现的函数调用、计费事件、可观察性和使用指标,以及错误处理。
Plain Text |
这是一个有趣的例子——一个实验性pipeline,除了使用VAD外,还利用大型语言模型(LLM)来执行短语端点识别。该pipeline并行运行LLM作为判定推理和主要对话推理的子pipeline,以最小化延迟。
Plain Text |
其他资源:
OpenAI Realtime API概述文档:
https://platform.openai.com/docs/guides/realtime/realtime-api-beta#quickstart
API参考:
https://platform.openai.com/docs/api-reference/realtime
优秀的Realtime API控制台示例应用:
https://github.com/openai/openai-realtime-console
Pipecat代码实现了将OpenAI Realtime API作为Pipecat服务。Pydantic事件定义可能对其他项目特别有用:
https://github.com/pipecat-ai/pipecat/tree/main/src/pipecat/services/openai_realtime_beta
原文:https://www.latent.space/p/realtime-api
OpenAI Realtime API: The Missing Manual
编译:Xu Yueyun
(文:Z Potentials)