Symptôme : Kimi K3 Tool Calls appelle le même outil en boucle.
Solution la plus rapide : sauvegardez d’abord la chaîne complète des messages, vérifiez tool_call_id et les paramètres assemblés, puis activez une coupure côté client avant de laisser l’Agent exécuter une nouvelle action.
Cette méthode s’applique lorsque le modèle répète le même outil avec les mêmes arguments, que le flux ne termine pas ou qu’une tâche sans surveillance risque de provoquer plusieurs écritures. Elle ne remplace pas une validation métier : pour l’envoi d’un message, le paiement, la création d’un ticket ou la modification de données, l’exécution doit rester protégée par une clé d’idempotence et un point de confirmation.
Dernière mise à jour : 3 août 2026. Les éléments relatifs à
finish_reason, àtool_call_id, à l’assemblage en streaming et aux répétitions sont vérifiés dans la documentation officielle disponible à cette date. Les seuils de protection métier restent à définir selon votre risque, et ne doivent pas être traités comme des valeurs universelles.
Cet article est destiné aux développeurs d’outils appelés par un Agent, aux équipes qui maintiennent une plateforme d’automatisation sans surveillance et aux organisations qui exposent des fonctions ayant des effets de bord. Si votre problème concerne plutôt la limite budgétaire générale, consultez aussi notre guide sur les limites de consommation de Kimi K3 lorsque vous aurez identifié la cause de la boucle.
Avant toute correction : constituer la preuve
Commencez par enregistrer un échantillon complet, mais dépersonnalisé. Une simple capture de l’interface ne permet pas de savoir si le modèle a réellement appelé l’outil deux fois. L’affichage peut être dupliqué par votre interface, le kit logiciel peut relancer une requête après une coupure réseau ou le serveur peut avoir traité une demande alors que le client n’a rien affiché.
Conservez au minimum :
- le corps
messagesenvoyé à chaque tour ; - la réponse complète et
finish_reason; - le
tool_call_idde chaque appel ; - le nom de la fonction ;
- la chaîne exacte de
function.arguments; - le résultat produit par l’outil ;
- l’identifiant de requête, l’usage retourné et le statut HTTP ;
- le nombre de relances effectuées par le client ou l’orchestrateur.
Les clés API, identifiants de clients, adresses privées, paramètres métier et numéros de commande doivent être remplacés par des valeurs fictives. Ne conservez pas un jeton secret dans un journal destiné à une équipe de support ou à un dépôt de code. La documentation officielle sur l’API rappelle également qu’une clé doit rester hors du code client, des dépôts publics et des journaux accessibles.
Placez immédiatement les outils d’écriture dans un environnement de lecture seule ou dans un bac à sable. Cette étape est prioritaire sur l’amélioration du prompt : un meilleur texte peut modifier le comportement du modèle, mais il ne peut pas annuler une écriture déjà exécutée.
| Observation dans les journaux | Cause la plus probable | Première action |
|---|---|---|
| L’interface affiche deux appels, mais un seul identifiant d’exécution existe | Duplication côté interface | Comparer les journaux serveur et les identifiants d’exécution |
| Deux requêtes API ont le même contexte et l’une suit une erreur réseau | Relance automatique du client | Désactiver temporairement la relance et comparer les réponses |
| Le même outil, les mêmes arguments et le même résultat reviennent | Boucle réelle de Tool Calls | Activer la détection de répétition et la coupure |
| Les arguments sont incomplets ou invalides en streaming | Assemblage incorrect des fragments | Rejouer en mode non diffusé, puis corriger l’agrégateur |
| Le modèle reçoit une réponse d’outil sans message assistant précédent | Chaîne messages altérée |
Restaurer la réponse assistant telle quelle |
Premier jalon : valider la chaîne des messages
Pourquoi Kimi K3 appelle-t-il toujours la même fonction ?
Une répétition apparente vient souvent de la conversation transmise au tour suivant, pas d’une décision autonome répétée par le modèle. Lorsque la réponse indique finish_reason=tool_calls, vous devez ajouter à messages le message assistant retourné par l’API sans le reconstruire à partir d’un résumé local. Ensuite, chaque appel doit recevoir son propre message role=tool.
Le lien entre les deux messages repose sur l’identifiant exact. Une différence de caractère, un identifiant tronqué ou une association faite uniquement par position peut rendre le résultat invisible pour l’Agent. Il peut alors considérer que l’outil n’a pas répondu et tenter la même action.
La séquence minimale doit ressembler à ceci :
[
{
"role": "user",
"content": "Vérifiez le statut de la tâche <TASK_ID>"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "<TOOL_CALL_ID>",
"type": "function",
"function": {
"name": "get_task_status",
"arguments": "{\"task_id\":\"<TASK_ID>\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "<TOOL_CALL_ID>",
"content": "{\"status\":\"<STATUS>\"}"
}
]
Ne remplacez pas le message assistant par une version simplifiée comme « l’outil a été appelé ». Les champs d’appel peuvent être nécessaires au tour suivant. La procédure officielle de dépannage demande de conserver le message assistant reçu, d’associer chaque tool_call à un message role=tool et de faire correspondre exactement les identifiants.
Le test de base doit être exécuté avec une requête minimale en mode non diffusé. Retirez temporairement l’orchestrateur, le cache de conversation, les sous-agents et les fonctions secondaires. Si la requête minimale fonctionne, la responsabilité se trouve probablement dans votre couche de transport ou de gestion des messages.
| Test de contrôle | Résultat attendu | Décision |
|---|---|---|
| Appel non diffusé avec un outil en lecture seule | Un message assistant contient tool_calls |
Passer au contrôle de l’identifiant |
| Message assistant réinjecté sans modification | Le tour suivant accepte la réponse outil | Conserver cette structure en production |
Un message role=tool par appel |
Chaque identifiant est couvert une seule fois | Corriger toute association par position |
| Résultat outil réellement nouveau | Le contexte progresse | Autoriser le tour suivant |
| Réponse vide, identique ou sans changement d’état | Aucun progrès mesurable | Déclencher la détection de répétition |
Deuxième jalon : stabiliser le flux diffusé
Le streaming ajoute une couche de risque parce que le nom de fonction, l’index, l’identifiant et les arguments peuvent arriver dans des fragments distincts. Il est dangereux d’exécuter l’outil dès réception du premier fragment ou de supposer que chaque événement contient un objet JSON complet.
Comment assembler correctement les paramètres de Tool Calls en streaming ?
Pour chaque choix et chaque index de tool_calls, créez un accumulateur séparé. Copiez l’identifiant et le type lorsqu’ils apparaissent, conservez le nom de fonction, puis concaténez les fragments de function.arguments dans l’ordre de réception. Ne triez pas les fragments selon leur taille et ne les fusionnez pas avec ceux d’un autre index.
L’exemple officiel de Tool Calls diffusés conserve les appels par index et ajoute successivement les fragments de la chaîne arguments. Le flux diffusé peut être traité comme un flux SSE, alors que le mode non diffusé renvoie le résultat complet dans une seule réponse.
Un accumulateur simplifié peut suivre cette logique :
calls = {}
def recevoir_fragment(fragment):
for delta in fragment.get("choices", []):
for part in delta.get("delta", {}).get("tool_calls", []):
index = part["index"]
call = calls.setdefault(index, {
"id": None,
"type": None,
"name": None,
"arguments": ""
})
if part.get("id"):
call["id"] = part["id"]
if part.get("type"):
call["type"] = part["type"]
function = part.get("function", {})
if function.get("name"):
call["name"] = function["name"]
if function.get("arguments"):
call["arguments"] += function["arguments"]
L’exécution ne doit avoir lieu qu’après la fin du flux et après une validation stricte :
- l’identifiant existe ;
- le nom de fonction est autorisé ;
- la chaîne des arguments forme un JSON valide ;
- le schéma attendu est respecté ;
- les valeurs sensibles ou interdites sont rejetées ;
- la demande n’est pas déjà en cours d’exécution.
Si le décodage JSON échoue, ne demandez pas silencieusement au parseur de « réparer » la chaîne. Enregistrez les fragments originaux, l’index, le résultat assemblé et l’erreur. Une réparation implicite peut transformer une erreur de transport en action métier imprévisible.
Pour le premier diagnostic, utilisez stream: false. Le paramètre stream est désactivé par défaut dans la référence officielle des paramètres de requête ; vous pouvez donc comparer le résultat complet avant de réactiver l’affichage progressif.
Règle d’exploitation : un fragment n’est pas une autorisation d’exécuter. L’autorisation arrive seulement après assemblage complet, validation du schéma et contrôle d’idempotence.
Troisième jalon : reconnaître la répétition réelle
Une boucle ne se définit pas par le seul nom de l’outil. Pour éviter les faux positifs, comparez au moins trois éléments :
- le nom de la fonction ;
- les arguments normalisés ;
- l’évolution du résultat outil ou de l’état métier.
La normalisation doit supprimer les différences sans importance, comme l’ordre des propriétés JSON, mais elle ne doit pas effacer une différence fonctionnelle. Par exemple, deux requêtes portant sur deux tâches différentes ne sont pas identiques simplement parce qu’elles utilisent la même fonction.
Le test de répétition devient pertinent lorsque le même outil reçoit les mêmes arguments pendant des tours consécutifs et que le résultat ne produit aucun nouvel état. La documentation officielle décrit cette combinaison comme le signal à utiliser pour éviter de confondre une répétition légitime avec une boucle. Elle propose des rappels progressifs après trois, cinq et huit répétitions consécutives, mais ces valeurs sont des recommandations de traitement du prompt, pas une limite universelle pour tous les Agents. (Voir les recommandations de dépannage sur les appels répétés)
Utilisez le rappel dans le prompt comme intervention douce, jamais comme protection principale. Le programme doit pouvoir couper l’exécution même si le modèle ignore le rappel.
signature = (
tool_name,
canonical_json(arguments),
hash_result(tool_result)
)
if signature == previous_signature and not state_changed:
repeated_count += 1
else:
repeated_count = 0
if repeated_count >= REPEAT_LIMIT:
return {
"status": "blocked_no_progress",
"message": "Exécution arrêtée : aucun progrès détecté."
}
Ne fabriquez pas une réponse « succès » pour sortir de la boucle. Renvoyez un état explicite, tel que blocked_no_progress, et transmettez-le à votre interface ou à votre file de reprise. L’opérateur doit savoir que la tâche est suspendue, avec le dernier point contrôlé et la raison de la coupure.
Premier jour : protéger les effets de bord
Les outils de lecture et les outils d’écriture ne doivent pas avoir le même niveau de liberté. Une fonction qui consulte un statut peut souvent être rejouée sans dommage ; une fonction qui envoie un courriel, crée une commande, facture un client ou modifie un dossier doit vérifier si l’action a déjà été acceptée.
Ajoutez une clé d’idempotence calculée à partir de l’intention métier, et non à partir d’un identifiant temporaire de requête. Le service appelé doit enregistrer cette clé avant l’écriture ou dans la même transaction. Si la même clé revient, il doit retourner le résultat déjà enregistré au lieu d’exécuter une nouvelle fois l’action.
Ajoutez aussi :
- une étape de confirmation pour les actions irréversibles ;
- une séparation entre simulation et exécution ;
- une limite de durée par tâche ;
- une limite de coût cumulée ;
- une limite de tours d’outils ;
- un verrou pour empêcher deux travailleurs de traiter la même tâche ;
- un point de reprise après chaque action validée ;
- une entrée de reprise humaine avec le contexte et la dernière décision.
Combien de tours d’outil sont raisonnables pour Kimi K3 ?
Il n’existe pas de nombre unique qui convienne à chaque AI Agent. Une tâche de recherche, de montage audio ou de préparation vidéo peut nécessiter plusieurs consultations, tandis qu’un paiement ou une création de commande doit souvent être arrêté dès que l’état métier n’évolue plus.
Choisissez la limite à partir de vos scénarios de test :
- si la tâche est purement informative, autorisez plusieurs tours mais imposez une durée et une consommation maximales ;
- si l’outil modifie une donnée, exigez une clé d’idempotence et une confirmation avant l’écriture ;
- si l’outil déclenche une action externe, coupez dès qu’une répétition sans progrès est détectée ;
- si l’Agent travaille sans opérateur, prévoyez une reprise manuelle plutôt qu’une relance infinie.
La limite doit être documentée comme une règle métier versionnée. Ne la cachez pas dans une constante sans explication, car une équipe de maintenance doit pouvoir comprendre pourquoi la tâche a été interrompue.
Vous pouvez également examiner le champ usage à chaque tour. L’API expose notamment les jetons d’entrée, de sortie et, selon la réponse, les jetons mis en cache. Le modèle Kimi K3 dispose d’une valeur par défaut de 131072 jetons pour max_completion_tokens et la documentation indique une valeur maximale de 1048576 ; ces paramètres contrôlent la longueur générée, pas le nombre acceptable d’appels d’outils. (Vérifier les limites officielles de génération)
Première semaine : valider avant la production
Préparez quatre familles de tests, avec des identifiants et des paramètres fictifs :
- Appel normal : l’outil est appelé une fois et son résultat permet au modèle de poursuivre.
- Paramètres modifiés : le même outil reçoit une nouvelle valeur fonctionnelle et ne doit pas être bloqué comme doublon.
- Répétition sans progrès : le même nom, les mêmes arguments et un résultat sans changement déclenchent la coupure.
- Relance réseau : le client réessaie après une interruption, mais l’idempotence empêche une deuxième écriture.
Pour chaque échantillon, vérifiez le chemin complet :
request_id
└── tour_agent
├── tool_call_id
├── identifiant_execution_outil
├── resultat_outil
├── usage_api
└── decision : poursuivre ou couper
Après une coupure, recherchez les requêtes encore actives en arrière-plan. Une interface peut afficher « arrêté » alors qu’un travail asynchrone continue côté serveur. Contrôlez la file de tâches, les verrous, les journaux d’écriture et les systèmes externes concernés.
Votre procès-verbal d’acceptation doit répondre à ces questions :
- le message assistant est-il réinjecté sans perte de champs ?
- chaque
tool_call_idpossède-t-il exactement une réponse correspondante ? - les fragments diffusés sont-ils conservés avant et après assemblage ?
- une erreur JSON empêche-t-elle l’exécution ?
- la détection compare-t-elle outil, arguments et progrès ?
- la coupure laisse-t-elle un état explicite ?
- une relance réseau peut-elle reproduire une écriture ?
- l’équipe peut-elle reprendre la tâche depuis le dernier point sûr ?
- les journaux relient-ils requête, outil, usage et décision finale ?
Pour les essais prolongés, installez l’Agent dans un environnement où le processus reste accessible, où les journaux sont conservés et où une session peut être inspectée après une interruption. Les intégrations compatibles avec l’API Kimi utilisent une interface de type Chat Completions, mais les couches intermédiaires peuvent ajouter leurs propres relances ou transformations ; comparez donc au moins une exécution directe avec l’exécution dans votre plateforme.
Choisir l’environnement de reprise
Si vous développez principalement sur votre poste, le bac à sable local reste le meilleur choix pour corriger l’agrégation des fragments et inspecter les messages. En revanche, une campagne de régression qui doit rester active pendant une longue période a besoin d’un environnement en ligne, stable, avec conservation des journaux et accès distant.
| Situation | Environnement conseillé | Mesure indispensable |
|---|---|---|
| Reproduire rapidement une erreur de chaîne | Poste local ou bac à sable | Mode non diffusé et données fictives |
| Tester l’assemblage SSE | Machine de développement persistante | Conservation des fragments originaux |
| Exécuter des régressions nocturnes | Environnement distant toujours accessible | Journalisation et arrêt automatique |
| Valider une action d’écriture | Bac à sable métier | Clé d’idempotence et résultat vérifiable |
| Reprendre une tâche interrompue | Environnement distant avec accès administrateur | Point de contrôle et intervention humaine |
Pour les tests audio, vidéo ou design, la disponibilité de la machine compte autant que le modèle : une session interrompue au milieu d’un rendu peut masquer une relance de l’Agent ou laisser un fichier partiellement généré. Si vous devez exécuter des régressions longues avec une interface graphique, vous pouvez comparer votre poste actuel à une machine Mac distante toujours accessible. L’objectif n’est pas de remplacer votre infrastructure applicative, mais de disposer d’un poste de test connecté, inspectable et dédié aux scénarios de reprise.
Votre solution actuelle peut rester adaptée pour un prototype court. Elle devient moins confortable lorsque les tâches dépendent d’une machine qui se met en veille, d’un terminal fermé, d’un tunnel réseau instable ou de journaux dispersés entre plusieurs services. Dans ce cas précis, louer un environnement Mac auprès de Kvmkit peut offrir une continuité plus simple pour les essais prolongés : vous gardez un accès distant, vous laissez les scripts de régression actifs et vous conservez un point d’observation pour analyser la prochaine boucle.
Avant de déplacer quoi que ce soit, reproduisez toutefois le défaut dans un bac à sable, vérifiez l’idempotence des outils et définissez la procédure d’arrêt. La location est pertinente pour une campagne temporaire, une validation d’intégration ou un besoin de machine accessible à distance ; l’achat d’un Mac reste plus logique pour une charge stable à long terme ou lorsqu’un périphérique physique local est indispensable.
CI/CD sur Mac mini M4 — le plus simple
Xcode, Fastlane, CocoaPods, and SPM are first-class on macOS. Mac mini M4 unified memory keeps signing and archiving smooth; ~4W standby power suits 24/7 build nodes.