Construye ShopBot: un agente de soporte de LangGraph con memoria, herramientas y capacidad para aprobar reembolsos
Construir un agente de soporte al cliente en el módulo LangGraph módulo por módulo: estado, herramientas, nodos, enrutamiento, resúmenes dinámicos y una interrupción que espera la aprobación del reembolso.
Fuentes: Your First Real LangGraph Project: Building a Customer Support Agent.
Este proyecto integra tres conceptos de LangGraph en un único agente: arquitectura de grafos, memoria de conversación y aprobación humana. El agente es un bot de soporte al cliente llamado ShopBot, destinado a una tienda de comercio electrónico ficticia. Los clientes pueden consultar sobre pedidos, solicitar reembolsos y elevar quejas. El bot recuerda la conversación, busca los datos del pedido con herramientas reales y espera la aprobación de una persona antes de procesar cualquier reembolso. El código se desarrolla módulo por módulo, con cada parte explicada, y no se requiere experiencia previa con LangGraph.
Planifique primero el grafo
Un error común entre los principiantes es abrir un archivo vacío y comenzar a escribir. LangGraph premia dibujar primero el grafo, ya sea en papel o en la mente. En papel, ShopBot es sencillo: el paso del agente, el ejecutor de herramientas, la revisión de reembolsos y el resumidor constituyen los nodos; dos ramas determinan el flujo, una única interrupt() pausa el proceso para obtener aprobación, y un disparador activa la resumen. El código solo transcribe esa estructura.
Configuración
Cree un archivo llamado shopbot.py o abra una notebook de Colab. Instale las dependencias necesarias:
pip install langgraph langchain langchain-openai langgraph-checkpoint-sqlite python-dotenv
Luego cree un archivo .env en la misma carpeta para almacenar la clave:
OPENAI_API_KEY=your-key-here
Módulo 1: importaciones y configuración
Todas las importaciones del archivo completo se colocan al principio, sin importaciones a mitad del mismo. La configuración también se encuentra aquí.
# ============================================================
# MODULE 1: IMPORTS & CONFIGURATION
# ============================================================
import os
import sqlite3
from typing import Annotated, Literal
from datetime import datetime
from dotenv import load_dotenv
# LangChain - the AI layer
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage,
AIMessage,
SystemMessage,
BaseMessage,
RemoveMessage,
)
from langchain_core.tools import tool
# LangGraph - the graph layer
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
load_dotenv()
# ── The LLM ─────────────────────────────────────────────────
# temperature=0 means deterministic - the agent behaves
# consistently, which is what you want for a support bot.
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
La elección del modelo es gpt-4o-mini. Es lo suficientemente capaz para tareas de soporte, lo suficientemente económico como para usarlo con frecuencia y lo suficientemente rápido como para que los usuarios no noten demoras. Un sistema de producción podría pasar a gpt-4o para tareas de razonamiento más complejas, pero es sensato comenzar con uno más sencillo.
Módulo 2: estado
El estado es la memoria de trabajo del agente, por lo que todo lo que transita a través del grafo debe almacenarse en ella. Decida qué debe saber el agente en cada etapa.
# ============================================================
# MODULE 2: STATE
# ============================================================
class SupportState(MessagesState):
# MessagesState already gives us:
# messages: Annotated[list[BaseMessage], add_messages]
# We add three more fields for our specific needs:
# The running summary of the conversation (Part 2 pattern).
# Starts empty. Gets written by summarize_node when conversation gets long.
summary: str
# The name of the customer, extracted early in the conversation.
# Used to personalise every response. Starts empty.
customer_name: str
# Tracks the current ticket category, set by the agent.
# Helps the human reviewer understand context during escalation.
# Values: "order_inquiry" | "refund_request" | "complaint" | "general"
ticket_category: str
Existen tres campos adicionales además del historial de mensajes. summary alberga un registro comprimido de las interacciones anteriores: una vez que la conversación supera los seis mensajes, el agente condensa el historial en este campo y elimina los mensajes originales antiguos, de modo que los costos por token se mantengan estables. customer_name es un campo práctico para casi cualquier agente de soporte; al tener el nombre almacenado, el agente puede saludar personalmente a las personas sin que cada nodo tenga que volver a leer el chat para encontrarlo. ticket_category es lo que permite la enrutación condicional, ya que el agente lo establece y una función de enrutamiento lo lee para decidir si se debe activar el proceso de aprobación humana.
Módulo 3: herramientas
Las herramientas son las manos del agente. Sin ellas solo puede hablar, mientras que con ellas puede buscar información y tomar acciones. ShopBot cuenta con tres, respaldadas por una base de datos en memoria ficticia:
# ============================================================
# MODULE 3: TOOLS
# ============================================================
# ── Fake Database ────────────────────────────────────────────
# In a real project, these would be database queries or API calls.
# For learning purposes, we use a simple Python dictionary.
ORDERS_DB = {
"ORD-001": {
"customer": "Alex",
"product": "Wireless Headphones",
"status": "Delivered",
"amount": 89.99,
"delivery_date": "2025-06-10",
},
"ORD-002": {
"customer": "Sam",
"product": "Phone Case",
"status": "In Transit",
"amount": 14.99,
"delivery_date": "Expected 2025-06-18",
},
"ORD-003": {
"customer": "Jordan",
"product": "Laptop Stand",
"status": "Processing",
"amount": 45.00,
"delivery_date": "Expected 2025-06-20",
},
}
@tool
def lookup_order(order_id: str) -> str:
"""Look up the details of a customer's order by order ID.
Use this when the customer provides an order number and wants
to know the status, product name, or delivery date of their order.
Args:
order_id: The order ID string, e.g. 'ORD-001'
Returns:
A formatted string with full order details, or an error message
if the order is not found.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"No order found with ID '{order_id}'. Please double-check the order number."
return (
f"Order {order_id.upper()}: {order['product']} | "
f"Status: {order['status']} | "
f"Amount: ${order['amount']:.2f} | "
f"Delivery: {order['delivery_date']}"
)
@tool
def check_refund_eligibility(order_id: str) -> str:
"""Check whether an order is eligible for a refund.
Use this BEFORE processing any refund request. An order is eligible
for a refund only if its status is 'Delivered'. Orders Fthat are
'In Transit' or 'Processing' cannot be refunded yet.
Args:
order_id: The order ID string, e.g. 'ORD-001'
Returns:
A string stating whether the order is eligible and why.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"Cannot check refund: order '{order_id}' not found."
if order["status"] == "Delivered":
return (
f"Order {order_id.upper()} IS eligible for a refund. "
f"Product: {order['product']}, Amount: ${order['amount']:.2f}. "
f"Proceed to refund processing."
)
else:
return (
f"Order {order_id.upper()} is NOT eligible for a refund yet. "
f"Current status: {order['status']}. Refunds are only available "
f"for delivered orders."
)
@tool
def process_refund(order_id: str, reason: str) -> str:
"""Process a refund for a delivered order.
IMPORTANT: This tool actually issues the refund. It should only be
called AFTER human approval has been obtained. Never call this tool
without prior confirmation.
Args:
order_id: The order ID to refund
reason: The customer's stated reason for the refund
Returns:
A confirmation string with the refund reference number.
"""
order = ORDERS_DB.get(order_id.upper())
if not order:
return f"Refund failed: order '{order_id}' not found."
# In a real system, this would hit your payments API.
refund_ref = f"REF-{order_id.upper()}-{datetime.now().strftime('%H%M%S')}"
return (
f"Refund APPROVED and PROCESSED. Reference: {refund_ref}. "
f"${order['amount']:.2f} will be returned to the original payment method "
f"within 3–5 business days. Reason logged: '{reason}'."
)
# ── Collect tools and bind to LLM ───────────────────────────
# All three tools in one list.
tools = [lookup_order, check_refund_eligibility, process_refund]
# llm_with_tools = the LLM that KNOWS about the tools and can decide to call them.
# This is what we use inside agent_node.
llm_with_tools = llm.bind_tools(tools)
# tool_node = the pre-built node that EXECUTES whatever tool the LLM chose.
# This is what we register in Module 6.
tool_node = ToolNode(tools)
Observe atentamente la documentación de process_refund, que indica que esta herramienta realmente procesa el reembolso y solo debe ser llamada después de la aprobación humana. El modelo lee las documentaciones y las utiliza para decidir qué hacer. Escríbalas como si estuviera informando a un pasante inteligente que aún no conoce las reglas de su negocio, porque eso es, en efecto, lo que sucede.
Módulo 4: nodos
Los nodos son los que realizan el trabajo. Cada uno recibe el estado, realiza alguna acción y devuelve un diccionario con las actualizaciones, nada más. ShopBot cuenta con tres nodos que usted debe crear, además del tool_node ya preparado del módulo anterior. Estos incluyen el prompt del sistema, el paso principal del agente, el paso de revisión del reembolso con interrupt() y el resumidor:
# ============================================================
# MODULE 4: NODES
# ============================================================
# ── The System Prompt ────────────────────────────────────────
# Written once, used in every call to the LLM from agent_node.
# This is the personality and rulebook of your agent.
SYSTEM_PROMPT = """You are ShopBot, a friendly and professional customer support \
agent for an e-commerce store.
Your capabilities:
- Look up order details using the lookup_order tool
- Check if an order qualifies for a refund using check_refund_eligibility
- Process approved refunds using the process_refund tool
Your rules:
- Always greet the customer by name once you know it
- Always check refund eligibility BEFORE attempting to process a refund
- For refund requests, set ticket_category to "refund_request" in your reasoning
- Be empathetic, clear, and concise
- If you cannot help, offer to escalate to a human agent
Important: The process_refund tool requires prior human approval. Do not call it \
unless the conversation shows that a human has already approved the refund."""
# ── Node 1: agent_node ──────────────────────────────────────
def agent_node(state: SupportState) -> dict:
"""The brain of the operation. Reads state, calls the LLM, and decides
whether to use a tool, give a final answer, or do something else.
This node handles two cases:
1. Normal conversation - just call the LLM and respond
2. Long conversation - if a summary exists, prepend it so the LLM
has context without seeing all the raw messages
"""
# Part 2 pattern: check for an existing summary
summary = state.get("summary", "")
if summary:
# Build context: system prompt + compressed history + recent messages
system_with_summary = SystemMessage(
content=f"{SYSTEM_PROMPT}\n\nSummary of conversation so far:\n{summary}"
)
messages_to_send = [system_with_summary] + state["messages"]
else:
# No summary yet - full history is short enough to send as-is
system_msg = SystemMessage(content=SYSTEM_PROMPT)
messages_to_send = [system_msg] + state["messages"]
# Call the LLM. It sees tools and can choose to call one.
response = llm_with_tools.invoke(messages_to_send)
# Detect ticket category from the response for routing purposes.
# A smarter version would have the LLM explicitly set this -
# for now, we scan for keywords.
content_lower = response.content.lower() if response.content else ""
updates: dict = {"messages": [response]}
if "refund" in content_lower or (
hasattr(response, "tool_calls")
and any("refund" in str(tc).lower() for tc in (response.tool_calls or []))
):
updates["ticket_category"] = "refund_request"
return updates
# ── Node 2: review_refund ───────────────────────────────────
def review_refund(state: SupportState) -> dict:
"""The human approval gate. Pauses execution, shows the pending refund
details to a human agent, and waits for their decision.
This implements the Part 3 interrupt() pattern. Execution stops here
until someone calls graph.invoke(Command(resume=...), config).
Three outcomes the human can choose:
- "approve" → let the refund tool call proceed unchanged
- "reject" → cancel the refund, send a message to the customer
- "escalate" → hand the entire ticket to a human support agent
"""
last_message = state["messages"][-1]
# Find the refund-related tool call in the last AI message.
# We look for process_refund specifically - the "real action" tool.
refund_tool_call = None
if hasattr(last_message, "tool_calls"):
for tc in last_message.tool_calls:
if "refund" in tc["name"].lower():
refund_tool_call = tc
break
# Surface the context to the human reviewer via interrupt().
# Everything in this dict is what the human sees before deciding.
human_decision = interrupt({
"message": "⚠️ Refund approval required",
"customer_name": state.get("customer_name", "Unknown"),
"tool_being_called": refund_tool_call["name"] if refund_tool_call else "refund tool",
"arguments": refund_tool_call["args"] if refund_tool_call else {},
"conversation_summary": state.get("summary", "No summary yet"),
"options": ["approve", "reject", "escalate"],
})
# ── Handle the human's decision ─────────────────────────
if human_decision == "approve":
# Do nothing to state - let tool_node execute the tool call as-is
return {}
elif human_decision == "reject":
# Cancel the tool call. The LLM will see a ToolMessage explaining why,
# and generate a polite response to the customer.
from langchain_core.messages import ToolMessage
return {
"messages": [
ToolMessage(
content=(
"Refund request was reviewed and declined by our support team. "
"Please inform the customer politely and offer alternatives."
),
tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
)
]
}
elif human_decision == "escalate":
# Signal escalation - in a real system you'd open a ticket,
# ping Slack, or transfer to a live agent queue.
from langchain_core.messages import ToolMessage
return {
"messages": [
ToolMessage(
content=(
"This ticket has been escalated to a senior support agent. "
"Inform the customer that a human agent will contact them "
"within 2 business hours."
),
tool_call_id=refund_tool_call["id"] if refund_tool_call else "unknown",
)
]
}
# Fallback - treat as approve
return {}
# ── Node 3: summarize_node ──────────────────────────────────
def summarize_node(state: SupportState) -> dict:
"""Triggered when the conversation exceeds 6 messages. Compresses the
full message history into a short summary, then deletes old raw messages.
This is the rolling summary pattern from Part 2. The summary grows
richer turn by turn. Token costs stay nearly flat no matter how long
the conversation runs.
"""
existing_summary = state.get("summary", "")
if existing_summary:
# Extend the existing summary with new messages
summary_instruction = (
f"Current summary:\n{existing_summary}\n\n"
"Extend this summary with the new messages above. "
"Keep it under 5 sentences. Focus on: the customer's name, "
"their issue, any orders mentioned, and what actions were taken."
)
else:
# First time summarising
summary_instruction = (
"Summarise this customer support conversation in under 5 sentences. "
"Include: the customer's name (if mentioned), their issue, "
"any order numbers discussed, and what actions were taken so far."
)
messages = state["messages"] + [HumanMessage(content=summary_instruction)]
response = llm.invoke(messages) # Plain llm, no tools needed here
# Delete all but the 2 most recent messages.
# The summary now holds everything that was in the deleted messages.
messages_to_delete = [
RemoveMessage(id=m.id) for m in state["messages"][:-2]
]
return {
"summary": response.content,
"messages": messages_to_delete,
}
Módulo 5: aristas y enrutamiento
Las funciones de enrutamiento deciden a dónde ir a continuación. Inspeccionan el estado y devuelven una cadena de texto, y nunca modifican el estado, ya que esa es la tarea de los nodos. Se necesitan dos:
# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================
def route_after_agent(state: SupportState) -> Literal[
"review_refund", "tools", "summarize_node", "__end__"
]:
"""Called after agent_node runs. Decides what happens next.
Four possible routes:
1. The LLM wants to call process_refund → must go through human review first
2. The LLM wants to call any other tool → go directly to tool_node
3. The LLM gave a plain text answer AND the conversation is long → summarise
4. The LLM gave a plain text answer and conversation is short → we're done
"""
last_message = state["messages"][-1]
has_tool_calls = hasattr(last_message, "tool_calls") and bool(last_message.tool_calls)
if has_tool_calls:
# Check if ANY of the tool calls is the sensitive process_refund tool
tool_names = [tc["name"] for tc in last_message.tool_calls]
if "process_refund" in tool_names:
return "review_refund" # → Pause for human approval first
return "tools" # → Safe tool, run it directly
# No tool call - the LLM gave a plain response.
# Check if the conversation is long enough to need summarisation.
if len(state["messages"]) > 6:
return "summarize_node"
return "__end__" # → Conversation turn is complete
def route_after_review(state: SupportState) -> Literal["tools", "agent_node"]:
"""Called after review_refund runs (i.e., after the human has decided).
Two routes:
1. Human approved or escalated → run the tool (tool_node handles the call)
2. Human rejected → the review node already added a ToolMessage cancelling
the tool call, so skip tool_node and go back to agent_node to respond
"""
last_message = state["messages"][-1]
# If the last message is a ToolMessage, the review node cancelled the call.
# Go back to agent_node so it can generate a customer-facing response.
from langchain_core.messages import ToolMessage
if isinstance(last_message, ToolMessage):
return "agent_node"
# Otherwise, the review node returned {} (approved) - proceed to tools.
return "tools"
Módulo 6: montaje del grafo
Todas las piezas ya existen, y el montaje sigue siempre cinco pasos: inicializar, registrar nodos, establecer el punto de entrada, agregar aristas y compilar.
# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================
# ── Step 1: Initialize ──────────────────────────────────────
graph_builder = StateGraph(SupportState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("string_name", function)
# The string name is what you use in every edge definition below.
graph_builder.add_node("agent_node", agent_node)
graph_builder.add_node("tools", tool_node) # Pre-built from Module 3
graph_builder.add_node("review_refund", review_refund)
graph_builder.add_node("summarize_node", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# The first node that runs when a user sends a message.
graph_builder.add_edge(START, "agent_node")
# ── Step 4: Wire the Edges ──────────────────────────────────
# After agent_node: conditional - depends on what the LLM decided
graph_builder.add_conditional_edges(
"agent_node", # Source
route_after_agent, # Router function from Module 5
{
"review_refund": "review_refund", # Refund tool → human review first
"tools": "tools", # Other tools → run directly
"summarize_node": "summarize_node", # Long conversation → summarise
"__end__": END, # Plain answer → done
}
)
# After review_refund: conditional - depends on human's decision
graph_builder.add_conditional_edges(
"review_refund",
route_after_review,
{
"tools": "tools", # Approved → execute the tool
"agent_node": "agent_node", # Rejected → back to agent to respond
}
)
# After tools run: always go back to agent_node
# (the ReAct loop - agent sees tool result, decides what to do next)
graph_builder.add_edge("tools", "agent_node")
# After summarization: conversation turn is done
graph_builder.add_edge("summarize_node", END)
# ── Step 5: Compile ─────────────────────────────────────────
# Using MemorySaver for development.
# For production, swap this one line to SqliteSaver or PostgresSaver.
memory = MemorySaver()
shopbot = graph_builder.compile(checkpointer=memory)
En Colab o Jupyter, puedes representar el gráfico como un diagrama:
from IPython.display import display, Image
from langchain_core.runnables.graph import MermaidDrawMethod
display(Image(
shopbot.get_graph().draw_mermaid_png(
draw_method=MermaidDrawMethod.API
)
))
Ver un diagrama de flujo real de lo que se ha creado es una de las mejores características de LangGraph para los aprendices. Si el diagrama parece incorrecto, entonces los enlaces también lo están.
Módulo 7: punto de entrada
Aquí es donde realmente se ejecuta el agente. En un script es el bloque if __name__ == "__main__", y en un servidor API es el manejador de rutas, aunque el patrón es el mismo en ambos casos.
# ============================================================
# MODULE 7: ENTRYPOINT
# ============================================================
def run_shopbot():
"""
Interactive command-line session with ShopBot.
Demonstrates: multi-turn conversation, tool use, and human-in-the-loop.
"""
print("=" * 55)
print(" ShopBot - Customer Support Agent")
print(" Powered by LangGraph")
print("=" * 55)
print("Type your message below. Type 'exit' to quit.")
print("Type 'state' to inspect what ShopBot currently remembers.\n")
# One config per session.
# thread_id is the session key - same ID = same memory thread.
# Change the ID to start a completely fresh conversation.
config = {"configurable": {"thread_id": "customer-session-001"}}
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() == "exit":
print("ShopBot: Thank you for contacting support. Have a great day!")
break
# ── Debug: inspect current state ────────────────────
if user_input.lower() == "state":
snapshot = shopbot.get_state(config)
print("\n[DEBUG] Current State:")
print(f" Messages in state : {len(snapshot.values.get('messages', []))}")
print(f" Customer name : {snapshot.values.get('customer_name', '(not set)')}")
print(f" Ticket category : {snapshot.values.get('ticket_category', '(not set)')}")
print(f" Summary : {snapshot.values.get('summary', '(none yet)')}")
print(f" Next node(s) : {snapshot.next}\n")
continue
# ── Normal message: invoke the graph ─────────────────
result = shopbot.invoke(
{"messages": [HumanMessage(content=user_input)]},
config=config,
)
# ── Check if graph paused for human approval ─────────
# This is how you detect that interrupt() was called inside review_refund.
while "__interrupt__" in result:
interrupt_data = result["__interrupt__"][0].value
print("\n" + "=" * 55)
print(" HUMAN APPROVAL REQUIRED")
print("=" * 55)
print(f" Customer : {interrupt_data.get('customer_name', 'Unknown')}")
print(f" Action : {interrupt_data.get('tool_being_called', 'refund')}")
print(f" Arguments : {interrupt_data.get('arguments', {})}")
print(f" Context : {interrupt_data.get('conversation_summary', 'N/A')}")
print("=" * 55)
print("Options: [a] Approve [r] Reject [e] Escalate")
human_choice = input("Your decision: ").strip().lower()
if human_choice == "a":
resume_value = "approve"
elif human_choice == "r":
resume_value = "reject"
elif human_choice == "e":
resume_value = "escalate"
else:
print("Invalid choice. Defaulting to reject.")
resume_value = "reject"
# Resume the graph with the human's decision.
# Command(resume=...) answers the pending interrupt() call.
result = shopbot.invoke(
Command(resume=resume_value),
config=config,
)
# ── Print the agent's final response ─────────────────
last_message = result["messages"][-1]
print(f"\nShopBot: {last_message.content}\n")
if __name__ == "__main__":
run_shopbot()
Una sesión real
Una conversación de ejemplo muestra qué esperar al ejecutarla:
=======================================================
ShopBot — Customer Support Agent
Powered by LangGraph
=======================================================
Type your message below. Type 'exit' to quit.
Type 'state' to inspect what ShopBot currently remembers.
You: Hi, my name is Alex and I need help with order ORD-001
ShopBot: Hi Alex! I'd be happy to help you with order ORD-001.
I can see your order:
- Product: Wireless Headphones
- Status: Delivered on 2025-06-10
- Amount: $89.99
What can I help you with regarding this order?
You: I want to return it and get a refund
ShopBot: I've checked your order ORD-001 and it's eligible for a refund
since it's been delivered. Let me process that for you right away.
=======================================================
HUMAN APPROVAL REQUIRED
=======================================================
Customer : Alex
Action : process_refund
Arguments : {'order_id': 'ORD-001', 'reason': 'Customer requested return'}
Context : No summary yet
=======================================================
Options: [a] Approve [r] Reject [e] Escalate
Your decision: a
ShopBot: Great news, Alex! Your refund has been successfully processed.
Refund Reference: REF-ORD-001-143022
Amount: $89.99 will be returned to your original payment method within
3–5 business days.
Is there anything else I can help you with?
You: state
[DEBUG] Current State:
Messages in state : 6
Customer name : (not set)
Ticket category : refund_request
Summary : (none yet)
Next node(s) : ()
You: exit
ShopBot: Thank you for contacting support. Have a great day!
Lo que demuestra el proyecto
La estructura de siete módulos muestra la arquitectura de principio a fin: StateGraph, add_node, add_conditional_edges y compile, todos en el orden correcto. En cuanto a la memoria, existe el campo summary, la función summarize_node y el patrón de resumen dinámico en el que los mensajes nuevos se comprimen y los antiguos se eliminan, además de un puntero de verificación MemorySaver que mantiene viva la historia a lo largo de las turnos. Para la intervención humana, el nodo review_refund se pausa con interrupt(), el punto de entrada se reanuda con Command(resume=...), y un bucle while "__interrupt__" in result permite gestionar varias aprobaciones en un solo turno, ofreciendo opciones de aprobar, rechazar o escalar el caso.
Varias cosas provienen de las herramientas. Las herramientas reales se declaran con @tool; el ToolNode preconstruido las ejecuta, y bind_tools las conecta al modelo. El bucle ReAct sigue el patrón herramientas, nodo de agente, herramientas, etc. Además, el enrutamiento trata las herramientas de manera diferente: las seguras van directamente a tool_node, mientras que las sensibles pasan primero por review_refund.
Próximos pasos
El proyecto está diseñado intencionadamente para ser completo pero mínimo. Tres extensiones, comenzando por las más sencillas:
- Mejor detección de categorías. Actualmente
ticket_categoryse obtiene mediante el análisis de palabras clave enagent_node. Una forma más eficiente es pedirle al modelo que categorice explícitamente la solicitud con una salida estructurada; por lo tanto, examine la funciónwith_structured_output(). - Persistencia de SQLite. Sustituya
MemorySaver()porSqliteSaver, un cambio de una sola línea en el Módulo 6, y agregue un mensaje al iniciar que pregunte si se trata de la misma sesión o de una nueva. De esta manera, las conversaciones sobrevivirán a los reinicios. - Recorrido de escalación de quejas. Añada un cuarto nodo,
escalate_node, que se activa cuandoticket_category == "complaint"y la emoción del cliente es"FRUSTRATED". Este nodo llama ainterrupt()para alertar a un agente humano y se conecta aroute_after_agentcomo una nueva rama.
Referencia rápida
Lo nuevo en este proyecto son algunos patrones. ToolNode(tools) es un nodo preconstruido que ejecuta las llamadas a herramientas que se encuentran en el último mensaje del modelo. llm.bind_tools(tools) conecta la lista de herramientas al modelo, permitiéndole luego solicitar una herramienta al devolver un campo tool_calls en lugar de texto simple. @tool convierte una función de Python en algo que el modelo puede llamar, utilizando la documentación como referencia para decidir cuándo utilizarla. La comprobación hasattr(message, "tool_calls") and message.tool_calls es la forma estándar de verificar si el último mensaje contiene una llamada a herramienta, y hasattr debe utilizarse primero antes de acceder al atributo. El bucle while "__interrupt__" in result maneja una o más interrupciones pendientes, ya que cada Command(resume=...) responde a una e puede activar la siguiente. Finalmente, shopbot.get_state(config) muestra el estado actual completo en cualquier momento: .values proporciona el diccionario de estado y .next indica qué se ejecutará a continuación, lo cual es de gran valor para la depuración.
Existe realmente una brecha entre comprender un concepto y escribir código para él, y un proyecto pequeño que quepa en la mente pero que utilice todas las herramientas disponibles es la forma de cerrar esa brecha. Aquí se cuenta con un punto de verificación, herramientas reales, una puerta de aprobación y un disparador de resumen que funcionan juntos en un mismo agente. Constrúyelo, rompálo intencionadamente, corrígelo y luego amplíalo.
Leer más
- Puertas de validación y enrutamiento multiagente para un bot de soporte LangGraph — Añada puertas de lógica empresarial deterministas, un bucle de validación autocorregible y un supervisor con tres subgrafos especializados a un agente LangGraph para comercio electrónico.
- Triaje de tiquetes de soporte multiagente con LangGraph y Ollama — Cree un grafo de triaje de tiquetes con tres agentes utilizando LangGraph: un clasificador, un respondiente y una puerta humana, que funcionan con Ollama local o a través de una API, con solo una línea modificada.