边缘智能
本文介绍了如何使用边缘大模型网关平台预置的语音对话智能体。
边缘大模型网关平台预置了一个语音对话智能体。该智能体适用于语音对话场景,具备以下能力:
说明
对话打断只适用于单独使用语音对话智能体场景。对于组合使用语音对话智能体与您自己的 Coze 智能体场景,暂不支持对话打断。
语音对话智能体支持两种使用模式:单独使用、与您在 Coze 平台搭建的智能体组合使用。
完成第三方智能体调用准备工作,具体包括:
创建一个网关访问密钥。在 模型选择 中同时选择:
您的 Coze 智能体(渠道类型 为 自有三方智能体)
平台预置的 语音对话智能体(渠道类型 为 平台预置智能体)
完成创建后,该网关访问密钥可用于调用您的 Coze 智能体和平台预置的语音对话智能体。
参考查看密钥(API Key),获取网关访问密钥的 API key。
调用 Realtime API 实现与智能体进行语音对话。关于 API 的说明,请参见 Realtime API。
Realtime API 是一个有状态的、基于事件的 API,通过 WebSocket 进行通信。您可以使用 Realtime API 与边缘大模型网关的预置语音对话智能体构建语音对话。目前,Realtime API 支持音频作为输入和输出。
注意
Realtime API 仍处于测试阶段。
与 Realtime API 建立 WebSocket 连接需要以下参数:
wss://ai-gateway.vei.volces.com/v1/realtime
?model=AG-voice-chat-agent
Authorization: Bearer $YOUR_API_KEY
$YOUR_API_KEY 需要替换成绑定了 语音对话智能体 的网关访问密钥的 API key。详情请参见使用流程。
$YOUR_API_KEY
?model=AG-voice-chat-agent&ag-coze-bot-id=<Coze Bot ID>
X-Conversation-Id: sess_xxxx
X-Conversation-Id
session.created
session id
针对浏览器 JavaScript 环境发起的 websocket 请求,由于不支持添加请求头,参照 OpenAI 示例 采用了在 Sec-WebSocket-Protocol 请求头中添加子协议的方式来实现认证。该认证体现在语音对话智能体的 JavaScript 示例代码,如下图所示。
语音对话智能体支持通过心跳包(ping/pong 帧)来维持 WebSocket 连接的活跃状态,防止因长时间无数据传输导致连接被超时中断。 具体机制如下:
Realtime API 兼容 OpenAI 的 Realtime 接口,支持的事件如下表所示。
类型
事件
客户端
session.update
将此事件发送以更新会话的默认配置。
input_audio_buffer.append
发送此事件以将音频字节追加到输入音频缓冲区。
input_audio_buffer.commit
发送此事件以提交用户输入的音频缓冲区。
conversation.item.create
向对话的上下文中添加一个新项目,包括消息、函数调用响应。
response.create
此事件指示服务器创建一个响应,这意味着触发模型推理。
response.cancel
发送此事件来取消当前正在回复的语音应答。
服务端
在会话创建时返回。新连接建立时自动触发,作为第一个服务器事件。
session.updated
发送此事件以确认客户端更新的会话配置。
conversation.item.input_audio_transcription.completed
发送此事件输出用户音频的文本转录。
response.created
在创建新响应时返回。响应创建的第一个事件,此时响应处于初始的进行中状态。
response.output_item.added
在响应生成过程中创建新项目时返回。
response.function_call_arguments.done
当模型生成的函数调用参数完成流式传输时返回。
response.audio_transcript.delta
在模型生成的文本信息流式返回。
response.audio_transcript.done
在模型生成的文本信息完成流式传输时返回。
response.audio.delta
在模型生成的语音流式返回。
response.audio.done
在模型完成生成的音频流式传输时返回。
response.output_item.done
在一次响应完成流式传输时返回。
response.done
在响应完成流式传输时返回。
/
error
在发生错误时返回,这可能是客户端问题或服务器问题。
将此事件发送以更新会话的默认配置。如需更新配置,客户端必须在创建会话的一开始就发送该消息(发送音频之前),服务器将以 session.updated 事件响应,显示完整的有效配置。
event_id
type
session
modalities
["text", "audio"]
["audio"]
response.audio_transcript
instructions
你是一个玩具对话智能体,你的名字叫豆包,你的回答要尽量简短
voice
zh_female_tianmeixiaoyuan_moon_bigtts
input_audio_format
pcm16
output_audio_format
output_audio_sample_rate
silent_on_unrecognized_input
false
true
input_audio_transcription
model
"model": "any"
turn_detection
tools
function
name
如果后端为您自己的 Coze 智能体,那么 tools 字段不生效。实际生效的是您在 Coze 智能体中定义的端插件。
tool_choice
temperature
max_response_output_tokens
示例:
{ "type": "session.update", "session": { "modalities": ["audio"], "instructions": "你的名字叫豆包,你是一个智能助手,你的回答要尽量简短。", "voice": "zh_female_tianmeiyueyue_moon_bigtts", "input_audio_format": "pcm16", "output_audio_format": "pcm16", "input_audio_transcription": { "model": "any" }, "tools": [{ "type": "function", "name": "get_weather", "description": "获取当前天气", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } }], "temperature": 0.8 } }
发送此事件以将音频字节追加到输入音频缓冲区。音频缓冲区是临时存储,您可以在其中写入数据并稍后进行提交。
audio
{ "type": "input_audio_buffer.append", "audio": base64_audio }
发送此事件以提交用户输入的音频缓冲区。提交输入音频缓冲区将触发音频转录,但不会生成模型的响应。服务器将以 input_audio_buffer.committed 事件进行响应。
input_audio_buffer.committed
{ "type": "input_audio_buffer.commit" }
向对话的上下文中添加一个新项目,包括消息、函数调用响应。新增消息时,消息文本将直接作为输入发送到大模型以产生应答,此时无需再上报语音消息。如果成功,服务器将响应一个 conversation.item.created 事件,否则将发送错误事件。
conversation.item.created
previous_item_id
item
id
object
realtime.item
function_call_output
message
call_id
function_call
output
role
user
content
input_text
text
示例 - function_call_output:
{ "type": "conversation.item.create", "item": { "call_id": function_call_id, "type": "function_call_output", "output": "{"result": "打开成功"}, } }
示例 - message
{ "type": "conversation.item.create", "item": { "id": "msg_001", "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "今天天气怎么样?" } ] } }
此事件指示服务器创建一个响应,这意味着触发模型推理。服务器将以 response.created 事件进行响应,包含为项目和内容创建的事件,最后发送 response.done 事件以指示响应已完成。
response
{ "type": "response.create", "response": { "modalities": ["text", "audio"] } }
发送到服务器将来需取消正在应答的语音消息。可以在收到服务端的 response.audio.delta 后发送该消息来取消后续的语音消息,服务端最终会应答 response.done 中带 cancelled 状态以指示响应已完成。
cancelled
{ "type": "response.cancel" }
示例(服务端最终应答 response.done 中带 cancelled):
{ "event_id": "event_5fe8c9c224ee4d6d82cf7", "response": { "id": "resp_7a4c14b7ac884610a115c", "output": [], "object": "realtime.response", "status": "cancelled", "status_details": None, "usage": { "total_tokens": 201, "input_tokens": 131, "output_tokens": 70, "input_token_details": { "cached_tokens": 0, "text_tokens": 123, "audio_tokens": 8 }, "output_token_details": { "text_tokens": 35, "audio_tokens": 35 } } }, "type": "response.done" }
在会话创建时返回。新连接建立时自动触发,作为第一个服务器事件。此事件将包含默认的会话配置。
{ "event_id": "event_5408c86192a14ae088d55", "session": { "id": "sess_7441921809949130779", "model": "7441883325217882146", "expires_at": 1732709032, "object": "realtime.session", "modalities": ["text", "audio"], "instructions": None, "voice": "zh_male_shaonianzixin_moon_bigtts", "turn_detection": None, "input_audio_format": "pcm16", "output_audio_format": "pcm16", "input_audio_transcription": None, "tools": [], "tool_choice": "auto", "temperature": 0.8, "max_response_output_tokens": "inf" }, "type": "session.created" }
发送此事件以确认客户端更新的会话配置。目前只支持在连接刚创建的时候发送该消息以更新配置。服务器将以 session.updated 事件响应,显示完整的有效配置。只有存在的字段会被更新。
{ "event_id": "event_7ac8c51cda964aa38932f", "session": { "id": "sess_c3e26a46bd2043e184d06", "model": "ep-20240725155030-gd2s2", "expires_at": 1736846451, "object": "realtime.session", "modalities": ["audio"], "instructions": "你的名字叫豆包,你是一个智能助手,你的回答要尽量简短。", "voice": "zh_female_tianmeiyueyue_moon_bigtts", "turn_detection": None, "input_audio_format": "pcm16", "output_audio_format": "pcm16", "input_audio_transcription": { "model": "any" }, "tools": [{ "type": "function", "name": "get_weather", "description": "获取当前天气", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } }], "tool_choice": "auto", "temperature": 0.8, "max_response_output_tokens": "inf" }, "type": "session.updated" }
item_id
content_index: integer
transcript
{ "event_id": "event_df9885d3f04041e1832a7", "item_id": "item_504367449f1c42bb9ee8c", "content_index": 0, "transcript": "背唐诗登鹳雀楼。", "type": "conversation.item.input_audio_transcription.completed" }
realtime.response
status
in_progress
completed
status_details
usage
{ "event_id": "event_cb7646e899e648cfb269e", "response": { "id": "resp_06348064b26b412196e32", "output": [], "object": "realtime.response", "status": "in_progress", "status_details": None, "usage": None }, "type": "response.created" }
response_id
output_index
assistant
system
{ "event_id": "event_81efe6cf663040d6a6579", "response_id": "resp_06348064b26b412196e32", "output_index": 0, "item": { "content": [], "id": "item_73fe51150f4a446abd9d9", "status": "in_progress", "type": "message", "role": "assistant", "object": "realtime.item" }, "type": "response.output_item.added" }
arguments
{ "event_id": "event_c8f465ef567f402c8bf19", "type": "response.function_call_arguments.done", "item_id": "item_df02814b553144319728d", "response_id": "resp_33c3f294518b4102b2c19", "output_index": 0, "call_id": "call_w5z9kqyptdxasubd8y4smu60", "arguments": "{\"location\": \"上海\"}" }
content_index
delta
{ "event_id": "event_3ce9f18992e1461fb486d", "response_id": "resp_06348064b26b412196e32", "item_id": "item_73fe51150f4a446abd9d9", "output_index": 0, "content_index": 0, "delta": "你", "type": "response.audio_transcript.delta" }
{ "event_id": "event_bb2c02ef6fa542ed9027d", "response_id": "resp_06348064b26b412196e32", "item_id": "item_73fe51150f4a446abd9d9", "output_index": 0, "content_index": 0, "transcript": "你好,有没有好玩的事呀?", "type": "response.audio_transcript.done" }
{ "event_id": "event_3ce9f18992e1461fb486d", "response_id": "resp_06348064b26b412196e32", "item_id": "item_73fe51150f4a446abd9d9", "output_index": 0, "content_index": 0, "delta": "base64", "type": "response.audio.delta" }
{ "event_id": "event_51bb61cdab5e4ad8ac925", "response_id": "resp_06348064b26b412196e32", "item_id": "item_73fe51150f4a446abd9d9", "output_index": 0, "content_index": 0, "type": "response.audio.done" }
input_audio
纯文本消息(message)返回
{ "event_id": "event_57938730a5764a7c83cb4", "response_id": "resp_06348064b26b412196e32", "output_index": 0, "item": { "content": [ { "type": "audio", "transcript": "你好,有没有好玩的事呀?" } ], "id": "item_73fe51150f4a446abd9d9", "status": "completed", "type": "message", "role": "assistant", "object": "realtime.item" }, "type": "response.output_item.done" }
函数调用(function_call)返回
{ "event_id": "event_e26c05a0068e41189f2ec", "type": "response.output_item.done", "item": { "id": "item_84dfdf6448634d0990d4b", "type": "function_call", "status": "completed", "call_id": "call_gln0xh7bi7fx0mai1d3gsthh", "name": "get_weather", "arguments": "{\"location\": \"上海\"}" }, "response_id": "resp_b06b5bddad494be89b097", "output_index": 0 }
total_tokens
input_tokens
output_tokens
input_token_details
cached_tokens
text_tokens
audio_tokens
output_token_details
{ "event_id": "event_767d3f51a1c84e99a4185", "response": { "id": "resp_33a7a44490354861ab3c0", "output": [], "object": "realtime.response", "status": "completed", "status_details": None, "usage": { "total_tokens": 157, "input_tokens": 141, "output_tokens": 16, "input_token_details": { "cached_tokens": 0, "text_tokens": 133, "audio_tokens": 8 }, "output_token_details": { "text_tokens": 8, "audio_tokens": 8 } } }, "type": "response.done" }
在发生错误时返回,这可能是客户端问题或服务器问题。对于一些输入错误的请求,会返回应答但是不会影响连接。对于一些服务端的错误,会返回应答,并会断开连接,客户端这时必须重连。如果服务端在 120 秒内没有收到新的消息,会返回 error,并主动断开 WebSocket 连接。
invalid_request_error
server_error
code
param
{ "event_id": "event_f2aac7bbab6f4854a7c2d", "error": { "type": "server_error", "message": "There is problem in server side, please try again later", "code": "server_error", "param": None, "event_id": None }, "type": "error" }
以下是一个时序图示例:
pip install soundfile scipy numpy websockets==12.0
测试音频
import asyncio import base64 import json import wave import numpy as np import soundfile as sf from scipy.signal import resample import websockets def resample_audio(audio_data, original_sample_rate, target_sample_rate): number_of_samples = round( len(audio_data) * float(target_sample_rate) / original_sample_rate) resampled_audio = resample(audio_data, number_of_samples) return resampled_audio.astype(np.int16) def pcm_to_wav(pcm_data, wav_file, sample_rate=16000, num_channels=1, sample_width=2): print(f"saved to file {wav_file}") with wave.open(wav_file, 'wb') as wav: # Set the parameters # Number of channels (1 for mono, 2 for stereo) wav.setnchannels(num_channels) # Sample width in bytes (2 for 16-bit audio) wav.setsampwidth(sample_width) wav.setframerate(sample_rate) wav.writeframes(pcm_data) async def send_audio(client, audio_file_path: str): sample_rate = 16000 duration_ms = 100 samples_per_chunk = sample_rate * (duration_ms / 1000) bytes_per_sample = 2 bytes_per_chunk = int(samples_per_chunk * bytes_per_sample) audio_data, original_sample_rate = sf.read( audio_file_path, dtype="int16") if original_sample_rate != sample_rate: audio_data = resample_audio( audio_data, original_sample_rate, sample_rate) audio_bytes = audio_data.tobytes() for i in range(0, len(audio_bytes), bytes_per_chunk): await asyncio.sleep((duration_ms - 10)/1000) chunk = audio_bytes[i: i + bytes_per_chunk] base64_audio = base64.b64encode(chunk).decode("utf-8") append_event = { "type": "input_audio_buffer.append", "audio": base64_audio } await client.send(json.dumps(append_event)) commit_event = { "type": "input_audio_buffer.commit" } await client.send(json.dumps(commit_event)) event = { "type": "response.create", "response": { "modalities": ["text", "audio"] } } await client.send(json.dumps(event)) async def receive_messages(client, save_file_name): audio_list = bytearray() while not client.closed: message = await client.recv() if message is None: continue event = json.loads(message) message_type = event.get("type") if message_type == "response.audio.delta": audio_bytes = base64.b64decode(event["delta"]) audio_list.extend(audio_bytes) continue if message_type == 'response.done': pcm_to_wav(audio_list, save_file_name) break print(event) continue def get_session_update_msg(): config = { "modalities": ["text", "audio"], "instructions": "你的名字叫豆包,你是一个智能助手", "voice": "zh_female_tianmeixiaoyuan_moon_bigtts", "input_audio_format": "pcm16", "output_audio_format": "pcm16", "tool_choice": "auto", "turn_detection": None, "temperature": 0.8, } event = { "type": "session.update", "session": config } return json.dumps(event) async def with_realtime(audio_file_path: str, save_file_name: str): ws_url = "wss://ai-gateway.vei.volces.com/v1/realtime?model=AG-voice-chat-agent" key = "xxx" # 修改为你的 key headers = { "Authorization": f"Bearer {key}", } async with websockets.connect(ws_url, extra_headers=headers) as client: session_msg = get_session_update_msg() await client.send(session_msg) await asyncio.gather(send_audio(client, audio_file_path), receive_messages(client, save_file_name)) await asyncio.sleep(0.5) if __name__ == "__main__": audio_file_path = "demo_audio_nihaoya.wav" #下载示例音频 save_file_name = "demo_response.wav" asyncio.run(with_realtime(audio_file_path, save_file_name))
目前,Realtime API 需要使用音频输出或音频输入。只接受如下组合:
支持客户端通过发送 response.cancel 来打断当前输出的语音消息。
目前只支持保留当前 WebSocket 的对话历史,最多保留 10 轮。当 WebSocket 断开后,对话历史不会被保留。
目前暂不支持服务端的 VAD,必须客户端主动提交告知服务端完成当前的语音采集。
目前只支持 pcm16 的格式,默认为 16000Hz 的采样频率。
目前只支持在 语音技术 - 音色列表 通用场景 中 包含中文语种且以 zh_ 开头 的音色。默认为 zh_female_tianmeixiaoyuan_moon_bigtts。