跳至内容

Gemini 3.8 Live 教程:用 Python 构建全双工对话式智能体

学习如何在 Python 中流式传输麦克风音频、处理打断,并异步调用工具;同时比较 Gemini 3.8 Live 与其 Extended Thinking 变体。
更新 2026年9月25日  · 14分钟 读

用 AI 探索

ChatGPTClaudePerplexity

在本教程中,我们将使用 Google 近期发布的 Gemini 3.8 Live API,在 Python 中构建一个全双工、实时的语音助手。这里的全双工意味着助手和我都能在同一时间说话并倾听,就像自然的电话通话那样可以互相打断,而不是像对讲机那样轮流发言。

我们会在本地 Jupyter 笔记本中逐步构建智能体,便于您跟随操作。下面是智能体运行时的预览:

要点速览

  • Gemini 3.8 Live 通过一个 WebSocket 双向流式传输音频,因此您可以构建一边说话一边聆听、还能处理打断的语音助手。

  • 教程使用 Python 构建该助手,包含四个 asyncio 工作协程(麦克风录音、音频发送、接收、播放),由两个队列串联。

  • 插话(barge-in)通过在 Gemini 发送 interrupted 时清空本地播放队列来实现。

  • 添加一个工具(实时天气查询)可以看出两种模型的差异:标准模型在工具运行时会保持沉默,而 Extended Thinking 会持续说话。

  • 使用 Extended Thinking 时,应跟踪 interaction_status == "IDLE" 而不是 turn_complete,并将工具调用作为后台任务运行,以确保接收循环永不阻塞。

Gemini 3.8 Live 有何独到之处?

Google 的 Gemini 3.8 Live 是一款原生语音到语音模型,专为实时流式和交互式音频应用打造。Gemini 3.8 Live 通过持久的 WebSocket 连接直接处理多模态输入。

这种双向流式能力使开发者能够创建全双工的对话式智能体,能够同时聆听和说话,并支持自然的用户打断以及实时音频转写等功能。

在应用开发方面,Gemini 3.8 Live 引入了异步工具调用和后台推理,使智能体在与用户保持主动对话的同时,执行外部函数调用或进行数据检索。

如需了解其功能、基准和定价的完整概览,请参阅我们的 Gemini 3.8 Live 指南。

实时语音助手的工作原理:4 个工作协程与 2 个队列

在动手写代码之前,先理解一下实时语音助手的底层工作方式。

在标准 Python 脚本中,代码按顺序逐行运行:函数 A 完成后再运行函数 B。但在实时语音对话中,等待不可行:

  • 当我们在说话时,程序必须实时把您的声音流式传给 Gemini。
  • 当 Gemini 在回复时,程序必须在音频分片到达时立刻通过扬声器播放。
  • 更重要的是,即使 Gemini 在说话,程序也必须持续监听,以便我们可以插话(barge in)。

为实现这一点且不发生卡顿,我们使用 Python 的 asyncio 来运行 4 个轻量级后台任务(“工作协程”),它们通过两个 asyncio.Queue 缓冲区(把它们想象成传送带)进行通信:

1. 入站传送带(input_queue):

  • audio_recorder():持续监听麦克风,并将音频切片投到传送带上。

  • send_audio_loop():从传送带取走音频切片,并将其流式发送到 Gemini。

2. 出站传送带(audio_queue):

  • receive_loop():监听 Gemini。当文本到达时打印;当语音到达时,将音频分片投到传送带上。

  • audio_player():从传送带取出音频分片,并通过扬声器或耳机播放。

实时 Gemini 3.8 Live 语音助手架构图,展示四个工作协程如何与输入和输出音频交互。

由于每个工作协程都只专注于自己的一小块工作,这四个协程可以在 Python 事件循环上并发运行,互不干扰。

本教程使用的完整代码可在 此 GitHub 仓库获取。

如何生成并设置 Gemini API 密钥

要使用 Gemini API,我们需要创建并设置一个 API 密钥,使我们的代码能够与 API 通信。

最简单的方法是:

  • 访问 Google 的 AI Studio API 密钥页面并登录。

  • 点击右上角的“Create API key”按钮。

  • 将 API 密钥复制到与 Python 代码同一文件夹下名为 .env 的文件中,格式如下:

GEMINI_API_KEY=replace_with_api_key

请注意,使用 API 通常会产生费用。免费层覆盖对两种 Gemini 3.8 Live 模型的限量访问,但免费层数据将用于改进 Google 的产品。用于生产或需要更高限额时,需要在 Google 的 AI Studio 结算页面配置支付方式。

如何用 Gemini 3.8 Live 实现语音助手架构

以下步骤设计为在本地 Jupyter 笔记本中运行,每段代码对应一个单元格。由于需要访问麦克风和扬声器,因此无法在 Google Colab 等在线笔记本中开箱即用。

步骤 1:环境设置与导入

首先,确保安装所需软件包:

pip install google-genai sounddevice python-dotenv

这些包的作用如下:

  • google-genai:用于与 Gemini 模型交互的官方 Google 包。

  • sounddevice:用于处理音频硬件,从麦克风录音并通过扬声器播放。

  • python-dotenv:用于从 .env 文件加载 Gemini API 密钥的工具包。

现在我们可以加载环境变量、校验 API 密钥,并初始化 genai.Client。

import asyncio
import os
import sys
from dotenv import load_dotenv
from google import genai
from google.genai import types
import sounddevice as sd

# Load environment variables from .env file
load_dotenv()

api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
    raise ValueError("GEMINI_API_KEY not found. Please set it in your .env file or environment.")

# Initialize the Gemini Client
client = genai.Client(api_key=api_key)
print("Gemini Client initialized successfully!")

步骤 2:发起第一个请求

我们先通过发送一个文本轮次并接收流式语音与转写,来了解 Gemini Live 连接生命周期的核心流程。我们会发送文本提示并接收文本与音频响应。不过我们暂时不播放音频,只专注于收集音频分片。

Gemini Live API 使用持久的 WebSocket 连接,通过 client.aio.live.connect() 访问。要配置语音输出和实时转写,我们提供一个 config 字典:

# Session configuration
config = {
    "response_modalities": ["AUDIO"],
    "output_audio_transcription": {},
}
  • response_modalities:使用 ["AUDIO"] 指示 Gemini 用语音音频进行回应。

  • output_audio_transcription:值 {} 指示 Gemini 同步流式输出其语音内容的文本转写。

现在我们可以使用 session.send_client_content() 发送文本提示,并流式读取传入的文本转写。

print("Connecting to Gemini 3.8 Live API...")
async with client.aio.live.connect(model="gemini-3.8-live", config=config) as session:
    print("Connected! Sending text prompt...")
    
    await session.send_client_content(
        turns={"role": "user", "parts": [{"text": "Hello! In one short sentence, introduce yourself."}]},
        turn_complete=True,
    )
    
    print("\n[Gemini Transcription]: ", end="", flush=True)
    audio_chunks_received = 0
    total_audio_bytes = 0
    
    async for response in session.receive():
        server_content = response.server_content
        if server_content:
            # 1. Print real-time transcription as tokens arrive
            if server_content.output_transcription:
                print(server_content.output_transcription.text, end="", flush=True)
                
            # 2. Inspect audio chunks
            if server_content.model_turn:
                for part in server_content.model_turn.parts:
                    if part.inline_data and part.inline_data.data:
                        audio_chunks_received += 1
                        total_audio_bytes += len(part.inline_data.data)

    print(f"\n\nReceived {audio_chunks_received} audio chunks ({total_audio_bytes:,} bytes total).")

运行此代码时,我们应能看到类似输出:

Connecting to Gemini 3.8 Live API...
Connected! Sending text prompt...

[Gemini Transcription]: Hello, I am your helpful AI assistant designed to assist you with various tasks and answer your questions.

Received 22 audio chunks (304,800 bytes total).

代码捕获了音频分片,但我们尚未设置音频播放器,因此听不到。接下来学习如何定义音频播放器。

步骤 3:实时音频播放

在步骤 2 中,我们收到了成千上万字节的音频数据,但并未听到任何声音。如果在接收循环内直接写入音频硬件,任何网络延迟都会导致音频卡顿,而任何播放延迟又会阻塞网络接收。

为防止音频播放阻塞网络接收器,我们实现第一个工作协程:audio_player()。

您无需关心底层音频实现细节。建议将其视为黑盒即可。

OUTPUT_SAMPLE_RATE = 24000
CHANNELS = 1

async def audio_player(audio_queue: asyncio.Queue):
    """Plays raw 24kHz audio chunks from audio_queue through the speakers."""
    loop = asyncio.get_running_loop()
    with sd.RawOutputStream(
        samplerate=OUTPUT_SAMPLE_RATE, channels=CHANNELS, dtype="int16"
    ) as stream:
        while True:
            chunk = await audio_queue.get()
            if chunk is None:  # Sentinel value signaling end of stream
                audio_queue.task_done()
                break
            await loop.run_in_executor(None, stream.write, chunk)
            audio_queue.task_done()

print("Audio player defined!")

为了测试它,我们将 audio_player() 接到我们的请求上。这一次,我们会在观察流式转写的同时,实时听到 Gemini 大声说话:

audio_queue = asyncio.Queue()
player_task = asyncio.create_task(audio_player(audio_queue))

prompt_text = "Hello! In one short sentence, introduce yourself."
print(f"[User]: {prompt_text}")

async with client.aio.live.connect(model="gemini-3.8-live", config=config) as session:
    await session.send_client_content(
        turns={"role": "user", "parts": [{"text": prompt_text}]},
        turn_complete=True,
    )
    
    print("[Gemini]: ", end="", flush=True)
    async for response in session.receive():
        server_content = response.server_content
        if server_content:
            if server_content.output_transcription:
                print(server_content.output_transcription.text, end="", flush=True)

            if server_content.model_turn:
                for part in server_content.model_turn.parts:
                    if part.inline_data and part.inline_data.data:
                        await audio_queue.put(part.inline_data.data)

print()
# Signal the player to shut down and await completion
await audio_queue.put(None)
await player_task
print("Playback complete!")

运行该代码片段后,我们现在可以听到 Gemini 的响应。

步骤 4:采集用户语音输入

要与 Gemini 实时对话,我们需要持续地从麦克风捕获我们的声音。

第二个工作协程是 audio_recorder()。它在后台监听您的麦克风,将输入语音切分为小分片,并放入 input_queue。我们将采样率设置为 16 kHz,这是 Gemini 期望的标准语音格式。

INPUT_SAMPLE_RATE = 16000  # Gemini Live expects 16kHz audio input
CHUNK_SIZE = 1024          # Number of samples per audio chunk

async def audio_recorder(input_queue: asyncio.Queue, stop_event: asyncio.Event):
    """Captures microphone input and puts raw audio chunks into the input queue."""
    loop = asyncio.get_running_loop()

    def record_loop():
        with sd.RawInputStream(
            samplerate=INPUT_SAMPLE_RATE,
            channels=CHANNELS,
            dtype="int16",
            blocksize=CHUNK_SIZE,
        ) as stream:
            while not stop_event.is_set():
                data, _ = stream.read(CHUNK_SIZE)
                loop.call_soon_threadsafe(input_queue.put_nowait, bytes(data))

    await asyncio.to_thread(record_loop)

print("Audio recorder defined!")

步骤 5:编写连续流式发送音频的函数

在步骤 2 中,我们使用 send_client_content() 发送了静态文本轮次。对于连续语音流式传输,Live API 提供了 session.send_realtime_input()。

第三个工作协程是 send_audio_loop()。它监听 input_queue,一旦有来自麦克风的音频分片到达,就通过已打开的 WebSocket 转发给 Gemini。

请注意,我们无需手动告知 Gemini 何时开始或停止说话:Gemini 使用内置的语音活动检测(VAD)自动检测您开始与结束说话的时刻。

async def send_audio_loop(session, input_queue: asyncio.Queue, stop_event: asyncio.Event):
    """Continuously streams microphone chunks from input_queue to Gemini."""
    while not stop_event.is_set():
        try:
            chunk = await asyncio.wait_for(input_queue.get(), timeout=0.1)
            await session.send_realtime_input(
                audio=types.Blob(data=chunk, mime_type=f"audio/pcm;rate={INPUT_SAMPLE_RATE}")
            )
            input_queue.task_done()
        except asyncio.TimeoutError:
            continue

print("send_audio_loop defined!")

就像我们在步骤 3 中用文本提示测试音频播放一样,现在可以用一次口述问题来端到端测试我们的麦克风流式传输。

当运行下面的单元格时,请对着麦克风说出一个问题(例如:“法国的首都是哪里?”)。Gemini 会直接处理我们的语音,并用合成语音及实时转写进行回复:

audio_queue = asyncio.Queue()
input_queue = asyncio.Queue()
stop_event = asyncio.Event()

player_task = asyncio.create_task(audio_player(audio_queue))

print("Connecting to Gemini Live API...")
async with client.aio.live.connect(model="gemini-3.8-live", config=config) as session:
    print("Connected! Speak a question into your microphone (e.g. 'What is the capital of France?')...")
    recorder_task = asyncio.create_task(audio_recorder(input_queue, stop_event))
    sender_task = asyncio.create_task(send_audio_loop(session, input_queue, stop_event))

    print("\n[Gemini]: ", end="", flush=True)
    async for response in session.receive():
        server_content = response.server_content
        if server_content:
            # 1. As soon as Gemini starts replying, mute the microphone
            # so speaker audio cannot loop back into the mic and interrupt Gemini
            if not stop_event.is_set() and (server_content.output_transcription or server_content.model_turn):
                stop_event.set()

            # 2. Print transcription text as it streams
            if server_content.output_transcription:
                print(server_content.output_transcription.text, end="", flush=True)

            # 3. Queue audio parts for playback
            if server_content.model_turn:
                for part in server_content.model_turn.parts:
                    if part.inline_data and part.inline_data.data:
                        await audio_queue.put(part.inline_data.data)

            # 4. Turn complete
            if server_content.turn_complete:
                break

    # Clean up mic tasks cleanly
    stop_event.set()
    recorder_task.cancel()
    sender_task.cancel()
    await asyncio.gather(recorder_task, sender_task, return_exceptions=True)

# 5. Wait for playback queue to drain, then allow the soundcard buffer to finish playing
await audio_queue.join()
await asyncio.sleep(0.8)  # Prevents clipping the final syllables
await audio_queue.put(None)
await player_task

print("\nSingle-turn voice test complete!")

步骤 6:多轮对话与打断

注意上面测试中发生了什么:我们通过麦克风提出了问题,Gemini 直接理解了我们的语音并大声回复。然而,如果我们尝试追问,当前会话已经结束。

为了解决这个问题,我们需要处理构建真实语音助手的两个关键点:多轮持久化与打断。

多轮会话持久化:

在 google-genai SDK 中,session.receive() 是单轮的异步生成器。当 Gemini 说完答案,session.receive() 就结束了。如果不将其包裹在外层循环中,助手会在首次响应后终止。

为支持连续的多轮对话,我们将 session.receive() 包裹在外层的 while not stop_event.is_set(): 循环中:

while not stop_event.is_set():
    async for response in session.receive():
        ...

插话/打断与缓冲区清空:

Gemini 3.8 Live 具有原生语音活动检测与插话支持。如果当 Gemini 在说话时您开始说话,Gemini 会立刻停止生成音频并发送标志:server_content.interrupted == True。

即使 Gemini 停止发送新音频,我们本地的 audio_queue 里可能仍有一些等待播放的音频分片。如果不清空该队列,扬声器会继续播放之前的回答。

因此,一旦收到 server_content.interrupted,我们就清空队列,让播放立刻停止:

if server_content.interrupted:
    print("\n[Interrupted!]")
    while not audio_queue.empty():
        audio_queue.get_nowait()
        audio_queue.task_done()

整合到一起

下面是第四个也是最后一个工作协程:receive_loop()。它融合了多轮持久化、实时转写以及即时打断:

async def receive_loop(session, audio_queue: asyncio.Queue, stop_event: asyncio.Event):
    """Receives transcription and audio output from Gemini across multiple turns."""
    first_chunk_received = False
    try:
        while not stop_event.is_set():
            async for response in session.receive():
                if stop_event.is_set():
                    break

                server_content = response.server_content
                if server_content:
                    # 1. Handle user interruption (barge-in)
                    if server_content.interrupted:
                        print("\n[Interrupted!]")
                        # Flush remaining unplayed audio so speakers go silent immediately
                        while not audio_queue.empty():
                            try:
                                audio_queue.get_nowait()
                                audio_queue.task_done()
                            except asyncio.QueueEmpty:
                                break
                        first_chunk_received = False
                        print("\n[Listening... Speak now]")

                    # 2. Print real-time transcription
                    if server_content.output_transcription:
                        if not first_chunk_received:
                            print("\n[Gemini]: ", end="", flush=True)
                            first_chunk_received = True
                        print(server_content.output_transcription.text, end="", flush=True)

                    # 3. Enqueue synthesized audio for playback
                    if server_content.model_turn:
                        for part in server_content.model_turn.parts:
                            if part.inline_data and part.inline_data.data:
                                await audio_queue.put(part.inline_data.data)

                    # 4. Interaction complete: wait for audio to finish playing before prompt
                    # In Gemini 3.8, interaction_status tracks when the overall exchange is finished
                    is_done = False
                    if server_content.interaction_status is not None:
                        is_done = str(server_content.interaction_status).endswith("IDLE") or server_content.interaction_status == "IDLE"
                    elif server_content.turn_complete:
                        is_done = True

                    if is_done:
                        print()
                        await audio_queue.join()
                        first_chunk_received = False
                        print("\n[Listening... Speak now]")
    except asyncio.CancelledError:
        pass
    except Exception as e:
        print(f"\n[Receive Error]: {e}", file=sys.stderr)
        stop_event.set()

print("receive_loop defined!")

步骤 7:组装完整语音助手

现在我们在 run_voice_assistant 中编排四个并发工作协程:

  • audio_player():从 audio_queue 消费并写入扬声器。

  • audio_recorder():读取麦克风并将音频推入 input_queue。

  • send_audio_loop():从 input_queue 消费并通过 session.send_realtime_input() 流式发送给 Gemini。

  • receive_loop():通过 session.receive() 消费 Gemini 输出,打印转写并将音频推送到 audio_queue 进行播放。

Gemini 3.8 Live 语音助手的工作流程

async def run_voice_assistant():
    """Runs the full-duplex interactive voice assistant."""
    audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
    input_queue: asyncio.Queue[bytes] = asyncio.Queue()
    stop_event = asyncio.Event()

    player_task = asyncio.create_task(audio_player(audio_queue))

    print("Connecting to Gemini Live API...")
    async with client.aio.live.connect(model="gemini-3.8-live", config=config) as session:
        print("[Listening... Speak now]")

        recorder_task = asyncio.create_task(audio_recorder(input_queue, stop_event))
        sender_task = asyncio.create_task(send_audio_loop(session, input_queue, stop_event))
        receiver_task = asyncio.create_task(receive_loop(session, audio_queue, stop_event))

        try:
            while not stop_event.is_set():
                await asyncio.sleep(0.5)
        except (asyncio.CancelledError, KeyboardInterrupt):
            print("\nStopping voice assistant...")
        finally:
            stop_event.set()
            recorder_task.cancel()
            sender_task.cancel()
            receiver_task.cancel()
            await asyncio.gather(recorder_task, sender_task, receiver_task, return_exceptions=True)

    # Terminate player
    await audio_queue.put(None)
    await player_task
    print("\nSession finished cleanly.")

print("run_voice_assistant is ready to run!")

步骤 8:运行实时助手

在笔记本中这样运行语音助手:

await run_voice_assistant()

注意事项:

  • 强烈建议使用耳机。如果 Gemini 的声音通过笔记本扬声器外放,麦克风会拾取到,Gemini 会认为您在尝试打断它。
  • 要停止助手,只需点击笔记本的中断按钮(■)。
  • 如果我们在笔记本运行期间连接或断开耳机,音频设备设置可能会变化,从而引发音频错误。此时需要重启内核并按顺序重新运行单元格。

使用 Gemini 3.8 Live Extended Thinking 的高级用法

Gemini 3.8 Live 提供两个版本:

  • 标准版(gemini-3.8-live):针对超低时延的直接语音对话进行优化。调用工具时,会在工具返回前保持沉默。

  • Extended Thinking(gemini-3.8-live-extended-thinking):具备后台推理与并行对话填充。它可以在后台执行工具时,用自然的提示语(如“让我为您查一下……”)持续交流。

Gemini 3.8 Live 与 Gemini 3.8 Live Extended Thinking 对比

以下是两者差异的拆解:

 

gemini-3.8-live

gemini-3.8-live-extended-thinking

最佳适用场景

低时延语音代理、直接指令、快速工具

多步推理、规划、较慢或多工具并行

推理方式

交织、固定时延(无 thinking_level)

后台推理(thinking_level: low, medium, high)

工具运行期间

保持沉默

说出对话填充语

交互结束信号

turn_complete

interaction_status == "IDLE"

工具行为

BLOCKING 或 NON_BLOCKING(默认)

NON_BLOCKING 仅

何时使用 Gemini 3.8 Live 与 3.8 Live Extended Thinking

如果您不确定该使用哪一个版本,以下是我的决策框架。构建对话式智能体时:

  • 对于直接问答与快速语音指令,且以最小化时延为首要目标,使用 gemini-3.8-live。

  • 对于需要多步推理、外部数据检索或 API 调用,同时与用户保持自然主动对话的富交互助手与智能体,使用 gemini-3.8-live-extended-thinking。

如何用 Gemini 3.8 Live 实现工具调用

扩展思考版模型的一大优势是它可以在保持对话的同时,在后台进行推理并执行工具。

在深入代码之前,先看看实际效果。我为基础模型配置了一个查询天气的工具。这里有一段我询问纽约天气的视频;请注意模型在计算答案时是保持沉默的:

下面是相同的交互,但启用了扩展思考:

第二次交互更为灵动,感觉更像自然对话,因为模型可以在后台处理信息的同时继续维持对话。

在助手中构建要使用的工具

模型并不会替我们执行工具。工具配置的作用是让模型知道这些工具的存在、何时以及如何使用。当 Gemini 判断需要外部数据时,它会在 response.tool_call 中填入函数的名称与参数。

要将自定义工具集成到 Gemini 3.8 Live,我们必须把本地代码与模型的推理引擎桥接起来。这需要以下几步:

  • 执行逻辑:定义一个标准的 Python 函数来实际完成工作并返回结果。

  • 工具映射:创建一个字典(tool_map),将函数的字符串名称映射到可执行的 Python 对象。

  • 函数声明:构建一个 FunctionDeclaration 作为工具的“使用说明书”。通过清晰定义名称、描述和参数 Schema(包括类型与必填字段),我们教会 Gemini 何时使用该工具以及如何格式化其请求。我们还将 behavior="NON_BLOCKING" 设定为扩展思考所需,以便它能在工具运行时继续说话。

  • 会话配置:将该声明注入到会话的 tools_config 负载中。

为演示这一点,我们创建一个天气查询工具:

import urllib.request
import urllib.parse
import json
import asyncio

async def get_current_weather(location: str) -> str:
    """Fetch live real-time weather for any city in the world using Open-Meteo's free API."""
    def fetch():
        # 1. Geocode city name to lat/lon coordinates
        geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={urllib.parse.quote(location)}&count=1"
        req = urllib.request.Request(geo_url, headers={"User-Agent": "VoiceAssistantTutorial/1.0"})
        with urllib.request.urlopen(req, timeout=5) as r:
            geo_data = json.loads(r.read().decode("utf-8"))
            if not geo_data.get("results"):
                return f"Could not find coordinates for '{location}'."
            loc = geo_data["results"][0]
            lat, lon = loc["latitude"], loc["longitude"]
            city_name = loc.get("name", location)
            country = loc.get("country", "")

        # 2. Fetch current temperature
        weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current=temperature_2m"
        req2 = urllib.request.Request(weather_url, headers={"User-Agent": "VoiceAssistantTutorial/1.0"})
        with urllib.request.urlopen(req2, timeout=5) as r:
            weather_data = json.loads(r.read().decode("utf-8"))
            temp = weather_data.get("current", {}).get("temperature_2m")
            return f"The current temperature in {city_name}, {country} is {temp}°C."

    try:
        return await asyncio.to_thread(fetch)
    except Exception as e:
        return f"Error retrieving weather for {location}: {e}"

tool_map = {
    "get_current_weather": get_current_weather,
}

weather_tool = types.FunctionDeclaration(
    name="get_current_weather",
    description="Get the current live weather and temperature for a given city or location.",
    behavior="NON_BLOCKING", 
    parameters=types.Schema(
        type="OBJECT",
        properties={
            "location": types.Schema(
                type="STRING",
                description="The city or location name (e.g. Tokyo, Paris, New York).",
            )
        },
        required=["location"],
    ),
)

tools_config = {
    "response_modalities": ["AUDIO"],
    "output_audio_transcription": {},
    "tools": [
        {"function_declarations": [weather_tool]},
    ],
}

print("Tool and configuration defined!")

异步处理工具调用

一边说话一边运行工具需要两方面配合。服务器端,NON_BLOCKING 声明让扩展思考能够继续说话而不是等待结果。客户端,我们的代码也不能阻塞。如果在接收循环内直接运行工具,一个 1.5 秒的 API 调用会导致在工具结束之前,我们无法读取 Gemini 的填充语音与打断信号。

为实现真正的“边说边执行”,我们用两个关键设计更新 receive_loop_with_tools():

  1. 非阻塞执行:通过 asyncio.create_task() 将 handle_tool_call 作为并发后台任务启动。这样接收循环在 Python 并行获取天气的同时,仍能持续处理并播放 Gemini 的语音。

  2. 跟踪交互状态:在扩展思考模式下,当说完中间的填充短语(如“正在为您查询天气……”)时,Gemini 会发出 turn_complete: True。如果代码只检查 turn_complete,助手会在工具仍在运行时过早提示 [Listening... Speak now]!通过检查 server_content.interaction_status == "IDLE",客户端会等到所有后台推理、工具调用与最终发言真正完成后,才重新打开麦克风。

下面是 receive_loop_with_tools()。除了新增的 handle_tool_call() 辅助函数与第 1 块(分派工具调用)外,其余与 receive_loop() 相同:

async def receive_loop_with_tools(session, audio_queue: asyncio.Queue, stop_event: asyncio.Event):
    """Receives transcription and audio from Gemini, and automatically handles tool calls asynchronously."""
    first_chunk_received = False

    async def handle_tool_call(tool_call):
        """Executes tool calls in the background without blocking the audio receive loop."""
        try:
            function_responses = []
            for fc in tool_call.function_calls:
                print(f"\n[Tool Requested]: {fc.name}({fc.args})")
                fn = tool_map.get(fc.name)
                if fn:
                    if asyncio.iscoroutinefunction(fn):
                        result = await fn(**fc.args)
                    else:
                        result = fn(**fc.args)
                else:
                    result = f"Error: Unknown tool {fc.name}"
                print(f"[Tool Result]: {result}")
                function_responses.append(
                    types.FunctionResponse(
                        id=fc.id,
                        name=fc.name,
                        response={"result": result},
                    )
                )
            await session.send_tool_response(function_responses=function_responses)
        except Exception as e:
            print(f"\n[Tool Execution Error]: {e}", file=sys.stderr)

    try:
        while not stop_event.is_set():
            async for response in session.receive():
                if stop_event.is_set():
                    break

                # 1. Handle tool calls asynchronously (non-blocking)
                if response.tool_call:
                    asyncio.create_task(handle_tool_call(response.tool_call))

                server_content = response.server_content
                if server_content:
                    # 2. Handle user interruption (barge-in)
                    if server_content.interrupted:
                        print("\n[Interrupted!]")
                        while not audio_queue.empty():
                            try:
                                audio_queue.get_nowait()
                                audio_queue.task_done()
                            except asyncio.QueueEmpty:
                                break
                        first_chunk_received = False
                        print("\n[Listening... Speak now]")

                    # 3. Print real-time transcription
                    if server_content.output_transcription:
                        if not first_chunk_received:
                            print("\n[Gemini]: ", end="", flush=True)
                            first_chunk_received = True
                        print(server_content.output_transcription.text, end="", flush=True)

                    # 4. Enqueue synthesized audio for playback
                    if server_content.model_turn:
                        for part in server_content.model_turn.parts:
                            if part.inline_data and part.inline_data.data:
                                await audio_queue.put(part.inline_data.data)

                    # 5. Check if the interaction is complete
                    is_done = False
                    if server_content.interaction_status is not None:
                        is_done = str(server_content.interaction_status).endswith("IDLE") or server_content.interaction_status == "IDLE"
                    elif server_content.turn_complete:
                        is_done = True

                    if is_done:
                        print()
                        await audio_queue.join()
                        first_chunk_received = False
                        print("\n[Listening... Speak now]")
    except asyncio.CancelledError:
        pass
    except Exception as e:
        print(f"\n[Receive Error]: {e}", file=sys.stderr)
        stop_event.set()

print("receive_loop_with_tools defined!")

最后,我们实现 run_voice_assistant_with_tools()。除提供 tools_config 外,该函数允许在标准模型与扩展思考模型之间进行选择。由于扩展思考模型需要一个指定 thinking_level("low"、"medium" 或 "high")的 thinking_config 字典,我们会有条件地将其注入会话配置:

async def run_voice_assistant_with_tools(
    model: str = "gemini-3.8-live-extended-thinking",
    thinking_level: str = "low",
):
    """Runs the interactive voice assistant with tool calling enabled.
    
    Supports both:
    - 'gemini-3.8-live-extended-thinking' (requires thinking_level: 'low', 'medium', or 'high')
    - 'gemini-3.8-live' (standard, ultra-low latency, no thinking_level)
    """
    audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
    input_queue: asyncio.Queue[bytes] = asyncio.Queue()
    stop_event = asyncio.Event()

    player_task = asyncio.create_task(audio_player(audio_queue))

    # Extended Thinking models require thinking_config with thinking_level
    session_config = dict(tools_config)
    if "extended-thinking" in model:
        session_config["thinking_config"] = {
            "thinking_level": thinking_level,
        }

    print(f"Connecting to Gemini Live API with tools (model: {model})...")
    async with client.aio.live.connect(model=model, config=session_config) as session:
        print("[Listening... Speak now.]")

        recorder_task = asyncio.create_task(audio_recorder(input_queue, stop_event))
        sender_task = asyncio.create_task(send_audio_loop(session, input_queue, stop_event))
        receiver_task = asyncio.create_task(receive_loop_with_tools(session, audio_queue, stop_event))

        try:
            while not stop_event.is_set():
                await asyncio.sleep(0.5)
        except (asyncio.CancelledError, KeyboardInterrupt):
            print("\nStopping voice assistant...")
        finally:
            stop_event.set()
            recorder_task.cancel()
            sender_task.cancel()
            receiver_task.cancel()
            await asyncio.gather(recorder_task, sender_task, receiver_task, return_exceptions=True)

    # Terminate player
    await audio_queue.put(None)
    await player_task
    print("\nSession finished cleanly.")

print("run_voice_assistant_with_tools is ready to run!")

运行启用工具的助手

现在我们可以运行启用工具的语音助手,并比较两种模型的实时表现。

首先,用扩展思考进行测试:

await run_voice_assistant_with_tools("gemini-3.8-live-extended-thinking")

当助手开始监听后,提一个需要实时数据的问题,例如:

"What's the weather like in Tokyo right now?"

由于通过互联网查询 Open-Meteo API 需要约 1.5 秒,我们将观察到后台推理的实际表现:

  1. Gemini 会立刻口头确认我们的请求:“让我帮您查看一下东京现在的天气……”
  2. 当 Gemini 在说话时,我们的后台任务会并行获取实时天气数据。
  3. 一旦工具响应到达,Gemini 会转而播报实时气温。

接着,我们使用标准的 Gemini 3.8 Live 模型运行相同的助手:

await run_voice_assistant_with_tools("gemini-3.8-live")

当我们向标准模型提出同样的问题时,模型会在等待网络中的工具响应约 1.5 秒期间保持完全沉默,然后直接播报气温,不会说任何填充语。

要查看完整项目,请参见配套的 GitHub 仓库。

结语

在本教程中,我们使用 Python 和 Gemini 3.8 Live 构建了一个完整的全双工语音助手。它在实时场景中特别有用的三个特性:

  • 并发音频架构:四个轻量级 asyncio 工作协程通过两个队列通信,实现同时录音、实时音频流式传输、语音播放与即时插话打断。

  • 后台工具调用:将工具执行作为非阻塞后台任务(asyncio.create_task)启动,使 Gemini 3.8 Live Extended Thinking 能在推理和执行外部函数的同时持续对话。

  • 状态管理:跟踪 interaction_status == "IDLE" 可确保助手仅在所有后台推理、工具调用与最终发言结束后才继续监听。

如果您渴望开启 AI 工程领域的职业生涯,我推荐从我们的 AI Engineer for Developers 职业路线开始,它将教您如何使用 OpenAI API、Hugging Face、MCP 等更多工具!

FAQs

与之前的模型相比,Gemini 3.8 Live 的主要新特性有哪些?

与以往模型相比,Gemini 3.8 Live 引入了近乎实时的推理与智能、近乎实时的视觉对齐,以及覆盖 97 种语言的自动多语支持。此外,Gemini 3.8 Live Extended Thinking 支持同时推理与发声,使模型在执行后台工具与多步任务时,能够使用自然的口头提示和实时进度播报。

我可以在 Jupyter 笔记本上运行 Gemini 3.8 Live 吗?

在使用音频时,需要访问麦克风。Google Colab 并不原生支持这一点。不过,我们可以在本地 Jupyter 笔记本中运行 Gemini 3.8 Live。

我应该使用 Gemini 3.8 Live 还是 Gemini 3.8 Live Extended Thinking?

对于低时延语音代理、直接问答与快速工具,请使用 gemini-3.8-live。当智能体需要多步推理或调用耗时超过片刻的工具时,请使用 gemini-3.8-live-extended-thinking,因为它能在工作时持续说话。扩展思考还需要跟踪 interaction_status,而非 turn_complete。

Gemini 3.8 Live API 是免费的吗?

两种模型均可在 Gemini API 免费层中使用,包含免费的输入与输出 tokens,但免费层数据会用于改进 Google 的产品。在付费层,音频输入费用为每百万 tokens 3.00 美元(约合每分钟 0.005 美元),音频输出费用为每百万 tokens 12.00 美元(约合每分钟 0.018 美元)。

我可以将这段代码作为 Python 脚本而不是笔记本来运行吗?

可以,但您需要将顶层的 await 与 async with 调用包裹在一个异步函数中,并使用 asyncio.run() 启动,例如 asyncio.run(run_voice_assistant())。Jupyter 会为您运行事件循环,而普通 Python 脚本不会,因此直接运行这些单元会引发 SyntaxError。

为什么 Gemini 总是在打断自己?

可以,但您需要将顶层的 await 与 async with 调用包裹在一个异步函数中,并使用 asyncio.run() 启动,例如 asyncio.run(run_voice_assistant())。Jupyter 会为您运行事件循环,而普通 Python 脚本不会,因此直接运行这些单元会引发 SyntaxError。

主题
AI 代理
人工智能

与 DataCamp 一起学习 AI!

Tracks

面向开发者的 AI 工程师助理

26小时
了解如何使用 API 和开源库将 AI 集成到软件应用程序中。 今天就开始你的 AI 工程师之旅吧!
查看详情Right Arrow
开始课程
查看更多Right Arrow