Notion d'outil
Dans ce chapitre nous allons construire notre premier véritable agent. Comme d'habitude nous allons commencer doucement avec un agent qui aura à sa disposition un seul outil.
Les LLM ont un gros défaut : ils sont "bloqués" dans le passé. En effet, vu le coût de la phase d'entraînement d'un modèle, les modèles ne peuvent pas être ré-entrainés en permanence, les données utilisées pour leur entraînement peuvent dater de plusieurs mois. Ils sont donc incapables de répondre à des questions portant sur l'actualité récente.
Pourtant, dans ce chapitre, nous allons créer un chatbot capable de discuter avec vous de l'actualité récente. En effet notre chatbot ne sera pas basé sur un simple LLM, mais sur un agent (combinaison d'un LLM et d'outils). Cet agent aura une certaine autonomie et il devra "décider" par lui-même s'il se contente des connaissances acquises pendant son entraînement ou s'il a besoin d'utiliser un outil pour aller chercher des données plus récentes sur le Web.
Il existe pas mal d'outils "tout faits" pour faire des recherches sur le Web, langgraph nous en propose un gratuit : DuckDuckGoSearchRun
. Comme vous l'avez sans doute deviné, cet outil se base sur le moteur de recherche DuckDuckGo.
from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
from langchain.prompts import ChatPromptTemplate
from langgraph.checkpoint.memory import MemorySaver
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from langchain.tools import DuckDuckGoSearchRun
from langchain.agents import Tool
from langgraph.prebuilt import ToolNode, tools_condition
from dotenv import load_dotenv
from datetime import date
import os
import gradio as gr
import
: DuckDuckGoSearchRun
, Tool
, ToolNode
, tools_condition
et date
(va nous permettre d'obtenir la date du jour afin de rendre les réponses du chatbot plus précises sur les questions d'actualités)
load_dotenv()
API_KEY = os.getenv('GOOGLE_API_KEY')
now = date.today()
class State(BaseModel):
messages : Annotated[list, add_messages]
memory = MemorySaver()
graph_builder = StateGraph(State)
prompt_system = """
Tu es un assistant. Ton but est d'aider ton interlocuteur en lui apportant des réponses à ses questions les plus précises possibles.
Si tu ne trouves pas la réponse à une question, tu ne dois surtout pas inventer une réponse mais juste dire que tu n'as pas la réponse.
Si tu trouves tes informations sur le web, tu dois préciser ta source d'informations.
"""
prompt = ChatPromptTemplate([
("system", prompt_system),
("human", "Voici l'historique de la conversation :\n {historical}"),
("human", "Afin de rendre tes réponses les plus précises possible, voici la date du jour : {date_now}"),
("human", "{user_intervention}")
])
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
Rien de nouveau ci-dessus (à part le fait que nous avons rajouté la date du jour)
Nous allons maintenant commencer à construire notre outil de recherche We
def toolSearch(query: str):
search = DuckDuckGoSearchRun()
return search.run(query)
search_tool = Tool(
name="WebSearch",
func=toolSearch,
description="Recherche d'informations sur le web"
)
Nous commençons par écrire une fonction toolSearch
qui prend en paramètre la demande du LLM et renvoie la recherche effectuée sur DuckDuckGo.
Nous créons ensuite une instance de la classe Tool
. Au moment de la création de cette instance, on renseigne :
- le nom de notre outil
- le nom de la fonction qui va être utilisée par l'outil
- la description de l'outil
tools = [search_tool]
tools
.
llm_with_tools = llm.bind_tools(tools)
llm_tools_prompt = prompt | llm_with_tools
llm
et nous créons llm_with_tools
. Ensuite, nous associons notre prompt
avec llm_with_tools
, nous obtenons llm_tools_prompt
.
def conversation_history(h_list):
txt = ""
for d in h_list:
if isinstance(d, HumanMessage):
txt += f"INTERLOCUTEUR : {d.content}\n"
else :
txt += f"ASSISTANT : {d.content}\n"
return txt
def chatbot(state : State):
q = state.messages[-1].content
h = conversation_history(state.messages[:-1])
response = llm_tools_prompt.invoke({"historical": h,"date_now": now, "user_intervention" : q})
return {'messages': [response]}
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", ToolNode(tools=tools))
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_conditional_edges( "chatbot", tools_condition, "tools")
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile(checkpointer=memory)
3 nouveautés dans la définition du graphe :
- graph_builder.add_node("tools", ToolNode(tools=tools))
nous permet de créer un nœud "outils"
- graph_builder.add_conditional_edges( "chatbot", tools_condition, "tools")
est une arête conditionnelle. Le LLM "décidera" d'utiliser ou non un outil.
- graph_builder.add_edge("tools", "chatbot")
est l'arête qui permet de retourner au nœud chatbot
après utilisation d'un outil.
Voici une représentation graphique de notre graphe :
Comme vous pouvez le constater, chatbot
à 2 choix : soit répondre par lui-même, soit utiliser la "boîte à outils" mise à sa disposition. Une fois que l'outil a fait les recherches nécessaires sur le Web, les réponses sont passées au chatbot
qui peut alors élaborer sa propre réponse. Les arêtes conditionnelles apparaissent en pointillés sur le schéma.
config = {"configurable": {"thread_id": "1"}}
def chat(user_input: str, history):
result = graph.invoke({"messages": [{"role": "user", "content": user_input}]}, config=config)
return result["messages"][-1].content
gr.ChatInterface(chat, type="messages").launch()
Exercice : Modifiez le code afin de supprimer "la boîte à outils" et posez alors des questions d'actualité au chatbot (cela devrait vous permettre de vous rendre compte de l'utilité de cette "boite à outils").
Exercice : Il existe beaucoup d'outils "tout faits", par exemple WikipediaQueryRun
va permettre au chatbot de s'appuyer sur les articles de Wikipédia pour rédiger sa réponse. En vous aidant de la documentation, ajoutez cet outil à ce chatbot.
Petite précision : nous avons vu qu'il était possible d'utiliser des outils "tout faits", mais il est aussi évidemment possible de créer vos propres outils.
Ce chapitre est terminé, nous avons créé notre premier agent. Le prochain chapitre sera consacré aux systèmes multi-agents.
Code complet :
from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
from langchain.prompts import ChatPromptTemplate
from langgraph.checkpoint.memory import MemorySaver
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from langchain.tools import DuckDuckGoSearchRun
from langchain.agents import Tool
from langgraph.prebuilt import ToolNode, tools_condition
from dotenv import load_dotenv
from datetime import date
import os
import gradio as gr
load_dotenv()
API_KEY = os.getenv('GOOGLE_API_KEY')
now = date.today()
class State(BaseModel):
messages : Annotated[list, add_messages]
memory = MemorySaver()
graph_builder = StateGraph(State)
prompt_system = """
Tu es un assistant. Ton but est d'aider ton interlocuteur en lui apportant des réponses à ses questions les plus précises possibles.
Si tu ne trouves pas la réponse à une question, tu ne dois surtout pas inventer une réponse mais juste dire que tu n'as pas la réponse.
Si tu trouves tes informations sur le web, tu dois préciser ta source d'informations.
"""
prompt = ChatPromptTemplate([
("system", prompt_system),
("human", "Voici l'historique de la conversation :\n {historical}"),
("human", "Afin de rendre tes réponses les plus précises possible, voici la date du jour : {date_now}"),
("human", "{user_intervention}")
])
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
def toolSearch(query: str):
search = DuckDuckGoSearchRun()
return search.run(query)
search_tool = Tool(
name="WebSearch",
func=toolSearch,
description="Recherche d'informations sur le web"
)
tools = [search_tool]
llm_with_tools = llm.bind_tools(tools)
llm_tools_prompt = prompt | llm_with_tools
def conversation_history(h_list):
txt = ""
for d in h_list:
if isinstance(d, HumanMessage):
txt += f"INTERLOCUTEUR : {d.content}\n"
else :
txt += f"ASSISTANT : {d.content}\n"
return txt
def chatbot(state : State):
q = state.messages[-1].content
h = conversation_history(state.messages[:-1])
response = llm_tools_prompt.invoke({"historical": h,"date_now": now, "user_intervention" : q})
return {'messages': [response]}
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", ToolNode(tools=tools))
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_conditional_edges( "chatbot", tools_condition, "tools")
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}
def chat(user_input: str, history):
result = graph.invoke({"messages": [{"role": "user", "content": user_input}]}, config=config)
return result["messages"][-1].content
gr.ChatInterface(chat, type="messages").launch()