Agentes SQL
Construye agentes de consulta SQL con LangGraph, vinculación de herramientas de base de datos, generación texto-a-SQL, validación y ejecución de consultas e interpretación de resultados.
Los agentes SQL permiten a los usuarios consultar bases de datos en lenguaje natural. El agente convierte la pregunta a SQL, la valida, la ejecuta e interpreta los resultados. Esta lección cubre patrones de text-to-SQL seguros y fiables.
Arquitectura
Natural Language Query
↓
SQL Agent
├── 1. Understand schema → Describe tables and columns
├── 2. Generate SQL → LLM converts question to SQL
├── 3. Validate SQL → Syntax check, safety checks
├── 4. Execute SQL → Run against database
└── 5. Interpret results → LLM explains the results
↓
Natural Language Answer
Conexión con Base de Datos
from sqlalchemy import create_engine, text, inspect
from sqlalchemy.engine import Engine
from langchain_core.tools import tool
# Database connection
DATABASE_URL = "postgresql://user:pass@localhost:5432/mydb"
engine = create_engine(DATABASE_URL)Nunca expongas tu base de datos directamente a los usuarios. Usa un usuario de base de datos de solo lectura y una lista blanca de consultas permitidas. Valida siempre el SQL antes de ejecutarlo.
Herramienta de Descubrimiento de Esquema
@tool
def get_schema() -> str:
"""Get the database schema (tables, columns, types, and relationships)."""
inspector = inspect(engine)
schema = []
for table_name in inspector.get_table_names():
columns = inspector.get_columns(table_name)
cols = [f" - {c['name']}: {c['type']}" for c in columns]
schema.append(f"Table: {table_name}\n" + "\n".join(cols))
# Foreign keys
fks = inspector.get_foreign_keys(table_name)
for fk in fks:
schema.append(f" FK: {fk['constrained_columns']} → {fk['referred_table']}.{fk['referred_columns']}")
return "\n\n".join(schema)Generación Texto-a-SQL
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o")
sql_prompt = ChatPromptTemplate.from_messages([
("system", "You are a SQL expert. Convert natural language questions to SQL queries.\n"
"Database schema:\n{schema}\n\n"
"Rules:\n"
"1. Use only SELECT statements (no INSERT/UPDATE/DELETE)\n"
"2. Use LIMIT to restrict results (max 100 rows)\n"
"3. Use proper JOIN syntax\n"
"4. Return ONLY the SQL query, no explanations"),
("human", "{question}")
])
def generate_sql(question: str, schema: str) -> str:
chain = sql_prompt | llm | StrOutputParser()
sql = chain.invoke({"question": question, "schema": schema})
# Clean the SQL (remove markdown fences if present)
sql = sql.replace("```sql", "").replace("```", "").strip()
return sqlValidación SQL
import re
from typing import Tuple
FORBIDDEN_PATTERNS = [
r'\bINSERT\b', r'\bUPDATE\b', r'\bDELETE\b',
r'\bDROP\b', r'\bALTER\b', r'\bCREATE\b',
r'\bTRUNCATE\b', r'\bGRANT\b', r'\bREVOKE\b',
r'\bEXEC\b', r'\bEXECUTE\b', r';.*;' # Multiple statements
]
def validate_sql(sql: str) -> Tuple[bool, str]:
"""Validate SQL for safety and syntax."""
# Check for forbidden operations
for pattern in FORBIDDEN_PATTERNS:
if re.search(pattern, sql, re.IGNORECASE):
return False, f"Forbidden SQL operation detected: {pattern.strip('\\\\b')}"
# Must be a SELECT statement
if not sql.strip().upper().startswith("SELECT"):
return False, "Only SELECT queries are allowed"
# Syntax check (try to explain the query)
try:
with engine.connect() as conn:
conn.execute(text(f"EXPLAIN {sql}"))
except Exception as e:
return False, f"SQL syntax error: {str(e)}"
return True, "Valid"El comando EXPLAIN prueba la sintaxis sin ejecutar la consulta, lo que lo convierte en un paso de validación seguro antes de ejecutar la consulta real.
Herramienta de Ejecución SQL
@tool
def execute_sql(sql: str) -> str:
"""Execute a SQL SELECT query and return results as formatted text."""
# Validate first
valid, msg = validate_sql(sql)
if not valid:
return f"Validation error: {msg}"
# Execute
try:
with engine.connect() as conn:
result = conn.execute(text(sql))
rows = result.fetchmany(100)
columns = result.keys()
# Format as text table
output = []
output.append(" | ".join(columns))
output.append("-" * len(output[0]))
for row in rows:
output.append(" | ".join(str(val)[:50] for val in row))
return "\n".join(output) if len(output) > 2 else "No results found."
except Exception as e:
return f"Execution error: {str(e)}"Agente SQL Completo
from langgraph.graph import StateGraph, START, END, add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolExecutor
from langchain_core.messages import SystemMessage, ToolMessage
from typing_extensions import TypedDict, Annotated
from typing import List, Any
tools = [get_schema, execute_sql]
llm_with_tools = llm.bind_tools(tools)
tool_executor = ToolExecutor(tools)
class SQLState(TypedDict):
messages: Annotated[List[Any], add_messages]
schema: str
def agent_node(state: SQLState) -> dict:
system = """You are a SQL assistant. Follow these steps:
1. Use get_schema to understand the database structure
2. Generate a SQL query for the user's question
3. Use execute_sql to run it
4. Interpret the results for the user in plain English
Rules:
- Only use SELECT queries
- Explain results in natural language
- If a query returns an error, fix it and retry"""
messages = [SystemMessage(system)] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
def tools_node(state: SQLState) -> dict:
last = state["messages"][-1]
if not last.tool_calls:
return {}
return {"messages": [
ToolMessage(content=str(tool_executor.invoke(tc)), tool_call_id=tc["id"])
for tc in last.tool_calls
]}
def router(state: SQLState) -> str:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return "end"
builder = StateGraph(SQLState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tools_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", router, {"tools": "tools", "end": END})
builder.add_edge("tools", "agent")
app = builder.compile(checkpointer=MemorySaver())
# Example usage
result = app.invoke({
"messages": [("human", "Show me the top 5 customers by total orders")],
"schema": ""
})
print(result["messages"][-1].content)El agente descubre el esquema de forma autónoma, genera el SQL, lo valida y lo ejecuta, y luego explica los resultados, todo en un bucle hasta que se responda la pregunta del usuario.
Conversaciones SQL de Múltiples Turnos
# Follow-up questions maintain context
session = {"configurable": {"thread_id": "sql-session-1"}}
app.invoke({"messages": [("human", "What tables exist in the database?")]}, session)
# Agent: Uses get_schema, describes the tables
app.invoke({"messages": [("human", "How many customers are from New York?")]}, session)
# Agent: Already knows the schema, generates and executes the query
app.invoke({"messages": [("human", "Show me the details")]}, session)
# Agent: Understands context from conversation historyRecuperación de Errores
def resilient_agent(state: SQLState) -> dict:
"""Agent with error recovery — retries with fixed SQL on error."""
last = state["messages"][-1]
error_attempts = sum(1 for m in state["messages"]
if hasattr(m, 'content') and "Execution error:" in m.content)
if error_attempts > 3:
return {"messages": [AIMessage(
"I'm having trouble with this query. Let me try a different approach."
)]}
# Continue with normal agent logic
return agent_node(state)Lleva la cuenta de los intentos fallidos en el estado. Tras N fallos, prueba un enfoque distinto (consulta más simple, haz una pregunta de aclaración) en vez de reintentar indefinidamente.
Preguntas de Práctica (Practice Questions)
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What is the first step a SQL agent should take when processing a query?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What SQL operations should be forbidden in a SQL agent?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How can you validate SQL syntax without executing the query?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
Why should the SQL agent use a read-only database user?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What does the get_schema tool return?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How should a SQL agent handle a query that returns an error?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What is a good maximum number of retry attempts for a SQL agent?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How does the agent in the SQL example decide when to stop executing tools?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What formatting does the execute_sql tool use for results?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
Why is the schema discovery important for text-to-SQL generation?
Conclusiones Clave
- Los agentes SQL siguen: descubrimiento del esquema → generación de SQL → validación → ejecución → interpretación
- Usa un usuario de base de datos de solo lectura y valida el SQL antes de ejecutarlo
- EXPLAIN ofrece validación segura de sintaxis sin ejecutar la consulta
- Recuperación de errores: analiza el error, corrige el SQL y reintenta (hasta un límite)
- Las conversaciones con varios turnos mantienen el contexto sobre la base de datos y las consultas anteriores
- Formatea los resultados como tablas de texto para la interpretación del LLM
- El descubrimiento correcto del esquema es esencial para un text-to-SQL preciso