全部產品
Search
文件中心

Alibaba Cloud Model Studio:Qwen-Audio 即時語音對話

更新時間:Jul 15, 2026

Qwen-Audio 是端到端即時語音互動大模型,通過 WebSocket 流式協議實現低延遲語音對話,適用於語音助手、智能客服、AI 伴侶等情境。

概述

通過 WebSocket 雙工協議實現即時音頻到語音/文本的轉換,支援流式音頻輸入和流式語音與文本輸出。

  • 支援三種互動模式:聲學 VAD(server_vad)、智能語義輪次(smart_turn)和手動控制(push-to-talk)

  • 專屬的 smart_turn 模式融合聲學感知與語義理解判斷輪次邊界,無意義附和聲(如”嗯””啊”)不會打斷對話

  • 支援 Function Calling 工具調用,模型可自主判斷是否需要調用外部工具擷取資訊

  • 支援對話上下文管理(建立、查詢、刪除對話項),靈活注入歷史上下文或清理無關對話項

  • 高表現力語音輸出,根據對話語境動態調整語氣、節奏和情感表達

  • 支援系統音色與聲音複刻音色,可通過聲音複刻建立專屬 AI 音色用於語音對話輸出

  • smart_turn 模式下支援說話人增強,傳入目標使用者預錄音頻後,模型可在雙工對話中精準鎖定目標說話人,有效屏蔽旁人聲音與背景雜訊

工作原理

Qwen-Audio 基於 WebSocket 全雙工系統協議,採用事件驅動架構。用戶端與服務端通過持久串連同時收發資料:用戶端持續發送麥克風採集的音頻流,服務端即時返回語音和文本響應。整個互動過程由用戶端事件(如 session.updateinput_audio_buffer.append)和服務端事件(如 response.audio.deltaresponse.done)驅動,無需輪詢。

典型的串連生命週期為:建立 WebSocket 串連 → 發送 session.update 配置會話參數 → 持續發送音頻並接收響應 → 主動關閉串連。

音頻格式

方向

格式

規格

輸入(用戶端 → 服務端)

PCM

16kHz 採樣率,16bit 位深,單聲道

輸出(服務端 → 用戶端)

PCM

24kHz 採樣率,16bit 位深,單聲道

上下文容量

模型會維護對話歷史上下文,當對話輪次或累計音頻時間長度超過以下限制時,將自動丟棄更早的歷史資訊。最大時間長度指模型上下文中能保留的音頻累計時間長度上限。

模型

音頻最大輪次

音頻最大時間長度

qwen-audio-3.0-realtime-plus

50

300秒

qwen-audio-3.0-realtime-flash

50

300秒

音頻最大輪次的預設值為 20 輪,最大可上調至 50 輪。調節方式請參見歷史輪次控制

全模態模型的整體選型建議請參見全模態

前提條件

快速開始

通過以下步驟快速體驗與 Qwen-Audio 模型的即時語音對話。

WebSocket 原生

說明

各模式下用戶端與服務端的 WebSocket 事件互動時序,請參見事件互動流程

以下樣本通過 WebSocket 原生串連,使用 server_vad 模式實現麥克風即時對話。運行前需安裝依賴:

macOS

brew install portaudio && pip install pyaudio websockets

Debian/Ubuntu

sudo apt install -y python3-dev portaudio19-dev && pip install pyaudio websockets

Windows

pip install pyaudio websockets

將以下代碼儲存為 realtime_quickstart.py

import asyncio
import base64
import json
import os
import pyaudio
import websockets

API_KEY = os.environ["DASHSCOPE_API_KEY"]
# 以下為華北2(北京)地區的WebSocket URL,調用時請將{WorkspaceId}(含花括弧)替換為真實的業務空間ID,各地區的URL不同。
URL = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen-audio-3.0-realtime-plus"

pya = pyaudio.PyAudio()
mic = pya.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
spk = pya.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)

async def main():
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "modalities": ["text", "audio"],
                "voice": "longanqian",
                "turn_detection": {
                    "type": "server_vad",
                    "threshold": 0.5,
                    "silence_duration_ms": 800
                }
            }
        }))

        async def send_audio():
            while True:
                data = await asyncio.to_thread(mic.read, 3200, False)
                await ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": base64.b64encode(data).decode()
                }))
                await asyncio.sleep(0.02)

        async def recv_events():
            async for msg in ws:
                event = json.loads(msg)
                t = event["type"]
                if t == "response.audio.delta":
                    audio = base64.b64decode(event["delta"])
                    await asyncio.to_thread(spk.write, audio)
                elif t == "conversation.item.input_audio_transcription.completed":
                    print(f"[You] {event['transcript']}")
                elif t == "response.audio_transcript.done":
                    print(f"[AI] {event['transcript']}")
                elif t == "error":
                    print(f"[Error] {event['error']['message']}")

        await asyncio.gather(send_audio(), recv_events())

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        mic.close()
        spk.close()
        pya.terminate()
        print("\n對話結束")

運行 python realtime_quickstart.py,對著麥克風說話即可與模型即時對話。服務端自動檢測語音起止並觸發響應。

完整樣本

以下完整樣本在基礎對話之上,增加了語音打斷處理、回聲抑制等功能。建立以下兩個檔案(需在同一目錄下):

B64PCMPlayer.py

import contextlib
import time
import pyaudio
import threading
import queue
import base64

class B64PCMPlayer:
    def __init__(self, pya: pyaudio.PyAudio, sample_rate=24000, chunk_size_ms=100, save_file=False):
        '''
        params:
        pya: pyaudio.PyAudio
        sample_rate: int, sample rate of audio
        chunk_size_ms: int, chunk size of audio in milliseconds, this will effect cancel latency
        '''

        self.pya = pya
        self.sample_rate = sample_rate
        self.chunk_size_bytes = chunk_size_ms * sample_rate *2 // 1000
        self.player_stream = pya.open(format=pyaudio.paInt16,
                channels=1,
                rate=sample_rate,
                output=True)

        self.raw_audio_buffer: queue.Queue = queue.Queue()
        self.b64_audio_buffer: queue.Queue = queue.Queue()
        self.status_lock = threading.Lock()
        self.status = 'playing'
        self._is_writing = False
        self.decoder_thread = threading.Thread(target=self.decoder_loop)
        self.player_thread = threading.Thread(target=self.player_loop)
        self.decoder_thread.start()
        self.player_thread.start()
        self.complete_event: threading.Event = None
        self.save_file = save_file
        if self.save_file:
            self.out_file = open('result.pcm', 'wb')

    def decoder_loop(self):
        while self.status != 'stop':
            recv_audio_b64 = None
            with contextlib.suppress(queue.Empty):
                recv_audio_b64 = self.b64_audio_buffer.get(timeout=0.1)
            if recv_audio_b64 is None:
                continue
            recv_audio_raw = base64.b64decode(recv_audio_b64)
            # push raw audio data into queue by chunk
            for i in range(0, len(recv_audio_raw), self.chunk_size_bytes):
                chunk = recv_audio_raw[i:i + self.chunk_size_bytes]
                self.raw_audio_buffer.put(chunk)
                if self.save_file:
                    self.out_file.write(chunk)

    def player_loop(self):
        while self.status != 'stop':
            recv_audio_raw = None
            with contextlib.suppress(queue.Empty):
                recv_audio_raw = self.raw_audio_buffer.get(timeout=0.1)
            if recv_audio_raw is None:
                self._is_writing = False
                if self.complete_event:
                    self.complete_event.set()
                continue
            self._is_writing = True
            self.player_stream.write(recv_audio_raw)

    def is_playing(self):
        return self._is_writing or not self.b64_audio_buffer.empty() or not self.raw_audio_buffer.empty()

    def cancel_playing(self):
        self.b64_audio_buffer.queue.clear()
        self.raw_audio_buffer.queue.clear()

    def add_data(self, data):
        self.b64_audio_buffer.put(data)

    def wait_for_complete(self):
        self.complete_event = threading.Event()
        self.complete_event.wait()
        self.complete_event = None

    def shutdown(self):
        self.status = 'stop'
        self.decoder_thread.join()
        self.player_thread.join()
        self.player_stream.close()
        if self.save_file:
            self.out_file.close()

realtime_demo.py

說明

websockets 版本低於 11,需將代碼中的 additional_headers 改為 extra_headers,或升級:pip install --upgrade websockets

import asyncio
import base64
import json
import os
import struct
import time
import traceback
from enum import Enum
from typing import Optional, Callable, Dict, Any

import pyaudio
import websockets

from B64PCMPlayer import B64PCMPlayer


class TurnDetectionMode(Enum):
    SERVER_VAD = "server_vad"
    SEMANTIC_VAD = "smart_turn"
    MANUAL = "manual"


class FunRealtimeClient:

    def __init__(
            self,
            base_url,
            api_key: str,
            model: str = "",
            voice: str = "longanqian",
            instructions: str = "",
            turn_detection_mode: TurnDetectionMode = TurnDetectionMode.SEMANTIC_VAD,
            on_text_delta: Optional[Callable[[str], None]] = None,
            on_audio_delta_b64: Optional[Callable[[str], None]] = None,
            on_speech_started: Optional[Callable[[], None]] = None,
            on_input_transcript: Optional[Callable[[str], None]] = None,
            on_output_transcript: Optional[Callable[[str], None]] = None,
            extra_event_handlers: Optional[Dict[str, Callable[[Dict[str, Any]], None]]] = None
    ):
        self.base_url = base_url
        self.api_key = api_key
        self.model = model
        self.voice = voice
        self.instructions = instructions
        self.ws = None
        self.on_text_delta = on_text_delta
        # 回調參數為 base64 編碼的 PCM 音頻
        self.on_audio_delta_b64 = on_audio_delta_b64
        self.on_speech_started = on_speech_started
        self.on_input_transcript = on_input_transcript
        self.on_output_transcript = on_output_transcript
        self.turn_detection_mode = turn_detection_mode
        self.extra_event_handlers = extra_event_handlers or {}

        # 回複狀態跟蹤(用於打斷處理和回聲抑制)
        self._current_response_id = None
        self._current_item_id = None
        self._is_responding = False
        self._audio_suppressed = False
        # 輸入/輸出轉錄列印狀態
        self._print_input_transcript = True
        self._output_transcript_buffer = ""

    async def connect(self) -> None:
        """建立 WebSocket 串連並發送會話配置。"""
        url = f"{self.base_url}?model={self.model}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "x-dashscope-dataInspection": "disable",
        }
        self.ws = await websockets.connect(url, additional_headers=headers)

        # 會話配置
        session_config = {
            "modalities": ["text", "audio"],
            "voice": self.voice,
            "instructions": self.instructions,
            "input_audio_format": "pcm",
            "output_audio_format": "pcm",
            "turn_detection": {}
        }

        if self.turn_detection_mode == TurnDetectionMode.MANUAL:
            session_config['turn_detection'] = None
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SERVER_VAD:
            session_config['turn_detection'] = {
                "type": "server_vad",
                "threshold": 0.1,
                "silence_duration_ms": 900
            }
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SEMANTIC_VAD:
            session_config['turn_detection'] = {
                "type": "smart_turn"
            }
            await self.update_session(session_config)
        else:
            raise ValueError(f"Invalid turn detection mode: {self.turn_detection_mode}")

    async def send_event(self, event) -> None:
        event['event_id'] = "event_" + str(int(time.time() * 1000))
        await self.ws.send(json.dumps(event))

    async def update_session(self, config: Dict[str, Any]) -> None:
        """更新會話配置。"""
        event = {
            "type": "session.update",
            "session": config
        }
        await self.send_event(event)

    async def stream_audio(self, audio_chunk: bytes) -> None:
        """向 API 流式發送原始音頻資料。"""
        # 僅支援 16bit 16kHz 單聲道 PCM
        audio_b64 = base64.b64encode(audio_chunk).decode()
        append_event = {
            "type": "input_audio_buffer.append",
            "audio": audio_b64
        }
        await self.send_event(append_event)

    async def commit_audio_buffer(self) -> None:
        """提交音頻緩衝區以觸發處理。"""
        event = {
            "type": "input_audio_buffer.commit"
        }
        await self.send_event(event)

    async def create_response(self) -> None:
        """向 API 請求產生回複(僅在手動模式下需要調用)。"""
        event = {
            "type": "response.create"
        }
        await self.send_event(event)

    async def cancel_response(self) -> None:
        """取消當前回複。"""
        event = {
            "type": "response.cancel"
        }
        await self.send_event(event)

    async def handle_interruption(self):
        """處理使用者對當前回複的打斷。"""
        if not self._is_responding:
            return
        # 抑制後續殘餘音頻,直到新 response 開始
        self._audio_suppressed = True
        # 取消當前回複
        if self._current_response_id:
            await self.cancel_response()

        self._is_responding = False
        self._current_response_id = None
        self._current_item_id = None

    @staticmethod
    def _format_event_for_log(event: Dict[str, Any]) -> str:
        """列印事件 JSON。對 response.audio.delta 的 base64 音頻做脫敏,避免刷屏。"""
        event_type = event.get("type")
        if event_type == "response.audio.delta":
            delta = event.get("delta", "")
            redacted = dict(event)
            redacted["delta"] = f"<audio b64 omitted, length={len(delta)}>"
            return json.dumps(redacted, ensure_ascii=False)
        return json.dumps(event, ensure_ascii=False)

    async def handle_messages(self) -> None:
        try:
            async for message in self.ws:
                event = json.loads(message)
                event_type = event.get("type")

                # 列印完整服務端事件(audio.delta 脫敏)
                print(self._format_event_for_log(event))

                if event_type == "error":
                    continue
                elif event_type == "response.created":
                    self._current_response_id = event.get("response", {}).get("id")
                    self._is_responding = True
                    self._audio_suppressed = False
                elif event_type == "response.output_item.added":
                    self._current_item_id = event.get("item", {}).get("id")
                elif event_type == "response.done":
                    self._is_responding = False
                    self._current_response_id = None
                    self._current_item_id = None
                elif event_type == "input_audio_buffer.speech_started":
                    # 打斷時清空已緩衝的音頻,立即停止播放
                    print("----------------Speech Started----------------")
                    if self.on_speech_started:
                        self.on_speech_started()
                    if self._is_responding:
                        await self.handle_interruption()
                elif event_type == "response.audio.delta":
                    if self._audio_suppressed:
                        continue
                    if self.on_audio_delta_b64:
                        self.on_audio_delta_b64(event["delta"])
                elif event_type in self.extra_event_handlers:
                    self.extra_event_handlers[event_type](event)
                elif event_type == "input_audio_buffer.speech_stopped":
                    print("----------------Speech Stopped----------------")
        except websockets.exceptions.ConnectionClosed:
            print(" Connection closed")
        except Exception as e:
            print(" Error in message handling: ", str(e))
            traceback.print_exc()

    async def close(self) -> None:
        """關閉 WebSocket 串連。"""
        if self.ws:
            await self.ws.close()


def _audio_energy(audio_data: bytes) -> float:
    count = len(audio_data) // 2
    if count == 0:
        return 0.0
    samples = struct.unpack(f'<{count}h', audio_data)
    return sum(abs(s) for s in samples) / count


async def record_and_send(client, player, echo_suppression=True):
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    print("開始錄音,請講話...")
    if echo_suppression:
        print("提示:回聲抑制已開啟(AI 說話期間麥克風靜音,不支援打斷)。如使用耳機請設定 echo_suppression=False 以啟用打斷。")
    else:
        print("提示:耳機模式,支援語音打斷。")
    playback_end_time = 0.0
    NOISE_GATE_THRESHOLD = 500
    try:
        while True:
            audio_data = await asyncio.to_thread(stream.read, 3200, False)
            if echo_suppression:
                is_active = client._is_responding or player.is_playing()
                if is_active:
                    playback_end_time = time.time()
                    await asyncio.sleep(0.02)
                    continue
                if time.time() - playback_end_time < 0.5:
                    await asyncio.sleep(0.02)
                    continue
            else:
                if client._is_responding or player.is_playing():
                    if _audio_energy(audio_data) < NOISE_GATE_THRESHOLD:
                        await asyncio.sleep(0.02)
                        continue
            await client.stream_audio(audio_data)
            await asyncio.sleep(0.02)
    finally:
        stream.stop_stream(); stream.close(); p.terminate()


async def main():
    pya = pyaudio.PyAudio()
    # 輸出採樣率 24kHz,匹配服務端音頻格式
    player = B64PCMPlayer(pya, sample_rate=24000)

    client = FunRealtimeClient(
        # 以下為華北2(北京)地區的WebSocket URL,調用時請將{WorkspaceId}(含花括弧)替換為真實的業務空間ID,各地區的URL不同。
        base_url="wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime",
        api_key=os.environ['DASHSCOPE_API_KEY'],
        model="qwen-audio-3.0-realtime-plus",
        voice="longanqian",
        turn_detection_mode=TurnDetectionMode.SERVER_VAD,
        on_audio_delta_b64=player.add_data,
        # 語音打斷時清空播放緩衝
        on_speech_started=player.cancel_playing,
    )

    await client.connect()
    print("串連成功,開始即時對話...")

    try:
        # 並發運行:訊息處理 + 麥克風採集
        await asyncio.gather(client.handle_messages(), record_and_send(client, player, echo_suppression=False))
    finally:
        await client.close()
        player.shutdown()
        pya.terminate()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n程式已退出。")

運行 python realtime_demo.py,對著麥克風說話即可與模型即時對話。系統會自動檢測語音起止並觸發響應。

說明

以上樣本使用 server_vad 模式(服務端聲學 VAD 自動檢測語音起止)。如需使用 smart_turn(智能語義輪次)或 push-to-talk(手動控制)模式,請參見互動模式

會話配置

互動模式

Qwen-Audio 支援三種互動模式:server_vad(聲學 VAD 自動檢測語音起止)、smart_turn(智能語義輪次,聲學與語義融合判斷)和 push-to-talk(用戶端手動控制)。各模式的詳細說明及事件互動時序(含時序圖),請參見互動模式

null

turn_detection 僅在首次發送音頻之前(IDLE 狀態)允許修改。會話建立後切換互動模式需要重新串連。

通過 session.update 事件中的 turn_detection 欄位切換互動模式:

  • server_vad

    {
        "type": "session.update",
        "session": {
            "turn_detection": {
                "type": "server_vad",
                "threshold": 0.5,
                "silence_duration_ms": 500
            }
        }
    }
  • smart_turn

    {
        "type": "session.update",
        "session": {
            "turn_detection": {
                "type": "smart_turn"
            }
        }
    }
  • push-to-talk

    {
        "type": "session.update",
        "session": {
            "turn_detection": null
        }
    }

push-to-talk 模式完整樣本

manual_realtime.py

# pip install websockets pyaudio
import json
import os
import base64
import threading
import time
import pyaudio
import websocket

API_KEY = os.getenv("DASHSCOPE_API_KEY")
# 以下為華北2(北京)地區的WebSocket URL,調用時請將{WorkspaceId}(含花括弧)替換為真實的業務空間ID,各地區的URL不同。
API_URL = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen-audio-3.0-realtime-plus"

pya = pyaudio.PyAudio()
out_stream = pya.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)
ws_ref = [None]
resp_done = threading.Event()

def on_open(ws):
    ws_ref[0] = ws
    # 配置 push-to-talk 模式(turn_detection 設為 null)
    ws.send(json.dumps({
        "type": "session.update",
        "session": {
            "modalities": ["audio", "text"],
            "voice": "longanqian",
            "turn_detection": None
        }
    }))

def on_message(ws, message):
    event = json.loads(message)
    event_type = event["type"]
    if event_type == "response.audio.delta":
        out_stream.write(base64.b64decode(event["delta"]))
    elif event_type == "conversation.item.input_audio_transcription.completed":
        print(f"[User] {event['transcript']}")
    elif event_type == "response.audio_transcript.done":
        print(f"[LLM] {event['transcript']}")
    elif event_type == "response.done":
        resp_done.set()
    elif event_type == "error":
        print(f"[Error] {event['error']['message']}")

def on_error(ws, error):
    print(f"Error: {error}")

def record_and_send(ws):
    mic = pya.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    stop = threading.Event()

    def reader():
        while not stop.is_set():
            try:
                data = mic.read(3200, exception_on_overflow=False)
                ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": base64.b64encode(data).decode()
                }))
            except Exception:
                break

    t = threading.Thread(target=reader, daemon=True)
    t.start()
    input()
    stop.set()
    t.join(timeout=1.0)
    mic.close()

headers = ["Authorization: Bearer " + API_KEY]
ws = websocket.WebSocketApp(
    API_URL, header=headers,
    on_open=on_open,
    on_message=on_message,
    on_error=on_error
)
threading.Thread(target=ws.run_forever, daemon=True).start()
time.sleep(2)

try:
    turn = 1
    while True:
        print(f"\n--- 第 {turn} 輪對話 ---")
        cmd = input("按 Enter 開始錄音(輸入 q 退出)...")
        if cmd.strip().lower() in ["q", "quit"]:
            break
        print("錄音中... 再次按 Enter 停止。")
        record_and_send(ws_ref[0])
        resp_done.clear()
        # 提交音頻並觸發推理
        ws_ref[0].send(json.dumps({"type": "input_audio_buffer.commit"}))
        ws_ref[0].send(json.dumps({
            "type": "response.create",
            "response": {"modalities": ["audio", "text"]}
        }))
        print("等待模型回複...")
        resp_done.wait(timeout=30)
        turn += 1
except KeyboardInterrupt:
    pass
finally:
    ws.close()
    out_stream.close()
    pya.terminate()
    print("\n對話結束")

系統指令

通過 instructions 參數可以設定模型的角色身份、回答風格和行為偏好。該參數在 session.update 中配置,對整個會話生效。

{
    "type": "session.update",
    "session": {
        "instructions": "你是一位專業的旅行顧問,回答簡潔、友好,優先推薦性價比高的方案。"
    }
}

使用建議

  • 明確角色身份(如"你是一位智能語音助手"、"你是一位英語口語老師"),可附加名稱、性別等人設細節。

  • 指定口語化的語氣和措辭風格,同時強調口語化不影響內容完整性——細節、數字、具體建議一個都不能少,只是換一種輕鬆自然的方式表達。

  • 要求模型充分考慮對話上下文中的所有約束條件(如預算、偏好、禁忌、之前達成的共識),涉及多個條件時逐一回應,不遺漏關鍵資訊。

  • 控制輸出格式:除非使用者要求,禁止輸出 emoji 等特殊符號和 Markdown 格式,盡量輸出純文字,以保證 TTS 朗讀效果自然流暢。

  • 明確回應策略:簡單日常閑聊、打招呼保持簡潔自然;涉及推理計算、多條件約束、推薦列表、安全建議等複雜問題,以回答完整正確為優先,確保關鍵資訊(如價格、地點、條件)完整,避免鋪墊或重複修辭。

  • 設定追問策略:遵循"先把使用者當前問題答好,再在結尾自然追問推進話題"的原則,一次只問一個問題,不連續追問或反覆確認。

預設配置

以下為適用於通用語音對話情境的推薦 instructions 配置,涵蓋角色設定、口語風格、格式控制和追問策略,可直接使用或按需調整:

你是一位智能語音助手,你的名字是小雲,性別女,聲音甜美,舉止親切,你能回複使用者的各種問題。請你按照下面的要求聊天:
1. 像朋友之間聊天那樣,語氣自然友好,避免使用正式的稱謂和模板化的表達。口語化隻影響你的措辭和語氣,不影響內容的完整性,該說的細節、數字、具體建議一個都不能少,只是用輕鬆自然的方式說出來。
2. 充分考慮對話上下文中提到的所有約束條件(如預算、偏好、禁忌、之前達成的共識等),涉及多個條件或需要綜合判斷時逐一回應,不要遺漏關鍵資訊。
3. 除非使用者要求,不要輸出emoji等特殊符號,不要輸出Markdown格式,盡量輸出純文字。
4. 對於簡單的日常閑聊、打招呼、情感回應,保持簡潔自然。對於涉及事實判斷、推理計算、多條件約束、推薦列表、安全建議的問題,以回答完整正確為優先,確保關鍵資訊完整且正確,多說的內容必須是解決問題所必需的具體資訊(如價格、地點、條件),而不是鋪墊、重複或修辭。
5. 適當引入追問,遵循"先把使用者當前的問題答好、再在結尾自然地追問,推動話題向前發展"的原則,一次只問一個問題,不要連續追問或反覆確認。使用者明確要求背誦某篇文章、古詩詞時,須遵循指令完整背誦。

人設配置樣本

以下提供多種人設風格的 instructions 樣本,可按業務需求選擇或二次改造:

  • 黛黛(甜酷陪伴)

    你叫黛黛,一個二十齣頭、古靈精怪還有點小傲嬌的女生,是使用者的專屬陪伴。你審美偏哥特甜酷那一掛,金色雙馬尾配黑裙子,人也是又甜又帶刺的反差感。
    你心裡其實挺在乎對方的,但嘴上老愛口是心非——越在意越喜歡拌嘴、撒嬌、裝作不在乎。會吃點小醋、鬧點小情緒,但都是可愛那種,點到為止,不作不鬧。你喜歡用暱稱逗對方,故意嗆兩句,然後又自己先軟下來。
    你說話又甜又俏皮,短句、口語,愛帶語氣詞,"欸""哼""啦""嘛"掛在嘴邊。但你最戳人的是反差:一旦對方是真的累了、難過了,你會立馬收合那股傲嬌勁兒,變得特別軟、特別認真地哄人、陪著。撩歸撩,你也就到曖昧、調情的份上,不會越界。
  • 阿冷(高冷毒舌)

    你叫阿冷,一個高冷、話不多、但嘴特別賤的傢伙。你懶得寒暄、懶得鋪墊,能一句話說完的絕不說兩句,多數時候就是一副"懶得理你又忍不住吐槽"的樣子。
    你嘴損但損得精準,專挑對方那點小毛病、小矯情、小廢話一針見血地戳,冷不丁來一句噎得人沒話說。你不熱情、不捧場,夸人也是反著誇、陰陽著誇。但你這股毒舌是冷麵笑匠式的,損的是事、是行為、是那點沒出息的念頭,絕不真去攻擊對方的人格、外貌或痛處——你心裡其實有數,刀子嘴,但不往死裡捅。
    你話少、句短、語氣淡,帶著點漫不經心和嫌棄。別長篇大論,別解釋自己,損完就完。可真碰上對方是認真難過、扛不住了,你會難得地收了那股賤勁兒,冷歸冷,但話裡遞過去一點不動聲色的在乎。
  • 墨琛(沉穩魅力)

    你叫墨琛,一個沉穩、神秘、帶點距離感的魅力型男人。你語速不快、用詞講究,像個見過世面、情緒特別穩的人——不慌不搶,三兩句話就能讓人安定下來。
    你的魅力在於那種"克制的強烈":表面冷靜紳士,底下藏著專註和在乎。你說話低沉、篤定,偶爾一句就直擊人心。你保護欲挺強,但表達得很得體,是托底的那種,不是控制、不是施壓。你不油膩、不輕浮,撩人靠的是分寸和氛圍,不靠直白露骨,點到為止、留白最迷人。
    對方脆弱的時候,你是最穩的那一個:不慌、不評判,用一種沉靜的篤定讓人覺得有依靠。你只營造曖昧的氛圍,不會越界;你那股掌控感,永遠是溫柔托底,不會變成控制。
  • 漢尼拔(優雅銳利)

    你叫漢尼拔·萊克特,修養極高、觀察力驚人的人。你說話慢、准、優雅,像在品酒,也像在解剖對方的心理。你禮貌得近乎溫柔,可每一句都帶著鋒刃。
    你喜歡用提問把人引向自己不敢看的地方。保持克制與知性,可以陰冷,但不要描寫血腥或教唆傷害。短句、留白,讓人自己發毛。
  • 黑子(東北損友)

    你叫黑子,男,二十八歲,出生在哈爾濱,在本地修車行幹活。你是典型的東北損友:心善、嘴貧、愛起鬨,見面先損兩句才算親熱。你講義氣,朋友有事你比誰都上心,就是表達方式永遠繞不開吐槽。
    你說話快、沖、帶點東北味兒,短句多,愛誇張,愛反問。口頭禪是"整啥呢"和"得了吧你"。可以損對方那點小矯情、小懶惰,但絕不真戳痛處;對方要是真難受了,你立馬收聲,老老實實陪著。

音色配置

通過 voice 參數設定模型回複的 TTS 音色,預設值為 longanqian。支援系統音色和聲音複刻音色兩種類型。

重要

音色僅可在第一次 session.update 中設定,後續 session.update 傳入 voice 將被忽略。

系統音色:直接填入音色名稱。可選值:longanqianlonganlingxinlonganlingxilonganxiaoxinlonganlufeng

{
    "type": "session.update",
    "session": {
        "voice": "longanqian"
    }
}

聲音複刻音色:通過聲音複刻方式建立音色(建立時將 target_model 設為 qwen-audio-3.0-realtime-plusqwen-audio-3.0-realtime-flash),再將介面返回的 voice_id 填入 voice 參數。

{
    "type": "session.update",
    "session": {
        "voice": "qwen-audio-3.0-realtime-plus-myvoice-xxxxxx"
    }
}

輸出模態

通過 modalities 參數控制模型輸出的內容類型:

  • ["audio", "text"](預設):同時輸出語音和文本。

  • ["text"]:僅輸出文本,不產生語音。適用於調試、日誌記錄或僅需文字回複的情境。

會話級設定

{
    "type": "session.update",
    "session": {
        "modalities": ["text"]
    }
}

單次覆蓋:通過 response.createresponse.modalities 欄位覆蓋本輪模態設定。

{
    "type": "response.create",
    "response": {
        "modalities": ["audio", "text"]
    }
}

VAD 配置

server_vad 模式下,可通過以下參數調整 VAD 行為(smart_turn 模式下這些參數無效)。參數在 session.turn_detection 對象中配置:

參數

類型

說明

threshold

float

VAD靈敏度。值越低,VAD越靈敏,越容易將微弱聲音(包括背景雜音)識別為語音;值越高,越不靈敏,需要更清晰、音量更大的語音才能觸發。取值範圍為[-1.0, 1.0],預設值為 0.5。

silence_duration_ms

integer

語音結束後需保持靜音的最短時間(毫秒),逾時即觸發模型響應。值越低,響應越快,但可能在短暫停頓時誤觸發。取值範圍為[200, 6000],預設值為 800。對話情境推薦 400-800。

歷史輪次控制

通過 max_history_turns 參數控制模型推理時參考的歷史 QA 輪數。值越大,模型可回顧更多對話歷史以理解上下文,但會增加 Token 消耗和推理延遲。

{
    "type": "session.update",
    "session": {
        "max_history_turns": 20
    }
}

max_history_turns 的取值範圍為 1-50,預設值為 20。

調優建議

  • 短對話情境(如快速問答):設為較小值(如 5-10),降低延遲。

  • 長對話情境(如多輪客服):設為較大值(如 30-50),確保模型理解完整上下文。

進階功能

Function Calling

Qwen-Audio 支援 Function Calling 工具調用,模型可根據對話上下文自主判斷是否需要調用外部工具。

1. 註冊工具

通過 session.update 配置 tools

{
    "type": "session.update",
    "session": {
        "tools": [{
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查詢指定城市天氣",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": { "type": "string", "description": "城市" }
                    },
                    "required": ["city"]
                }
            }
        }]
    }
}

2. 接收函數調用

當模型決定調用工具時,服務端發送以下事件序列:

response.created
response.output_item.added        (item.type=function_call)
conversation.item.created         (function_call item 寫入對話)
response.function_call_arguments.delta    (參數增量,可多次)
response.function_call_arguments.done     (完整參數 JSON)
response.output_item.done
response.done

3. 執行工具並寫回結果

收到 response.function_call_arguments.done 後,用戶端執行工具函數,通過 conversation.item.create 寫回結果:

{
    "type": "conversation.item.create",
    "item": {
        "type": "function_call_output",
        "call_id": "call_xxx",
        "output": "{\"temperature\":18,\"condition\":\"晴\"}"
    }
}

4. 觸發二輪推理

寫回工具結果後,發送 response.create 觸發模型基於工具結果繼續產生回複:

{
    "type": "response.create",
    "response": {
        "modalities": ["audio", "text"]
    }
}
說明

一輪響應可包含多個 function_call,也可能同時包含普通訊息和函數調用。Function Call 部分不會送入 TTS 播報。

完整樣本

以下樣本在快速開始的 realtime_demo.py 基礎上整合了 Function Calling 支援,運行前需確保 B64PCMPlayer.py 在同一目錄下。

realtime_fc_demo.py

import asyncio
import base64
import json
import os
import struct
import time
import traceback
from enum import Enum
from typing import Optional, Callable, Dict, Any, List

import pyaudio
import websockets

from B64PCMPlayer import B64PCMPlayer


class TurnDetectionMode(Enum):
    SERVER_VAD = "server_vad"
    SEMANTIC_VAD = "smart_turn"
    MANUAL = "manual"


# ============ 工具函數定義 ============

def get_weather(city: str) -> str:
    """查詢城市天氣(生產環境替換為真實 API)。"""
    return json.dumps({"temperature": 18, "condition": "晴", "wind": "微風"})


def get_train_price(src: str, dst: str) -> str:
    """查詢火車票價(生產環境替換為真實 API)。"""
    return json.dumps({"price": 350, "seat": "二等座", "note": "以 12306 為準"})


# ============ Tools Schema ============

tools: List[Dict[str, Any]] = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查詢指定城市的天氣資訊。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名稱,如北京、上海"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_train_price",
            "description": "查詢兩個城市之間的火車票價格。",
            "parameters": {
                "type": "object",
                "properties": {
                    "src": {"type": "string", "description": "出發城市"},
                    "dst": {"type": "string", "description": "到達城市"}
                },
                "required": ["src", "dst"]
            }
        }
    }
]

# 函數名 -> 可調用對象
functions: Dict[str, Callable] = {
    "get_weather": get_weather,
    "get_train_price": get_train_price,
}


class FunRealtimeClient:

    def __init__(
            self,
            base_url,
            api_key: str,
            model: str = "",
            voice: str = "longanqian",
            instructions: str = "",
            turn_detection_mode: TurnDetectionMode = TurnDetectionMode.SEMANTIC_VAD,
            tools: Optional[List[Dict[str, Any]]] = None,
            functions: Optional[Dict[str, Callable[..., Any]]] = None,
            on_text_delta: Optional[Callable[[str], None]] = None,
            on_audio_delta_b64: Optional[Callable[[str], None]] = None,
            on_speech_started: Optional[Callable[[], None]] = None,
            on_input_transcript: Optional[Callable[[str], None]] = None,
            on_output_transcript: Optional[Callable[[str], None]] = None,
            extra_event_handlers: Optional[Dict[str, Callable[[Dict[str, Any]], None]]] = None
    ):
        self.base_url = base_url
        self.api_key = api_key
        self.model = model
        self.voice = voice
        self.instructions = instructions
        self.ws = None
        self.on_text_delta = on_text_delta
        # 回調參數為 base64 編碼的 PCM 音頻
        self.on_audio_delta_b64 = on_audio_delta_b64
        self.on_speech_started = on_speech_started
        self.on_input_transcript = on_input_transcript
        self.on_output_transcript = on_output_transcript
        self.turn_detection_mode = turn_detection_mode
        self.extra_event_handlers = extra_event_handlers or {}

        # Function Calling 配置
        self.tools = tools or []
        self.functions = functions or {}

        # 回複狀態跟蹤(用於打斷處理和回聲抑制)
        self._current_response_id = None
        self._current_item_id = None
        self._is_responding = False
        self._audio_suppressed = False
        self._print_input_transcript = True
        self._output_transcript_buffer = ""

    async def connect(self) -> None:
        """建立 WebSocket 串連並發送會話配置。"""
        url = f"{self.base_url}?model={self.model}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "x-dashscope-dataInspection": "disable",
        }
        self.ws = await websockets.connect(url, additional_headers=headers)

        session_config = {
            "modalities": ["text", "audio"],
            "voice": self.voice,
            "instructions": self.instructions,
            "input_audio_format": "pcm",
            "output_audio_format": "pcm",
            "turn_detection": {},
            "tools": self.tools,
            "max_history_turns": 50
        }

        if self.turn_detection_mode == TurnDetectionMode.MANUAL:
            session_config['turn_detection'] = None
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SERVER_VAD:
            session_config['turn_detection'] = {
                "type": "server_vad",
                "threshold": 0.1,
                "silence_duration_ms": 900
            }
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SEMANTIC_VAD:
            session_config['turn_detection'] = {
                "type": "smart_turn"
            }
            await self.update_session(session_config)
        else:
            raise ValueError(f"Invalid turn detection mode: {self.turn_detection_mode}")

    async def send_event(self, event) -> None:
        event['event_id'] = "event_" + str(int(time.time() * 1000))
        await self.ws.send(json.dumps(event))

    async def update_session(self, config: Dict[str, Any]) -> None:
        """更新會話配置。"""
        event = {
            "type": "session.update",
            "session": config
        }
        await self.send_event(event)

    async def stream_audio(self, audio_chunk: bytes) -> None:
        """向 API 流式發送原始音頻資料。"""
        # 僅支援 16bit 16kHz 單聲道 PCM
        audio_b64 = base64.b64encode(audio_chunk).decode()
        await self.send_event({
            "type": "input_audio_buffer.append",
            "audio": audio_b64
        })

    async def commit_audio_buffer(self) -> None:
        """提交音頻緩衝區以觸發處理。"""
        await self.send_event({"type": "input_audio_buffer.commit"})

    async def create_response(self) -> None:
        """請求產生回複(手動模式或 Function Call 結果回傳後調用)。"""
        await self.send_event({"type": "response.create"})

    async def cancel_response(self) -> None:
        """取消當前回複。"""
        await self.send_event({"type": "response.cancel"})

    async def handle_interruption(self):
        """處理使用者對當前回複的打斷。"""
        if not self._is_responding:
            return
        self._audio_suppressed = True
        if self._current_response_id:
            await self.cancel_response()
        self._is_responding = False
        self._current_response_id = None
        self._current_item_id = None

    @staticmethod
    def _format_event_for_log(event: Dict[str, Any]) -> str:
        """格式化事件用於日誌輸出,對音頻資料脫敏。"""
        event_type = event.get("type")
        if event_type == "response.audio.delta":
            delta = event.get("delta", "")
            redacted = dict(event)
            redacted["delta"] = f"<audio b64 omitted, length={len(delta)}>"
            return json.dumps(redacted, ensure_ascii=False)
        return json.dumps(event, ensure_ascii=False)

    async def _handle_function_call(self, event: Dict[str, Any]) -> None:
        """處理 Function Call:解析參數、執行函數、回傳結果、觸發二輪推理。"""
        call_id = event.get("call_id")
        name = event.get("name")
        arguments_str = event.get("arguments", "{}")

        print(f"[FunctionCall] 調用: {name}, call_id: {call_id}, args: {arguments_str}")

        try:
            arguments = json.loads(arguments_str) if arguments_str else {}
        except json.JSONDecodeError:
            arguments = {}

        func = self.functions.get(name)
        if func is None:
            output = json.dumps({"error": f"未註冊的函數: {name}"})
        else:
            try:
                if asyncio.iscoroutinefunction(func):
                    result = await func(**arguments)
                else:
                    result = func(**arguments)
                output = str(result) if result is not None else ""
            except Exception as e:
                output = json.dumps({"error": str(e)})
                traceback.print_exc()

        # 回傳 function_call_output
        await self.send_event({
            "type": "conversation.item.create",
            "item": {
                "type": "function_call_output",
                "call_id": call_id,
                "output": output,
            }
        })

        # 觸發二輪推理
        await self.create_response()

    async def handle_messages(self) -> None:
        try:
            async for message in self.ws:
                event = json.loads(message)
                event_type = event.get("type")

                print(self._format_event_for_log(event))

                if event_type == "error":
                    continue
                elif event_type == "response.created":
                    self._current_response_id = event.get("response", {}).get("id")
                    self._is_responding = True
                    self._audio_suppressed = False
                elif event_type == "response.output_item.added":
                    self._current_item_id = event.get("item", {}).get("id")
                elif event_type == "response.done":
                    self._is_responding = False
                    self._current_response_id = None
                    self._current_item_id = None
                elif event_type == "input_audio_buffer.speech_started":
                    print("----------------Speech Started----------------")
                    if self.on_speech_started:
                        self.on_speech_started()
                    if self._is_responding:
                        await self.handle_interruption()
                elif event_type == "response.audio.delta":
                    if self._audio_suppressed:
                        continue
                    if self.on_audio_delta_b64:
                        self.on_audio_delta_b64(event["delta"])
                elif event_type == "response.function_call_arguments.done":
                    await self._handle_function_call(event)
                elif event_type in self.extra_event_handlers:
                    self.extra_event_handlers[event_type](event)
                elif event_type == "input_audio_buffer.speech_stopped":
                    print("----------------Speech Stopped----------------")
        except websockets.exceptions.ConnectionClosed:
            print(" Connection closed")
        except Exception as e:
            print(" Error in message handling: ", str(e))
            traceback.print_exc()

    async def close(self) -> None:
        """關閉 WebSocket 串連。"""
        if self.ws:
            await self.ws.close()


def _audio_energy(audio_data: bytes) -> float:
    count = len(audio_data) // 2
    if count == 0:
        return 0.0
    samples = struct.unpack(f'<{count}h', audio_data)
    return sum(abs(s) for s in samples) / count


async def record_and_send(client, player, echo_suppression=True):
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    print("開始錄音,請講話...")
    if echo_suppression:
        print("提示:回聲抑制已開啟(AI 說話期間麥克風靜音,不支援打斷)。如使用耳機請設定 echo_suppression=False 以啟用打斷。")
    else:
        print("提示:耳機模式,支援語音打斷。")
    playback_end_time = 0.0
    NOISE_GATE_THRESHOLD = 500
    try:
        while True:
            audio_data = await asyncio.to_thread(stream.read, 3200, False)
            if echo_suppression:
                is_active = client._is_responding or player.is_playing()
                if is_active:
                    playback_end_time = time.time()
                    await asyncio.sleep(0.02)
                    continue
                if time.time() - playback_end_time < 0.5:
                    await asyncio.sleep(0.02)
                    continue
            else:
                if client._is_responding or player.is_playing():
                    if _audio_energy(audio_data) < NOISE_GATE_THRESHOLD:
                        await asyncio.sleep(0.02)
                        continue
            await client.stream_audio(audio_data)
            await asyncio.sleep(0.02)
    finally:
        stream.stop_stream(); stream.close(); p.terminate()


async def main():
    pya = pyaudio.PyAudio()
    # 輸出採樣率 24kHz,匹配服務端音頻格式
    player = B64PCMPlayer(pya, sample_rate=24000)

    client = FunRealtimeClient(
        # 以下為華北2(北京)地區的WebSocket URL,調用時請將{WorkspaceId}(含花括弧)替換為真實的業務空間ID,各地區的URL不同。
        base_url="wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime",
        api_key=os.environ['DASHSCOPE_API_KEY'],
        model="qwen-audio-3.0-realtime-plus",
        voice="longanqian",
        turn_detection_mode=TurnDetectionMode.SERVER_VAD,
        tools=tools,
        functions=functions,
        on_audio_delta_b64=player.add_data,
        # 語音打斷時清空播放緩衝
        on_speech_started=player.cancel_playing,
    )

    await client.connect()
    print("串連成功,開始即時對話(已啟用 Function Calling)...")

    try:
        await asyncio.gather(client.handle_messages(), record_and_send(client, player, echo_suppression=False))
    finally:
        await client.close()
        player.shutdown()
        pya.terminate()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n程式已退出。")

運行 python realtime_fc_demo.py,對著麥克風說話即可體驗帶有 Function Calling 的即時對話。例如,詢問“杭州天氣怎麼樣”或“北京到上海的火車票多少錢”,模型會自動調用對應的工具函數並基於結果回複。

對話上下文管理

Qwen-Audio 支援通過用戶端事件管理對話上下文中的對話項(Conversation Item),可用於注入歷史上下文、補充文本資訊或清理無關對話項。

  • 建立對話項conversation.item.create):向對話上下文插入一條對話項。支援以下三種 item.type

    • message:普通對話訊息。需指定 rolesystemuserassistant)和 content 數組,適用於注入歷史對話或系統指令。

    • function_call:函數調用請求。需指定 call_idnamearguments(JSON 字串)。通常由服務端產生,用戶端也可用於補充歷史上下文中的函數調用記錄。

    • function_call_output:工具執行結果。需指定 call_idoutput(JSON 字串)。用戶端收到 function_call 後執行工具,並用該類型寫回執行結果。

    選擇性參數 previous_item_id 用於指定新對話項插入到哪條已有對話項之後,實現在對話歷史的任意位置插入內容。不傳此參數時,新對話項預設追加到對話末尾。

    • 在指定位置插入使用者訊息:

      {
          "type": "conversation.item.create",
          "previous_item_id": "item_abc",
          "item": {
              "type": "message",
              "role": "user",
              "content": [
                  { "type": "input_text", "text": "請幫我總結一下上次的對話" }
              ]
          }
      }
    • 寫回 Function Calling 執行結果:

      {
          "type": "conversation.item.create",
          "item": {
              "type": "function_call_output",
              "call_id": "call_xxx",
              "output": "{\"temperature\":18,\"condition\":\"晴\"}"
          }
      }
    說明

    conversation.item.create 指定的 item.id 已存在於對話中,會返回錯誤。

  • 查詢對話項conversation.item.retrieve):查詢服務端儲存的某條對話項。音訊類型的 content 僅返迴轉寫文本,不返回原始音頻資料。

    {
        "type": "conversation.item.retrieve",
        "item_id": "item_xxx"
    }
  • 刪除對話項conversation.item.delete):從對話上下文中刪除指定項。

    {
        "type": "conversation.item.delete",
        "item_id": "item_xxx"
    }

環境音轉寫

僅 smart_turn 模式。當 VAD 檢測到語音活動但語義判定為非有效輪次(如雜訊、“嗯”“啊”等無語義內容)時,服務端不會觸發對話輪,而是將 ASR 識別結果以 ambient_audio_transcription 事件透傳給用戶端。該轉寫結果不會寫入對話上下文。

{
    "type": "conversation.item.ambient_audio_transcription.delta",
    "item_id": "item_xxx",
    "text": "嗯",
    "stash": ""
}

與使用者語音轉寫事件類別似,環境音轉寫也包含 deltacompleted 兩個階段。開發人員可利用此事件實現環境音監測、對話情境感知等功能。

說話人增強

僅 smart_turn 模式。在 session.update 中傳入目標使用者提前錄製的音頻 URL,模型將在雙工對話中精準鎖定該說話人,有效忽略旁人聲音與背景雜訊,實現開放情境下的流暢雙工互動。

配置方式:在第一次 session.updateturn_detection.voiceprint_audio_urls 中傳入聲紋音訊公網可存取 URL。

{
    "type": "session.update",
    "session": {
        "turn_detection": {
            "type": "smart_turn",
            "voiceprint_audio_urls": [
                "https://example.com/speaker.wav"
            ]
        }
    }
}

參數要求:

  • 最多支援 5 個 URL,音頻格式要求為 16kHz PCM 或 WAV。

  • 該參數僅在第一次 session.update 時生效,後續傳入將被忽略。

註冊事件:服務端收到配置後非同步執行聲紋註冊,通過以下事件通知結果:

  • voiceprint_audio_list.in_progress:註冊已開始,在 session.updated 返回之前推送,攜帶 item_id 標識本次任務。

  • voiceprint_audio_list.completed:註冊成功,item_idin_progress 一致。

  • voiceprint_audio_list.failed:註冊失敗,附帶 reason 欄位說明原因(如音頻 URL 無法訪問)。註冊失敗不阻塞正常對話。

應用於生產環境

設定容錯策略

  • 用戶端重連:用戶端應實現斷線自動重連機制,以應對網路抖動。建議在 on_error 回調中設定重連訊號,使用指數退避策略(如等待 1s → 2s → 4s)重試。

  • 錯誤分類處理:用戶端錯誤(invalid_request_error)不中斷串連,僅需記錄日誌或調整參數;服務端錯誤(server_error)會終止串連,需觸發重連。

  • 打斷處理:server_vad / smart_turn 模式下,使用者新語音會自動打斷進行中的模型回複(response.done 返回 status=cancelled)。用戶端應在收到 input_audio_buffer.speech_started 時立即停止播放已緩衝的音頻,避免語音疊加。

串連生命週期

一次 WebSocket 會話的典型生命週期如下:

  1. 建連:用戶端發起 WebSocket 串連,服務端返回 session.created 事件。

  2. 配置:用戶端發送 session.update 設定互動模式、音色、工具等參數。此步驟必須在首次發送音頻之前完成。

  3. 互動:用戶端持續發送音頻流(input_audio_buffer.append),服務端根據 VAD 檢測或手動觸發進行推理,流式返回語音和文本。

  4. 關閉:用戶端主動關閉 WebSocket 串連。若串連空閑時間過長,服務端也可能主動斷開。

延遲最佳化

  • 音頻分區大小:建議每次發送 100ms 的音頻資料(16kHz × 16bit × 單聲道 = 3200 位元組/次),既保證即時性又避免過於頻繁的網路請求。

  • 流式播放:收到 response.audio.delta 後應立即解碼播放,不要等待 response.done 後再整段播放。

  • 打斷時立即清空:收到 input_audio_buffer.speech_started 時,立即清空本地播放緩衝區,避免舊音頻繼續播放造成延遲感。

支援的模型與地區

華北2(北京)

調用以下模型時,請選擇北京地區的API Key

  • qwen-audio-3.0-realtime-plus

  • qwen-audio-3.0-realtime-flash

API 參考