Tester le comportement d'un accès à une IA générative
Intégrer l'IA générative dans les fonctionnalités des produits logiciels devient de plus en plus facile. Quelques lignes de code suffisent.
Je suis convaincu que ce phénomène ne va aller qu'en s'amplifiant mais, en tant que développeur de logiciels, je me pose quand même des questions sur la capacité de prévoir le comportement de telles intégrations.
Et, quand on creuse un peu, la facilité n'est qu'une façade.
Dans un logiciel, prévoir le comportement, ça veut dire tester. Et, si on parle d'IA générative, le contenu des réponses générées n'est pas facilement prévisible. Donc je ne vais pas essayer de les tester.
Par contre, quand un fournisseur d'IA générative propose une API avec des fonctionnalités documentées, on peut s'attendre à un comportement cohérent de cette API. Et là ça devient plus intéressant.
Google fournit une API pour Gemini et j'ai eu envie de l'essayer l'appel de fonctions par le modèle.
L'implémentation en Python est très simple : on définit une fonction et on la passe en paramètre comme tool lors de l'interrogation du modèle.
1import os
2import google.generativeai as genai
3from google.generativeai.types import content_types
4
5
6def is_paragraph_relevant(paragraph_text: str, concepts: list[str]) -> bool:
7 """ filter a paragraph text using identified concepts in the paragraph """
8 return len(concepts) < 5
9
10
11def find_concept(input: str):
12 genai.configure(api_key=os.environ["GEMINI_API_KEY"])
13 model = genai.GenerativeModel(
14 'gemini-1.5-flash-8b',
15 system_instruction="""
16You are helping users to find interesting parts of a text.
17You analyze each paragraph of the input text to find its concepts and decide if it is relevant.
18 """,
19 tools=[is_paragraph_relevant]
20 )
21 return model.generate_content(input)
Cet exemple demande au modèle d'identifier des concepts dans un texte et d'identifier les parties de texte qui sont intéressantes. Mais le modèle ne va pas décider lui même de l'intérêt : on lui fournit une fonction déterministe qui décide si c'est intéressant en fonction des concepts identifiés.
Ce que j'ai envie de tester ici, c'est : est-ce que le modèle va effectivement appeler ma fonction is_paragraph_relevant
?
J'essaie ce bout de code avec le texte de notre Albert Camus numérique et là, magie, la réponse du modèle contient les appels qui ont été réalisés avec les concepts identifiés.
1[
2 {
3 "name": "is_paragraph_relevant",
4 "args": {
5 "paragraph_text": "Ah, cher interlocuteur, vous touchez là à une vérité qui a quelque chose de sibyllin, et même, disons-le, de profondément moderne. [...]",
6 "concepts": ["transition", "management", "language", "transformation", "human experience", "organization", "change"]
7 }
8 },
9 {
10 "name": "is_paragraph_relevant",
11 "args": {
12 "paragraph_text": "Dans L'Étranger, j'ai parlé du soleil qui accable Meursault, mais c'était une lumière crue, honnête, qui dévoilait sans détour. [...]",
13 "concepts": ["transition", "human experience", "rupture", "existential pain"]
14 }
15 },
16 {
17 "name": "is_paragraph_relevant",
18 "args": {
19 "paragraph_text": "Il est essentiel, même dans le cadre des organisations, de nommer les choses avec exactitude et vérité. [...]",
20 "concepts": ["transition", "loss", "redefinition", "organization"]
21 }
22 },
23 {
24 "name": "is_paragraph_relevant",
25 "args": {
26 "paragraph_text": "Ainsi, pour éviter d'ajouter au \"malheur des organisations\", nous pourrions dire qu'il est bon de \"comprendre une transition\", [...]",
27 "concepts": ["transition", "words", "experience", "change"]
28 }
29 }
30]
Je relance le test. Les concepts ont changé et un paragraphe de plus a été détecté. Normal, c'est du contenu. Ça varie selon l'humeur du modèle. Mais j'ai toujours mes appels à is_paragraph_relevant
.
Je relance le test. Plus aucun appel à ma fonction. Cette fois, le modèle a décidé qu'il allait plutôt me répondre du code Python correspondant vaguement à ma requête.
1import dataclasses
2from typing import Union
3
4
5def is_paragraph_relevant(
6 concepts: list[str],
7 paragraph_text: str,
8) -> dict:
9 """ filter a paragraph text using identified concepts in the paragraph
10
11 Args:
12 concepts:
13 paragraph_text:
14 """
15 # Basic concept matching
16 match_count = sum(1 for concept in concepts if concept.lower() in paragraph_text.lower())
17
18 # Check if there are at least 2 matching concepts
19 if match_count >= 2:
20 return {"is_relevant": True, "paragraph_text": paragraph_text}
21 else:
22 return {"is_relevant": False, "paragraph_text": paragraph_text}
23
24# Example usage (replace with your concepts)
25concepts = ["transition", "management", "change", "organization", "language", "meaning"]
26paragraph_list = [
27 "Ah, cher interlocuteur, vous touchez là à une vérité qui a quelque chose de sibyllin, et même, disons-le, de profondément moderne.",
28 "Dans cette question, on entend la résonance du langage technique qui habite désormais toutes nos entreprises humaines, cette façon d'emprunter des mots et de les tailler en concepts pratiques, parfois opaques.",
29 "Oui, \"manager une transition\", c'est à la fois un acte et une abdication : on s'efforce de donner forme au changement, mais on le fait avec des mots qui appauvrissent l'âme de ce que l'on transforme.",
30 "Dans L'Étranger, j'ai parlé du soleil qui accable Meursault, mais c'était une lumière crue, honnête, qui dévoilait sans détour.",
31 "En revanche, quand on dit \"manager une transition\", on maquille le réel, on applique un vernis rationnel à ce qui est souvent une rupture brutale, une déchirure humaine et existentielle.",
32 "À mal nommer la transformation, c’est vrai, on alourdit la peine de ceux qui doivent la vivre, car on réduit leur expérience à des cases, des processus, des méthodes, tous dénués de chair.",
33 "Il est essentiel, même dans le cadre des organisations, de nommer les choses avec exactitude et vérité.",
34 "Une \"transition\" n’est jamais seulement un objectif à atteindre ; c'est une perte, une redéfinition, et parfois même un déchirement.",
35 "Les mots dont on l'habille doivent pouvoir faire écho à ces réalités, sans quoi l'organisation elle-même se mutile, se robotise.",
36 "Si l'on ne se méfie pas, elle devient un organisme qui perd le sens, aveuglé par sa propre recherche de performance.",
37 "Ainsi, pour éviter d'ajouter au \"malheur des organisations\", nous pourrions dire qu'il est bon de \"comprendre une transition\", ou mieux, de \"traverser un changement\", car ce sont là des mots qui invitent à l'expérience, à la marche, et non à la gestion froide d’une manœuvre."
38]
39
40relevant_paragraphs = []
41for paragraph in paragraph_list:
42 result = is_paragraph_relevant(concepts, paragraph)
43 if result["is_relevant"]:
44 relevant_paragraphs.append(result)
45
46for paragraph_data in relevant_paragraphs:
47 print(paragraph_data["paragraph_text"])
48 print("-" * 20)
Après quelques recherches dans les docs, je comprends que l'on peut configurer le mécanisme d'appel aux fonctions.
Par défaut, en mode AUTO
, le modèle décide lui même s'il appelle ou pas des fonctions. Mais on peut indiquer un mode ANY
où le modèle est obligé de faire un appel de fonction.
1model = genai.GenerativeModel(
2 'gemini-1.5-flash-8b',
3 system_instruction="""
4You are helping users to find interesting parts of a text.
5You analyze each paragraph of the input text to find its concepts and decide if it is relevant.
6""",
7 tools=[is_paragraph_relevant],
8 tool_config=content_types.to_tool_config({"function_calling_config": {"mode": "ANY"}})
9)
Avec ce mode ANY
, j'ai relancé plusieurs fois le test et ma fonction a toujours été appelée. Mais est-ce que ce sera toujours le cas ? Mystère !
Et si j'ai plusieurs fonctions, quelles va-t-il appeler ?
On n'a pas le clavier sorti des ronces.