Inicio / Artículos

Construir un asistente de IA mínimo desde cero con agentes, herramientas y habilidades.

Un pequeño proyecto en Python que dirige una solicitud a los agentes, llama a una calculadora y a una herramienta de conversión de unidades, ejecuta una habilidad de resumen y envía la solicitud reestructurada a un LLM.

Editor: REACTAPP.TOP12 min de lectura2649 palabras

Fuentes: Agents, Tools, and Skills for a Working Mini AI Assistant

Pregúntale a un chatbot cómo era el clima en Chicago la semana pasada y él adivinará. Dale al mismo chatbot una herramienta que verifique realmente la información, y él lo sabrá. La diferencia entre un modelo que adivina y un sistema que sabe es precisamente lo que los agentes, herramientas y habilidades reducen, y esto explica en gran medida por qué la IA generativa se ha convertido en un tema tan discutido dentro de las tecnologías de la información.

La barrera de entrada es más baja de lo que parece. Si puedes realizar una búsqueda decente en Google, también puedes escribir un prompt adecuado. Sin embargo, un solo prompt tiene sus limitaciones. La parte interesante comienza cuando dejas de esperar que el modelo sepa todo y le proporcionas agentes, herramientas y habilidades en las que apoyarse. A continuación encontrarás una explicación de los tres, con ejemplos en Python que puedes ejecutar tú mismo, basados en un pequeño ejemplo de planificación de viajes desde el principio hasta el final.

Herramientas, habilidades y agentes definidos

Las herramientas suelen provenir de una fuente externa, como un servidor MCP (Model Context Protocol). Al llamar a ese servidor, generalmente se obtiene una lista de herramientas que ayudan con la solicitud. Supongamos que deseas información meteorológica en una solicitud. Un servidor MCP de clima podría exponer herramientas como estas, donde get_forecast devuelve el pronóstico actual. Si tu solicitud pregunta cuánto ha cambiado el clima en Chicago durante la semana pasada, dicha herramienta añadirá la instrucción adecuada para que la búsqueda tenga éxito.

get_current_conditions
get_forecast
get_alerts
get_historical_weather
get_air_quality
get_marine_conditions
get_river_conditions
get_wildfire_info
search_location
check_service_status

Esa lista es solo una muestra de lo que podría ofrecer un servidor de herramientas. El ejemplo práctico más abajo reemplaza estas por dos herramientas personalizadas más simples: una calculadora y un convertidor de distancias.

Las habilidades son algo similar a recetas. Si escribes para Medium, una de tus habilidades podría ser una guía de estilo. En muchos proyectos de ingeniería de datos, se trata de listas de cálculos o valores comparables, lo que las convierte en algo parecido a tablas de referencia. Si una solicitud pide los precios de todos los vuelos desde la zona de Chicago, una habilidad podría incluir ORD (O’Hare), MDW (Midway) o CHI, el código que abarca a todos los aeropuertos de la ciudad. Aquí están las familias de medidas que podría contener una habilidad:

Temperature = Celsius, Fahrenheit, Kelvin

Length / Distance = Inches, Feet, Yards, Miles, Millimeters, Centimeters, Meters, Kilometers

Mass / Weight = Ounces, Pounds, Stone, Grams, Kilograms, Metric Tons

Volume / Capacity = Fluid Ounces, Cups, Pints, Quarts, Gallons, Milliliters, Liters, Cubic Meters

Area =  Square Feet, Square Meters, Acres, Hectares

Speed =  Miles per Hour (mph), Kilometers per Hour (km/h), Knots, Meters per Second (m/s)

Time Zones =  UTC/GMT offsets, Daylight Saving Time (DST) transitions, Unix timestamps to human-readable dates

Storage =  Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)

Otra vez, esa es una ilustración general de lo que podría contener la tabla de búsqueda de una habilidad. El ejemplo aquí solo necesita las filas de distancia, para convertir entre millas, kilómetros, metros y pies.

Los agentes son lo que hace que el proceso de una IA parezca que está razonando. En la práctica, tu prompt pasa por varios bucles, cada uno basado en el anterior. El primer prompt proporciona al sistema suficiente orientación para dar el primer paso. Luego decide cuál debería ser el siguiente paso y continúa mientras tu código lo permita. Piensa en un libro de “Elige tu propia aventura”: cada iteración puede ramificarse en una dirección diferente para llegar a una respuesta adecuada.

Desarrollándolo en código

La arquitectura es intencionalmente sencilla. Puedes escribir el código en un cuaderno de notas de Jupyter o en Google Colab; no cuesta nada y es fácil de ajustar. La guía aborda primero las partes del código, en orden inverso de ejecución, y la sección posterior muestra cómo se comporta todo cuando se procesa una solicitud real.

Paso 1: importaciones

Las bibliotecas requests, getpass y os obtienen la clave y llaman al modelo Groq LLM. La biblioteca re ayuda a encontrar patrones de caracteres dentro de la solicitud, y todas las herramientas y habilidades dependen de ella. Necesitas Python 3.10 o una versión más reciente. Si requests no está instalado, ejecuta primero !pip install requests.

import re
import os
import getpass
import requests

Paso 2: la calculadora

Una expresión regular extrae cada monto en dólares del texto de entrada y la función los suma. Simplemente busca un $ seguido de dígitos y suma esos números. Si trabajas con libras esterlinas, tendrías que cambiar el patrón.

def calculator_tool(prompt: str) -> str:
    print("    [Tool 1: Calculator] scanning prompt for dollar amounts...")
    dollar_amounts = re.findall(r'\$\s?(\d+(?:\.\d{1,2})?)', prompt)

    if not dollar_amounts:
        result = "no dollar amounts found"
        print(f"    [Tool 1: Calculator] {result}")
        return result

    values = [float(a) for a in dollar_amounts]
    total = sum(values)
    breakdown = " + ".join(f"${v:g}" for v in values)
    result = f"{breakdown} = ${total:.2f}"
    print(f"    [Tool 1: Calculator] found {len(values)} amount(s) {values} -> {result}")
    return result

Paso 3: una herramienta de conversor de unidades

Esta herramienta convierte unidades de distancia comunes. Primero normaliza cualquier unidad que haya utilizado el usuario, a través del diccionario UNIT_ALIASES, y luego convierte cada distancia a millas mediante el diccionario DISTANCE_TO_MILES, lo que facilita el cálculo total. Dependiendo de su caso de uso, también podría agregar conversiones de kilómetros o de recorrido caminado.

UNIT_ALIASES = {
    "kilometers": "km", "kilometer": "km", "km": "km",
    "mile": "miles", "miles": "miles",
    "m": "meters", "meter": "meters", "meters": "meters",
    "ft": "feet", "foot": "feet", "feet": "feet",
}

DISTANCE_TO_MILES = {"km": 0.621371, "miles": 1.0, "meters": 0.000621371, "feet": 0.000189394}

def unit_converter_tool(prompt: str) -> str:
    print("    [Tool 2: Unit Converter] scanning prompt for distance legs...")
    legs = re.findall(r'(\d+(?:\.\d+)?)\s*(kilometers?|km|miles?|meters?|feet|ft)\b',
                       prompt, re.IGNORECASE)

    if not legs:
        result = "no distances found"
        print(f"    [Tool 2: Unit Converter] {result}")
        return result

    total_miles = 0.0
    breakdown = []
    for value, unit in legs:
        value = float(value)
        unit_norm = UNIT_ALIASES.get(unit.lower(), unit.lower())
        total_miles += value * DISTANCE_TO_MILES.get(unit_norm, 1.0)
        breakdown.append(f"{value:g} {unit_norm}")
    result = f"{' + '.join(breakdown)} = {round(total_miles, 2)} miles total"
    print(f"    [Tool 2: Unit Converter] found {len(legs)} leg(s) -> {result}")
    return result

Paso 4: una habilidad de resumen

Un resumidor ligero. Evalúa cada oración eliminando las palabras de parada enumeradas en el conjunto stopwords y clasifica lo que queda según la cantidad de palabras significativas que quedan. El objetivo es descartar las oraciones que no aportan nada al prompt y reducir el uso innecesario de tokens.

def summarizer_skill(text: str, max_sentences: int = 2) -> str:
    print("    [Skill: Summarizer] scanning message for key sentence(s)...")
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s]

    if len(sentences) <= max_sentences:
        print(f"    [Skill: Summarizer] only {len(sentences)} sentence(s) -- returning as-is")
        return text.strip()

    stopwords = {"the","a","an","is","are","was","were","in","on","at","to","of","and",
                 "or","for","it","this","that","i","you","he","she","they","we","really"}
    words = re.findall(r'\b\w+\b', text.lower())
    freq = {}
    for w in words:
        if w not in stopwords:
            freq[w] = freq.get(w, 0) + 1

    scored = []
    for idx, sentence in enumerate(sentences):
        s_words = re.findall(r'\b\w+\b', sentence.lower())
        score = sum(freq.get(w, 0) for w in s_words)
        scored.append((score, idx, sentence))

    top = sorted(scored, key=lambda x: x[0], reverse=True)[:max_sentences]
    top_in_order = sorted(top, key=lambda x: x[1])
    summary = " ".join(s for _, _, s in top_in_order)
    print(f"    [Skill: Summarizer] kept {len(top_in_order)} of {len(sentences)} sentence(s) -> {summary}")
    return summary

Paso 5: conectar un LLM

Aquí el código se comunica con un modelo real. Se utiliza Groq porque cuenta con una versión gratuita, pero es necesario contar con la propia clave API desde la consola de Groq para que funcione.

GROQ_API_KEY = os.environ.get("GROQ_API_KEY") or getpass.getpass(
    "Enter your free Groq API key (from https://console.groq.com/keys), "
    "or press Enter to skip: "
)

GROQ_MODEL = "openai/gpt-oss-20b"
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"

def call_llm(augmented_prompt: str,
             system_prompt: str = "You are a helpful, concise assistant.") -> str:
    if not GROQ_API_KEY:
        return ("[No LLM reply -- no Groq API key was provided. Get a free one at "
                 "https://console.groq.com/keys, then re-run the setup cell above.]\n"
                 f"Here is the augmented prompt that would have been sent:\n\"\"\"\n{augmented_prompt}\n\"\"\"")
    try:
        response = requests.post(
            GROQ_ENDPOINT,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {GROQ_API_KEY}",
            },
            json={
                "model": GROQ_MODEL,
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": augmented_prompt},
                ],
                "temperature": 0.7,
                "max_tokens": 400,
            },
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()
        return data["choices"][0]["message"]["content"].strip()
    except requests.exceptions.RequestException as e:
        return f"[LLM request failed -- {e}]"
    except (KeyError, IndexError, ValueError):
        return "[LLM returned an unexpected response format.]"

print("LLM configured." if GROQ_API_KEY else "No key entered -- running in fallback mode.")

Paso 6: los agentes

Aquí es donde se configuran los agentes. Ellos aportan contexto a tu solicitud, y podrás ver cómo utilizan las herramientas y habilidades para obtener datos precisos.

def show_step(step_num: int, label: str, content: str) -> None:
    """Small helper so every agent prints its pipeline the same, readable way."""
    print(f"\n  STEP {step_num} - {label}:")
    for line in str(content).splitlines() or [""]:
        print(f"    {line}")


def trip_planner_agent(prompt: str) -> str:
    """Agent 1. Condition: 2+ dollar costs AND 2+ distances -- a multi-stop itinerary."""
    print("[Router] -> Agent 1: Trip Planner Agent activated (detected an itinerary: multiple costs + multiple distances)")
    show_step(1, "Original prompt", prompt)

    cost_result = calculator_tool(prompt)
    show_step(2, "Tool result (Calculator -- total cost)", cost_result)

    distance_result = unit_converter_tool(prompt)
    show_step(3, "Tool result (Unit Converter -- total distance)", distance_result)

    augmented_prompt = (
        f"The user asked: \"{prompt}\"\n\n"
        f"A calculator tool already computed the total cost: {cost_result}\n"
        f"A distance tool already computed the total distance traveled: {distance_result}\n\n"
        "Using those two verified totals (don't redo either calculation yourself), give the "
        "user a short, friendly trip summary that reports both totals clearly."
    )
    show_step(4, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(5, "Final response from Groq", reply)

    return f"🧳 Agent 1: Trip Planner Agent:\n{reply}"


def text_agent(prompt: str) -> str:
    """Agent 2. Condition: default for any prompt, OR chained after Agent 1
    when the itinerary prompt also has an extra narrative sentence."""
    print("[Router] -> Agent 2: Text Analysis Agent activated")
    show_step(1, "Original prompt", prompt)

    summary = summarizer_skill(prompt)
    show_step(2, "Skill result (Summarizer)", summary)

    augmented_prompt = (
        f"The user wrote: \"{prompt}\"\n\n"
        f"Automatic summary of their message: {summary}\n\n"
        "Write a short, thoughtful, natural-sounding reply to the user that responds "
        "to what they actually said, informed by (but not just repeating) this summary."
    )
    show_step(3, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(4, "Final response from Groq", reply)

    return f"📝 Agent 2: Text Analysis Agent:\n{reply}"

Paso 7: el router

El router es el cerebro de toda la operación. Busca características específicas en una solicitud y la envía al agente o agentes adecuados. Un detalle importante: has_extra_narrative es una condición que se verifica únicamente después de que trip_planner_agent ya haya realizado la correspondencia. El text_agent se ejecuta como valor predeterminado para cualquier solicitud o junto con el planificador de viajes siempre que esa condición sea verdadera.

def looks_like_itinerary(prompt: str) -> bool:
    dollar_amounts = re.findall(r'\$\s?\d+(?:\.\d{1,2})?', prompt)
    distance_legs = re.findall(r'\d+(?:\.\d+)?\s*(?:kilometers?|km|miles?|meters?|feet|ft)\b',
                                prompt, re.IGNORECASE)
    return len(dollar_amounts) >= 2 and len(distance_legs) >= 2

def has_extra_narrative(prompt: str) -> bool:
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', prompt.strip()) if s]
    extra = [
        s for s in sentences
        if "quot; not in s
        and not re.search(r'\b(?:miles?|km|kilometers?|feet|ft)\b', s, re.IGNORECASE)
        and not s.strip().endswith("?")
    ]
    return len(extra) >= 1

def route_prompt(prompt: str) -> str:
    if looks_like_itinerary(prompt):
        reply = trip_planner_agent(prompt)
        if has_extra_narrative(prompt):
            print("[Router] -> also routing to Agent 2: Text Analysis Agent (extra narrative sentence detected)")
            reply += "\n\n" + text_agent(prompt)
        return reply
    else:
        return text_agent(prompt)

Paso 8: probarlo

Es hora de poner a prueba todo.

prompt = input("Ask me anything: ")

print("=" * 60)
print(f"PROMPT: {prompt}")
print("=" * 60)
final_answer = route_prompt(prompt)
print(f"\n  >>> RETURNED: {final_answer}")
print("-" * 60 + "\n")

Siguiendo una instrucción a través del sistema

Tome esta instrucción y siga cada etapa:

"Un partido de los Cubs en Wrigley Field (boleto de $55) dio inicio al día. Luego vino un viaje de 5.5 millas hasta el Art Institute of Chicago (entrada de $32), 1.5 millas más hasta el Shedd Aquarium ($20) y otras 2.3 millas hasta una cena de tipo deep-dish en Giordano’s, en River North, por $30. ¿Cuánto costó el día en total y cuán largo fue el recorrido? Béisbol, arte, vida marina y pizza convirtieron ese día en uno muy ocupado; sería agradable incluir un breve resumen en el diario del viaje."

Ruta

Todo comienza en el router de solicitudes. Si detecta algo similar a moneda, como un $, junto con una distancia en millas o km, dirige la solicitud al planificador de viajes, Agente 1. A continuación evalúa has_extra_narrative, la cual solo se ejecuta una vez que el planificador de viajes encuentra coincidencias, y busca una oración adicional no relacionada con el costo o la distancia. Si existe alguna, el router también llama al agente de texto, Agente 2. Cuando ni el patrón del itinerario ni una oración adicional aparecen, el agente de texto maneja la solicitud por sí solo. En este caso se cumplen ambas condiciones, por lo que ambos agentes se activan. Resaltar la solicitud en color permite saber qué partes activaron a cada uno.

El planificador de viajes llama a sus herramientas

El agente del planificador de viajes llama tanto al calculador como al conversor de unidades.

El calculador encuentra todas las cantidades en dólares y devuelve una cadena de texto:

$55 + $32 + $20 + $30 = $137.00

El conversor de unidades calcula las distancias y muestra:

5.5 millas, 1.5 millas y 2.3 millas suman un total de 9.3 millas

Se reconstruye la solicitud

Los dos resultados vuelven al planificador de viajes, el cual los utiliza para reconstruir la solicitud, añadiendo los totales calculados al texto original. Esa nueva solicitud se envía a Groq, el LLM utilizado en la demostración, el cual podría responder más o menos así:

Qué día tan maravilloso en Chicago.

  • Gasto: $137
  • Distancia: 9.3 millas

Conserva los recuerdos, y también el deep-dish.

El agente de texto y el resumidor

Después de que el primer agente terminó, has_extra_narrative volvió a ser verdadero, por lo que se activó el agente de texto y la solicitud original fue enviada a la habilidad de resumen. Dado que la solicitud contenía más oraciones de lo permitido por el límite de dos oraciones de dicha habilidad, esta conservó las dos oraciones con la puntuación más alta y descartó por completo la pregunta intermedia. Al comparar la solicitud original con la resultante, se observa que la pregunta sobre costos y millas ha desaparecido, quedando únicamente las oraciones narrativas.

Esa solicitud compuesta únicamente por texto narrativo es enviada a Groq una segunda vez, y el modelo genera una reflexión detallada en forma de diario sin tener que recalcular nada. Las cifras relacionadas con dólares y millas en su respuesta provienen del Agente 1, en una etapa anterior del proceso, y no de este paso, que es precisamente el objetivo de dividir el trabajo entre los agentes:

El costo total de tu día fue de $137 y cubrió 9.3 millas.

Resumen del diario:

  • Juego en Wrigley Field: $55
  • Instituto de Arte, a 5.5 millas: $32
  • Aquario Shedd, a 1.5 millas: $20
  • Deep-dish en Giordano's, a 2.3 millas: $30

Béisbol, arte, criaturas marinas y pizza de Chicago para un día lleno de actividades.

Por qué vale la pena tener este patrón

El ejemplo muestra que los agentes, habilidades y herramientas permiten a un sistema reconstruir tu prompt en segundo plano, sin que tú veas los pasos adicionales. Un router y los agentes trabajan juntos para decidir qué herramientas o habilidades se necesitan, recopilar los datos, remodelar el prompt en función de ellos y pasar el resultado al LLM. En implementaciones reales se sucederían varias rondas, permitiendo que el sistema enriquezca y perfeccione el prompt cada vez que aprende algo nuevo.

Las herramientas aquí son simplemente funciones. En entornos de producción, con frecuencia se encuentran en un servidor MCP creado por otra persona y están listas para ser llamadas. Las habilidades también serían ligeramente diferentes, ya que los desarrolladores suelen escribir esa guía en un archivo Markdown en lugar de en una función de Python como la mostrada anteriormente.

No necesitas un equipo grande ni un framework complejo para comenzar. Un router más dos o tres herramientas pequeñas, junto con un modelo que permita depender de ellas en lugar de improvisar, te ayudará a dar los primeros pasos. Puedes reemplazarlas por tus propias herramientas, habilidades o agentes para personalizarlo, y luego crear las adecuadas para cualquier problema que tengas entre manos.