LLMs

En este artículo, nos sumergiremos en un ejemplo práctico de construcción de un Agente usando LangGraph, LangChain, y un backend LLM de Groq (ej., llama3-8b-8192). El asistente:

  • Utiliza la memoria para gestionar el estado de la conversación.
  • Integra herramientas como la búsqueda de Google.
  • Gestiona la toma de decisiones sobre si llamar a herramientas o responder directamente.
  • Recorta el contexto para gestionar los límites de tokens.

Tabla de contenidos

¿Qué es un agente?

Un agente en LangChain es un sistema inteligente de toma de decisiones construido sobre un gran modelo lingüístico (LLM) que puede elegir dinámicamente las acciones a tomar basándose en la entrada del usuario y en el contexto actual de la conversación. Los agentes razonan paso a paso y deciden si llamar a una función (herramienta), hacer preguntas de seguimiento o devolver una respuesta final. Los agentes son especialmente útiles para flujos de trabajo complejos en los que las decisiones dependen de las condiciones de ejecución, las entradas del usuario o los resultados intermedios de las herramientas. Simulan el razonamiento pensando a través de una tarea e invocando herramientas según sea necesario.

🧠 ¿Qué es la llamada a herramientas?

La llamada a herramientas es el mecanismo que permite a un agente ampliar sus capacidades más allá de la mera generación de lenguaje. Cuando un agente recibe una pregunta, el LLM analiza si puede responder directamente o si necesita información o acciones externas. Si necesita una herramienta, el LLM devuelve un comando estructurado (JSON) que especifica el nombre de la herramienta y los argumentos necesarios. A continuación, LangChain analiza esta llamada a la herramienta, ejecuta la función Python real (por ejemplo, una API de búsqueda, una calculadora o una consulta a una base de datos) y devuelve el resultado al agente. El agente continúa razonando con esta nueva información y puede realizar pasos adicionales o responder al usuario.

  1. LangChain analiza la llamada a la herramienta, la ejecuta y devuelve el resultado a la conversación.
  2. LangGraph automatiza este flujo utilizando un grafo de nodos.

📦 Importaciones e inicialización

# Standard Python typing. from typing import Any, Dict, List
# Initializes a chat model like llama3 with a specified provider (Groq).
from langchain.chat_models import init_chat_model
# Handles message types and trimming for context management.
from langchain_core.messages import HumanMessage, SystemMessage, trim_messages
# Used for creating a structured message prompt for the model.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# Registers a function as a tool for the LLM to use.
from langchain_core.tools import tool

# Core of LangGraph - builds the state machine and controls the flow between nodes.
from langgraph.graph import END, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

# Adds memory to persist and recall conversation history.
from langgraph.checkpoint.memory import MemorySaver

⚙️ Configuración del modelo y de la herramienta

1. Inicialización del modelo de chat

chat_model = init_chat_model("llama3-8b-8192", model_provider="groq")

Esto inicializa el modelo llama3 alojado en Groq. Puedes sustituirlo por otros proveedores (por ejemplo, OpenAI, Anthropic).

2. Herramienta de búsqueda de Google

# Initializes a wrapper to interact with the Serper API.
search = GoogleSerperAPIWrapper()

@tool
def google_search(query: str) -> str:
"""Search the web for information related to a query.

Args:
query: The search query

Returns:
Search results as text
"""
try:
result = search.run(query)
return result
except Exception as e:
return "I couldn't perform the search due to a technical issue."

Registra una herramienta de búsqueda personalizada. Utiliza la API Serper y devuelve resultados de búsqueda para la cadena de consulta. Esta herramienta puede ser llamada por el LLM cuando sea necesario.

3. Contexto de recorte para la eficiencia de tokens

trimmer = trim_messages(
max_tokens=512,
strategy="last",
token_counter=chat_model,
include_system=True,
allow_partial=False,
start_on="human",
)

Esto recorta la lista de mensajes para garantizar que el aviso se mantiene dentro del límite de tokens (512 tokens), dando prioridad a los mensajes humanos recientes.

4. Plantilla del prompt

instructions = "You are my personal assistant. Your role is to assist me with research, idea generation, and planning. Provide clear, concise, and actionable insights. Always prioritize accuracy and relevance in your responses. When generating ideas, aim for creativity and practicality. Help me stay organized and focused on achieving my goals efficiently."

instructions_tools = "You are my personal assistant. Use the output from the tool to answer my query accurately. Ensure your response is concise, friendly, and focused on the requested information. Use the tool's output as a reference to support your explanation. If necessary, provide a simple and clear explanation, avoiding unnecessary technical jargon."
prompt_template = ChatPromptTemplate.from_messages(
[SystemMessage(content=instructions_tools), MessagesPlaceholder(variable_name="messages")]
)

Define cómo se estructurarán los mensajes del sistema y los mensajes dinámicos para cada llamada LLM.

🛠️Tool Binding

TOOL_LIST = [google_search]
llm_with_tools = chat_model.bind_tools(TOOL_LIST)

🔍 Explicación:

  • Indica al modelo las herramientas que puede llamar (google_search).
  • Permite al LLM decidir dinámicamente si utilizar una herramienta en función de la solicitud.

🔁 Nodos LangGraph

LangGraph se utiliza para crear una máquina de estados que define cómo fluyen los datos entre los nodos.

graph_builder = StateGraph(MessagesState)

# Add nodes
graph_builder.add_node("query_or_respond", query_or_respond)
graph_builder.add_node("tools", ToolNode(TOOL_LIST))
graph_builder.add_node("generate", generate)

# Set entry point
graph_builder.set_entry_point("query_or_respond")

# Define edges
graph_builder.add_conditional_edges(
"query_or_respond",
tools_condition,
{END: END, "tools": "tools"},
)

graph_builder.add_edge("tools", "generate")
graph_builder.add_edge("generate", END)

# Add memory
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)

1. LLM con herramientas

llm_with_tools = chat_model.bind_tools(TOOL_LIST)

Esto permite al LLM invocar herramientas externas (como google_search) durante la inferencia.

2. Nodo 1: query_or_respond

def query_or_respond(state: MessagesState) -> Dict[str, List]:
"""Generate a tool call or a direct response based on the input message.

Args:
state: Current conversation state

Returns:
Updated state with AI response
"""
messages = trimmer.invoke(state["messages"])
prmpt = prompt_template.invoke(messages)
response = llm_with_tools.invoke(prmpt)
return {"messages": [response]}

Este nodo

  • Permite que el LLM responda directamente.
  • O activa una llamada a la herramienta.

Pasos:

  • Recorta la conversación.
  • Crea un aviso utilizando los mensajes recortados.
  • Llamar al modelo de herramienta.

3. Nodo 2: herramientas (ToolNode)

Ejecuta cualquier herramienta necesaria (por ejemplo, google_search) solicitada por el nodo anterior.

4. Nodo 3: generar

def generate(state: MessagesState) -> Dict[str, List]:
"""Generate a comprehensive answer based on tool outputs.

Args:
state: Current conversation state including tool outputs

Returns:
Updated state with final AI response
"""
# Get generated tool messages
recent_tool_messages = []
for message in reversed(state["messages"]):
if message.type == "tool":
recent_tool_messages.append(message)
else:
break
tool_messages = recent_tool_messages[::-1]

# Format into prompt
docs_content = "\n\n".join(doc.content for doc in tool_messages)
system_message_content = instructions_tools + "\n\n" + f"{docs_content}"

# Prepare conversation context (excluding tool messages)
conversation_messages = [
message
for message in state["messages"]
if message.type in ("human", "system")
or (message.type == "ai" and not message.tool_calls)
]
prompt = [SystemMessage(content=system_message_content)] + conversation_messages

prompt = trimmer.invoke(prompt)

# Generate response
response = chat_model.invoke(prompt)
return {"messages": [response]}

Este nodo:

  • Recoge los mensajes de salida de la herramienta.
  • Los añade a las instrucciones del sistema.
  • Vuelve a generar una respuesta final utilizando el modelo de chat sin procesar (sin vinculación a la herramienta).

Esto garantiza que los resultados de la herramienta se incorporen a una respuesta coherente.

Almacena el historial de la conversación en la memoria durante la ejecución.

🧾 Función de chat

thread_id = "1"
message = "What is the capital of France?"

config = {"configurable": {"thread_id": thread_id}}

output = graph.invoke({"messages": HumanMessage(content=message)}, config)

for message in output["messages"]:
print(f"{message.type}")
print(message)

Esta es la función de inferencia principal para interactuar con el asistente.

  • Toma un mensaje de usuario y un ID de hilo (útil para el aislamiento de memoria).
  • Invoca el motor LangGraph con el mensaje.
  • Registra los mensajes de respuesta devueltos por el asistente.
# Expected Output
human
content='What is the capital of France?' additional_kwargs={} response_metadata={} id='fe525805-06fd-4a8e-b726-109299c84b80'
ai
content='' additional_kwargs={'tool_calls': [{'id': 'call_pc1v', 'function': {'arguments': '{"query":"What is the capital of France?"}', 'name': 'google_search'}, 'type': 'function'}]} response_metadata={'token_usage': {'completion_tokens': 67, 'prompt_tokens': 998, 'total_tokens': 1065, 'completion_time': 0.055833333, 'prompt_time': 0.125277397, 'queue_time': 0.01990629299999999, 'total_time': 0.18111073}, 'model_name': 'llama3-8b-8192', 'system_fingerprint': 'fp_dadc9d6142', 'finish_reason': 'tool_calls', 'logprobs': None} id='run-fba65750-32c8-47ff-9e2d-d40f44f5fb2c-0' tool_calls=[{'name': 'google_search', 'args': {'query': 'What is the capital of France?'}, 'id': 'call_pc1v', 'type': 'tool_call'}] usage_metadata={'input_tokens': 998, 'output_tokens': 67, 'total_tokens': 1065}
tool
content="Paris is the capital and largest city of France. With an estimated population of 2,048,472 residents in January 2025 in an area of more than 105 km2 (41 sq ... Paris is the capital of France, the largest country of Europe with 550 000 km2 (65 millions inhabitants). Paris has 2.234 million inhabitants end 2011. France is a semi-presidential republic and its capital, largest city and main cultural and economic centre is Paris. Paris, city and capital of France, located along the Seine River, in the north-central part of the country. Paris is one of the world's most important and ... Paris is the capital and most populous city of France. Situated on the Seine River, in the north of the country, it is in the centre of the Île-de-France ... The capital and by far the most important city of France is Paris, one of the world's preeminent cultural and commercial centres. Paris is the city of romance par excellence, the fashion capital and the best example of French art de vivre. Exploring Paris is an essential rite of passage ... Besides Paris, what is the capital of France? - Answer: F. Paris became capital of France because France evolved from a federation of counties to a kingdom, where the king was living a Paris. The ... What Is the Capital of France? Paris. The very name brings to mind tree-lined boulevards, grand monuments, sidewalk cafes, and so much more." name='google_search' id='5c1000a6-b172-4b77-bceb-76dfc3e3234b' tool_call_id='call_pc1v'
ai
content='According to the provided information, the answer is: Paris.' additional_kwargs={} response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 393, 'total_tokens': 406, 'completion_time': 0.010833333, 'prompt_time': 0.049380433, 'queue_time': 0.017139350999999997, 'total_time': 0.060213766}, 'model_name': 'llama3-8b-8192', 'system_fingerprint': 'fp_a97cfe35ae', 'finish_reason': 'stop', 'logprobs': None} id='run-eb34a578-b383-4379-ae6f-ad4209ea6f1a-0' usage_metadata={'input_tokens': 393, 'output_tokens': 13, 'total_tokens': 406}

✅ Resumen: Qué hace este código

Componente Descripción init_chat_model Inicializa el LLM (LLaMA3 de Groq) @tool google_search Una herramienta de búsqueda de Google personalizada trimmer Recorta el historial de mensajes para mantenerse dentro del límite de tokens query_or_respond Decide si responder o llamar a una herramienta ToolNode Ejecuta herramientas como search generate Genera una respuesta final utilizando la salida de la herramienta LangGraph Gestiona el flujo utilizando una máquina de estados MemorySaver Realiza un seguimiento de las conversaciones en curso chat() Interfaz unificada para interactuar con el asistente.

import logging
from typing import Any, Dict, List

from langchain.chat_models import init_chat_model
from langchain_community.utilities import GoogleSerperAPIWrapper
from langchain_core.messages import HumanMessage, SystemMessage, trim_messages
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

import api_keys

# os.environ["SERPER_API_KEY"] = "my-api-key"
# os.environ["GROQ_API_KEY"] = "my-api-key"

# Initialize chat model
chat_model = init_chat_model("llama3-8b-8192", model_provider="groq")

search = GoogleSerperAPIWrapper()


@tool
def google_search(query: str) -> str:
"""Search the web for information related to a query.

Args:
query: The search query

Returns:
Search results as text
"""
try:
result = search.run(query)
return result
except Exception as e:
return "I couldn't perform the search due to a technical issue."


# Message trimmer to manage context window
trimmer = trim_messages(
max_tokens=512,
strategy="last",
token_counter=chat_model,
include_system=True,
allow_partial=False,
start_on="human",
)

# Load system instructions
instructions = "You are my personal assistant. Your role is to assist me with research, idea generation, and planning. Provide clear, concise, and actionable insights. Always prioritize accuracy and relevance in your responses. When generating ideas, aim for creativity and practicality. Help me stay organized and focused on achieving my goals efficiently."

instructions_tools = "You are my personal assistant. Use the output from the tool to answer my query accurately. Ensure your response is concise, friendly, and focused on the requested information. Use the tool's output as a reference to support your explanation. If necessary, provide a simple and clear explanation, avoiding unnecessary technical jargon."

prompt_template = ChatPromptTemplate.from_messages(
[
SystemMessage(content=instructions_tools),
MessagesPlaceholder(variable_name="messages"),
]
)

# Define available tools
TOOL_LIST = [google_search]

llm_with_tools = chat_model.bind_tools(TOOL_LIST)


def query_or_respond(state: MessagesState) -> Dict[str, List]:
"""Generate a tool call or a direct response based on the input message.

Args:
state: Current conversation state

Returns:
Updated state with AI response
"""
messages = trimmer.invoke(state["messages"])
prmpt = prompt_template.invoke(messages)
response = llm_with_tools.invoke(prmpt)
return {"messages": [response]}


def generate(state: MessagesState) -> Dict[str, List]:
"""Generate a comprehensive answer based on tool outputs.

Args:
state: Current conversation state including tool outputs

Returns:
Updated state with final AI response
"""
# Get generated tool messages
recent_tool_messages = []
for message in reversed(state["messages"]):
if message.type == "tool":
recent_tool_messages.append(message)
else:
break
tool_messages = recent_tool_messages[::-1]

# Format into prompt
docs_content = "\n\n".join(doc.content for doc in tool_messages)
system_message_content = instructions_tools + "\n\n" + f"{docs_content}"

# Prepare conversation context (excluding tool messages)
conversation_messages = [
message
for message in state["messages"]
if message.type in ("human", "system")
or (message.type == "ai" and not message.tool_calls)
]
prompt = [SystemMessage(content=system_message_content)] + conversation_messages

prompt = trimmer.invoke(prompt)

# Generate response
response = chat_model.invoke(prompt)
return {"messages": [response]}


# Define and build the state graph
def build_graph():
"""Build and return the state graph for conversation flow."""
graph_builder = StateGraph(MessagesState)

# Add nodes
graph_builder.add_node("query_or_respond", query_or_respond)
graph_builder.add_node("tools", ToolNode(TOOL_LIST))
graph_builder.add_node("generate", generate)

# Set entry point
graph_builder.set_entry_point("query_or_respond")

# Define edges
graph_builder.add_conditional_edges(
"query_or_respond",
tools_condition,
{END: END, "tools": "tools"},
)

graph_builder.add_edge("tools", "generate")
graph_builder.add_edge("generate", END)

# Add memory
memory = MemorySaver()
return graph_builder.compile(checkpointer=memory)


# Build graph
graph = build_graph()


def chat(message: str, thread_id: str) -> str:
"""Process a user message and return an AI response.

Args:
message: User input message
thread_id: Unique identifier for the conversation thread

Returns:
AI response text
"""
# Set up configuration
config = {"configurable": {"thread_id": thread_id}}

output = graph.invoke({"messages": HumanMessage(content=message)}, config)

# For Debugging
for message in output["messages"]:
print(f"{message.type}")
print(message)

# End

return output["messages"][-1].content

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *