Salut tout le monde, ici Nina, de retour de mon laboratoire numérique (qui, soyons honnêtes, est surtout ma table de cuisine avec beaucoup de café tiède). Aujourd’hui, je veux parler de quelque chose qui fait du bruit dans mes canaux Slack et qui hante mes sessions de codage tard dans la nuit : le monde parfois frustrant, souvent brillant des SDK d’IA, en particulier lorsque vous essayez de créer un agent conversationnel qui ressent réellement comme une conversation, pas juste un bot FAQ glorifié.
Mon obsession particulière ces derniers temps a été d’intégrer l’API Assistants d’OpenAI. Maintenant, je sais ce que vous pensez, “Nina, l’API Assistants ? C’est de l’histoire ancienne !” Et oui, vous n’avez pas tort. Ça fait un moment qu’elle est disponible. Mais écoutez-moi : je ne suis pas ici pour vous donner un tutoriel de base sur “comment appeler client.beta.assistants.create()“. Nous avons tous vu ça. Mon objectif aujourd’hui est de vous parler d’un problème spécifique et épineux que j’ai rencontré et de la manière dont j’ai lutté avec le SDK pour qu’il fasse ce que je voulais : gérer des conversations multi-tours avec état à l’aide d’outils personnalisés sans perdre ma tête (ou le contexte de mon utilisateur). Pensez au-delà d’une simple FAQ. Pensez à des flux de travail complexes, des questions de suivi et des appels d’outils dynamiques basés sur l’évolution de l’intention de l’utilisateur.
L’État de ma Santé Mentale : Pourquoi l’API Assistants (Continue) à Compter
Avant l’API Assistants, construire quelque chose d’étatique avec les modèles d’OpenAI semblait souvent être comme jongler avec trop de balles en l’air. Vous étiez responsable de la gestion de l’historique des messages, de l’ingénierie des invites pour le contexte, et de l’orchestration des appels d’outils tout seul. C’était… épuisant. L’API Assistants promettait de soulager beaucoup de ce fardeau en gérant les fils, les messages, et même l’orchestration des outils. Et pour les cas simples, elle fonctionne à merveille.
Mon projet actuel consiste à construire un assistant personnel de finances “intelligent”. Pas seulement un qui vous dit votre solde, mais un qui peut vous aider à planifier des budgets, suggérer des stratégies d’investissement en fonction de votre tolérance au risque, et même simuler des scénarios financiers futurs. Cela nécessite beaucoup d’allers-retours, de se souvenir des déclarations précédentes (“Je veux économiser pour une maison,” “Mes revenus sont de X,” “Que se passerait-il si j’investissais dans Y ?”), et surtout, d’appeler des fonctions spécifiques (comme récupérer des données boursières en temps réel ou exécuter un algorithme de projection budgétaire) au bon moment.
Ma première tentative était un glorieux désastre. Je passais tout l’historique des messages d’avant en arrière, essayant de glisser des instructions complexes dans l’invite du système, et vérifiant manuellement si un appel d’outil était nécessaire. Ça a fonctionné, mais c’était fragile, lent et un véritable cauchemar à déboguer. C’est à ce moment-là que j’ai décidé de vraiment m’appuyer sur l’API Assistants et son SDK.
L’Étreinte du SDK : Fils, Messages, et le Fuyard `run`
Au cœur de l’API Assistants se trouve le concept de `Threads` et `Messages`. Vous créez un `Thread` pour une conversation, y ajoutez des `Messages`, puis “exécutez” l’`Assistant` sur ce `Thread`. L’`Assistant` traite alors les messages, décide s’il doit appeler un outil et génère une réponse. Assez simple, non ?
Voici où cela devient intéressant. Mon assistant financier doit faire des choses comme :
- Suivre les objectifs : “Je veux économiser 50 000 $ pour un acompte.”
- Rassembler des informations : “Quel est votre revenu mensuel actuel ?”
- Exécuter des calculs : “En fonction de cela, combien de temps faudra-t-il si j’économise 1 000 $ par mois ?”
- Fournir des conseils : “Envisagez de diversifier votre portefeuille avec des fonds indiciels à faible coût.”
Chacune de ces tâches nécessite souvent un appel d’outil personnalisé. Par exemple, `calculate_savings_timeline(goal_amount, monthly_income, monthly_savings)`. Le défi n’est pas seulement de définir l’outil ; c’est d’amener l’Assistant à :
- Reconnaître qu’il a besoin de l’outil.
- Identifier tous les paramètres nécessaires (même s’ils sont répartis sur plusieurs messages utilisateur).
- Demander à l’utilisateur les paramètres manquants avec délicatesse.
- Exécuter l’outil et incorporer les résultats.
Le Problème des Paramètres : Quand l’Assistant Demande Plus
Supposons qu’un utilisateur dise, “Combien de temps me faudra-t-il pour économiser pour une maison ?” Mon outil `calculate_savings_timeline` a besoin de `goal_amount`, `monthly_income`, et `monthly_savings`. L’Assistant, étant intelligent, réalisera souvent qu’il manque des informations. Le SDK, à travers l’objet `run`, vous dira qu’il `requires_action`.
Mon approche initiale consistait simplement à attendre le statut `requires_action` puis à présenter un générique “Quelles informations avez-vous besoin ?” à l’utilisateur. C’était maladroit. L’utilisateur ne savait souvent pas quels paramètres étaient nécessaires pour l’outil. Une meilleure approche, sur laquelle je suis finalement tombé, a consisté à inspecter les `tool_calls` dans le statut `requires_action`.
def submit_tool_outputs(client, thread_id, run_id, tool_outputs):
return client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run_id,
tool_outputs=tool_outputs
)
# ... dans votre boucle de conversation ...
if run.status == 'requires_action':
tool_outputs = []
missing_params = {}
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Voici le truc : vérifiez si un paramètre requis manque
# On suppose que vos outils sont définis avec un schéma
# Pour simplifier, disons que nous savons que 'monthly_savings' est toujours nécessaire
if function_name == "calculate_savings_timeline" and "monthly_savings" not in arguments:
missing_params['monthly_savings'] = 'Combien comptez-vous économiser chaque mois ?'
# ... vérification des paramètres plus sophistiquée ici ...
if missing_params:
# Informez l'utilisateur de ce qui manque
user_prompt = "J'ai besoin d'un peu plus d'informations pour vous aider. "
for param, question in missing_params.items():
user_prompt += f"{question} "
return {"status": "waiting_for_user_input", "prompt": user_prompt}
else:
# Tous les paramètres sont présents, exécutez l'outil
output = execute_tool_function(function_name, arguments) # Votre fonction pour exécuter la logique de l'outil
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": json.dumps(output)
})
if tool_outputs:
run = submit_tool_outputs(client, thread.id, run.id, tool_outputs)
# Continuez à interroger le run jusqu'à ce qu'il soit terminé ou nécessite une nouvelle action
else:
# Ce cas gère si nous avons détecté des paramètres manquants et avons demandé à l'utilisateur
pass
Ce code est une version simplifiée, mais l’idée principale est d’intercepter `requires_action`, de regarder ce que l’Assistant veut faire, et s’il manque des arguments, de demander spécifiquement à l’utilisateur. Cela rend la conversation beaucoup plus naturelle, comme un humain posant des questions de clarification, plutôt qu’un générique “erreur”.
L’État Évasif : Garder une Trace entre les Tours
Un des plus gros maux de tête était de s’assurer que si un utilisateur fournissait une information (“Mes revenus sont de 5 000 $ par mois”), l’Assistant s’en souviendrait pour les appels d’outils ultérieurs au sein de la même conversation, même si elle n’était pas immédiatement utilisée. L’API Assistants gère cela plutôt bien au sein d’un `Thread` en gardant l’historique des messages. Cependant, parfois, vous voulez le guider explicitement ou lui donner des informations qui ne font pas partie d’un message utilisateur direct.
Je me suis retrouvé à utiliser `metadata` sur les objets `Thread` et `Message` plus que je ne le pensais au départ. Par exemple, si l’utilisateur indique explicitement sa tolérance au risque, je pourrais stocker cela dans les métadonnées du fil. Bien que l’Assistant ne “lise” pas directement ces métadonnées dans son processus de raisonnement, elles sont inestimables pour ma logique d’application lorsque je dois pré-remplir des arguments pour des appels d’outils ou prendre des décisions sur les outils qui sont même pertinents.
Un autre modèle que j’ai adopté était d’injecter des messages “similaires à des systèmes” dans le fil si je devais rappeler explicitement à l’Assistant certains faits dérivés d’un appel d’outil ou d’un processus interne. Par exemple, après que `calculate_savings_timeline` ait retourné qu’il faudrait 10 ans, je pourrais ajouter un message comme : `{“role”: “user”, “content”: “Le plan d’épargne de l’utilisateur indique un délai de 10 ans pour atteindre son objectif.”}`. Cela ne provient pas directement de l’utilisateur, mais cela aide à renforcer le contexte pour les réponses suivantes de l’Assistant.
Gestion de la Sortie de l’Outil et des Actions Suivantes
Lorsqu’un outil s’exécute, vous obtenez une sortie. L’Assistant prend ensuite cette sortie et génère une réponse. Mais que se passe-t-il si la sortie de l’outil elle-même suscite un nouvel ensemble de questions ou un nouvel appel d’outil ? Par exemple, l’outil `calculate_savings_timeline` pourrait retourner que l’objectif est inatteignable avec les économies actuelles. L’Assistant devrait idéalement suggérer, “Peut-être devrions-nous examiner des moyens d’augmenter vos économies mensuelles ou de réduire votre montant cible.” Ce n’est pas seulement une question de sortie de texte; il s’agit d’enchaîner des étapes logiques.
Le SDK de l’API Assistants gère cela admirablement en gardant l’objet `run` actif. Après que vous ayez `submit_tool_outputs`, le `run` passe souvent à `in_progress`, puis peut générer un nouveau `requires_action` (pour un autre appel d’outil) ou `completed`. Ma boucle principale pour gérer le flux de conversation ressemble à quelque chose comme ceci :
def obtenir_reponse_assistant(client, thread_id, assistant_id, message_utilisateur):
client.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content=message_utilisateur
)
run = client.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=assistant_id
)
while run.status in ['queued', 'in_progress', 'cancelling']:
time.sleep(1) # Soyez un bon citoyen, ne surchargez pas l'API
run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run.status == 'completed':
messages = client.beta.threads.messages.list(thread_id=thread_id, order="desc")
for msg in messages.data:
if msg.role == "assistant":
# Trouver le dernier message de l'assistant, souvent le premier
return msg.content[0].text.value # En supposant que le contenu est textuel
return "Pas de réponse de l'assistant."
elif run.status == 'requires_action':
sorties_outil = []
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
nom_fonction = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Ici, votre logique personnalisée pour gérer les paramètres manquants ou exécuter des outils
# Pour cet exemple simplifié, supposons que tous les paramètres sont présents et que nous exécutons
print(f"L'assistant veut appeler {nom_fonction} avec args : {arguments}")
output = execute_tool_function(nom_fonction, arguments) # Votre logique d'exécution d'outil
sorties_outil.append({
"tool_call_id": tool_call.id,
"output": json.dumps(output)
})
# Soumettre les sorties d'outils et continuer l'exécution
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run.id,
tool_outputs=sorties_outil
)
# Appeler cette fonction récursivement pour traiter l'exécution mise à jour
# Attention à la profondeur de récursion en production, vous pourriez avoir besoin d'une boucle
return obtenir_reponse_assistant(client, thread_id, assistant_id, "") # Passer un message vide
elif run.status == 'failed':
return f"L'exécution de l'assistant a échoué : {run.last_error.message}"
else:
return f"Statut d'exécution inattendu : {run.status}"
Cette fonction `obtenir_reponse_assistant`, bien que simplifiée, montre la boucle centrale. L’essentiel est que `requires_action` n’est pas une impasse. C’est une occasion pour votre application d’intervenir, d’exécuter l’outil demandé, puis de renvoyer les résultats à l’Assistant pour continuer son raisonnement. Ce retour d’information en boucle fermée est ce qui rend possible des conversations véritablement dynamiques et évolutives.
Points à retenir pour votre prochain projet d’assistant IA
- Adoptez le cycle de vie `run.status` : Ne vous contentez pas de vérifier si c’est `completed`. `requires_action` est votre ami, pas une erreur. Créez des gestionnaires solides pour chaque statut pertinent.
- Inspectez `tool_calls` pour les paramètres manquants : Au lieu de simples indications, plongez dans `run.required_action.submit_tool_outputs.tool_calls` pour comprendre *exactement* ce dont l’Assistant a besoin. Cela améliore votre expérience utilisateur.
- Utilisation stratégique des messages de type système : Si votre logique d’application interne déduit de nouveaux faits ou doit souligner certains contextes, envisagez d’injecter des messages “utilisateur” qui représentent ces faits. L’Assistant les traitera comme faisant partie de l’historique du fil.
- Métadonnées pour l’état de l’application : Bien que l’Assistant ne puisse pas directement “lire” les métadonnées des fils ou des messages pour son raisonnement, c’est un endroit puissant pour votre application pour stocker et récupérer des états pertinents à la conversation. Pensez aux préférences utilisateur, aux variables de session actuelles, etc.
- Testez les cas particuliers pour l’orchestration d’outils : Que se passe-t-il si un outil échoue ? Que se passe-t-il si l’utilisateur change d’avis pendant l’invocation d’un outil ? Conception de votre `execute_tool_function` et gestion des erreurs dans la boucle `requires_action` avec soin.
Construire des agents IA conversationnels vraiment intelligents ne concerne pas seulement le choix du dernier modèle. Il s’agit de manier habilement les SDK qu’ils fournissent pour gérer les complexités de l’interaction humaine. L’API des Assistants OpenAI, avec un peu d’ingénierie réfléchie autour de sa gestion d’état et de l’orchestration des outils, peut vraiment améliorer votre expérience conversationnelle au-delà d’un simple échange. C’est encore un voyage, et j’apprends encore de nouvelles astuces, mais j’espère que ces idées vous éviteront quelques nuits blanches et cafés froids !
Articles connexes
- Arting IA : Libérez votre potentiel créatif avec des générateurs d’art IA
- Meilleurs outils de documentation d’API pour développeurs
- Outils de productivité des développeurs qui ont réellement changé comment je publie du code
🕒 Published: