Qu'est-ce qu'un Agent IA ?
- Comprendre la définition et les caractéristiques d'un agent IA
- Maîtriser la boucle agentique (perception-raisonnement-action)
- Identifier les différences entre chatbot et agent autonome
- Construire votre premier agent conceptuel en Python
1. Définition d'un Agent IA
Un agent IA est un système qui perçoit son environnement, prend des décisions basées sur ses perceptions et ses objectifs, puis agit de manière autonome pour atteindre ces objectifs.
┌─────────────────────────────────────────────────────────────┐
│ ANATOMIE D'UN AGENT IA │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ENVIRONNEMENT│◄────────┤ ACTIONS │ │
│ │ (Monde) │ │ (Effecteurs)│ │
│ └──────┬───────┘ └──────▲───────┘ │
│ │ │ │
│ │ Perceptions │ Commandes │
│ │ │ │
│ ┌──────▼───────┐ ┌─────┴────────┐ │
│ │ CAPTEURS │ │ RAISONNEMENT│ │
│ │ (Perceive) ├────────►│ (Decide) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ MÉMOIRE │ │
│ │ (Context) │ │
│ └──────────────┘ │
│ │
│ BOUCLE AGENTIQUE: Perceive → Decide → Act → Repeat │
└─────────────────────────────────────────────────────────────┘
2. Chatbot vs Agent IA
| Caractéristique | Chatbot Classique | Agent IA Autonome |
|---|---|---|
| Mode d'interaction | Réactif (répond uniquement) | Proactif (initie des actions) |
| Autonomie | Faible (attend des prompts) | Élevée (boucle autonome) |
| Capacités | Génération de texte | Utilisation d'outils, API, navigation |
| Planification | Aucune | Multi-étapes, sous-objectifs |
| Mémoire | Contexte de conversation | Court/long terme, épisodique |
| Correction d'erreurs | Non | Oui (réflexion, retry) |
Un agent IA n'est pas juste un LLM avec des outils. C'est un système complet avec une boucle de contrôle, une mémoire, et la capacité de poursuivre des objectifs complexes de manière autonome.
3. Composants Essentiels d'un Agent
3.1 Perception (Sensors)
L'agent doit pouvoir observer son environnement :
- Entrées utilisateur : Questions, commandes, feedback
- État du système : Fichiers, bases de données, APIs
- Résultats d'actions précédentes : Succès/échec des outils
- Contexte externe : Temps, localisation, événements
3.2 Raisonnement (Brain)
Le cœur décisionnel de l'agent :
- Modèle de langage : GPT-4, Claude, Llama, Mistral
- Planification : Décomposer l'objectif en sous-tâches
- Sélection d'outils : Choisir l'outil approprié
- Réflexion : Évaluer les résultats, corriger les erreurs
3.3 Action (Effectors)
L'agent peut agir sur le monde :
- Génération de texte : Réponses, rapports, code
- Appel d'outils : APIs, calculatrices, recherche web
- Manipulation de fichiers : Lecture/écriture
- Contrôle d'interfaces : Navigation web, CLI
3.4 Mémoire (Memory)
Persistance de l'information :
- Court terme : Contexte de la tâche en cours
- Long terme : Connaissances accumulées, préférences
- Épisodique : Historique des actions et résultats
- Sémantique : Base de connaissances structurée
┌────────────────────────────────────────────────────────────────┐
│ BOUCLE AGENTIQUE DÉTAILLÉE │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. OBSERVATION │
│ └─► Lire l'environnement (user input, state, context) │
│ │
│ 2. RÉFLEXION │
│ └─► LLM analyse la situation │
│ "Quel est l'objectif ? Que sais-je déjà ?" │
│ │
│ 3. PLANIFICATION │
│ └─► Décomposer en sous-tâches │
│ "Étape 1: rechercher info, Étape 2: calculer..." │
│ │
│ 4. SÉLECTION D'ACTION │
│ └─► Choisir l'outil ou la réponse │
│ "J'utilise l'outil 'web_search'" │
│ │
│ 5. EXÉCUTION │
│ └─► Appeler l'outil, obtenir le résultat │
│ Result: {...} │
│ │
│ 6. ÉVALUATION │
│ └─► Est-ce que ça a fonctionné ? │
│ ├─► Succès → Continuer │
│ └─► Échec → Corriger et réessayer │
│ │
│ 7. MÉMORISATION │
│ └─► Stocker l'expérience pour apprentissage │
│ │
│ 8. DÉCISION │
│ └─► Objectif atteint ? → STOP │
│ Sinon → Retour à l'étape 1 │
│ │
└────────────────────────────────────────────────────────────────┘
4. Premier Agent Conceptuel en Python
Créons un agent minimaliste qui illustre la boucle agentique :
import openai
import json
from typing import Dict, List, Any
class SimpleAgent:
"""Agent IA minimaliste avec boucle perception-raisonnement-action"""
def __init__(self, model: str = "gpt-4"):
self.model = model
self.memory = [] # Historique des interactions
self.tools = {
"calculator": self.calculator,
"web_search": self.web_search,
"final_answer": self.final_answer
}
self.max_iterations = 10
def calculator(self, expression: str) -> Dict:
"""Outil: calculatrice simple"""
try:
result = eval(expression)
return {"result": result, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
def web_search(self, query: str) -> Dict:
"""Outil: simulation de recherche web"""
mock_results = f"Résultats pour '{query}': Information pertinente..."
return {"results": mock_results, "success": True}
def final_answer(self, answer: str) -> Dict:
"""Outil: fourni la réponse finale"""
return {"answer": answer, "success": True, "final": True}
def perceive(self, user_input: str) -> str:
"""1. PERCEPTION: Capturer l'input utilisateur"""
observation = f"User: {user_input}"
self.memory.append({"role": "user", "content": user_input})
return observation
def reason(self) -> Dict[str, Any]:
"""2. RAISONNEMENT: Le LLM décide de l'action"""
system_prompt = """Tu es un agent IA autonome.
Outils disponibles:
- calculator(expression): calcule une expression mathématique
- web_search(query): recherche sur le web
- final_answer(answer): fourni la réponse finale
Réponds au format JSON:
{
"thought": "mon raisonnement",
"tool": "nom_outil",
"tool_input": "paramètre",
"final": false
}
Quand tu as la réponse finale, utilise final_answer et met final=true."""
messages = [{"role": "system", "content": system_prompt}] + self.memory
response = openai.ChatCompletion.create(
model=self.model,
messages=messages,
temperature=0
)
content = response.choices[0].message.content
try:
decision = json.loads(content)
except:
decision = {
"thought": content,
"tool": "final_answer",
"tool_input": content,
"final": True
}
return decision
def act(self, decision: Dict) -> Dict:
"""3. ACTION: Exécuter l'outil choisi"""
tool_name = decision.get("tool")
tool_input = decision.get("tool_input")
if tool_name not in self.tools:
return {"error": f"Outil inconnu: {tool_name}", "success": False}
tool_result = self.tools[tool_name](tool_input)
self.memory.append({
"role": "assistant",
"content": json.dumps(decision)
})
self.memory.append({
"role": "system",
"content": f"Tool result: {json.dumps(tool_result)}"
})
return tool_result
def run(self, task: str) -> str:
"""Boucle agentique complète"""
print(f"\n🤖 Agent démarré avec la tâche: {task}\n")
self.perceive(task)
for iteration in range(self.max_iterations):
print(f"--- Itération {iteration + 1} ---")
decision = self.reason()
print(f"💭 Pensée: {decision.get('thought')}")
print(f"🔧 Outil: {decision.get('tool')}({decision.get('tool_input')})")
result = self.act(decision)
print(f"📊 Résultat: {result}\n")
if decision.get("final") or result.get("final"):
print("✅ Tâche terminée!")
return result.get("answer", str(result))
print("⚠️ Limite d'itérations atteinte")
return "Tâche non terminée"
# UTILISATION
if __name__ == "__main__":
agent = SimpleAgent()
result = agent.run("Calcule (15 * 8) + 42")
print(f"\nRéponse finale: {result}")
L'utilisation d'eval() pour la calculatrice est dangereuse en production. Utilisez une bibliothèque comme numexpr ou ast.literal_eval() pour évaluer des expressions en toute sécurité.
5. Types d'Agents IA
| Type d'Agent | Description | Cas d'usage |
|---|---|---|
| Agent Réflexe Simple | Condition → Action directe | Chatbot FAQ, Autoréponses |
| Agent Basé sur Modèle | Maintient un modèle du monde | Assistant personnel, Recommandations |
| Agent Orienté Objectif | Planifie pour atteindre un but | Résolution de problèmes, Recherche |
| Agent Basé sur l'Utilité | Optimise une fonction d'utilité | Trading, Optimisation de ressources |
| Agent Apprenant | S'améliore avec l'expérience | Jeux, Personnalisation |
6. Caractéristiques Clés d'un Bon Agent
- Autonomie : Opère sans intervention constante
- Réactivité : Répond aux changements de l'environnement
- Proactivité : Prend des initiatives pour atteindre ses objectifs
- Sociabilité : Interagit avec d'autres agents et humains
- Robustesse : Gère les erreurs et situations imprévues
- Adaptabilité : Apprend et s'améliore avec l'expérience
- Transparence : Explique ses raisonnements et actions
Commencez toujours par un agent simple et ajoutez de la complexité progressivement. Un agent mal conçu peut entrer dans des boucles infinies ou faire des appels coûteux inutiles. Le logging est votre meilleur ami pour comprendre ce qui se passe.
Patterns Agentiques
- Maîtriser le pattern ReAct (Reasoning + Acting)
- Comprendre Plan-and-Execute pour les tâches complexes
- Implémenter Reflexion pour l'auto-correction
- Découvrir LATS (Language Agent Tree Search)
1. ReAct: Reasoning + Acting
ReAct est le pattern agentique le plus populaire, introduit dans un paper de 2022. Il combine raisonnement explicite (Reasoning) et actions (Acting) en une boucle itérative où l'agent verbalise sa pensée à chaque étape.
┌────────────────────────────────────────────────────────────────┐
│ PATTERN REACT │
├────────────────────────────────────────────────────────────────┤
│ │
│ User: "Quelle est la météo à Paris aujourd'hui ?" │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Iteration 1 │ │
│ │ │ │
│ │ Thought: "Je dois chercher la météo actuelle à Paris" │ │
│ │ Action: web_search("météo Paris aujourd'hui") │ │
│ │ Observation: "Paris: 15°C, nuageux, vent 10km/h" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Iteration 2 │ │
│ │ │ │
│ │ Thought: "J'ai l'info nécessaire, je peux répondre" │ │
│ │ Action: final_answer("Il fait 15°C à Paris...") │ │
│ │ Observation: DONE │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Format: Thought → Action → Observation → Repeat │
└────────────────────────────────────────────────────────────────┘
1.1 Anatomie d'une Itération ReAct
Chaque itération comporte trois éléments obligatoires :
- Thought (Pensée) : Le raisonnement explicite de l'agent sur ce qu'il doit faire
- Action : L'outil à appeler et son paramètre
- Observation : Le résultat retourné par l'outil
1.2 Implémentation ReAct Complète
import openai
import re
from typing import Dict, Optional, List
class ReActAgent:
"""Agent utilisant le pattern ReAct (Reasoning + Acting)"""
def __init__(self, model: str = "gpt-4", api_key: str = None):
self.model = model
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
openai.api_key = self.api_key
self.tools = self.register_tools()
self.max_iterations = 10
def register_tools(self) -> Dict:
"""Enregistrer les outils disponibles"""
return {
"web_search": {
"description": "Search the web for current information",
"function": self.web_search
},
"calculator": {
"description": "Calculate mathematical expressions",
"function": self.calculator
},
"python_repl": {
"description": "Execute Python code and return result",
"function": self.python_repl
},
"database_query": {
"description": "Query a database with SQL",
"function": self.database_query
}
}
def web_search(self, query: str) -> str:
"""Simuler une recherche web (utiliser Tavily/SerpAPI en prod)"""
# En production: import tavily; client = tavily.TavilyClient(api_key=...)
# results = client.search(query=query, max_results=3)
mock_results = f"[Mock Search] Top 3 results for '{query}':\n"
mock_results += "1. Relevant information about the topic\n"
mock_results += "2. Additional context and data\n"
mock_results += "3. Related findings"
return mock_results
def calculator(self, expression: str) -> str:
"""Calculatrice sécurisée"""
try:
# En production: utiliser numexpr pour la sécurité
# import numexpr as ne
# result = ne.evaluate(expression)
result = eval(expression) # ⚠️ Dangereux en prod
return str(result)
except Exception as e:
return f"Error: {str(e)}"
def python_repl(self, code: str) -> str:
"""Exécuter du code Python (nécessite sandbox en prod!)"""
try:
# ⚠️ TRÈS DANGEREUX sans sandbox
# En production: utiliser docker/E2B/modal.com
exec_globals = {"__builtins__": __builtins__}
exec_locals = {}
exec(code, exec_globals, exec_locals)
# Chercher une variable 'result' ou la dernière valeur
if 'result' in exec_locals:
return str(exec_locals['result'])
elif exec_locals:
return str(list(exec_locals.values())[-1])
else:
return "Code executed successfully (no output)"
except Exception as e:
return f"Error: {str(e)}"
def database_query(self, sql: str) -> str:
"""Simuler une requête SQL"""
# En production: connexion réelle à la DB
return f"[Mock DB] Executed: {sql}\nReturned 42 rows"
def create_prompt(self, query: str, scratchpad: str) -> str:
"""Créer le prompt ReAct avec l'historique"""
tools_desc = "\n".join([
f"- {name}: {info['description']}"
for name, info in self.tools.items()
])
prompt = f"""You are a ReAct (Reasoning + Acting) agent. Answer the user's question by reasoning step by step and using tools when needed.
Available tools:
{tools_desc}
You MUST use this EXACT format for each step:
Thought: [your reasoning about what to do next]
Action: [tool_name]
Action Input: [input for the tool]
After calling a tool, you will receive:
Observation: [result from the tool]
Then continue with another Thought/Action/Observation cycle, or provide the final answer:
Thought: I now know the final answer
Final Answer: [your complete answer to the user]
IMPORTANT RULES:
1. Always start with a Thought
2. Only call ONE tool per iteration
3. Wait for the Observation before the next Thought
4. When you have enough information, provide Final Answer
Question: {query}
{scratchpad}"""
return prompt
def parse_action(self, text: str) -> Optional[tuple]:
"""Parser la réponse du LLM pour extraire l'action"""
# Chercher le pattern Thought/Action/Action Input
thought_match = re.search(r'Thought:\s*(.+?)(?:\n|$)', text, re.DOTALL)
action_match = re.search(r'Action:\s*(\w+)', text)
input_match = re.search(r'Action Input:\s*(.+?)(?:\n|$)', text, re.DOTALL)
if action_match and input_match:
action = action_match.group(1).strip()
action_input = input_match.group(1).strip()
thought = thought_match.group(1).strip() if thought_match else "No thought provided"
return (action, action_input, thought)
# Vérifier si c'est une réponse finale
if "Final Answer:" in text:
final_answer = text.split("Final Answer:")[1].strip()
thought = thought_match.group(1).strip() if thought_match else "Ready to answer"
return ("final_answer", final_answer, thought)
return None
def run(self, query: str, verbose: bool = True) -> str:
"""Exécuter la boucle ReAct complète"""
scratchpad = ""
if verbose:
print(f"\n{'='*70}")
print(f"🤖 ReAct Agent Starting")
print(f"{'='*70}")
print(f"Query: {query}\n")
for iteration in range(self.max_iterations):
if verbose:
print(f"\n{'─'*70}")
print(f"Iteration {iteration + 1}/{self.max_iterations}")
print(f"{'─'*70}\n")
# Créer le prompt avec l'historique
prompt = self.create_prompt(query, scratchpad)
# Appeler le LLM
try:
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=500
)
llm_output = response.choices[0].message.content
except Exception as e:
return f"Error calling LLM: {str(e)}"
if verbose:
print(f"🤖 LLM Output:\n{llm_output}\n")
# Parser l'action
parsed = self.parse_action(llm_output)
if parsed is None:
if verbose:
print("⚠️ Could not parse action. Retrying with guidance...")
scratchpad += f"\n{llm_output}\n"
scratchpad += "\n[System: Invalid format. Please follow the exact format: Thought/Action/Action Input]\n"
continue
action, action_input, thought = parsed
if verbose:
print(f"💭 Thought: {thought}")
print(f"🔧 Action: {action}")
print(f"📝 Input: {action_input}\n")
# Vérifier si c'est la réponse finale
if action == "final_answer":
if verbose:
print(f"✅ Final Answer:\n{action_input}")
print(f"\n{'='*70}")
print(f"Agent completed in {iteration + 1} iterations")
print(f"{'='*70}\n")
return action_input
# Exécuter l'outil
if action not in self.tools:
observation = f"❌ Error: Unknown tool '{action}'. Available tools: {', '.join(self.tools.keys())}"
else:
try:
tool_func = self.tools[action]["function"]
observation = tool_func(action_input)
except Exception as e:
observation = f"❌ Error executing {action}: {str(e)}"
if verbose:
print(f"📊 Observation: {observation}\n")
# Ajouter au scratchpad pour la prochaine itération
scratchpad += f"\n{llm_output}\nObservation: {observation}\n"
# Si on atteint la limite d'itérations
if verbose:
print(f"⚠️ Reached maximum iterations ({self.max_iterations})")
return f"Task incomplete after {self.max_iterations} iterations"
# EXEMPLES D'UTILISATION
if __name__ == "__main__":
agent = ReActAgent()
# Test 1: Calcul mathématique
print("\n" + "="*70)
print("TEST 1: Mathematical Reasoning")
print("="*70)
result1 = agent.run("What is 25% of 80, then multiply the result by 3?")
# Test 2: Recherche + raisonnement
print("\n" + "="*70)
print("TEST 2: Information Retrieval")
print("="*70)
result2 = agent.run("Find the capital of Japan and tell me its approximate population")
# Test 3: Tâche complexe multi-outils
print("\n" + "="*70)
print("TEST 3: Multi-tool Task")
print("="*70)
result3 = agent.run(
"Search for the current price of Bitcoin, "
"then calculate how many Bitcoin I can buy with $10,000"
)
- Transparence : Chaque étape de raisonnement est explicite et visible
- Debuggable : Facile d'identifier où l'agent fait une erreur
- Flexible : S'adapte à des tâches très variées
- Prouvé : Paper académique de 2022, largement utilisé en production
- Performant : Améliore significativement les résultats vs prompting classique
2. Plan-and-Execute
Pour les tâches complexes nécessitant plusieurs étapes, le pattern Plan-and-Execute est plus efficace. L'idée : planifier d'abord toutes les étapes, puis les exécuter séquentiellement.
┌────────────────────────────────────────────────────────────────┐
│ PATTERN PLAN-AND-EXECUTE │
├────────────────────────────────────────────────────────────────┤
│ │
│ User: "Analyse le marché crypto et recommande un achat" │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PHASE 1: PLANNING (Planner Agent) │ │
│ │ ──────────────────────────────── │ │
│ │ │ │
│ │ Step 1: Rechercher prix actuels BTC, ETH, SOL │ │
│ │ Step 2: Analyser tendances des 30 derniers jours │ │
│ │ Step 3: Comparer la volatilité de chaque crypto │ │
│ │ Step 4: Lire les news récentes sur chaque crypto │ │
│ │ Step 5: Synthétiser et recommander un achat │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PHASE 2: EXECUTION (Executor Agent) │ │
│ │ ─────────────────────────────── │ │
│ │ │ │
│ │ [Execute Step 1] → Result 1 ✓ │ │
│ │ [Execute Step 2] → Result 2 ✓ │ │
│ │ [Execute Step 3] → Result 3 ✓ │ │
│ │ [Execute Step 4] → Result 4 ✓ │ │
│ │ [Execute Step 5] → Result 5 ✓ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PHASE 3: REPLANNING (Optional, si échec) │ │
│ │ ──────────────────────────────────── │ │
│ │ │ │
│ │ Évaluer les résultats, ajuster le plan si nécessaire │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
2.1 Pourquoi Plan-and-Execute ?
| Aspect | ReAct | Plan-and-Execute |
|---|---|---|
| Planification | Pas de plan global | Plan complet dès le début |
| Vision | Myope (étape par étape) | Vision d'ensemble |
| Optimisation | Difficile d'optimiser | Peut paralléliser/optimiser |
| Coût | Variable | Plus prévisible |
| Meilleur pour | Tâches simples, exploratoires | Tâches complexes, structurées |
2.2 Implémentation Plan-and-Execute
from typing import List, Dict
import openai
import json
class PlanAndExecuteAgent:
"""Agent utilisant le pattern Plan-and-Execute"""
def __init__(self, model: str = "gpt-4"):
self.model = model
self.tools = {
"web_search": self.web_search,
"calculator": self.calculator,
"data_analysis": self.data_analysis,
"database_query": self.database_query
}
def web_search(self, query: str) -> str:
return f"[Search results for: {query}]\nRelevant information found..."
def calculator(self, expr: str) -> str:
try:
return str(eval(expr))
except:
return "Error in calculation"
def data_analysis(self, data: str) -> str:
return f"[Analysis completed for: {data}]\nKey insights: ..."
def database_query(self, sql: str) -> str:
return f"[Query executed: {sql}]\n42 rows returned"
def create_plan(self, objective: str) -> List[Dict]:
"""Phase 1: Créer un plan d'action détaillé"""
tools_list = ", ".join(self.tools.keys())
prompt = f"""You are a planning agent. Break down this complex objective into concrete, sequential steps.
Objective: {objective}
Available tools: {tools_list}
For each step, specify:
1. A clear action description
2. Which tool to use
3. What input to provide
Output format (JSON array):
[
{{"step": 1, "description": "...", "tool": "...", "input": "..."}},
{{"step": 2, "description": "...", "tool": "...", "input": "..."}},
...
]
Plan:"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0
)
plan_text = response.choices[0].message.content
# Tenter de parser le JSON
try:
# Extraire le JSON s'il est entre ```json et ```
if "```json" in plan_text:
plan_text = plan_text.split("```json")[1].split("```")[0]
elif "```" in plan_text:
plan_text = plan_text.split("```")[1].split("```")[0]
plan = json.loads(plan_text.strip())
return plan
except json.JSONDecodeError:
# Fallback: parser manuellement
steps = []
for i, line in enumerate(plan_text.split('\n'), 1):
line = line.strip()
if line and (line[0].isdigit() or line.startswith('-')):
step_text = line.split('.', 1)[-1].strip()
step_text = line.split('-', 1)[-1].strip()
if step_text:
steps.append({
"step": i,
"description": step_text,
"tool": "web_search", # Default
"input": step_text
})
return steps
def execute_step(self, step: Dict, context: str) -> Dict:
"""Phase 2: Exécuter une étape du plan"""
step_num = step.get("step", "?")
description = step.get("description", "")
tool_name = step.get("tool", "web_search")
tool_input = step.get("input", description)
print(f"\n[Step {step_num}] {description}")
print(f" Tool: {tool_name}")
print(f" Input: {tool_input[:100]}...")
# Exécuter l'outil
if tool_name in self.tools:
try:
result = self.tools[tool_name](tool_input)
success = True
except Exception as e:
result = f"Error: {str(e)}"
success = False
else:
result = f"Unknown tool: {tool_name}"
success = False
print(f" Result: {result[:150]}...")
return {
"step": step_num,
"description": description,
"tool": tool_name,
"input": tool_input,
"result": result,
"success": success
}
def should_replan(self, results: List[Dict]) -> bool:
"""Décider si on doit replanner (si trop d'échecs)"""
failures = sum(1 for r in results if not r.get("success", True))
failure_rate = failures / len(results) if results else 0
return failure_rate > 0.3 # Si plus de 30% d'échecs
def replan(self, objective: str, previous_plan: List[Dict], results: List[Dict]) -> List[Dict]:
"""Créer un nouveau plan basé sur les résultats précédents"""
results_summary = "\n".join([
f"Step {r['step']}: {'✓' if r.get('success') else '✗'} {r['description']}"
for r in results
])
prompt = f"""The previous plan had some failures. Create an improved plan.
Objective: {objective}
Previous attempt:
{results_summary}
Create a NEW, BETTER plan (JSON format):"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
# Parser le nouveau plan
return self.create_plan(objective)
def synthesize(self, objective: str, results: List[Dict]) -> str:
"""Phase 3: Synthétiser tous les résultats"""
results_text = "\n".join([
f"Step {r['step']} ({r['tool']}): {r['result'][:200]}"
for r in results
])
prompt = f"""Synthesize these execution results to answer the objective.
Objective: {objective}
Execution Results:
{results_text}
Provide a comprehensive final answer:"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
return response.choices[0].message.content
def run(self, objective: str, allow_replanning: bool = True) -> str:
"""Exécuter le cycle complet Plan-and-Execute"""
print(f"\n{'='*70}")
print(f"PLAN-AND-EXECUTE AGENT")
print(f"{'='*70}")
print(f"Objective: {objective}\n")
# PHASE 1: PLANNING
print(f"{'─'*70}")
print("📋 PHASE 1: PLANNING")
print(f"{'─'*70}\n")
plan = self.create_plan(objective)
print(f"Plan created with {len(plan)} steps:")
for step in plan:
print(f" {step['step']}. {step['description']}")
# PHASE 2: EXECUTION
print(f"\n{'─'*70}")
print("⚙️ PHASE 2: EXECUTION")
print(f"{'─'*70}")
context = ""
results = []
for step in plan:
result = self.execute_step(step, context)
results.append(result)
context += f"\nStep {step['step']}: {result['result'][:100]}"
# Vérifier si replanning nécessaire
if allow_replanning and self.should_replan(results):
print(f"\n⚠️ Too many failures. REPLANNING...\n")
new_plan = self.replan(objective, plan, results)
print(f"New plan created with {len(new_plan)} steps")
# Ré-exécuter avec le nouveau plan
results = []
for step in new_plan:
result = self.execute_step(step, context)
results.append(result)
# PHASE 3: SYNTHESIS
print(f"\n{'─'*70}")
print("📊 PHASE 3: SYNTHESIS")
print(f"{'─'*70}\n")
final_answer = self.synthesize(objective, results)
print(f"Final Answer:\n{final_answer}")
print(f"\n{'='*70}")
print(f"Execution Summary:")
print(f" Total steps: {len(results)}")
print(f" Successful: {sum(1 for r in results if r.get('success'))}")
print(f" Failed: {sum(1 for r in results if not r.get('success'))}")
print(f"{'='*70}\n")
return final_answer
# TESTS
if __name__ == "__main__":
agent = PlanAndExecuteAgent()
# Test complexe multi-étapes
result = agent.run(
"Research the top 3 AI startups in France founded after 2020, "
"compare their funding amounts, and recommend which one has "
"the best growth potential based on market trends"
)
- Tâches longues : Plus de 5-10 étapes nécessaires
- Objectifs bien définis : Le but final est clair dès le début
- Besoin d'optimisation : Certaines étapes peuvent être parallélisées
- Budget contrôlé : Coût plus prévisible qu'avec ReAct
- Qualité critique : Le plan peut être revu par un humain avant exécution
3. Reflexion: Auto-Correction et Amélioration
Le pattern Reflexion ajoute une capacité d'auto-évaluation et d'amélioration itérative. L'agent génère une solution, la critique lui-même, puis l'améliore en boucle jusqu'à satisfaction.
┌────────────────────────────────────────────────────────────────┐
│ PATTERN REFLEXION │
├────────────────────────────────────────────────────────────────┤
│ │
│ Task: "Write an efficient prime number finder in Python" │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Iteration 1: GENERATE │ │
│ │ │ │
│ │ def find_primes(n): │ │
│ │ primes = [] │ │
│ │ for num in range(2, n): │ │
│ │ if all(num % i != 0 for i in range(2, num)): │ │
│ │ primes.append(num) │ │
│ │ return primes │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ REFLECT: Score 4/10 │ │
│ │ │ │
│ │ Issues: │ │
│ │ - Very inefficient for large n (O(n²)) │ │
│ │ - No docstring │ │
│ │ - Could use Sieve of Eratosthenes │ │
│ │ │ │
│ │ Suggestions: │ │
│ │ - Implement Sieve algorithm │ │
│ │ - Add documentation │ │
│ │ - Add type hints │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Iteration 2: IMPROVE │ │
│ │ │ │
│ │ def find_primes(n: int) -> List[int]: │ │
│ │ """Find all primes up to n using Sieve.""" │ │
│ │ if n < 2: return [] │ │
│ │ sieve = [True] * (n + 1) │ │
│ │ sieve[0] = sieve[1] = False │ │
│ │ for i in range(2, int(n**0.5) + 1): │ │
│ │ if sieve[i]: │ │
│ │ sieve[i*i:n+1:i] = [False] * len(...) │ │
│ │ return [i for i, is_prime in enumerate(sieve) ...] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ REFLECT: Score 9/10 ✓ │ │
│ │ Excellent! Efficient algorithm, well-documented │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Loop: Generate → Reflect → Improve → Repeat │
└────────────────────────────────────────────────────────────────┘
3.1 Implémentation Reflexion
import openai
from typing import Dict, List
class ReflexionAgent:
"""Agent avec capacité d'auto-réflexion et amélioration itérative"""
def __init__(self, model: str = "gpt-4"):
self.model = model
self.max_iterations = 3
self.target_score = 8 # Score minimum pour considérer la solution acceptable
self.trajectory = []
def generate(self, task: str, previous_attempt: str = None, feedback: str = None) -> str:
"""Générer une solution (ou l'améliorer si feedback fourni)"""
if previous_attempt is None:
# Première tentative
prompt = f"""Solve this task to the best of your ability:
Task: {task}
Provide your solution:"""
else:
# Amélioration basée sur le feedback
prompt = f"""Improve your previous solution based on the feedback provided.
Task: {task}
Previous attempt:
{previous_attempt}
Feedback received:
{feedback}
Provide an IMPROVED solution addressing all the feedback:"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.7
)
return response.choices[0].message.content
def reflect(self, task: str, attempt: str, iteration: int) -> Dict:
"""Auto-évaluer la solution de manière critique"""
prompt = f"""You are a critical evaluator. Evaluate this solution objectively and harshly.
Task: {task}
Solution (Iteration {iteration}):
{attempt}
Provide a detailed evaluation:
1. **Score (0-10)**: How good is this solution?
- 0-3: Poor, major issues
- 4-6: Acceptable but needs improvement
- 7-8: Good, minor issues
- 9-10: Excellent, minimal issues
2. **Strengths**: What's good about this solution?
3. **Issues**: What are the problems? Be specific.
4. **Suggestions**: How can it be improved?
Format:
Score: [0-10]
Strengths:
- [strength 1]
- [strength 2]
Issues:
- [issue 1]
- [issue 2]
Suggestions:
- [suggestion 1]
- [suggestion 2]"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.3 # Moins de randomness pour évaluation
)
reflection_text = response.choices[0].message.content
# Parser le score
score = 5 # Default
for line in reflection_text.split('\n'):
if line.strip().startswith('Score:'):
try:
score_part = line.split('Score:')[1].strip()
score = int(''.join(filter(str.isdigit, score_part.split('/')[0])))
score = max(0, min(10, score)) # Clamp entre 0-10
except:
pass
return {
"score": score,
"reflection": reflection_text,
"should_continue": score < self.target_score,
"iteration": iteration
}
def extract_feedback(self, reflection: str) -> str:
"""Extraire les parties pertinentes de la réflexion pour le feedback"""
# Extraire Issues et Suggestions
feedback_parts = []
if "Issues:" in reflection:
issues_section = reflection.split("Issues:")[1]
if "Suggestions:" in issues_section:
issues_section = issues_section.split("Suggestions:")[0]
feedback_parts.append(f"Issues identified:\n{issues_section.strip()}")
if "Suggestions:" in reflection:
suggestions_section = reflection.split("Suggestions:")[1]
# Prendre jusqu'à la fin ou la prochaine section
feedback_parts.append(f"Improvement suggestions:\n{suggestions_section.strip()}")
return "\n\n".join(feedback_parts)
def run(self, task: str, verbose: bool = True) -> Dict:
"""Exécuter la boucle Reflexion complète"""
if verbose:
print(f"\n{'='*70}")
print(f"REFLEXION AGENT")
print(f"{'='*70}")
print(f"Task: {task}")
print(f"Target Score: {self.target_score}/10")
print(f"Max Iterations: {self.max_iterations}\n")
current_attempt = None
feedback = None
for iteration in range(1, self.max_iterations + 1):
if verbose:
print(f"\n{'─'*70}")
print(f"Iteration {iteration}/{self.max_iterations}")
print(f"{'─'*70}\n")
# GENERATE (ou IMPROVE si pas la première itération)
if verbose:
print("📝 GENERATING solution...")
current_attempt = self.generate(task, current_attempt, feedback)
if verbose:
print(f"\nSolution (length: {len(current_attempt)} chars):")
print(current_attempt[:300] + "..." if len(current_attempt) > 300 else current_attempt)
print()
# REFLECT
if verbose:
print("🤔 REFLECTING on solution...")
reflection_result = self.reflect(task, current_attempt, iteration)
score = reflection_result["score"]
reflection = reflection_result["reflection"]
if verbose:
print(f"\n📊 Evaluation (Score: {score}/10):")
print(reflection)
print()
# Sauvegarder dans la trajectoire
self.trajectory.append({
"iteration": iteration,
"attempt": current_attempt,
"score": score,
"reflection": reflection
})
# Vérifier si on peut s'arrêter
if not reflection_result["should_continue"]:
if verbose:
print(f"✅ Target score reached! ({score}/10 >= {self.target_score}/10)")
break
if iteration < self.max_iterations:
# Préparer le feedback pour la prochaine itération
feedback = self.extract_feedback(reflection)
if verbose:
print(f"🔄 Preparing to improve in next iteration...")
else:
if verbose:
print(f"⚠️ Max iterations reached")
# Choisir la meilleure solution
best = max(self.trajectory, key=lambda x: x["score"])
if verbose:
print(f"\n{'='*70}")
print(f"REFLEXION COMPLETE")
print(f"{'='*70}")
print(f"Iterations: {len(self.trajectory)}")
print(f"Best Score: {best['score']}/10 (Iteration {best['iteration']})")
print(f"{'='*70}\n")
print("Best Solution:")
print(best["attempt"])
print()
return {
"solution": best["attempt"],
"score": best["score"],
"iteration": best["iteration"],
"trajectory": self.trajectory
}
# TESTS
if __name__ == "__main__":
agent = ReflexionAgent()
# Test 1: Tâche de programmation
result1 = agent.run(
"Write a Python function to find all prime numbers up to N. "
"It should be efficient, well-documented, and include type hints."
)
print("\n" + "="*70 + "\n")
# Test 2: Rédaction
result2 = agent.run(
"Write a compelling 3-paragraph product description for an AI-powered "
"code review tool that helps developers write better code."
)
- Qualité supérieure : Amélioration itérative garantit de meilleurs résultats
- Auto-correction : Détecte et corrige ses propres erreurs
- Apprentissage : Accumule de l'expérience dans la trajectoire
- Explicabilité : Chaque amélioration est documentée avec raisons
4. LATS: Language Agent Tree Search
LATS (Language Agent Tree Search) combine recherche arborescente (inspirée de Monte Carlo Tree Search) avec des LLMs pour explorer systématiquement l'espace des solutions.
┌────────────────────────────────────────────────────────────────┐
│ PATTERN LATS (Tree Search) │
├────────────────────────────────────────────────────────────────┤
│ │
│ [Root: Tâche initiale] │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ [Action A] [Action B] [Action C] │
│ V=0.6 V=0.8 V=0.5 │
│ │ │ │
│ │ ┌─────┴─────┐ │
│ │ ▼ ▼ │
│ │ [B→D] [B→E] │
│ │ V=0.7 V=0.9 ← BEST │
│ │ │ │
│ │ ┌─────┴─────┐ │
│ │ ▼ ▼ │
│ │ [E→F] [E→G] │
│ │ V=0.95 V=0.85 │
│ │ │ │
│ │ ▼ │
│ │ [GOAL!] ✓ │
│ │
│ V = Value (score estimé du nœud) │
│ │
│ Processus: │
│ 1. SELECT: Choisir le nœud le plus prometteur (UCB1) │
│ 2. EXPAND: Générer plusieurs actions enfants │
│ 3. EVALUATE: Scorer chaque action │
│ 4. BACKPROPAGATE: Propager les scores vers le haut │
│ 5. REPEAT: Jusqu'à trouver la solution ou timeout │
│ │
└────────────────────────────────────────────────────────────────┘
4.1 Quand utiliser LATS ?
LATS est particulièrement puissant pour :
- Problèmes avec espace de solutions vaste : Quand de nombreuses approches sont possibles
- Nécessité d'exploration : Quand la meilleure solution n'est pas évidente
- Tâches de programmation : Générer du code avec plusieurs approches possibles
- Planification complexe : Optimisation de stratégies
LATS peut générer des dizaines d'appels LLM pour une seule tâche. Avec GPT-4, cela peut coûter plusieurs dollars par requête. Toujours définir un budget maximum et un timeout.
5. Comparaison Complète des Patterns
| Pattern | Complexité | Appels LLM | Coût | Qualité | Latence | Cas d'usage |
|---|---|---|---|---|---|---|
| ReAct | Faible | 3-10 | 💰 Moyen | ⭐⭐⭐ Bonne | Rapide | Tâches simples, outils |
| Plan-Execute | Moyenne | 5-15 | 💰💰 Moyen | ⭐⭐⭐⭐ Très bonne | Moyen | Tâches complexes, multi-étapes |
| Reflexion | Moyenne | 6-18 | 💰💰💰 Élevé | ⭐⭐⭐⭐⭐ Excellente | Lent | Qualité critique, code |
| LATS | Élevée | 20-100+ | 💰💰💰💰 Très élevé | ⭐⭐⭐⭐⭐ Excellente | Très lent | Exploration, optimisation |
6. Patterns Hybrides
En production, on combine souvent plusieurs patterns :
6.1 ReAct + Reflexion
ReAct avec auto-évaluation après chaque action majeure.
6.2 Plan-Execute + ReAct
Planifier globalement avec Plan-Execute, mais exécuter chaque étape avec ReAct pour plus de flexibilité.
6.3 Reflexion + LATS
Recherche arborescente où chaque nœud est évalué avec Reflexion.
class HybridAgent:
"""Agent combinant Plan-Execute pour la structure et ReAct pour l'exécution"""
def __init__(self):
self.planner = PlanAndExecuteAgent()
self.executor = ReActAgent()
def run(self, objective: str) -> str:
# Phase 1: Créer un plan global
plan = self.planner.create_plan(objective)
print(f"Global Plan: {len(plan)} steps")
# Phase 2: Exécuter chaque étape avec ReAct
results = []
for step in plan:
print(f"\nExecuting step {step['step']} with ReAct...")
result = self.executor.run(step['description'], verbose=False)
results.append(result)
# Phase 3: Synthèse
return self.planner.synthesize(objective, results)
En production, commencez TOUJOURS par ReAct. C'est le pattern le plus simple, le mieux testé, et il suffit pour 80% des cas. Passez à Plan-Execute uniquement si vos tâches ont systématiquement plus de 7-8 étapes. Reflexion et LATS sont réservés aux cas où la qualité est absolument critique et le budget le permet (recherche, génération de code complexe, etc.).
7. Métriques et Monitoring
Pour chaque pattern, trackez :
- Nombre d'itérations : Combien de tours de boucle ?
- Nombre d'appels LLM : Coût direct
- Tokens consommés : Input + output
- Latence totale : Temps de bout en bout
- Taux de succès : % de tâches réussies du premier coup
- Taux d'erreur d'outils : % d'appels d'outils échoués
import time
from dataclasses import dataclass
from typing import Dict
@dataclass
class AgentMetrics:
iterations: int = 0
llm_calls: int = 0
tool_calls: int = 0
tool_errors: int = 0
total_tokens: int = 0
start_time: float = 0
end_time: float = 0
def record_llm_call(self, tokens: int):
self.llm_calls += 1
self.total_tokens += tokens
def record_tool_call(self, success: bool):
self.tool_calls += 1
if not success:
self.tool_errors += 1
def finalize(self):
self.end_time = time.time()
def summary(self) -> Dict:
duration = self.end_time - self.start_time
return {
"iterations": self.iterations,
"llm_calls": self.llm_calls,
"tool_calls": self.tool_calls,
"tool_error_rate": self.tool_errors / self.tool_calls if self.tool_calls > 0 else 0,
"total_tokens": self.total_tokens,
"duration_seconds": duration,
"tokens_per_second": self.total_tokens / duration if duration > 0 else 0
}
Tool Use & Function Calling
- Maîtriser l'API Function Calling d'OpenAI et Anthropic
- Définir des outils robustes avec JSON Schema
- Implémenter le parallel function calling
- Créer un routeur d'outils intelligent et gérer les erreurs
1. Qu'est-ce que le Function Calling ?
Le Function Calling (aussi appelé Tool Use chez Anthropic) est une fonctionnalité native des LLMs modernes qui permet à l'agent de décider quand et comment appeler des fonctions externes, avec validation automatique des arguments via JSON Schema.
┌────────────────────────────────────────────────────────────────┐
│ FUNCTION CALLING FLOW │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ DÉFINITION DES OUTILS │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ tools = [{ │ │
│ │ "type": "function", │ │
│ │ "function": { │ │
│ │ "name": "get_weather", │ │
│ │ "description": "Get current weather for a location", │ │
│ │ "parameters": { │ │
│ │ "type": "object", │ │
│ │ "properties": { │ │
│ │ "location": {"type": "string"}, │ │
│ │ "unit": {"enum": ["C", "F"]} │ │
│ │ }, │ │
│ │ "required": ["location"] │ │
│ │ } │ │
│ │ } │ │
│ │ }] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 2️⃣ USER INPUT │
│ "Quelle est la météo à Paris ?" │
│ │
│ 3️⃣ LLM DÉCIDE D'APPELER UN OUTIL │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "tool_calls": [{ │ │
│ │ "id": "call_abc123", │ │
│ │ "function": { │ │
│ │ "name": "get_weather", │ │
│ │ "arguments": { │ │
│ │ "location": "Paris", │ │
│ │ "unit": "C" │ │
│ │ } │ │
│ │ } │ │
│ │ }] │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 4️⃣ VOTRE CODE EXÉCUTE │
│ result = get_weather("Paris", "C") │
│ → {"temp": 15, "condition": "cloudy"} │
│ │
│ 5️⃣ RENVOYER LE RÉSULTAT AU LLM │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "role": "tool", │ │
│ │ "tool_call_id": "call_abc123", │ │
│ │ "content": '{"temp": 15, "condition": "cloudy"}' │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 6️⃣ LLM GÉNÈRE LA RÉPONSE FINALE │
│ "Il fait 15°C à Paris avec un ciel nuageux." │
│ │
└────────────────────────────────────────────────────────────────┘
2. Function Calling avec OpenAI
2.1 Définir des Outils avec JSON Schema
import openai
import json
import os
# Configuration
openai.api_key = os.getenv("OPENAI_API_KEY")
# Définir les outils disponibles avec JSON Schema
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for current information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"num_results": {
"type": "integer",
"description": "Number of results to return",
"minimum": 1,
"maximum": 10,
"default": 5
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Perform mathematical calculations",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate, e.g. '2 + 2 * 3'"
}
},
"required": ["expression"]
}
}
}
]
# Implémenter les fonctions réelles
def get_current_weather(location: str, unit: str = "celsius") -> dict:
"""Simule un appel API météo"""
# En production: utiliser OpenWeatherMap, WeatherAPI, etc.
mock_data = {
"location": location,
"temperature": 15 if unit == "celsius" else 59,
"unit": unit,
"condition": "partly cloudy",
"humidity": 65,
"wind_speed": 10
}
return mock_data
def search_web(query: str, num_results: int = 5) -> dict:
"""Simule une recherche web"""
# En production: utiliser Tavily, SerpAPI, DuckDuckGo
mock_results = {
"query": query,
"results": [
{
"title": f"Result {i+1} for {query}",
"url": f"https://example.com/result-{i+1}",
"snippet": f"Relevant information about {query}..."
}
for i in range(num_results)
],
"total": num_results
}
return mock_results
def calculate(expression: str) -> dict:
"""Calculatrice sécurisée"""
try:
# ⚠️ eval() est dangereux en production
# Utiliser numexpr ou ast.literal_eval
result = eval(expression)
return {
"expression": expression,
"result": result,
"success": True
}
except Exception as e:
return {
"expression": expression,
"error": str(e),
"success": False
}
# Mapper les noms de fonctions aux implémentations
available_functions = {
"get_current_weather": get_current_weather,
"search_web": search_web,
"calculate": calculate
}
def run_agent_with_tools(user_message: str, verbose: bool = True):
"""Agent avec function calling"""
messages = [{"role": "user", "content": user_message}]
if verbose:
print(f"\n{'='*70}")
print(f"User: {user_message}")
print(f"{'='*70}\n")
# Premier appel: demander au LLM s'il veut appeler un outil
response = openai.ChatCompletion.create(
model="gpt-4-turbo-preview",
messages=messages,
tools=tools,
tool_choice="auto" # Le LLM décide automatiquement
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
# Vérifier si le LLM veut appeler des outils
if tool_calls:
if verbose:
print(f"🔧 LLM wants to call {len(tool_calls)} tool(s):\n")
# Ajouter la réponse du LLM aux messages
messages.append(response_message)
# Exécuter chaque outil appelé
for tool_call in tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
if verbose:
print(f" Tool: {function_name}")
print(f" Args: {function_args}")
# Appeler la vraie fonction
if function_name in available_functions:
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
if verbose:
print(f" Result: {json.dumps(function_response, indent=2)}\n")
# Ajouter le résultat aux messages
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": json.dumps(function_response)
})
else:
if verbose:
print(f" ⚠️ Unknown function: {function_name}\n")
# Deuxième appel: le LLM génère la réponse finale
if verbose:
print("💬 Generating final response...\n")
second_response = openai.ChatCompletion.create(
model="gpt-4-turbo-preview",
messages=messages
)
final_answer = second_response.choices[0].message.content
if verbose:
print(f"✅ Final Answer:\n{final_answer}\n")
return final_answer
else:
# Pas d'outil nécessaire, réponse directe
if verbose:
print("💬 Direct response (no tools needed):\n")
print(f"{response_message.content}\n")
return response_message.content
# TESTS
if __name__ == "__main__":
# Test 1: Météo
result1 = run_agent_with_tools("What's the weather like in Tokyo?")
# Test 2: Recherche web
result2 = run_agent_with_tools("Search for recent news about Claude AI")
# Test 3: Calcul
result3 = run_agent_with_tools("Calculate 15% of 250, then add 42")
# Test 4: Pas d'outil nécessaire
result4 = run_agent_with_tools("What is the capital of France?")
- Structuré : Arguments validés automatiquement via JSON Schema
- Fiable : Le LLM ne hallucine pas les appels de fonction
- Type-safe : Validation des types, enums, min/max, patterns
- Natif : Supporté par OpenAI, Anthropic, Mistral, Llama 3.1+
3. JSON Schema Avancé
JSON Schema offre de puissantes fonctionnalités de validation :
# Outil complexe avec validation avancée
advanced_tool = {
"type": "function",
"function": {
"name": "create_user",
"description": "Create a new user account with comprehensive validation",
"parameters": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Username (3-20 chars, alphanumeric + underscore)",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_]+$"
},
"email": {
"type": "string",
"format": "email",
"description": "Valid email address"
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 120,
"description": "User age (must be 18+)"
},
"role": {
"type": "string",
"enum": ["admin", "user", "guest", "moderator"],
"default": "user",
"description": "User role in the system"
},
"permissions": {
"type": "array",
"items": {
"type": "string",
"enum": ["read", "write", "delete", "manage"]
},
"uniqueItems": True,
"maxItems": 10,
"description": "User permissions (unique, max 10)"
},
"preferences": {
"type": "object",
"properties": {
"newsletter": {
"type": "boolean",
"default": False
},
"theme": {
"type": "string",
"enum": ["light", "dark", "auto"],
"default": "auto"
},
"language": {
"type": "string",
"pattern": "^[a-z]{2}$",
"description": "ISO 639-1 language code (e.g., 'en', 'fr')"
}
},
"required": ["theme"]
},
"metadata": {
"type": "object",
"additionalProperties": True,
"description": "Additional metadata (flexible)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 5,
"description": "User tags (1-5 tags)"
},
"website": {
"type": "string",
"format": "uri",
"description": "User website URL"
},
"birth_date": {
"type": "string",
"format": "date",
"description": "Birth date in YYYY-MM-DD format"
}
},
"required": ["username", "email", "age"],
"additionalProperties": False
}
}
}
# Le LLM respectera automatiquement:
# - minLength/maxLength
# - pattern (regex)
# - enum (valeurs possibles)
# - minimum/maximum
# - format (email, uri, date, date-time, etc.)
# - required fields
# - uniqueItems dans les arrays
# - type validation
3.1 Formats JSON Schema Standard
| Format | Description | Exemple |
|---|---|---|
email |
Adresse email valide | user@example.com |
uri |
URI complète | https://example.com/path |
date |
Date ISO 8601 | 2024-12-31 |
date-time |
DateTime ISO 8601 | 2024-12-31T23:59:59Z |
time |
Heure | 14:30:00 |
ipv4 |
Adresse IPv4 | 192.168.1.1 |
ipv6 |
Adresse IPv6 | 2001:0db8::1 |
uuid |
UUID valide | 550e8400-e29b-41d4-a716-446655440000 |
4. Parallel Function Calling
Les LLMs modernes peuvent appeler plusieurs outils en parallèle en une seule fois, ce qui réduit la latence et améliore l'efficacité.
┌────────────────────────────────────────────────────────────────┐
│ PARALLEL FUNCTION CALLING │
├────────────────────────────────────────────────────────────────┤
│ │
│ User: "Weather in Paris + news about Eiffel Tower" │
│ │
│ LLM Decision │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ get_weather("Paris") search_web("Eiffel Tower") │
│ │ │ │
│ │ PARALLEL │ │
│ │ EXECUTION │ │
│ │ │ │
│ ▼ ▼ │
│ {"temp": 15°C} [news_results...] │
│ │ │ │
│ └──────────┬───┘ │
│ ▼ │
│ LLM Synthesis │
│ │ │
│ ▼ │
│ "Il fait 15°C à Paris. Actualités: ..." │
│ │
│ Avantage: 1 round au lieu de 2 (séquentiel) │
│ Gain latence: ~50% │
└────────────────────────────────────────────────────────────────┘
def run_agent_with_parallel_tools(user_message: str):
"""Agent supportant les appels d'outils parallèles"""
messages = [{"role": "user", "content": user_message}]
print(f"\n{'='*70}")
print(f"User: {user_message}")
print(f"{'='*70}\n")
response = openai.ChatCompletion.create(
model="gpt-4-turbo-preview",
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
if tool_calls:
print(f"🔧 LLM requested {len(tool_calls)} tool(s) IN PARALLEL:\n")
messages.append(response_message)
# Exécuter tous les outils (potentiellement en parallèle)
for i, tool_call in enumerate(tool_calls, 1):
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f" [{i}] {function_name}({function_args})")
# Exécuter
function_to_call = available_functions[function_name]
function_response = function_to_call(**function_args)
print(f" → Result: {json.dumps(function_response, indent=8)[:100]}...\n")
# Ajouter le résultat
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": json.dumps(function_response)
})
# Réponse finale synthétisant tous les résultats
print("💬 Synthesizing all results...\n")
final_response = openai.ChatCompletion.create(
model="gpt-4-turbo-preview",
messages=messages
)
final_answer = final_response.choices[0].message.content
print(f"✅ Final Answer:\n{final_answer}\n")
return final_answer
return response_message.content
# TEST: Le LLM appellera plusieurs outils en parallèle
result = run_agent_with_parallel_tools(
"What's the weather in New York? Also search for recent news about the city, "
"and calculate 25% of 500"
)
- Requêtes indépendantes : Les outils n'ont pas de dépendances entre eux
- Gain de latence : Réduire le nombre de rounds d'aller-retour
- Tâches composites : "Fais X et Y et Z"
5. Tool Use avec Anthropic Claude
Claude utilise une API légèrement différente mais avec le même concept :
import anthropic
import json
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
# Définir les outils pour Claude (syntaxe légèrement différente)
claude_tools = [
{
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city name, e.g. Paris"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
},
{
"name": "search_web",
"description": "Search the web for information",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
}
},
"required": ["query"]
}
}
]
def run_claude_with_tools(user_message: str):
"""Agent Claude avec tool use"""
messages = [{"role": "user", "content": user_message}]
print(f"\n{'='*70}")
print(f"User: {user_message}")
print(f"{'='*70}\n")
# Premier appel
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=claude_tools,
messages=messages
)
print(f"Stop reason: {response.stop_reason}\n")
# Vérifier si Claude veut utiliser un outil
if response.stop_reason == "tool_use":
# Extraire les tool uses
tool_uses = [block for block in response.content if block.type == "tool_use"]
print(f"🔧 Claude wants to use {len(tool_uses)} tool(s):\n")
# Ajouter la réponse de Claude
messages.append({"role": "assistant", "content": response.content})
# Préparer les résultats des outils
tool_results = []
for tool_use in tool_uses:
tool_name = tool_use.name
tool_input = tool_use.input
print(f" Tool: {tool_name}")
print(f" Input: {tool_input}")
# Exécuter l'outil
if tool_name == "get_weather":
result = get_current_weather(**tool_input)
elif tool_name == "search_web":
result = search_web(**tool_input)
else:
result = {"error": "Unknown tool"}
print(f" Result: {result}\n")
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)
})
# Ajouter les résultats
messages.append({
"role": "user",
"content": tool_results
})
# Deuxième appel pour la réponse finale
print("💬 Getting final response from Claude...\n")
final_response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=claude_tools,
messages=messages
)
# Extraire le texte de la réponse
final_text = next(
(block.text for block in final_response.content if hasattr(block, 'text')),
str(final_response.content)
)
print(f"✅ Claude's Answer:\n{final_text}\n")
return final_text
else:
# Réponse directe
final_text = next(
(block.text for block in response.content if hasattr(block, 'text')),
str(response.content)
)
print(f"💬 Direct answer:\n{final_text}\n")
return final_text
# TEST
result = run_claude_with_tools(
"What's the weather in Tokyo in Fahrenheit? Also search for recent tech news"
)
6. Tool Router Intelligent
Pour gérer de nombreux outils (20+), créez un routeur qui sélectionne intelligemment les outils pertinents :
from typing import List, Dict, Callable
import openai
class ToolRouter:
"""Routeur intelligent d'outils avec catégorisation"""
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.model = model
self.tools = {}
self.tool_schemas = []
self.categories = {}
def register_tool(
self,
func: Callable,
description: str,
parameters: Dict,
category: str = "general"
):
"""Enregistrer un outil dans une catégorie"""
tool_name = func.__name__
self.tools[tool_name] = func
schema = {
"type": "function",
"function": {
"name": tool_name,
"description": description,
"parameters": parameters
}
}
self.tool_schemas.append(schema)
# Catégoriser
if category not in self.categories:
self.categories[category] = []
self.categories[category].append(tool_name)
def select_relevant_tools(self, query: str) -> List[Dict]:
"""Sélectionner les outils pertinents pour la requête"""
if len(self.tool_schemas) <= 5:
# Si peu d'outils, tous sont pertinents
return self.tool_schemas
categories_desc = "\n".join([
f"- {cat}: {', '.join(tools)}"
for cat, tools in self.categories.items()
])
prompt = f"""Given this user query, which tool categories are most relevant?
Query: {query}
Available categories:
{categories_desc}
Respond with ONLY the category names that are relevant (comma-separated):"""
response = openai.ChatCompletion.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=100
)
selected_cats = response.choices[0].message.content.strip().split(',')
selected_cats = [cat.strip() for cat in selected_cats]
# Filtrer les outils
relevant_tools = []
for schema in self.tool_schemas:
tool_name = schema["function"]["name"]
for cat in selected_cats:
if tool_name in self.categories.get(cat, []):
relevant_tools.append(schema)
break
return relevant_tools if relevant_tools else self.tool_schemas[:5]
def run(self, user_message: str, use_routing: bool = True):
"""Exécuter l'agent avec routing optionnel"""
# Sélectionner les outils pertinents
if use_routing and len(self.tool_schemas) > 5:
tools = self.select_relevant_tools(user_message)
print(f"🔀 Router selected {len(tools)}/{len(self.tool_schemas)} tools\n")
else:
tools = self.tool_schemas
# Appeler le LLM avec les outils sélectionnés
messages = [{"role": "user", "content": user_message}]
response = openai.ChatCompletion.create(
model=self.model,
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
if tool_calls:
messages.append(response_message)
for tool_call in tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f"🔧 Calling: {function_name}({function_args})")
# Exécuter l'outil
if function_name in self.tools:
function_response = self.tools[function_name](**function_args)
else:
function_response = {"error": f"Tool {function_name} not found"}
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": json.dumps(function_response)
})
# Réponse finale
final_response = openai.ChatCompletion.create(
model=self.model,
messages=messages
)
return final_response.choices[0].message.content
return response_message.content
# EXEMPLE D'UTILISATION
router = ToolRouter()
# Catégorie: Data
def query_database(sql: str) -> dict:
return {"rows": 42, "query": sql}
router.register_tool(
func=query_database,
description="Execute SQL query on database",
parameters={
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL query"}
},
"required": ["sql"]
},
category="data"
)
# Catégorie: Web
def scrape_website(url: str) -> dict:
return {"content": f"Content from {url}", "status": 200}
router.register_tool(
func=scrape_website,
description="Scrape content from a website",
parameters={
"type": "object",
"properties": {
"url": {"type": "string", "format": "uri"}
},
"required": ["url"]
},
category="web"
)
# Catégorie: Communication
def send_email(to: str, subject: str, body: str) -> dict:
return {"status": "sent", "to": to, "timestamp": "2024-01-01T12:00:00Z"}
router.register_tool(
func=send_email,
description="Send an email",
parameters={
"type": "object",
"properties": {
"to": {"type": "string", "format": "email"},
"subject": {"type": "string"},
"body": {"type": "string"}
},
"required": ["to", "subject", "body"]
},
category="communication"
)
# TEST avec routing
result = router.run("Query the database for users created last month", use_routing=True)
print(f"\n✅ Result: {result}")
- Performance : Moins d'outils dans le prompt = réponse plus rapide
- Coût : Moins de tokens = moins cher
- Précision : Le LLM choisit mieux parmi 5 outils que 50
- Limite de context : Certains LLMs ont des limites sur la taille des tools
7. Error Handling & Retry Logic
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
class RobustToolAgent:
"""Agent avec gestion d'erreurs robuste"""
def __init__(self):
self.tools = {
"web_search": self.web_search,
"calculator": self.calculator,
"database_query": self.database_query
}
self.fallbacks = {
"web_search": "duckduckgo_search",
"calculator": "python_eval"
}
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((ConnectionError, TimeoutError))
)
def web_search(self, query: str) -> dict:
"""Recherche web avec retry automatique"""
# Simuler un appel qui peut échouer
import random
if random.random() < 0.3: # 30% chance d'échec
raise ConnectionError("API temporarily unavailable")
return {"results": f"Results for {query}"}
def calculator(self, expression: str) -> dict:
"""Calculatrice avec validation"""
try:
# Valider que l'expression est sûre
allowed_chars = set("0123456789+-*/(). ")
if not all(c in allowed_chars for c in expression):
raise ValueError("Invalid characters in expression")
result = eval(expression)
return {"result": result, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
def database_query(self, sql: str) -> dict:
"""Query avec validation SQL basique"""
# Vérifier que c'est un SELECT (pas de modification)
sql_upper = sql.strip().upper()
if not sql_upper.startswith("SELECT"):
return {"error": "Only SELECT queries allowed", "success": False}
# Bloquer les mots-clés dangereux
dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]
if any(word in sql_upper for word in dangerous):
return {"error": "Dangerous SQL detected", "success": False}
return {"rows": 42, "success": True}
def call_tool_with_fallback(self, tool_name: str, **kwargs) -> dict:
"""Appeler un outil avec fallback si échec"""
print(f"🔧 Calling {tool_name}...")
try:
# Essayer l'outil principal
result = self.tools[tool_name](**kwargs)
if isinstance(result, dict) and not result.get("success", True):
raise Exception(f"Tool returned error: {result.get('error')}")
print(f" ✓ Success")
return result
except Exception as e:
print(f" ✗ Error: {str(e)}")
# Essayer le fallback si disponible
if tool_name in self.fallbacks:
fallback_name = self.fallbacks[tool_name]
print(f" 🔄 Trying fallback: {fallback_name}...")
try:
fallback_result = self.tools.get(fallback_name, lambda **k: {"error": "No fallback"})(** kwargs)
print(f" ✓ Fallback succeeded")
return fallback_result
except Exception as fallback_error:
print(f" ✗ Fallback also failed: {fallback_error}")
# Si tout échoue, retourner l'erreur
return {
"error": str(e),
"tool": tool_name,
"success": False
}
# TEST
agent = RobustToolAgent()
# Test avec potentiel d'échec et retry
for i in range(3):
print(f"\nAttempt {i+1}:")
result = agent.call_tool_with_fallback("web_search", query="AI agents")
print(f"Result: {result}\n")
8. Comparaison: OpenAI vs Anthropic vs Open Source
| Provider | API Name | Parallel Calls | Validation | Streaming | Notes |
|---|---|---|---|---|---|
| OpenAI | Function Calling | ✅ Oui | ✅ Strict JSON Schema | ✅ Oui | Le plus mature, documentation excellente |
| Anthropic | Tool Use | ✅ Oui | ✅ JSON Schema | ✅ Oui | API légèrement différente mais puissante |
| Mistral | Function Calling | ✅ Oui | ✅ JSON Schema | ✅ Oui | Compatible OpenAI API |
| Llama 3.1+ | Tool Use (native) | ✅ Oui | ⚠️ Moins strict | ✅ Oui | Nécessite prompting spécifique |
| Older Models | Prompt-based | ❌ Non | ❌ Manuel | N/A | Parsing manuel du texte libre |
En production, toujours implémenter : validation stricte des inputs, retry logic avec backoff exponentiel, timeouts, rate limiting, et logging complet de tous les appels d'outils. Un outil qui échoue ne doit JAMAIS faire crasher tout l'agent.
Mémoire pour Agents
- Implémenter la mémoire court terme (contexte conversationnel)
- Créer une mémoire long terme avec vector store
- Comprendre la mémoire épisodique vs sémantique
- Optimiser la gestion de la fenêtre de contexte
1. Types de Mémoire pour Agents
Un agent intelligent a besoin de plusieurs types de mémoire pour fonctionner efficacement et maintenir un contexte cohérent sur de longues périodes.
┌────────────────────────────────────────────────────────────────┐
│ SYSTÈME DE MÉMOIRE AGENT │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. MÉMOIRE DE TRAVAIL (Working Memory) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Contexte actuel, variables temporaires │ │
│ │ • Durée: Session courante uniquement │ │
│ │ • Taille: Limitée par context window du LLM │ │
│ │ • Exemple: "L'utilisateur vient de demander X" │ │
│ │ • Implémentation: Messages dans le prompt │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 2. MÉMOIRE ÉPISODIQUE (Episodic Memory) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Historique des interactions et expériences │ │
│ │ • Durée: Persistante (jours, semaines, mois) │ │
│ │ • Taille: Illimitée (stockage externe) │ │
│ │ • Exemple: "Le 15/02, l'utilisateur a créé X" │ │
│ │ • Implémentation: Vector DB, recherche sémantique │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 3. MÉMOIRE SÉMANTIQUE (Semantic Memory) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Connaissances factuelles, concepts appris │ │
│ │ • Durée: Permanente │ │
│ │ • Taille: Structurée, optimisée │ │
│ │ • Exemple: "API key format: sk-...", "User prefs" │ │
│ │ • Implémentation: DB relationnelle + Vector DB │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 4. MÉMOIRE PROCÉDURALE (Procedural Memory) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Comment faire des tâches (workflows, procédures) │ │
│ │ • Durée: Permanente │ │
│ │ • Exemple: "Pour deploy: build → test → push" │ │
│ │ • Implémentation: Templates, knowledge base │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
2. Implémentation de la Mémoire Court Terme
from typing import List, Dict
from collections import deque
import tiktoken
class ShortTermMemory:
"""Mémoire court terme avec fenêtre glissante et gestion des tokens"""
def __init__(self, max_messages: int = 20, max_tokens: int = 4000, model: str = "gpt-4"):
self.messages = deque(maxlen=max_messages)
self.max_tokens = max_tokens
self.model = model
self.encoding = tiktoken.encoding_for_model(model)
self.system_message = None
def set_system_message(self, content: str):
"""Définir le message système (toujours gardé)"""
self.system_message = {"role": "system", "content": content}
def add_message(self, role: str, content: str):
"""Ajouter un message à la mémoire"""
message = {"role": role, "content": content}
self.messages.append(message)
self._trim_to_token_limit()
def count_tokens(self, text: str) -> int:
"""Compter précisément le nombre de tokens"""
return len(self.encoding.encode(text))
def count_messages_tokens(self, messages: List[Dict]) -> int:
"""Compter les tokens d'une liste de messages"""
total = 0
for message in messages:
# ~4 tokens par message (overhead)
total += 4
total += self.count_tokens(message.get("content", ""))
if "name" in message:
total += self.count_tokens(message["name"])
total += 2 # Completion prompt
return total
def _trim_to_token_limit(self):
"""Réduire les messages pour respecter la limite de tokens"""
while len(self.messages) > 1:
messages = self.get_messages()
total_tokens = self.count_messages_tokens(messages)
if total_tokens <= self.max_tokens:
break
# Supprimer le message le plus ancien (sauf system)
# On garde toujours le premier message user et le dernier assistant
if len(self.messages) > 3:
# Supprimer l'élément à l'index 0 (le plus vieux)
removed = self.messages.popleft()
print(f"⚠️ Trimmed message ({self.count_tokens(removed['content'])} tokens): {removed['content'][:50]}...")
else:
break
def get_messages(self) -> List[Dict]:
"""Récupérer tous les messages avec le system message"""
if self.system_message:
return [self.system_message] + list(self.messages)
return list(self.messages)
def clear(self):
"""Vider la mémoire (garder le system message)"""
self.messages.clear()
def summarize_and_compress(self, llm_client) -> str:
"""Résumer les vieux messages pour gagner de l'espace"""
if len(self.messages) < 8:
return None
# Prendre les messages du milieu pour résumer (garder début et fin)
to_summarize = list(self.messages)[2:-2]
if not to_summarize:
return None
conversation = "\n".join([
f"{msg['role']}: {msg['content']}"
for msg in to_summarize
])
prompt = f"""Summarize this conversation very concisely (2-3 sentences max).
Focus on key facts, decisions, and context that would be useful later.
Conversation:
{conversation}
Concise summary:"""
# Appeler le LLM pour résumer
response = llm_client.create_completion(prompt)
summary = response.strip()
print(f"📝 Compressed {len(to_summarize)} messages into summary:")
print(f" {summary}\n")
# Remplacer les messages résumés par le résumé
# Garder les 2 premiers et 2 derniers
new_messages = deque(list(self.messages)[:2], maxlen=self.messages.maxlen)
new_messages.append({"role": "system", "content": f"[Previous conversation summary: {summary}]"})
new_messages.extend(list(self.messages)[-2:])
self.messages = new_messages
return summary
def get_stats(self) -> Dict:
"""Statistiques sur la mémoire"""
messages = self.get_messages()
return {
"num_messages": len(self.messages),
"total_tokens": self.count_messages_tokens(messages),
"max_tokens": self.max_tokens,
"utilization": f"{(self.count_messages_tokens(messages) / self.max_tokens * 100):.1f}%"
}
# EXEMPLE D'UTILISATION
if __name__ == "__main__":
memory = ShortTermMemory(max_messages=10, max_tokens=1000)
memory.set_system_message("You are a helpful assistant.")
# Simuler une conversation
memory.add_message("user", "Hello! My name is Alice.")
memory.add_message("assistant", "Hello Alice! Nice to meet you.")
memory.add_message("user", "I'm working on a Python project about AI agents.")
memory.add_message("assistant", "That sounds interesting! What specific aspects are you working on?")
# Afficher les statistiques
stats = memory.get_stats()
print(f"Memory stats: {stats}")
# Récupérer les messages
messages = memory.get_messages()
print(f"\nCurrent messages: {len(messages)}")
for msg in messages:
print(f" {msg['role']}: {msg['content'][:50]}...")
- Fenêtre glissante : Garder les N derniers messages (simple mais perte de contexte)
- Résumé progressif : Résumer les vieux messages périodiquement
- Mémoire sélective : Garder les messages "importants" (avec scoring)
- Compression : Utiliser un modèle plus petit pour compresser le contexte
3. Mémoire Long Terme avec Vector Store
import chromadb
from chromadb.config import Settings
from datetime import datetime
import uuid
import hashlib
class LongTermMemory:
"""Mémoire long terme avec recherche sémantique"""
def __init__(self, collection_name: str = "agent_memory", persist_directory: str = "./chroma_db"):
self.client = chromadb.PersistentClient(
path=persist_directory,
settings=Settings(anonymized_telemetry=False)
)
self.collection = self.client.get_or_create_collection(
name=collection_name,
metadata={"hnsw:space": "cosine"}
)
def store_interaction(
self,
user_message: str,
agent_response: str,
metadata: Dict = None
) -> str:
"""Sauvegarder une interaction complète"""
interaction_id = str(uuid.uuid4())
timestamp = datetime.now().isoformat()
# Construire le document (combinaison user + agent)
document = f"User: {user_message}\nAgent: {agent_response}"
meta = {
"type": "interaction",
"timestamp": timestamp,
"user_message": user_message,
"agent_response": agent_response,
**(metadata or {})
}
self.collection.add(
documents=[document],
metadatas=[meta],
ids=[interaction_id]
)
return interaction_id
def store_fact(self, fact: str, category: str = "general", source: str = None):
"""Sauvegarder un fait/connaissance"""
# Créer un ID basé sur le hash du contenu (éviter les doublons)
fact_hash = hashlib.md5(fact.encode()).hexdigest()
fact_id = f"fact_{fact_hash}"
# Vérifier si existe déjà
existing = self.collection.get(ids=[fact_id])
if existing['ids']:
print(f"ℹ️ Fact already exists, updating...")
self.collection.update(
ids=[fact_id],
documents=[fact],
metadatas=[{
"type": "fact",
"category": category,
"source": source,
"updated_at": datetime.now().isoformat()
}]
)
else:
self.collection.add(
documents=[fact],
metadatas={
"type": "fact",
"category": category,
"source": source,
"timestamp": datetime.now().isoformat()
},
ids=[fact_id]
)
return fact_id
def store_user_preference(self, key: str, value: str):
"""Sauvegarder une préférence utilisateur"""
pref_id = f"pref_{key}"
self.collection.upsert(
documents=[f"{key}: {value}"],
metadatas=[{
"type": "preference",
"key": key,
"value": value,
"timestamp": datetime.now().isoformat()
}],
ids=[pref_id]
)
def recall(self, query: str, n_results: int = 3, filter_type: str = None) -> List[Dict]:
"""Rechercher dans la mémoire avec filtrage optionnel"""
where_filter = {"type": filter_type} if filter_type else None
results = self.collection.query(
query_texts=[query],
n_results=n_results,
where=where_filter
)
memories = []
if results['documents'][0]:
for i in range(len(results['documents'][0])):
memories.append({
"id": results['ids'][0][i],
"content": results['documents'][0][i],
"metadata": results['metadatas'][0][i],
"distance": results['distances'][0][i],
"relevance": 1 - results['distances'][0][i] # Cosine similarity
})
return memories
def get_recent_memories(self, n: int = 5, memory_type: str = None) -> List[Dict]:
"""Récupérer les N mémoires les plus récentes"""
# ChromaDB ne supporte pas le tri natif, on doit tout récupérer et trier
where_filter = {"type": memory_type} if memory_type else None
all_memories = self.collection.get(
where=where_filter,
limit=100 # Limiter pour la performance
)
# Trier par timestamp
memories_with_time = []
for i in range(len(all_memories['ids'])):
meta = all_memories['metadatas'][i]
timestamp = meta.get('timestamp', meta.get('updated_at', '1970-01-01'))
memories_with_time.append({
"id": all_memories['ids'][i],
"content": all_memories['documents'][i],
"metadata": meta,
"timestamp": timestamp
})
sorted_memories = sorted(
memories_with_time,
key=lambda x: x['timestamp'],
reverse=True
)
return sorted_memories[:n]
def forget(self, memory_id: str):
"""Supprimer une mémoire"""
self.collection.delete(ids=[memory_id])
def get_stats(self) -> Dict:
"""Statistiques sur la mémoire"""
count = self.collection.count()
# Compter par type
types_count = {}
all_data = self.collection.get()
for meta in all_data['metadatas']:
mem_type = meta.get('type', 'unknown')
types_count[mem_type] = types_count.get(mem_type, 0) + 1
return {
"total_memories": count,
"by_type": types_count
}
# Agent avec mémoire complète
class MemoryAgent:
"""Agent avec système de mémoire court et long terme"""
def __init__(self, model: str = "gpt-4"):
self.model = model
self.short_term = ShortTermMemory(max_messages=10, max_tokens=4000)
self.long_term = LongTermMemory(collection_name="my_agent_memory")
# System prompt initial
self.short_term.set_system_message(
"You are a helpful assistant with memory. "
"You can remember past conversations and learn from them."
)
def chat(self, user_message: str) -> str:
"""Conversation avec mémoire"""
print(f"\n{'='*70}")
print(f"User: {user_message}")
print(f"{'='*70}\n")
# 1. Rappel de mémoires pertinentes du long terme
print("🔍 Searching long-term memory...")
relevant_memories = self.long_term.recall(user_message, n_results=2)
context = ""
if relevant_memories:
print(f" Found {len(relevant_memories)} relevant memories:\n")
context = "\n\nRelevant past information:\n"
for i, mem in enumerate(relevant_memories, 1):
relevance = mem['relevance']
content_preview = mem['content'][:100]
print(f" {i}. (relevance: {relevance:.2f}) {content_preview}...")
context += f"- {mem['content']}\n"
# 2. Ajouter le contexte au prompt si pertinent
if context and relevant_memories[0]['relevance'] > 0.7:
enhanced_system = self.short_term.system_message['content'] + context
self.short_term.set_system_message(enhanced_system)
# 3. Ajouter à la mémoire court terme
self.short_term.add_message("user", user_message)
# 4. Appeler le LLM
print("\n💬 Generating response...")
messages = self.short_term.get_messages()
response = openai.ChatCompletion.create(
model=self.model,
messages=messages,
temperature=0.7
)
agent_response = response.choices[0].message.content
# 5. Mettre à jour les mémoires
self.short_term.add_message("assistant", agent_response)
self.long_term.store_interaction(user_message, agent_response)
# 6. Afficher les stats
print(f"\n📊 Memory stats:")
print(f" Short-term: {self.short_term.get_stats()}")
print(f" Long-term: {self.long_term.get_stats()}")
print(f"\n✅ Agent: {agent_response}\n")
return agent_response
def learn_fact(self, fact: str, category: str = "general"):
"""Apprendre un nouveau fait"""
fact_id = self.long_term.store_fact(fact, category)
print(f"✅ Learned fact (ID: {fact_id}): {fact}")
def set_preference(self, key: str, value: str):
"""Définir une préférence utilisateur"""
self.long_term.store_user_preference(key, value)
print(f"✅ Preference set: {key} = {value}")
def recall_memories(self, query: str, n: int = 5):
"""Rechercher dans les mémoires"""
print(f"\n🔍 Searching memories for: '{query}'")
memories = self.long_term.recall(query, n_results=n)
for i, mem in enumerate(memories, 1):
print(f"\n{i}. Relevance: {mem['relevance']:.2f}")
print(f" {mem['content'][:200]}...")
print(f" Type: {mem['metadata'].get('type')}")
print(f" Date: {mem['metadata'].get('timestamp', 'N/A')[:10]}")
# TEST
if __name__ == "__main__":
agent = MemoryAgent()
# Conversation 1
agent.chat("My name is Alice and I love Python programming")
# Apprendre des faits
agent.learn_fact("Python was created by Guido van Rossum in 1991", category="tech")
agent.learn_fact("Alice prefers dark mode in her IDE", category="preferences")
# Conversation 2 (plus tard)
agent.chat("What's my name and what do I like?")
# Conversation 3 (test de rappel)
agent.chat("Tell me about Python's creator")
# Rechercher dans les mémoires
agent.recall_memories("Alice preferences", n=3)
4. Stratégies de Gestion de Mémoire
| Stratégie | Description | Avantages | Inconvénients | Cas d'usage |
|---|---|---|---|---|
| Fenêtre glissante | Garder les N derniers messages | Simple, prévisible, rapide | Perte du contexte ancien | Conversations courtes |
| Résumé progressif | Résumer l'historique périodiquement | Préserve l'essence, flexible | Coût LLM, perte détails | Sessions longues |
| Mémoire externe | Vector DB, rappel sémantique | Capacité illimitée, pertinence | Complexité, latence | Multi-sessions |
| Mémoire hiérarchique | Niveaux: court/moyen/long terme | Optimal qualité/coût | Complexe à implémenter | Production avancée |
| Mémoire sélective | Scorer et garder l'important | Maximise pertinence | Coût de scoring | Conversations critiques |
Pour un agent production, utilisez une approche hybride : fenêtre glissante (10-20 messages) pour le contexte immédiat + vector store (ChromaDB/Pinecone) pour la mémoire long terme + base relationnelle (PostgreSQL) pour les données structurées (préférences, paramètres). N'oubliez pas d'implémenter un mécanisme de "oubli" pour respecter le RGPD.
LangChain Agents
- Maîtriser l'AgentExecutor de LangChain
- Créer et utiliser des outils LangChain
- Implémenter des output parsers personnalisés
- Utiliser les callbacks pour le monitoring et streaming
1. Introduction à LangChain Agents
LangChain est le framework le plus populaire pour construire des applications avec des LLMs. Les Agents LangChain implémentent les patterns agentiques (principalement ReAct) de manière standardisée et facile à utiliser.
┌────────────────────────────────────────────────────────────────┐
│ ARCHITECTURE LANGCHAIN AGENT │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ AgentExecutor │ │
│ │ (Orchestre) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Agent │ │ Tools │ │ Memory │ │
│ │ (ReAct) │ │ (Outils)│ │(Optional)│ │
│ └────┬────┘ └────┬────┘ └──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ LLM │ │ Tool │ │
│ │(GPT-4, │ │ Execute │ │
│ │Claude...)│ │ │ │
│ └─────────┘ └─────────┘ │
│ │
│ Flow: User Input → Agent → LLM → Tool Selection → │
│ Tool Execute → Result → LLM → Final Answer │
│ │
└────────────────────────────────────────────────────────────────┘
2. Installation et Setup
# Installer LangChain et dépendances
pip install langchain langchain-openai langchain-community langchain-core
# Outils additionnels
pip install langchainhub # Pour accéder aux prompts partagés
pip install tavily-python # Pour la recherche web
pip install duckduckgo-search # Alternative gratuite
# Imports essentiels
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import Tool, StructuredTool
from langchain.prompts import PromptTemplate
from langchain import hub
from langchain.callbacks import StdOutCallbackHandler
import os
# Configuration
os.environ["OPENAI_API_KEY"] = "your-api-key"
os.environ["TAVILY_API_KEY"] = "your-tavily-key" # Pour web search
3. Créer des Outils LangChain
3.1 Outils Simples avec Tool
from langchain.tools import Tool
from langchain.utilities import DuckDuckGoSearchAPIWrapper
import requests
# Méthode 1: Tool simple avec fonction
def calculate(expression: str) -> str:
"""Calculate a mathematical expression."""
try:
result = eval(expression)
return f"The result is: {result}"
except Exception as e:
return f"Error: {str(e)}"
calculator_tool = Tool(
name="Calculator",
func=calculate,
description="Useful for when you need to calculate mathematical expressions. Input should be a valid Python expression like '2 + 2' or '10 * 3.5'"
)
# Méthode 2: Tool avec classe wrapper
search = DuckDuckGoSearchAPIWrapper()
search_tool = Tool(
name="Web_Search",
func=search.run,
description="Useful for when you need to search the web for current information. Input should be a search query string."
)
# Méthode 3: Tool personnalisé
def get_weather(location: str) -> str:
"""Get current weather for a location."""
# En production: utiliser une vraie API météo
mock_data = {
"Paris": "15°C, partly cloudy",
"London": "12°C, rainy",
"Tokyo": "20°C, sunny"
}
return mock_data.get(location, f"Weather data not available for {location}")
weather_tool = Tool(
name="Weather",
func=get_weather,
description="Get the current weather for a specific city. Input should be a city name like 'Paris' or 'Tokyo'."
)
# Liste des outils
tools = [calculator_tool, search_tool, weather_tool]
3.2 Outils Structurés avec StructuredTool
from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import Optional
# Définir le schéma d'input avec Pydantic
class SearchInput(BaseModel):
query: str = Field(description="The search query")
num_results: int = Field(default=5, description="Number of results to return", ge=1, le=10)
def advanced_search(query: str, num_results: int = 5) -> str:
"""Perform an advanced web search."""
# Simuler une recherche
results = [f"Result {i+1} for '{query}'" for i in range(num_results)]
return "\n".join(results)
# Créer l'outil structuré
advanced_search_tool = StructuredTool.from_function(
func=advanced_search,
name="Advanced_Search",
description="Perform an advanced web search with configurable number of results",
args_schema=SearchInput
)
# Autre exemple: Outil avec plusieurs paramètres
class EmailInput(BaseModel):
to: str = Field(description="Recipient email address")
subject: str = Field(description="Email subject")
body: str = Field(description="Email body content")
priority: Optional[str] = Field(default="normal", description="Email priority: low, normal, high")
def send_email(to: str, subject: str, body: str, priority: str = "normal") -> str:
"""Send an email."""
return f"Email sent to {to} with subject '{subject}' (priority: {priority})"
email_tool = StructuredTool.from_function(
func=send_email,
name="Send_Email",
description="Send an email to a recipient",
args_schema=EmailInput
)
4. Créer un Agent avec AgentExecutor
from langchain.agents import create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub
# 1. Initialiser le LLM
llm = ChatOpenAI(
model="gpt-4-turbo-preview",
temperature=0,
streaming=True # Pour voir la réponse en temps réel
)
# 2. Récupérer un prompt ReAct depuis LangChain Hub
# Ou créer un prompt personnalisé
prompt = hub.pull("hwchase17/react")
# Prompt personnalisé (optionnel)
from langchain.prompts import PromptTemplate
custom_prompt = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought: {agent_scratchpad}""")
# 3. Créer l'agent ReAct
agent = create_react_agent(
llm=llm,
tools=tools,
prompt=prompt # ou custom_prompt
)
# 4. Créer l'AgentExecutor
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # Afficher les étapes intermédiaires
handle_parsing_errors=True, # Gérer les erreurs de parsing
max_iterations=10, # Limite de sécurité
max_execution_time=60, # Timeout en secondes
return_intermediate_steps=True # Retourner les étapes pour debugging
)
# 5. Utiliser l'agent
if __name__ == "__main__":
# Test 1: Calcul simple
result1 = agent_executor.invoke({
"input": "What is 15% of 250, then multiply by 3?"
})
print(f"\n{'='*70}")
print("Final Answer:", result1["output"])
print(f"{'='*70}\n")
# Test 2: Recherche web
result2 = agent_executor.invoke({
"input": "Search for recent news about Claude AI and summarize the top 3 results"
})
print(f"\n{'='*70}")
print("Final Answer:", result2["output"])
print(f"{'='*70}\n")
# Test 3: Multi-outils
result3 = agent_executor.invoke({
"input": "What's the weather in Paris? Also calculate how many days until Christmas 2024"
})
print(f"\n{'='*70}")
print("Final Answer:", result3["output"])
# Accéder aux étapes intermédiaires
if result3.get("intermediate_steps"):
print("\nIntermediate Steps:")
for i, (action, observation) in enumerate(result3["intermediate_steps"], 1):
print(f"\nStep {i}:")
print(f" Action: {action.tool} - {action.tool_input}")
print(f" Observation: {observation[:100]}...")
- Batteries incluses : Patterns agentiques pré-implémentés
- Écosystème riche : 100+ intégrations d'outils et services
- Abstractions propres : Code réutilisable et maintenable
- Production-ready : Error handling, retries, callbacks intégrés
5. Callbacks pour Monitoring et Streaming
from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import AgentAction, AgentFinish, LLMResult
from typing import Any, Dict, List
import time
class CustomCallbackHandler(BaseCallbackHandler):
"""Callback personnalisé pour tracking détaillé"""
def __init__(self):
self.start_time = None
self.llm_calls = 0
self.tool_calls = 0
self.total_tokens = 0
def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any) -> None:
"""Appelé quand le LLM démarre"""
self.llm_calls += 1
print(f"\n🤖 LLM Call #{self.llm_calls}")
print(f" Prompt length: {len(prompts[0])} chars")
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
"""Appelé quand le LLM termine"""
if response.llm_output:
tokens = response.llm_output.get("token_usage", {})
self.total_tokens += tokens.get("total_tokens", 0)
print(f" Tokens used: {tokens.get('total_tokens', 0)}")
def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
"""Appelé en cas d'erreur LLM"""
print(f" ❌ LLM Error: {str(error)}")
def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> None:
"""Appelé quand un outil démarre"""
self.tool_calls += 1
tool_name = serialized.get("name", "Unknown")
print(f"\n🔧 Tool Call #{self.tool_calls}: {tool_name}")
print(f" Input: {input_str[:100]}...")
def on_tool_end(self, output: str, **kwargs: Any) -> None:
"""Appelé quand un outil termine"""
print(f" Output: {output[:100]}...")
def on_tool_error(self, error: Exception, **kwargs: Any) -> None:
"""Appelé en cas d'erreur d'outil"""
print(f" ❌ Tool Error: {str(error)}")
def on_agent_action(self, action: AgentAction, **kwargs: Any) -> None:
"""Appelé quand l'agent décide d'une action"""
print(f"\n💭 Agent Thought:")
print(f" Tool: {action.tool}")
print(f" Input: {action.tool_input}")
if action.log:
print(f" Reasoning: {action.log[:150]}...")
def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> None:
"""Appelé quand l'agent termine"""
print(f"\n✅ Agent Finished")
print(f" Final Answer: {finish.return_values.get('output', '')[:150]}...")
def get_stats(self) -> Dict:
"""Retourner les statistiques"""
return {
"llm_calls": self.llm_calls,
"tool_calls": self.tool_calls,
"total_tokens": self.total_tokens
}
# Callback pour streaming en temps réel
class StreamingCallbackHandler(BaseCallbackHandler):
"""Callback pour afficher la réponse token par token"""
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
"""Appelé pour chaque nouveau token généré"""
print(token, end="", flush=True)
# Utiliser les callbacks
custom_callback = CustomCallbackHandler()
streaming_callback = StreamingCallbackHandler()
agent_executor_with_callbacks = AgentExecutor(
agent=agent,
tools=tools,
verbose=False, # Désactiver le verbose par défaut
callbacks=[custom_callback, streaming_callback],
return_intermediate_steps=True
)
# Exécuter
print("\n" + "="*70)
print("Running agent with custom callbacks...")
print("="*70)
result = agent_executor_with_callbacks.invoke({
"input": "What's 25% of 80, then search for info about Python"
})
# Afficher les stats
print("\n\n" + "="*70)
print("📊 Execution Statistics:")
print("="*70)
stats = custom_callback.get_stats()
for key, value in stats.items():
print(f" {key}: {value}")
print("="*70)
6. Memory Integration
from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory
from langchain.agents import initialize_agent, AgentType
# Memory type 1: Buffer (garde tout)
buffer_memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# Memory type 2: Summary (résume automatiquement)
summary_memory = ConversationSummaryMemory(
llm=llm,
memory_key="chat_history",
return_messages=True
)
# Créer un agent avec mémoire
agent_with_memory = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
memory=buffer_memory,
verbose=True,
handle_parsing_errors=True
)
# Conversation avec mémoire
print("\nConversation 1:")
result1 = agent_with_memory.invoke({
"input": "My name is Alice and I live in Paris"
})
print(f"Agent: {result1['output']}\n")
print("Conversation 2:")
result2 = agent_with_memory.invoke({
"input": "What's the weather in my city?"
})
print(f"Agent: {result2['output']}\n")
print("Conversation 3:")
result3 = agent_with_memory.invoke({
"input": "What's my name?"
})
print(f"Agent: {result3['output']}\n")
# L'agent se souvient que Alice vit à Paris!
7. Agents Spécialisés de LangChain
| Type d'Agent | Description | Cas d'usage |
|---|---|---|
| ZERO_SHOT_REACT_DESCRIPTION | Agent ReAct standard | Usage général |
| CHAT_CONVERSATIONAL_REACT_DESCRIPTION | ReAct avec mémoire conversationnelle | Chatbots, assistants |
| STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION | ReAct avec outils structurés | Outils complexes multi-paramètres |
| OPENAI_FUNCTIONS | Utilise l'API Function Calling d'OpenAI | Performance optimale avec OpenAI |
| SELF_ASK_WITH_SEARCH | Agent qui se pose des sous-questions | Recherche complexe, reasoning |
| REACT_DOCSTORE | ReAct spécialisé pour chercher dans des documents | Q&A sur documentation |
8. Output Parsers Personnalisés
from langchain.schema import AgentAction, AgentFinish
from langchain.agents.output_parsers import ReActSingleInputOutputParser
import re
class CustomReActParser(ReActSingleInputOutputParser):
"""Parser personnalisé pour format ReAct modifié"""
def parse(self, text: str):
"""Parser le texte de sortie du LLM"""
# Chercher "Final Answer:"
if "Final Answer:" in text:
return AgentFinish(
return_values={"output": text.split("Final Answer:")[-1].strip()},
log=text
)
# Parser Action et Action Input (format personnalisé)
# Supporter plusieurs formats pour plus de robustesse
# Format 1: Action: XXX\nAction Input: YYY
action_match = re.search(r"Action:\s*(.+?)(?:\n|$)", text, re.IGNORECASE)
input_match = re.search(r"Action Input:\s*(.+?)(?:\n|$)", text, re.IGNORECASE | re.DOTALL)
if action_match and input_match:
action = action_match.group(1).strip()
action_input = input_match.group(1).strip()
return AgentAction(
tool=action,
tool_input=action_input,
log=text
)
# Format 2: [ACTION: XXX](INPUT: YYY)
alt_match = re.search(r"\[ACTION:\s*(.+?)\]\(INPUT:\s*(.+?)\)", text, re.DOTALL)
if alt_match:
return AgentAction(
tool=alt_match.group(1).strip(),
tool_input=alt_match.group(2).strip(),
log=text
)
# Si aucun format reconnu, erreur
raise ValueError(f"Could not parse LLM output: {text}")
# Utiliser le parser personnalisé
from langchain.agents import create_react_agent
custom_parser = CustomReActParser()
agent_with_custom_parser = create_react_agent(
llm=llm,
tools=tools,
prompt=custom_prompt,
output_parser=custom_parser
)
executor = AgentExecutor(
agent=agent_with_custom_parser,
tools=tools,
verbose=True
)
LangChain est excellent pour prototyper rapidement, mais pour la production critique, considérez implémenter votre propre logique d'agent. LangChain ajoute des abstractions qui peuvent rendre le debugging difficile. Utilisez les callbacks extensivement pour comprendre ce qui se passe sous le capot.
9. Exemple Complet: Agent de Recherche
from langchain.agents import Tool, AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.callbacks import StdOutCallbackHandler
from langchain.utilities import DuckDuckGoSearchAPIWrapper, WikipediaAPIWrapper
# Outils de recherche
search = DuckDuckGoSearchAPIWrapper()
wikipedia = WikipediaAPIWrapper()
tools = [
Tool(
name="Web_Search",
func=search.run,
description="Search the web for current information. Use this for recent events, news, current data."
),
Tool(
name="Wikipedia",
func=wikipedia.run,
description="Search Wikipedia for factual, historical information. Use this for well-established facts."
),
Tool(
name="Calculator",
func=lambda x: str(eval(x)),
description="Calculate mathematical expressions. Input must be a valid Python expression."
)
]
# LLM
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)
# Prompt optimisé pour la recherche
research_prompt = PromptTemplate.from_template("""You are a research assistant. Your goal is to find accurate, up-to-date information.
Tools available:
{tools}
When researching:
1. Use Web_Search for current events and recent data
2. Use Wikipedia for established facts and historical info
3. Use Calculator for any numerical computations
4. Always verify information from multiple sources when possible
5. Cite your sources in the final answer
Format:
Question: the research question
Thought: think about the best approach
Action: choose a tool from [{tool_names}]
Action Input: input for the tool
Observation: result from the tool
... (repeat as needed)
Thought: I have enough information to answer
Final Answer: comprehensive answer with sources
Question: {input}
{agent_scratchpad}""")
# Créer l'agent
agent = create_react_agent(llm=llm, tools=tools, prompt=research_prompt)
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=7,
return_intermediate_steps=True,
handle_parsing_errors=True
)
# Tester avec une question de recherche complexe
question = """Research and answer: What was the population of Paris in 2020,
and how much has it changed compared to 2010? Also calculate the percentage change."""
result = executor.invoke({"input": question})
print("\n" + "="*70)
print("RESEARCH RESULT")
print("="*70)
print(result["output"])
print("="*70)
LangGraph : Graphes d'Agents
- Maîtriser StateGraph pour créer des workflows d'agents
- Implémenter des nœuds, edges et routage conditionnel
- Utiliser les checkpoints pour la persistence
- Créer des workflows avec cycles et human-in-the-loop
1. Introduction à LangGraph
LangGraph est une extension de LangChain qui permet de créer des workflows d'agents sous forme de graphes. Contrairement aux agents classiques (chaîne linéaire), LangGraph permet des cycles, des branches conditionnelles, et des workflows complexes.
┌────────────────────────────────────────────────────────────────┐
│ LANGGRAPH vs AGENT CLASSIQUE │
├────────────────────────────────────────────────────────────────┤
│ │
│ AGENT CLASSIQUE (Linéaire): │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Input │──►│ LLM │──►│Tool │──►│Output│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ LANGGRAPH (Graphe): │
│ ┌─────────┐ │
│ │ Input │ │
│ └────┬────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Analyze Query │ │
│ └────┬────────┬─────┘ │
│ │ │ │
│ Simple │ │ Complex │
│ │ │ │
│ ┌───────▼──┐ ┌──▼────────┐ │
│ │ Direct │ │ Plan │ │
│ │ Answer │ │ & Execute│ │
│ └────┬─────┘ └───┬───────┘ │
│ │ │ │
│ │ ┌─────▼─────┐ │
│ │ │ Tool Call │ │
│ │ └─────┬─────┘ │
│ │ │ │
│ │ ┌─────▼─────┐ ┌──────────┐ │
│ │ │ Review │──No──►│ Revise │ │
│ │ │ Result │ └────┬─────┘ │
│ │ └─────┬─────┘ │ │
│ │ │ Yes │ │
│ │ │◄─────────────────┘ │
│ └────────┬───┘ │
│ │ │
│ ┌────▼────┐ │
│ │ Output │ │
│ └─────────┘ │
│ │
│ Avantages: Cycles, Conditions, Parallélisme, Checkpoints │
└────────────────────────────────────────────────────────────────┘
2. Installation et Setup
# Installer LangGraph
pip install langgraph langchain langchain-openai
# Imports essentiels
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
import operator
3. Concepts de Base: State, Nodes, Edges
3.1 Définir l'État (State)
from typing import TypedDict, Annotated
import operator
# L'état est une structure de données partagée entre tous les nœuds
class AgentState(TypedDict):
"""État partagé dans le graphe"""
# Messages de la conversation
messages: Annotated[list, operator.add] # operator.add pour append automatique
# Autres champs
current_task: str
completed_tasks: list[str]
iteration: int
max_iterations: int
final_answer: str | None
# État minimal pour commencer
class SimpleState(TypedDict):
messages: Annotated[list, operator.add]
3.2 Créer des Nœuds (Nodes)
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)
# Nœud 1: Appeler le LLM
def call_model(state: AgentState) -> AgentState:
"""Nœud qui appelle le LLM"""
messages = state["messages"]
response = llm.invoke(messages)
# Retourner l'état modifié
return {
"messages": [response], # Sera ajouté à la liste grâce à operator.add
"iteration": state.get("iteration", 0) + 1
}
# Nœud 2: Exécuter un outil
def execute_tool(state: AgentState) -> AgentState:
"""Nœud qui exécute un outil"""
last_message = state["messages"][-1]
# Extraire l'outil à appeler depuis le message
tool_name = "search" # Simplifié
tool_input = "AI agents"
# Simuler l'exécution
tool_result = f"Results for {tool_input}..."
# Créer un message avec le résultat
result_message = HumanMessage(content=f"Tool result: {tool_result}")
return {
"messages": [result_message],
"completed_tasks": state.get("completed_tasks", []) + [tool_name]
}
# Nœud 3: Vérifier si terminé
def should_continue(state: AgentState) -> str:
"""Nœud de décision (routage conditionnel)"""
last_message = state["messages"][-1]
# Si le message contient "FINAL ANSWER", on termine
if "FINAL ANSWER" in last_message.content:
return "end"
# Si trop d'itérations, on termine
if state.get("iteration", 0) >= state.get("max_iterations", 10):
return "end"
# Sinon, on continue
return "continue"
4. Construire un Graphe Simple
from langgraph.graph import StateGraph, END
# 1. Créer le graphe
workflow = StateGraph(AgentState)
# 2. Ajouter les nœuds
workflow.add_node("model", call_model)
workflow.add_node("tool", execute_tool)
# 3. Définir le point d'entrée
workflow.set_entry_point("model")
# 4. Ajouter des edges (transitions)
# Edge simple: model → tool
workflow.add_edge("model", "tool")
# Edge conditionnel: tool → ?
workflow.add_conditional_edges(
"tool", # Depuis ce nœud
should_continue, # Fonction de décision
{
"continue": "model", # Si "continue", retour à model
"end": END # Si "end", terminer
}
)
# 5. Compiler le graphe
app = workflow.compile()
# 6. Utiliser le graphe
initial_state = {
"messages": [HumanMessage(content="Search for information about AI agents")],
"iteration": 0,
"max_iterations": 5,
"completed_tasks": []
}
# Exécuter le graphe
result = app.invoke(initial_state)
print("Final State:")
print(f"Iterations: {result['iteration']}")
print(f"Completed tasks: {result['completed_tasks']}")
print(f"Last message: {result['messages'][-1].content}")
- Flexibilité : Créer des workflows arbitrairement complexes
- Cycles : Permettre des boucles de rétroaction (impossible avec chains)
- Routage conditionnel : Décisions dynamiques sur le prochain nœud
- Checkpoints : Sauvegarder et reprendre l'état à n'importe quel point
- Human-in-the-loop : Interrompre pour intervention humaine
5. Workflow Avancé: Agent ReAct avec LangGraph
from langchain.tools import Tool
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
import json
# Définir les outils
def search_tool(query: str) -> str:
"""Recherche web simulée"""
return f"Search results for: {query}"
def calculator_tool(expression: str) -> str:
"""Calculatrice"""
try:
return str(eval(expression))
except:
return "Error in calculation"
tools = {
"search": search_tool,
"calculator": calculator_tool
}
# État pour agent ReAct
class ReActState(TypedDict):
messages: Annotated[list[BaseMessage], operator.add]
iteration: int
# Nœud: Agent décide de l'action
def agent_node(state: ReActState) -> ReActState:
"""L'agent décide quoi faire"""
messages = state["messages"]
# Ajouter le système prompt si premier appel
if len(messages) == 1:
system_msg = SystemMessage(content=f"""You are a helpful agent with tools.
Available tools:
- search(query): Search the web
- calculator(expression): Calculate math expressions
Use this format:
Thought: [your reasoning]
Action: [tool_name]
Action Input: [input for tool]
Or when done:
Thought: I have the answer
Final Answer: [your answer]""")
messages = [system_msg] + messages
response = llm.invoke(messages)
return {
"messages": [response],
"iteration": state.get("iteration", 0) + 1
}
# Nœud: Exécuter l'outil
def tool_node(state: ReActState) -> ReActState:
"""Exécuter l'outil demandé par l'agent"""
last_message = state["messages"][-1]
# Parser l'action depuis le message
content = last_message.content
import re
action_match = re.search(r'Action:\s*(\w+)', content)
input_match = re.search(r'Action Input:\s*(.+?)(?:\n|$)', content, re.DOTALL)
if action_match and input_match:
tool_name = action_match.group(1).strip()
tool_input = input_match.group(1).strip()
print(f"\n🔧 Executing {tool_name}({tool_input})")
# Exécuter l'outil
if tool_name in tools:
result = tools[tool_name](tool_input)
else:
result = f"Unknown tool: {tool_name}"
print(f"📊 Result: {result}")
# Créer un message avec le résultat
observation_msg = HumanMessage(content=f"Observation: {result}")
return {"messages": [observation_msg]}
# Si pas d'action trouvée, retourner un message d'erreur
return {"messages": [HumanMessage(content="Error: No valid action found")]}
# Fonction de routage
def should_continue_react(state: ReActState) -> str:
"""Décider si on continue ou termine"""
last_message = state["messages"][-1]
# Si c'est la réponse finale
if "Final Answer:" in last_message.content:
return "end"
# Si trop d'itérations
if state.get("iteration", 0) >= 10:
return "end"
# Si c'est un message de l'agent (pas un tool result), exécuter l'outil
if isinstance(last_message, AIMessage):
return "tool"
# Sinon, retour à l'agent
return "agent"
# Construire le graphe ReAct
react_workflow = StateGraph(ReActState)
react_workflow.add_node("agent", agent_node)
react_workflow.add_node("tool", tool_node)
react_workflow.set_entry_point("agent")
react_workflow.add_conditional_edges(
"agent",
should_continue_react,
{
"tool": "tool",
"end": END
}
)
react_workflow.add_edge("tool", "agent") # Toujours retourner à agent après outil
# Compiler
react_app = react_workflow.compile()
# Tester
print("\n" + "="*70)
print("TESTING REACT AGENT WITH LANGGRAPH")
print("="*70)
result = react_app.invoke({
"messages": [HumanMessage(content="Search for AI agents, then calculate 25% of 80")],
"iteration": 0
})
print("\n" + "="*70)
print("FINAL RESULT")
print("="*70)
print(f"Iterations: {result['iteration']}")
print(f"Last message: {result['messages'][-1].content}")
6. Checkpoints et Persistence
Les checkpoints permettent de sauvegarder l'état à chaque étape et de reprendre l'exécution plus tard.
from langgraph.checkpoint.memory import MemorySaver
# Créer un saver pour persister l'état
memory = MemorySaver()
# Compiler avec checkpoints
app_with_checkpoints = react_workflow.compile(checkpointer=memory)
# Configuration avec un thread_id pour identifier la session
config = {"configurable": {"thread_id": "conversation_1"}}
# Première exécution
print("=== First Run ===")
result1 = app_with_checkpoints.invoke(
{
"messages": [HumanMessage(content="Search for Python")],
"iteration": 0
},
config=config
)
print(f"After first run: {result1['iteration']} iterations")
# Deuxième exécution (reprend où la première s'est arrêtée)
print("\n=== Continue from checkpoint ===")
result2 = app_with_checkpoints.invoke(
{
"messages": [HumanMessage(content="Now calculate 10 * 5")]
},
config=config
)
print(f"After second run: {result2['iteration']} iterations (cumulative)")
# Récupérer l'historique complet
history = memory.list(config)
print(f"\nCheckpoint history: {len(list(history))} checkpoints saved")
7. Human-in-the-Loop
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
class HITLState(TypedDict):
messages: Annotated[list[BaseMessage], operator.add]
needs_approval: bool
approved: bool
def process_task(state: HITLState) -> HITLState:
"""Traiter une tâche qui nécessite potentiellement une approbation"""
messages = state["messages"]
last_msg = messages[-1].content
# Simuler un traitement
result = f"Processed: {last_msg}"
# Si tâche sensible, demander approbation
if "delete" in last_msg.lower() or "send" in last_msg.lower():
return {
"messages": [AIMessage(content=result)],
"needs_approval": True,
"approved": False
}
return {
"messages": [AIMessage(content=result)],
"needs_approval": False,
"approved": True
}
def human_approval_node(state: HITLState) -> HITLState:
"""Nœud d'attente d'approbation humaine"""
print("\n⏸️ WAITING FOR HUMAN APPROVAL")
print(f"Task: {state['messages'][-1].content}")
# En production, ceci serait une vraie interruption
# Ici on simule une approbation automatique
approval = input("Approve? (yes/no): ").lower() == "yes"
return {"approved": approval}
def should_wait_for_human(state: HITLState) -> str:
"""Décider si on attend l'humain"""
if state.get("needs_approval", False) and not state.get("approved", False):
return "wait_human"
return "continue"
def finalize(state: HITLState) -> HITLState:
"""Finaliser la tâche"""
if state.get("approved", True):
return {"messages": [AIMessage(content="Task completed successfully")]}
else:
return {"messages": [AIMessage(content="Task cancelled by user")]}
# Construire le graphe avec HITL
hitl_workflow = StateGraph(HITLState)
hitl_workflow.add_node("process", process_task)
hitl_workflow.add_node("human_approval", human_approval_node)
hitl_workflow.add_node("finalize", finalize)
hitl_workflow.set_entry_point("process")
hitl_workflow.add_conditional_edges(
"process",
should_wait_for_human,
{
"wait_human": "human_approval",
"continue": "finalize"
}
)
hitl_workflow.add_edge("human_approval", "finalize")
hitl_workflow.add_edge("finalize", END)
# Compiler avec checkpoints (important pour HITL)
memory = MemorySaver()
hitl_app = hitl_workflow.compile(checkpointer=memory)
# Tester
config = {"configurable": {"thread_id": "hitl_session_1"}}
# Tâche sensible nécessitant approbation
result = hitl_app.invoke(
{"messages": [HumanMessage(content="Delete all old files")]},
config=config
)
print(f"\nFinal result: {result['messages'][-1].content}")
8. Workflow Parallèle
from langgraph.graph import StateGraph, END
from concurrent.futures import ThreadPoolExecutor
class ParallelState(TypedDict):
query: str
web_results: str
db_results: str
wiki_results: str
final_answer: str
def search_web(state: ParallelState) -> ParallelState:
"""Recherche web (peut être lent)"""
import time
time.sleep(1) # Simuler latence
return {"web_results": f"Web results for: {state['query']}"}
def search_database(state: ParallelState) -> ParallelState:
"""Recherche DB (peut être lent)"""
import time
time.sleep(1)
return {"db_results": f"DB results for: {state['query']}"}
def search_wikipedia(state: ParallelState) -> ParallelState:
"""Recherche Wikipedia"""
import time
time.sleep(1)
return {"wiki_results": f"Wikipedia results for: {state['query']}"}
def aggregate_results(state: ParallelState) -> ParallelState:
"""Agréger tous les résultats"""
combined = f"""
Web: {state.get('web_results', 'N/A')}
DB: {state.get('db_results', 'N/A')}
Wiki: {state.get('wiki_results', 'N/A')}
"""
return {"final_answer": combined}
# Construire le graphe parallèle
parallel_workflow = StateGraph(ParallelState)
# Ajouter les nœuds
parallel_workflow.add_node("web", search_web)
parallel_workflow.add_node("db", search_database)
parallel_workflow.add_node("wiki", search_wikipedia)
parallel_workflow.add_node("aggregate", aggregate_results)
# Point d'entrée
parallel_workflow.set_entry_point("web")
# Exécuter web, db, wiki en parallèle
# (En pratique, ils démarrent tous depuis l'entrée)
parallel_workflow.add_edge("web", "aggregate")
parallel_workflow.add_edge("db", "aggregate")
parallel_workflow.add_edge("wiki", "aggregate")
parallel_workflow.add_edge("aggregate", END)
# Pour vraiment paralléliser, il faut démarrer tous les nœuds en même temps
# Une approche est d'avoir un nœud "dispatcher"
def dispatch(state: ParallelState) -> ParallelState:
return state
parallel_workflow.set_entry_point("web") # Simplification
# Compiler et tester
import time
parallel_app = parallel_workflow.compile()
start = time.time()
result = parallel_app.invoke({"query": "AI agents"})
duration = time.time() - start
print(f"\nParallel execution took: {duration:.2f}s")
print(f"Final answer:\n{result.get('final_answer', 'N/A')}")
9. Visualiser le Graphe
# LangGraph peut générer une visualisation du graphe
from IPython.display import Image, display
# Obtenir la représentation PNG du graphe
try:
graph_image = react_app.get_graph().draw_png()
display(Image(graph_image))
except Exception as e:
print(f"Could not display graph: {e}")
print("Install graphviz: pip install pygraphviz")
# Ou obtenir la représentation texte
print("\nGraph structure:")
print(react_app.get_graph().draw_ascii())
LangGraph est puissant pour des workflows complexes, mais n'en faites pas trop. Commencez simple avec des agents linéaires, puis ajoutez de la complexité (cycles, branches) seulement quand nécessaire. Les graphes complexes sont difficiles à débugger. Utilisez toujours les checkpoints en production pour pouvoir reprendre après une erreur.
10. Cas d'usage LangGraph
| Cas d'usage | Pattern | Avantage LangGraph |
|---|---|---|
| Agent avec retry | Cycle: agent → tool → validate → agent | Boucle de correction automatique |
| Multi-agent coordination | Branches: supervisor → [agent1, agent2, agent3] → synthesis | Orchestration parallèle |
| Human approval workflow | Interruption: process → approval → continue | Checkpoints natifs |
| Plan-Execute-Revise | Cycle: plan → execute → evaluate → replan | Loops conditionnels |
| Recherche itérative | Cycle: query → search → refine → search | Amélioration progressive |
Smolagents (Hugging Face)
- Maîtriser les agents Hugging Face Smolagents
- Utiliser ToolCallingAgent pour des tâches code-first
- Implémenter ManagedAgent pour la collaboration
- Créer des outils personnalisés avec @tool decorator
1. Introduction à Smolagents
Smolagents (anciennement Transformers Agents) est le framework d'agents de Hugging Face. Contrairement à LangChain qui est agnostique au modèle, Smolagents est optimisé pour les modèles Hugging Face et adopte une approche code-first.
┌────────────────────────────────────────────────────────────────┐
│ SMOLAGENTS ARCHITECTURE │
├────────────────────────────────────────────────────────────────┤
│ │
│ User Query │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ ToolCallingAgent│ │
│ │ (Code-based) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ LLM │ │ Tools │ │ Code │ │
│ │(Llama, │ │(HF Hub) │ │Executor │ │
│ │Mistral) │ │ │ │(Python) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Différence clé: L'agent génère du CODE Python │
│ au lieu de simple JSON function calling │
│ │
│ Exemple: │
│ Agent: result = search_tool("AI agents") │
│ answer = analyze(result) │
│ return answer │
│ │
└────────────────────────────────────────────────────────────────┘
2. Installation et Setup
# Installer smolagents
pip install smolagents
# Dépendances optionnelles
pip install transformers torch accelerate
pip install duckduckgo-search # Pour web search
# Imports
from smolagents import (
CodeAgent,
ToolCallingAgent,
ManagedAgent,
tool,
HfApiModel,
LiteLLMModel
)
3. Créer des Outils avec @tool
from smolagents import tool
import requests
# Méthode 1: Tool simple avec décorateur
@tool
def get_weather(location: str) -> str:
"""
Get the current weather for a location.
Args:
location: The city name (e.g., 'Paris', 'London')
Returns:
A string describing the weather
"""
# Simuler un appel API météo
# En production: utiliser OpenWeatherMap API
weather_data = {
"Paris": "15°C, partly cloudy",
"London": "12°C, rainy",
"Tokyo": "20°C, sunny",
"New York": "10°C, clear"
}
result = weather_data.get(location, f"Weather data not available for {location}")
return result
# Méthode 2: Tool avec calculs
@tool
def calculate_percentage(value: float, percentage: float) -> float:
"""
Calculate a percentage of a value.
Args:
value: The base value
percentage: The percentage to calculate (e.g., 25 for 25%)
Returns:
The calculated result
"""
result = (value * percentage) / 100
return result
# Méthode 3: Tool complexe avec API réelle
@tool
def web_search(query: str, max_results: int = 5) -> str:
"""
Search the web for information.
Args:
query: The search query string
max_results: Maximum number of results to return (default: 5)
Returns:
A formatted string with search results
"""
try:
from duckduckgo_search import DDGS
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=max_results))
formatted_results = []
for i, result in enumerate(results, 1):
formatted_results.append(
f"{i}. {result['title']}\n {result['body'][:150]}...\n URL: {result['href']}"
)
return "\n\n".join(formatted_results)
except Exception as e:
return f"Search error: {str(e)}"
# Méthode 4: Tool avec état
class CounterTool:
"""Tool qui maintient un état interne"""
def __init__(self):
self.count = 0
@tool
def increment_counter(self) -> int:
"""
Increment an internal counter and return the new value.
Returns:
The new counter value
"""
self.count += 1
return self.count
@tool
def get_count(self) -> int:
"""
Get the current counter value.
Returns:
The current count
"""
return self.count
counter = CounterTool()
# Liste des outils
tools = [
get_weather,
calculate_percentage,
web_search,
counter.increment_counter,
counter.get_count
]
- Docstring claire : Le LLM utilise la docstring pour comprendre l'outil
- Type hints : Toujours annoter les types des paramètres
- Description des Args : Expliquer chaque paramètre dans la docstring
- Retour explicite : Toujours documenter ce que retourne la fonction
- Error handling : Gérer les exceptions et retourner des messages clairs
4. ToolCallingAgent
Le ToolCallingAgent est l'agent principal de Smolagents. Il génère du code Python pour utiliser les outils.
from smolagents import ToolCallingAgent, HfApiModel
import os
# Option 1: Utiliser un modèle Hugging Face
model = HfApiModel(
model_id="meta-llama/Meta-Llama-3.1-70B-Instruct",
token=os.getenv("HF_TOKEN")
)
# Option 2: Utiliser OpenAI via LiteLLM
from smolagents import LiteLLMModel
openai_model = LiteLLMModel(
model_id="gpt-4-turbo-preview",
api_key=os.getenv("OPENAI_API_KEY")
)
# Créer l'agent
agent = ToolCallingAgent(
tools=tools,
model=openai_model, # ou model
max_steps=10,
verbosity_level=2 # 0=silent, 1=info, 2=debug
)
# Utiliser l'agent
if __name__ == "__main__":
print("="*70)
print("SMOLAGENTS - ToolCallingAgent Demo")
print("="*70 + "\n")
# Test 1: Tâche simple
result1 = agent.run("What's the weather in Tokyo?")
print(f"\nResult 1: {result1}\n")
# Test 2: Calcul
result2 = agent.run("Calculate 15% of 250")
print(f"\nResult 2: {result2}\n")
# Test 3: Multi-outils
result3 = agent.run(
"Search for information about Python programming, "
"then tell me what's the weather in Paris"
)
print(f"\nResult 3: {result3}\n")
# Test 4: Tâche complexe avec état
result4 = agent.run(
"Increment the counter, then increment it again, "
"then tell me what the count is"
)
print(f"\nResult 4: {result4}\n")
5. Code Execution et Sécurité
Smolagents exécute du code Python généré par le LLM. C'est puissant mais potentiellement dangereux.
# L'agent génère du code comme ceci:
"""
# Agent-generated code
weather_result = get_weather("Paris")
percentage_result = calculate_percentage(100, 15)
final_answer = f"Weather: {weather_result}, 15% of 100 is {percentage_result}"
"""
# Ce code est exécuté dans un environnement Python
# ⚠️ SÉCURITÉ: En production, utilisez un sandbox
# Options de sandboxing:
# 1. Docker container avec ressources limitées
from smolagents import DockerExecutor
docker_executor = DockerExecutor(
image="python:3.11-slim",
timeout=30, # Timeout en secondes
memory_limit="512m"
)
agent_with_docker = ToolCallingAgent(
tools=tools,
model=openai_model,
code_executor=docker_executor
)
# 2. Restricted Python (RestrictedPython)
from smolagents import RestrictedExecutor
restricted_executor = RestrictedExecutor(
allowed_imports=["math", "datetime", "json"],
timeout=10
)
agent_secure = ToolCallingAgent(
tools=tools,
model=openai_model,
code_executor=restricted_executor
)
# 3. E2B (Sandboxed cloud execution)
# pip install e2b
from smolagents import E2BExecutor
e2b_executor = E2BExecutor(
api_key=os.getenv("E2B_API_KEY"),
timeout=60
)
agent_e2b = ToolCallingAgent(
tools=tools,
model=openai_model,
code_executor=e2b_executor
)
JAMAIS exécuter du code généré par LLM en production sans sandbox. Un attacker pourrait faire générer du code malveillant via prompt injection. Utilisez toujours Docker, E2B, ou un environnement isolé avec restrictions strictes.
6. ManagedAgent pour Collaboration
Un ManagedAgent est un agent qui peut être utilisé comme outil par un autre agent (agent multi-niveaux).
from smolagents import ToolCallingAgent, ManagedAgent, tool
# Créer des agents spécialisés
# Agent 1: Spécialiste web search
@tool
def specialized_search(query: str) -> str:
"""Advanced web search with analysis"""
# Logique de recherche avancée
return f"Advanced search results for: {query}"
search_agent = ToolCallingAgent(
tools=[specialized_search, web_search],
model=openai_model,
name="SearchAgent",
description="Expert in web search and information retrieval"
)
# Agent 2: Spécialiste calculs
@tool
def advanced_calculation(expression: str) -> str:
"""Perform complex mathematical calculations"""
try:
import numpy as np
# Permettre numpy dans eval
result = eval(expression, {"np": np, "__builtins__": {}})
return str(result)
except Exception as e:
return f"Calculation error: {str(e)}"
calc_agent = ToolCallingAgent(
tools=[advanced_calculation, calculate_percentage],
model=openai_model,
name="CalculatorAgent",
description="Expert in mathematical calculations and analysis"
)
# Convertir les agents en ManagedAgents (tools)
search_managed = ManagedAgent(
agent=search_agent,
name="search_specialist",
description="Delegate to this agent for web search and research tasks"
)
calc_managed = ManagedAgent(
agent=calc_agent,
name="calculator_specialist",
description="Delegate to this agent for mathematical calculations"
)
# Agent superviseur qui utilise les sous-agents
supervisor_agent = ToolCallingAgent(
tools=[search_managed, calc_managed, get_weather],
model=openai_model,
name="SupervisorAgent"
)
# Utiliser le système multi-agents
if __name__ == "__main__":
print("\n" + "="*70)
print("MULTI-AGENT SYSTEM")
print("="*70 + "\n")
# Tâche qui nécessite plusieurs spécialistes
result = supervisor_agent.run(
"Search for the population of Paris in 2020, "
"then calculate what 10% of that population would be"
)
print(f"\nFinal Result: {result}\n")
# Le SupervisorAgent va:
# 1. Déléguer la recherche au SearchAgent
# 2. Déléguer le calcul au CalculatorAgent
# 3. Synthétiser les résultats
7. Comparaison: Smolagents vs LangChain
| Aspect | Smolagents | LangChain |
|---|---|---|
| Philosophie | Code-first (génère Python) | Prompt-first (JSON function calling) |
| Modèles optimisés | Hugging Face (Llama, Mistral) | OpenAI, Anthropic |
| Flexibilité | Très élevée (code arbitraire) | Limitée (outils prédéfinis) |
| Sécurité | ⚠️ Nécessite sandbox strict | ✅ Plus sûr (pas d'exec) |
| Performance | Excellente avec Llama 3.1 | Excellente avec GPT-4 |
| Écosystème | En croissance | Très mature (1000+ intégrations) |
| Multi-agents | ✅ ManagedAgent natif | ✅ Via LangGraph |
| Meilleur pour | Tâches code/data science | Applications d'entreprise |
8. Outils Avancés avec État
from dataclasses import dataclass
from typing import List, Dict
import json
@dataclass
class Task:
id: int
description: str
completed: bool = False
class TaskManager:
"""Gestionnaire de tâches avec état persistant"""
def __init__(self):
self.tasks: List[Task] = []
self.next_id = 1
@tool
def add_task(self, description: str) -> str:
"""
Add a new task to the list.
Args:
description: Description of the task
Returns:
Confirmation message with task ID
"""
task = Task(id=self.next_id, description=description)
self.tasks.append(task)
self.next_id += 1
return f"Task added with ID {task.id}: {description}"
@tool
def list_tasks(self) -> str:
"""
List all tasks with their status.
Returns:
Formatted list of tasks
"""
if not self.tasks:
return "No tasks found"
result = "Current tasks:\n"
for task in self.tasks:
status = "✓" if task.completed else "○"
result += f"{status} [{task.id}] {task.description}\n"
return result
@tool
def complete_task(self, task_id: int) -> str:
"""
Mark a task as completed.
Args:
task_id: The ID of the task to complete
Returns:
Confirmation message
"""
for task in self.tasks:
if task.id == task_id:
task.completed = True
return f"Task {task_id} marked as completed"
return f"Task {task_id} not found"
@tool
def get_stats(self) -> str:
"""
Get statistics about tasks.
Returns:
Task statistics
"""
total = len(self.tasks)
completed = sum(1 for t in self.tasks if t.completed)
pending = total - completed
return f"Total: {total}, Completed: {completed}, Pending: {pending}"
# Créer le manager et utiliser ses méthodes comme outils
task_manager = TaskManager()
agent_with_state = ToolCallingAgent(
tools=[
task_manager.add_task,
task_manager.list_tasks,
task_manager.complete_task,
task_manager.get_stats
],
model=openai_model
)
# Test avec état persistant
result1 = agent_with_state.run("Add a task: 'Buy groceries'")
print(result1)
result2 = agent_with_state.run("Add a task: 'Call dentist'")
print(result2)
result3 = agent_with_state.run("List all tasks")
print(result3)
result4 = agent_with_state.run("Complete task 1")
print(result4)
result5 = agent_with_state.run("Show me the statistics")
print(result5)
9. Streaming et Callbacks
from smolagents import ToolCallingAgent
from typing import Dict, Any
class CustomCallback:
"""Callback pour monitorer l'agent"""
def __init__(self):
self.steps = []
self.tool_calls = []
def on_step_start(self, step_info: Dict[str, Any]):
"""Appelé au début de chaque étape"""
print(f"\n🔄 Step {step_info.get('step_num', '?')} starting...")
self.steps.append(step_info)
def on_tool_start(self, tool_name: str, tool_input: Any):
"""Appelé avant l'exécution d'un outil"""
print(f"🔧 Calling tool: {tool_name}")
print(f" Input: {str(tool_input)[:100]}...")
self.tool_calls.append({"tool": tool_name, "input": tool_input})
def on_tool_end(self, tool_name: str, tool_output: Any):
"""Appelé après l'exécution d'un outil"""
print(f"✓ Tool completed: {tool_name}")
print(f" Output: {str(tool_output)[:100]}...")
def on_step_end(self, step_info: Dict[str, Any]):
"""Appelé à la fin de chaque étape"""
print(f"✓ Step {step_info.get('step_num', '?')} completed\n")
def get_stats(self) -> Dict:
"""Retourner les statistiques"""
return {
"total_steps": len(self.steps),
"total_tool_calls": len(self.tool_calls),
"tools_used": list(set(tc["tool"] for tc in self.tool_calls))
}
# Utiliser avec callback
callback = CustomCallback()
agent_with_callback = ToolCallingAgent(
tools=tools,
model=openai_model,
callbacks=[callback]
)
result = agent_with_callback.run(
"Search for AI agents and calculate 25% of 80"
)
print("\n" + "="*70)
print("EXECUTION STATISTICS")
print("="*70)
stats = callback.get_stats()
for key, value in stats.items():
print(f"{key}: {value}")
Smolagents est excellent pour les data scientists et les cas d'usage impliquant du code (data analysis, ML). Pour des applications business classiques, LangChain reste plus approprié. Si vous utilisez des modèles Hugging Face open source (Llama, Mistral), Smolagents est optimisé pour eux. Toujours utiliser un sandbox en production!
Quiz Module 6.1 : Fondamentaux des Agents
- Évaluer votre compréhension des concepts d'agents IA
- Tester vos connaissances sur les patterns agentiques
- Vérifier votre maîtrise des outils et frameworks
- Score minimum requis : 70% (11/15 questions)
15 Questions - Module 6.1
Si vous obtenez moins de 70%, revoyez les leçons 0-6 avant de continuer vers le Module 6.2. La compréhension des fondamentaux est essentielle pour les systèmes multi-agents avancés!
CrewAI : Orchestration d'Équipes IA
- Comprendre le concept de Crew (équipe) d'agents IA
- Créer des Agents spécialisés avec rôles et objectifs
- Orchestrer des Tasks avec dépendances
- Maîtriser les Process types (Sequential, Hierarchical, Consensus)
1. Introduction à CrewAI
CrewAI est un framework de pointe pour créer des équipes d'agents IA qui collaborent. Inspiré par les équipes humaines, chaque agent a un rôle, des objectifs et des compétences spécifiques.
┌────────────────────────────────────────────────────────────────┐
│ ARCHITECTURE CREWAI │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ CREW │ │
│ │(Manager) │ │
│ └────┬─────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Agent 1 │ │Agent 2 │ │Agent 3 │ │
│ │Researcher │Analyst│ │Writer │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Task 1 │ │Task 2 │ │Task 3 │ │
│ │Research│ │Analyze │ │Write │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ Process Flow: │
│ • Sequential: Task 1 → Task 2 → Task 3 │
│ • Hierarchical: Manager delegates → Agents execute │
│ • Consensus: All agents vote on decisions │
│ │
└────────────────────────────────────────────────────────────────┘
2. Installation
pip install crewai crewai-tools
# Dépendances pour les outils
pip install duckduckgo-search selenium playwright
3. Créer votre Premier Crew
from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI
import os
# Configurer le LLM
llm = ChatOpenAI(
model="gpt-4-turbo-preview",
temperature=0.7,
api_key=os.getenv("OPENAI_API_KEY")
)
# Agent 1: Researcher (Chercheur)
researcher = Agent(
role='Senior Research Analyst',
goal='Uncover cutting-edge developments in AI and data science',
backstory="""You are an expert research analyst with a PhD in Computer Science.
You have 10 years of experience in AI research and always find the most
relevant and up-to-date information. You're meticulous and cite sources.""",
verbose=True,
allow_delegation=False, # Ne peut pas déléguer à d'autres agents
llm=llm
)
# Agent 2: Writer (Rédacteur)
writer = Agent(
role='Tech Content Strategist',
goal='Craft compelling content on tech advancements',
backstory="""You are a renowned content creator, known for insightful
and engaging articles on technology and AI. You transform complex
technical concepts into clear, accessible content.""",
verbose=True,
allow_delegation=False,
llm=llm
)
# Créer les tâches
task1 = Task(
description="""Research the latest advancements in AI agents technology.
Focus on developments from 2024, including new frameworks, techniques,
and real-world applications. Provide detailed findings with sources.""",
agent=researcher,
expected_output="A comprehensive research report with key findings and sources"
)
task2 = Task(
description="""Using the research provided, write a 500-word blog post
about the latest AI agents advancements. Make it engaging and accessible
to a general tech audience. Include practical examples.""",
agent=writer,
expected_output="A well-written 500-word blog post",
context=[task1] # Cette tâche dépend du résultat de task1
)
# Créer le Crew
crew = Crew(
agents=[researcher, writer],
tasks=[task1, task2],
process=Process.sequential, # Exécution séquentielle
verbose=2 # Max verbosity pour voir tout
)
# Lancer l'exécution
result = crew.kickoff()
print("\n" + "="*70)
print("FINAL RESULT")
print("="*70)
print(result)
- Agent : Un membre de l'équipe avec role, goal, backstory
- Task : Une tâche assignée à un agent avec contexte
- Crew : L'équipe qui orchestre agents et tasks
- Process : Le mode d'exécution (sequential/hierarchical/consensus)
4. Agents avec Outils
from crewai_tools import (
SerperDevTool, # Google Search
ScrapeWebsiteTool,
FileReadTool,
DirectoryReadTool,
CodeInterpreterTool
)
# Initialiser les outils
search_tool = SerperDevTool()
scrape_tool = ScrapeWebsiteTool()
file_tool = FileReadTool()
code_tool = CodeInterpreterTool()
# Agent avec outils multiples
data_analyst = Agent(
role='Senior Data Analyst',
goal='Analyze data and provide actionable insights',
backstory="""You are an expert data analyst with strong skills in
Python, statistics, and data visualization. You can interpret
complex datasets and communicate findings clearly.""",
tools=[search_tool, code_tool, file_tool], # Outils disponibles
verbose=True,
llm=llm
)
# Agent spécialisé web
web_researcher = Agent(
role='Web Research Specialist',
goal='Find and extract information from websites',
backstory="""You excel at finding information online and extracting
relevant data from websites. You verify sources and cross-reference.""",
tools=[search_tool, scrape_tool],
verbose=True,
llm=llm
)
# Tâche nécessitant web research
research_task = Task(
description="""Search for the top 5 AI agent frameworks in 2024.
For each framework, find:
- Official website
- GitHub stars
- Key features
- Use cases
Scrape their websites for detailed information.""",
agent=web_researcher,
expected_output="Structured comparison of top 5 frameworks"
)
# Tâche d'analyse
analysis_task = Task(
description="""Analyze the research data and create a comparison table.
Identify which framework is best for different use cases.
Provide recommendations.""",
agent=data_analyst,
expected_output="Analysis report with recommendations",
context=[research_task]
)
# Crew avec outils
crew_with_tools = Crew(
agents=[web_researcher, data_analyst],
tasks=[research_task, analysis_task],
process=Process.sequential,
verbose=2
)
result = crew_with_tools.kickoff()
5. Process Types
5.1 Sequential Process
Les tâches s'exécutent dans l'ordre, chaque tâche peut utiliser les résultats des précédentes.
# Processus séquentiel: Recherche → Analyse → Rédaction → Révision
# Agent 1: Researcher
researcher = Agent(
role='Researcher',
goal='Find comprehensive information',
backstory='Expert researcher',
llm=llm
)
# Agent 2: Analyst
analyst = Agent(
role='Data Analyst',
goal='Analyze and synthesize information',
backstory='Expert analyst',
llm=llm
)
# Agent 3: Writer
writer = Agent(
role='Content Writer',
goal='Create engaging content',
backstory='Professional writer',
llm=llm
)
# Agent 4: Editor
editor = Agent(
role='Editor',
goal='Review and improve content',
backstory='Experienced editor',
llm=llm
)
# Tâches en chaîne
t1 = Task(description="Research AI agents", agent=researcher)
t2 = Task(description="Analyze findings", agent=analyst, context=[t1])
t3 = Task(description="Write article", agent=writer, context=[t2])
t4 = Task(description="Edit and finalize", agent=editor, context=[t3])
sequential_crew = Crew(
agents=[researcher, analyst, writer, editor],
tasks=[t1, t2, t3, t4],
process=Process.sequential
)
5.2 Hierarchical Process
Un manager agent délègue et coordonne les autres agents.
# Processus hiérarchique avec manager
# Agents spécialistes (pas de manager ici, il sera auto-créé)
specialist1 = Agent(
role='Backend Developer',
goal='Develop backend systems',
backstory='Expert in Python and databases',
llm=llm
)
specialist2 = Agent(
role='Frontend Developer',
goal='Create user interfaces',
backstory='Expert in React and UI/UX',
llm=llm
)
specialist3 = Agent(
role='DevOps Engineer',
goal='Handle deployment and infrastructure',
backstory='Expert in Docker, K8s, CI/CD',
llm=llm
)
# Tâche globale
project_task = Task(
description="""Build a complete web application for task management.
Requirements:
- RESTful API backend
- Modern React frontend
- Deployed on AWS
Coordinate the team to complete this project.""",
expected_output="Complete, deployed application"
)
# Crew hiérarchique (CrewAI crée automatiquement un manager)
hierarchical_crew = Crew(
agents=[specialist1, specialist2, specialist3],
tasks=[project_task],
process=Process.hierarchical, # Manager auto-créé
manager_llm=llm, # LLM pour le manager
verbose=2
)
# Le manager va:
# 1. Décomposer le projet en sous-tâches
# 2. Assigner chaque sous-tâche au spécialiste approprié
# 3. Coordonner l'exécution
# 4. Intégrer les résultats
6. Delegation et Collaboration
# Agent avec délégation activée
senior_agent = Agent(
role='Senior AI Architect',
goal='Design comprehensive AI systems',
backstory='20 years of experience in AI',
allow_delegation=True, # Peut déléguer aux autres agents du crew
llm=llm,
verbose=True
)
junior_researcher = Agent(
role='Junior Researcher',
goal='Assist with research tasks',
backstory='Eager learner, detail-oriented',
allow_delegation=False,
llm=llm
)
junior_coder = Agent(
role='Junior Developer',
goal='Implement code solutions',
backstory='Fresh graduate, strong coding skills',
allow_delegation=False,
llm=llm
)
# Tâche complexe
complex_task = Task(
description="""Design a multi-agent system for customer support automation.
Include:
- Architecture diagram
- Technology stack
- Implementation plan
- Sample code
You can delegate specific subtasks to team members.""",
agent=senior_agent,
expected_output="Complete system design document"
)
# Le senior_agent peut déléguer:
# - Research à junior_researcher
# - Coding à junior_coder
delegation_crew = Crew(
agents=[senior_agent, junior_researcher, junior_coder],
tasks=[complex_task],
process=Process.sequential,
verbose=2
)
result = delegation_crew.kickoff()
7. Memory et Context
from crewai import Agent, Task, Crew, Process
# Agent avec mémoire
agent_with_memory = Agent(
role='Personal Assistant',
goal='Help with daily tasks and remember preferences',
backstory='Attentive assistant who remembers details',
memory=True, # Active la mémoire long-terme
verbose=True,
llm=llm
)
# Session 1
task1 = Task(
description="The user's name is Alice and she likes Python programming",
agent=agent_with_memory
)
crew1 = Crew(
agents=[agent_with_memory],
tasks=[task1],
process=Process.sequential
)
crew1.kickoff()
# Session 2 (plus tard)
task2 = Task(
description="What is the user's name and what does she like?",
agent=agent_with_memory
)
crew2 = Crew(
agents=[agent_with_memory],
tasks=[task2],
process=Process.sequential
)
result = crew2.kickoff()
# L'agent se souvient: "Alice likes Python"
8. Exemple Complet: Équipe de Recherche
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
from langchain_openai import ChatOpenAI
# LLM
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0.7)
# Outils
search_tool = SerperDevTool()
scrape_tool = ScrapeWebsiteTool()
# Équipe de 4 agents spécialisés
# 1. Market Researcher
market_researcher = Agent(
role='Market Research Analyst',
goal='Conduct thorough market research and competitive analysis',
backstory="""Expert market researcher with 15 years of experience.
Skilled in identifying market trends, competitor strategies, and opportunities.""",
tools=[search_tool, scrape_tool],
verbose=True,
llm=llm
)
# 2. Tech Analyst
tech_analyst = Agent(
role='Technology Analyst',
goal='Analyze technical aspects and provide technical insights',
backstory="""Senior technology analyst with deep expertise in software
architecture, cloud technologies, and AI systems.""",
tools=[search_tool],
verbose=True,
llm=llm
)
# 3. Business Strategist
strategist = Agent(
role='Business Strategy Consultant',
goal='Develop strategic recommendations',
backstory="""Former McKinsey consultant with expertise in business strategy,
growth planning, and market positioning.""",
verbose=True,
llm=llm
)
# 4. Report Writer
report_writer = Agent(
role='Senior Report Writer',
goal='Create comprehensive, professional reports',
backstory="""Professional writer specializing in business reports.
Creates clear, well-structured documents with executive summaries.""",
verbose=True,
llm=llm
)
# Tâches
task1 = Task(
description="""Research the AI agent market in 2024.
Find:
- Market size and growth rate
- Major players and their market share
- Recent funding rounds
- Key trends and drivers
Provide detailed findings with sources.""",
agent=market_researcher,
expected_output="Detailed market research report with data and sources"
)
task2 = Task(
description="""Analyze the technical landscape of AI agents.
Cover:
- Popular frameworks and their features
- Technology stacks being used
- Technical challenges and solutions
- Emerging technologies""",
agent=tech_analyst,
expected_output="Technical analysis report",
context=[task1]
)
task3 = Task(
description="""Based on market and technical research, develop
strategic recommendations for a company wanting to enter
the AI agent market.
Include:
- Market opportunities
- Competitive positioning
- Go-to-market strategy
- Risk assessment""",
agent=strategist,
expected_output="Strategic recommendations document",
context=[task1, task2]
)
task4 = Task(
description="""Compile all research and analysis into a comprehensive
executive report.
Structure:
1. Executive Summary
2. Market Analysis
3. Technical Landscape
4. Strategic Recommendations
5. Conclusion
Make it professional and actionable.""",
agent=report_writer,
expected_output="Final executive report (2000+ words)",
context=[task1, task2, task3]
)
# Créer le Crew
research_crew = Crew(
agents=[market_researcher, tech_analyst, strategist, report_writer],
tasks=[task1, task2, task3, task4],
process=Process.sequential,
verbose=2
)
# Exécuter
print("\n" + "="*70)
print("RESEARCH CREW STARTING")
print("="*70 + "\n")
final_report = research_crew.kickoff()
print("\n" + "="*70)
print("FINAL EXECUTIVE REPORT")
print("="*70)
print(final_report)
# Sauvegarder le rapport
with open("ai_agents_market_report.txt", "w") as f:
f.write(final_report)
print("\n✅ Report saved to ai_agents_market_report.txt")
9. Best Practices CrewAI
| Best Practice | Description | Pourquoi |
|---|---|---|
| Rôles spécialisés | Chaque agent un rôle clair et spécifique | Meilleure qualité, moins de confusion |
| Backstory détaillée | Donner du contexte et de l'expertise | Le LLM joue mieux son rôle |
| Context entre tasks | Passer les résultats aux tâches suivantes | Continuité et cohérence |
| Expected output clair | Définir précisément ce qui est attendu | Résultats plus conformes |
| Outils appropriés | Donner les bons outils aux bons agents | Efficacité maximale |
| Process adapté | Sequential pour pipeline, Hierarchical pour projets | Meilleure coordination |
CrewAI excelle pour les workflows complexes multi-agents avec des rôles bien définis. Utilisez-le quand vous avez besoin d'une vraie collaboration d'équipe (recherche → analyse → rédaction). Pour des agents plus simples, LangChain suffit. Le coût augmente rapidement (chaque agent = plusieurs appels LLM), donc commencez avec 2-3 agents max.