← Retour au blog
Tutoriel

Gemini 3.8 Live Function Calling : comment extraire args, aligner l'id et renvoyer le Tool Result ?

Publié le 23/09/2026 Dernière mise à jour le 23/09/2026 Environ 16 min Boîte à outils JSON

Comment lire cet article

L'article précédent, 3.8 Live de la voix à l'outil, ne dessinait que les cinq sauts. Celui-ci est daté du 23/09/2026. Contre la page outils Live officielle (dernière mise à jour le 15/09/2026) et la référence WebSocket, on démonte toolCall, args et FunctionResponse pour les recetter. L'API Live n'exécute pas les outils. Forme du pipeline : article précédent. Pourquoi la couche contrat reste un objet : Pourquoi l'IA aime JSON.

L'utilisateur dit « allume la lumière du salon ». Quand le modèle tend la main, vous ne recevez pas une transcription, mais un objet déjà parsé : id, name, args. Oublier l'id, passer args comme chaîne à l'exécuteur, ou inventer un nouvel id au retour : l'audio peut encore dire « d'accord », la lumière s'est déjà allumée dans la mauvaise pièce. Côté Agent texte, c'est required_actions → tool_result. Même forme ; l'enveloppe est un événement Live. Voir Pourquoi l'Agents API a encore plus besoin de JSON.

En quoi ça diffère de l'article précédent

L'article précédent s'arrêtait à « l'audio entre, un objet toolCall sort ». Celui-ci fait trois choses :

  1. Extraire functionCalls[] du SDK ou du WebSocket.
  2. Traiter args comme un objet. Refuser avec un Schema les champs trop larges ou mal reconnus.
  3. Assembler FunctionResponse avec le même id / name. Traiter l'annulation si elle arrive.

Au 23/09/2026, le modèle stable reste gemini-3.8-live. Live ne prend pas en charge Structured Output. BidiGenerateContentSetup.generationConfig n'avale pas non plus responseSchema / responseJsonSchema. Ce que vous pouvez figer : le Schema des paramètres de fonction, et la validation de args avant exécution. La couche contrat de réponse : Qu'est-ce que la Structured Output.

En une phrase. Parser, ce n'est pas print tout le message. C'est d'abord obtenir l'objet, puis décider si on tend la main.

Les trois JSON à parser

La référence WebSocket plie les enveloppes client en quatre : setup, clientContent, realtimeInput, toolResponse. La boucle d'outils utilise la dernière. Le serveur pose toolCall ou toolCallCancellation sur BidiGenerateContentServerMessage. Ne mélangez pas avec le JSON WordInfo de la transcription ; voir 3.5 Transcribe.

Live Function Calling : trois objets à parser Le serveur émet toolCall ; le client valide args puis exécute ; toolResponse renvoie les FunctionResponse avec les id correspondants. 1. toolCall functionCalls[] 2. args Schema, puis exécuter 3. toolResponse Aligner id / name
Recettez chaque objet à part. Les fusionner en une chaîne « message brut » et l'étape suivante copiera le mauvais id.

Figer d'abord le contrat de champs :

{
  "incoming": {
    "toolCall": {
      "functionCalls": [
        { "id": "string", "name": "string", "args": {} }
      ]
    }
  },
  "outgoing": {
    "toolResponse": {
      "functionResponses": [
        { "id": "string", "name": "string", "response": {} }
      ]
    }
  },
  "cancel": {
    "toolCallCancellation": {
      "ids": ["string"]
    }
  }
}

Le SDK Python plie le camelCase en snake_case : response.tool_call.function_calls, session.send_tool_response(function_responses=...). Le SDK JS garde le camelCase : turn.toolCall.functionCalls, session.sendToolResponse({ functionResponses }). Sur un WebSocket brut, les champs JSON suivent le camelCase de la référence.

Extraire toolCall de la session

La page outils Live officielle dit net la responsabilité client : déclarer les fonctions dans la config de session ; à la réception d'un appel, renvoyer un ensemble de FunctionResponse via session.send_tool_response. Contrairement à generateContent, pas de tool response automatique. Forme de la boucle : Agents API vs MCP — là-bas c'est un harness ; ici, c'est votre propre receive().

Traitez-le d'abord comme un événement. Ne commencez pas par le tampon audio :

async for response in session.receive():
    if response.tool_call:
        for fc in response.tool_call.function_calls:
            call_id = fc.id
            name = fc.name
            args = fc.args or {}
            # 下一步:校验 args,再决定执不执行
            ...
        await session.send_tool_response(function_responses=function_responses)

Une trame WebSocket brute est un objet JSON. Le message serveur n'émet pas de messageType à part ; le champ est au sommet :

{
  "toolCall": {
    "functionCalls": [
      {
        "id": "call_light_01",
        "name": "set_light",
        "args": {
          "room": "living_room",
          "on": true
        }
      }
    ]
  }
}

L'ordre de parsing est fixe : confirmer que le sommet est toolCall → prendre le tableau → lire trois champs par entrée. functionCalls manquant, pas un tableau, ou une entrée sans id / name : toute la trame est mauvaise. Ne devinez pas. Un prompt peut émettre plusieurs appels en parallèle, donc il faut parcourir. Ne prenez pas seulement [0].

Lire FunctionCall champ par champ

Le FunctionCall de la référence n'a que trois jobs :

  • id : la clé de cette main tendue. Retour, annulation et journaux s'en servent. Ne le réécrivez pas. N'en inventez pas un autre.
  • name : doit tomber dans un functionDeclarations déjà envoyé. Nom inconnu : renvoyez une erreur. Pas de correspondance floue.
  • args : un objet. C'est le contrat. Vide = {}. Ne traitez pas null comme « pas de paramètres ».

Les déclarations restent dans config.tools de la session. L'exemple officiel minimal n'a que name. En production, ajoutez au moins description et parameters :

{
  "functionDeclarations": [
    {
      "name": "set_light",
      "description": "Set one allowlisted room light. Use when the user asks to turn a light on or off.",
      "behavior": "NON_BLOCKING",
      "parameters": {
        "type": "OBJECT",
        "properties": {
          "room": {
            "type": "STRING",
            "enum": ["living_room", "kitchen", "bedroom"],
            "description": "Allowlisted room id. Do not invent rooms."
          },
          "on": { "type": "BOOLEAN" }
        },
        "required": ["room", "on"]
      }
    }
  ]
}

3.8 Live est NON_BLOCKING par défaut. S'il faut attendre le résultat avant la phrase suivante, mettez cet outil en BLOCKING. La variante extended-thinking n'autorise que l'asynchrone. La forme des permissions est la même qu'un Agent texte : limiter les permissions d'outils avec un Schema.

Valider args avant d'exécuter

La voix rend la seconde couche plus dangereuse. Un lapsus, un accent, un nom de pièce entendu comme une autre valeur d'enum : le modèle peut encore émettre un args d'apparence légale. Sans Schema de réponse pour rattraper, la validation des paramètres avant exécution est la seule porte dure.

Repliez la déclaration en un Schema que vous pouvez faire tourner dans l'espace de travail :

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["room", "on"],
  "properties": {
    "room": {
      "type": "string",
      "enum": ["living_room", "kitchen", "bedroom"]
    },
    "on": { "type": "boolean" }
  }
}

Trois étapes, puis l'exécuteur :

  1. Type. args doit être un object. Si vous avez reçu une chaîne, JSON.parse / json.loads une seule fois. Échec ou résultat pas un object : renvoyez une erreur.
  2. Contrat. required, enum, additionalProperties: false. Un extra_cmd en trop est un refus.
  3. Nom. name doit égaler une clé de votre table de déclarations. Ne devinez pas living_room depuis une « pièce similaire ».

Figez d'abord les formes qui doivent échouer. Elles doivent passer au rouge dans l'espace de travail :

{
  "ok": { "room": "living_room", "on": true },
  "unknown_room": { "room": "garage", "on": true },
  "extra_field": { "room": "living_room", "on": true, "extra_cmd": "unlock" },
  "wrong_type": { "room": "living_room", "on": "开" },
  "args_as_string": "{\"room\":\"living_room\",\"on\":true}"
}

Seul ok entre dans set_light. garage, extra_cmd, "开", et la charge encore en chaîne : tous renvoient un objet d'erreur. Ne « faites pas au mieux ». Dériver un contrat depuis un échantillon : Comment générer un Schema.

Assembler FunctionResponse

Un retour WebSocket doit s'asseoir dans toolResponse. Chaque FunctionResponse apparie le FunctionCall d'origine par id :

{
  "toolResponse": {
    "functionResponses": [
      {
        "id": "call_light_01",
        "name": "set_light",
        "response": {
          "result": {
            "room": "living_room",
            "on": true,
            "applied": true
          },
          "scheduling": "INTERRUPT"
        }
      }
    ]
  }
}

Équivalent SDK :

function_response = types.FunctionResponse(
    id=fc.id,
    name=fc.name,
    response={
        "result": {"room": "living_room", "on": True, "applied": True},
        "scheduling": "INTERRUPT",
    },
)
await session.send_tool_response(function_responses=[function_response])

Au retour asynchrone, scheduling dit au modèle comment utiliser ce résultat :

  • INTERRUPT : couper la parole en cours, dire le résultat tout de suite.
  • WHEN_IDLE : attendre la fin du segment actuel.
  • SILENT : noter d'abord, utiliser plus tard.

Les exemples placent scheduling dans response, à côté de result. C'est un indice d'ordonnancement, pas un champ métier. L'exécuteur ne lit que result. Un échec de validation doit encore renvoyer. Ne restez pas silencieux :

{
  "id": "call_light_01",
  "name": "set_light",
  "response": {
    "error": {
      "code": "invalid_args",
      "message": "room is not in the allowlist"
    },
    "scheduling": "INTERRUPT"
  }
}

L'objet d'erreur porte le même id / name. Le modèle peut alors dire « cette pièce n'est pas sur la liste », au lieu de prétendre que la lumière est allumée. Refermez vous-même le result métier : quels champs, s'il faut un applied, figez-le avec le même Schema. Ne laissez pas scheduling fuiter dans l'exécuteur.

Appels parallèles et annulation

Depuis un prompt, le modèle peut émettre plusieurs functionCalls, puis encore des BidiGenerateContentToolCall. Responsabilité client :

  1. Parcourir tout le tableau. Ne traitez pas seulement la première entrée.
  2. Valider chaque args à part. Un échec n'empêche pas les autres retours, mais l'entrée en échec doit revenir en erreur.
  3. La longueur du tableau de retour aligne les appels reçus. En oublier un, le modèle n'apparie plus.
{
  "toolCall": {
    "functionCalls": [
      {
        "id": "call_light_01",
        "name": "set_light",
        "args": { "room": "living_room", "on": true }
      },
      {
        "id": "call_light_02",
        "name": "set_light",
        "args": { "room": "kitchen", "on": false }
      }
    ]
  }
}

Quand l'utilisateur interrompt le tour en cours, le serveur peut envoyer toolCallCancellation :

{
  "toolCallCancellation": {
    "ids": ["call_light_01"]
  }
}

Ne plus exécuter un id qui match. Si vous avez déjà tendu la main, la documentation autorise une tentative de rollback. Ce message n'apparaît que lorsque le client interrompt le tour serveur. Ne le fusionnez pas avec un signal audio interrupted ordinaire en un seul « tout annuler ».

Si un outil asynchrone doit lancer une tâche de fond, l'exemple cloud officiel pose la requête lente dans asyncio.create_task et laisse la boucle receive() continuer à écouter. Une fois fini, renvoyer avec l'id d'origine ; scheduling est souvent WHEN_IDLE. Ne bloquez pas cinq secondes dans receive().

Erreurs de parsing courantes

  • Traiter toute la trame WebSocket comme args. args vit à toolCall.functionCalls[i].args. Envoyer l'enveloppe à l'exécuteur : le Schema est trop large, ou échoue net.
  • Ne prendre que functionCalls[0]. Les lumières en parallèle perdent la seconde pièce.
  • Inventer un nouvel id au retour. Live apparie par id. Un nouvel id ne matche plus la main tendue d'origine.
  • Utiliser un args chaîne comme un objet. Vérifiez d'abord le type. Une chaîne n'a droit qu'à un parse.
  • Échouer la validation et ne rien renvoyer. Le modèle attend dans le vide ou parle n'importe comment. Une erreur est encore une FunctionResponse.
  • Écrire scheduling dans le store métier. C'est un indice pour le modèle, pas l'état d'une lumière.
  • Ignorer toolCallCancellation. L'utilisateur a déjà interrompu ; la lumière suit encore l'ancienne commande.

Ces erreurs se cachent plus facilement dans une boucle audio : l'utilisateur entend « d'accord », le journal tient un autre id. Passez d'abord les objets au rouge et au vert dans l'espace de travail, puis accrochez-les à une session Live.

Recetter dans la Boîte à outils JSON

  1. Collez le toolCall et le toolResponse ci-dessus dans le formatage JSON, confirmez que ça parse, et que chaque entrée du tableau a un id.
  2. Extrayez args. Avec JSON → Schema, générez le contrat cœur, puis ajoutez enum, required et additionalProperties.
  3. Les mêmes champs via Function Calling ; pour réutiliser sur d'autres Hosts, enveloppez encore dans MCP Tool.
  4. Jetez garage, extra_cmd et "开" dans la validation Schema, confirmez le refus. Pour aligner une forme de réponse, Structured Output — souvenez-vous : la session Live elle-même n'avale pas ce contrat de réponse.

Tout se traite dans le navigateur, rien n'est envoyé. D'abord un objet capable de refuser un mauvais args et de recopier le même id, ensuite seulement accrocher la fonction à gemini-3.8-live.

FAQ

tool_call du SDK et toolCall du WebSocket, c'est le même objet ?

Même contrat, casse différente. Le WebSocket utilise le camelCase ; le SDK Python le snake_case. Au retour, le SDK appelle send_tool_response ; une trame brute envoie toolResponse.

args est parfois une chaîne, parfois un objet. Que faire ?

Le contrat dit object. Vérifiez d'abord le type. Une chaîne n'a droit qu'à un parse. Échec ou résultat pas un object : renvoyez une erreur. Ne passez pas la chaîne à l'exécuteur.

Que se passe-t-il si le retour oublie l'id ?

Live apparie par id. L'oublier, le recopier faux, ou ne répondre qu'à la moitié : le modèle n'apparie plus cette main tendue. Recopiez aussi name. N'inventez pas un nouvel id.

Si toolCallCancellation arrive, faut-il encore exécuter ?

Non. Arrêtez les ids[] qui matchent. Un effet de bord déjà là peut tenter un rollback. N'apparaît que lorsque le client interrompt le tour serveur.

scheduling doit-il entrer dans le result métier ?

Non. Il est à côté de result, c'est un indice d'ordonnancement pour le modèle. L'exécuteur ne lit que l'objet métier.

En résumé

La boucle d'outils de Gemini 3.8 Live n'exécute rien toute seule. Vous parsez trois objets : toolCall.functionCalls, args, toolResponse.functionResponses. Peu de champs : id, name, un objet JSON. Le danger, c'est la voix qui rend args d'apparence légale. Avant d'exécuter, refusez garage et extra_cmd avec un Schema. Au retour, recopiez le même id. Ne tendez plus la main pour un id annulé. La suite n'est pas d'abord d'ajouter plus d'outils de pièces. C'est d'abord de recetter ces trois objets dans l'espace de travail.