Blocs de construction de LangGraph : état, nœuds, arêtes et invocation
Vocabulaire de LangGraph avant sa création : état saisi, nœuds de petite taille, arêtes conditionnelles, appels d’outils et messages en tant qu’état, illustré avec un premier agent simple.
Au fur et à mesure que les modèles ont acquis la capacité d’utiliser des outils et de raisonner en plusieurs étapes, les boucles ad hoc sont devenues difficiles à tester et à reprendre. LangGraph rend le flux de contrôle explicite : état, nœuds, arêtes et appels — ce qui permet de considérer les agents comme des graphes que l’on peut examiner, et non comme un enchevêtrement de commandes.
Le graphique le plus simple
Un flux d’agent minimal se compose de : début → nœud(s) de raisonnement/action → fin, avec un état partagé transmis entre les étapes.
Créer un premier agent simple
Receive Order
Prepare Food
Deliver Food
Receive Order
↓
Prepare Food
↓
Deliver Food
Pizza
Address: 123 Main Street
Customer: John
Receive Order
↓
Prepare Food
↓
Deliver Food
from typing_extensions import TypedDict
class State(TypedDict):
graph_state: strNodes
def node_1(state):
print("---Node 1---")
return {"graph_state": state['graph_state'] + "I am"}
def node_2(state):
print("---Node 2---")
return {"graph_state": state['graph_state'] + "happy!"}
def node_3(state):
print("---Node 3---")
return {"graph_state": state['graph_state'] + "sad!"}
import random
from typing import Literal
def decide_mood(state) -> Literal["node_2", "node_3"]:
# Often, we will use state to decide on the next node to visit
user_input = state['graph_state']
# Here, let's just do a 50 / 50 split between nodes 2, 3
if random.random() < 0.5
# 50% of the time, we return Node 2
return "node_2"
# 50% of the time, we return Node 3
return "node_3"
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END
#Build Graph
builder = StateGraph(state)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
#Logic
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
#Add
graph = builder.compile()
#View
display(Image(graph.get_graph().draw_mermaid_png()))
graph.invoke({"graph_state" : "Hi, this is Lance."})
###OUTPUT
---Node 1---
---Node 3---
{'graph_state': 'Hi, this is Lance. I am sad!'}
{'graph_state': 'Hi, this is Lance. I am sad!'}
%%capture --no-stderr
%pip install --quiet -U langchain_openai langchain_core langgraph
from pprint import pprint
from langchain_core.messages import AIMessage, HumanMessage
messages = [AIMessage(content=f"So you said you were researching ocean mammals?", name="Model")]
messages.append(HumanMessage(content=f"Yes, that's right.",name="Lance"))
messages.append(AIMessage(content=f"Great, what would you like to learn about.", name="Model"))
messages.append(HumanMessage(content=f"I want to learn about the best place to see Orcas in the US.", name="Lance"))
for m in messages:
m.pretty_print()
#OUTPUT
================================== Ai Message ==================================
Name: Model
So you said you were researching ocean mammals?
================================ Human Message =================================
Name: Lance
Yes, that's right.
================================== Ai Message ==================================
Name: Model
Great, what would you like to learn about.
================================ Human Message =================================
Name: Lance
I want to learn about the best place to see Orcas in the US.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
result = llm.invoke(messages)
type(result)
result
#OUTPUT
langchain_core.messages.ai.AIMessage
result
AIMessage(content='One of the best places to see orcas in the United States is the Pacific Northwest, particularly around the San Juan Islands in Washington State. Here are some details:\n\n1. **San Juan Islands, Washington**: These islands are a renowned spot for whale watching, with orcas frequently spotted between late spring and early fall. The waters around the San Juan Islands are home to both resident and transient orca pods, making it an excellent location for sightings.\n\n2. **Puget Sound, Washington**: This area, including places like Seattle and the surrounding waters, offers additional opportunities to see orcas, particularly the Southern Resident killer whale population.\n\n3. **Olympic National Park, Washington**: The coastal areas of the park provide a stunning backdrop for spotting orcas, especially during their migration periods.\n\nWhen planning a trip for whale watching, consider peak seasons for orca activity and book tours with reputable operators who adhere to responsible wildlife viewing practices. Additionally, land-based spots like Lime Kiln Point State Park, also known as “Whale Watch Park,” on San Juan Island, offer great opportunities for orca watching from shore.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 228, 'prompt_tokens': 67, 'total_tokens': 295, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_50cad350e4', 'finish_reason': 'stop', 'logprobs': None}, id='run-57ed2891-c426-4452-b44b-15d0a5c3f225-0', usage_metadata={'input_tokens': 67, 'output_tokens': 228, 'total_tokens': 295, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
def multiply(a:int, b:int)-> int:
return a*b
llm_with_tools = llm.bind_tools([multiply])
tool_call = llm_with_tools.invoke([HumanMessage(content=f"What is 2 multiplied by 3
", name="Lance")])
tool_call.tool_calls
#OUTPUT
[{'name': 'multiply',
'args': {'a': 2, 'b': 3},
'id': 'call_lBBBNo5oYpHGRqwxNaNRbsiT',
'type': 'tool_call'}]
from typing_extentions import TypedDict
from langchain_core.messges import AnyMessage
class MessagesState(TypedDict):
messages : list[AnyMessage]
from typing import Annotated
from langgraph.graph.message import add_messages
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
from langgraph.graph import MessagesState
class MessagesState(MessagesState):
# Add any keys needed beyond messages, which is pre-built
pass
# Initial state
initial_messages = [AIMessage(content="Hello! How can I assist you?", name="Model"),
HumanMessage(content="I'm looking for information on marine biology.", name="Lance")
]
# New message to add
new_message = AIMessage(content="Sure, I can help with that. What specifically are you interested in?", name="Model")
# Test
add_messages(initial_messages , new_message)
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END
# Node
def tool_calling_llm(state: MessagesState):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# Build graph
builder = StateGraph(MessagesState)
builder.add_node("tool_calling_llm", tool_calling_llm)
builder.add_edge(START, "tool_calling_llm")
builder.add_edge("tool_calling_llm", END)
graph = builder.compile()
# View
display(Image(graph.get_graph().draw_mermaid_png()))
messages = graph.invoke({"messages": HumanMessage(content="Hello!")})
for m in messages['messages']:
m.pretty_print()
#OUTPUT
================================ Human Message =================================
Hello!
================================== Ai Message ==================================
Hi there! How can I assist you today?
messages = graph.invoke({"messages": HumanMessage(content="Multiply 2 and 3")})
for m in messages['messages']:
m.pretty_print()
OUTPUT
================================ Human Message =================================
Multiply 2 and 3!
================================== Ai Message ==================================
Tool Calls:
multiply (call_Er4gChFoSGzU7lsuaGzfSGTQ)
Call ID: call_Er4gChFoSGzU7lsuaGzfSGTQ
Args:
a: 2
b: 3
État
L’état défini manuellement (souvent un TypedDict ou des reducers) contient les messages, les flags et les résultats intermédiaires. Choisissez les reducers en fonction de vos besoins : utilisez l’opération append pour les listes de messages et replace pour les valeurs scalaires.
Nœuds
Les nœuds sont des fonctions : elles lisent l’état, effectuent des opérations (LLM, outils, Python pur) et retournent une mise à jour partielle. Gardez-les simples et testables.
Arêtes
Les arêtes statiques fixent l’ordre ; les arêtes conditionnelles créent des branches en fonction de l’état. Les graphes deviennent utiles lorsque des branches et des boucles apparaissent.
Construction du graphique
Enregistrez les nœuds, connectez les arêtes, et compilez avec un point de contrôle optionnel pour assurer la durabilité.
Appel du graphique
Appelez-le avec un état d’entrée et des paramètres de configuration (identifiant de thread pour le point de contrôle). Diffusez des événements afin d’améliorer l’expérience utilisateur.
Chaîne vs graphique
Les chaînes linéaires permettent encore un traitement en une seule passe. Les graphiques permettent des pauses/reprises, des branches et un état nommé partagé.
Messages et modèles de chat
Les listes de messages représentent l’état conversationnel habituel. Associez les modèles de chat à l’intérieur des nœuds plutôt que de cacher les opérations d’entrée/sortie dans la logique des arêtes.
Outils
Faites exposer des outils aux nœuds d’appel de modèles ; analysez les appels aux outils ; écrivez les observations à nouveau dans l’état.
Utilisation des messages comme état
Lorsque les messages sont l’état, les réducteurs doivent s’ajouter de manière sécurisée et le troncage/la synthèse deviennent un nœud explicite — et non le résultat d’un hasard.
Conclusion
La partie 0 porte sur le vocabulaire : état, nœuds, arêtes, compilation, invocation. Les parties suivantes ajoutent des threads durables, des interruptions HITL et des modèles multi-agents sur cette même base.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de référence et vérifie quels nœuds ont été exécutés. La fonction compile() seule ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphiques lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphiques lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphiques lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude du processus métier.
Donnez des noms aux nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Les graphes lisible sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds en fonction des étapes métier (classify, retrieve, draft) plutôt que node1. Des graphiques lisibles sont des graphiques opérationnels.
Ajoutez un test unitaire qui invoque le graphe compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple fait d’appeler compile() ne prouve pas la complétude du processus métier.
Nommez les nœuds d’après les étapes métier (classify, retrieve, draft) plutôt que node1. Des graphes lisibles sont des graphes opérationnels.
Ajoutez un test unitaire qui invoque le graphique compilé avec un état de fixture et vérifie quels nœuds ont été exécutés. Le simple appel à compile() ne prouve pas la complétude métier.
Préférez des nœuds déterministes pour les tests de routage : injectez un LLM fictif qui renvoie des appels d’outils fixes afin que le CI n’ait pas besoin d’accès réseau.
Dokumentez tôt la règle d’autorisation thread_id : localiser un point de contrôle ne signifie pas avoir le droit de le lire.
Maintenez les outils ayant des effets secondaires derrière des arêtes d’approbation, même dans les prototypes de la Partie 0 — les habitudes se forment rapidement.
Préférez des nœuds déterministes pour les tests de routage : injectez un LLM fictif qui renvoie des appels d’outils fixes afin que le CI n’ait pas besoin d’accès réseau.
Dokumentez tôt la règle d’autorisation thread_id : localiser un point de contrôle ne signifie pas avoir le droit de le lire.
Gardez les outils ayant des effets secondaires derrière les étapes d’approbation, même dans les prototypes de la Partie 0 — les habitudes se forment rapidement.
Préférez des nœuds déterministes pour les tests de routage : injectez un LLM fictif qui renvoie des appels d’outils fixes afin que le CI n’ait pas besoin d’accès au réseau.
Dokumentez tôt la règle d’autorisation thread_id : localiser un point de contrôle ne signifie pas avoir le droit de le lire.
Gardez les outils ayant des effets secondaires derrière les étapes d’approbation, même dans les prototypes de la Partie 0 — les habitudes se forment rapidement.
Préférez des nœuds déterministes pour les tests de routage : injectez un LLM fictif qui renvoie des appels d’outils fixes afin que le CI n’ait pas besoin d’accès au réseau.