☀️ 🌙
Artefacto 4 · AI Engineering · Concepto 01 (Fase 2)

El agente por dentro

La maquinaria del "por detrás": function calling, el bucle piensa→actúa→observa, y por qué lo escribimos a mano.

En la Fase 1 escribiste tres funciones de Python y las llamaste tú, en orden, a mano. En la Fase 2 le quitas las manos del volante: le das las tres al modelo y es él quien decide cuál usar, en qué orden y cuándo ha terminado. Este documento es el plano de esa máquina — cada cacho de código de aquí es el que vamos a implementar a continuación.

Lo que hay dentro
  1. De la cadena al bucle (qué cambia respecto al RAG)
  2. Function calling: el contrato modelo ⇄ código
  3. El menú de herramientas (tool schema)
  4. El bucle: piensa → actúa → observa
  5. El despachador: de un nombre a una función
  6. La memoria: una lista de mensajes que crece
  7. Una vuelta completa, mensaje a mensaje
  8. Guardarraíles: que no se vuelva infinito
  9. Diccionario del mundo real (para tu CV)

01 De la cadena al bucle

El Artefacto 2 (RAG DOBE) es una cadena: un camino recto y fijo que tú programaste. Siempre pasa lo mismo, en el mismo orden:

RAG (Artefacto 2): pregunta → recuperar → umbral → generar → responder el camino lo fijaste TÚ en el código

Un agente es distinto: le das herramientas y una meta, y el camino lo traza el modelo en tiempo real. Puede usar una herramienta, ver el resultado, decidir usar otra, o parar. No hay un orden grabado a fuego.

Agente (Artefacto 4): oferta → [el modelo decide] → ¿herramienta? → observa ▲ │ └────────── repite ─────────────┘ hasta que dice "listo"

La frase para una entrevista: «un RAG es de un solo salto; un agente es un bucle que razona, actúa y observa hasta cumplir la meta. La diferencia no es el modelo — es que le das control del flujo.»

02 Function calling: el contrato

Aquí está la idea que lo sostiene todo, y que casi todo el mundo malentiende: el modelo nunca ejecuta tu código. No puede. Vive en los servidores de OpenAI, no tiene tu CV ni tu Python. Lo único que hace es pedir que ejecutes una función, en un mensaje estructurado. Tú la ejecutas en tu máquina y le devuelves el resultado.

MODELO TU CÓDIGO (Python en tu Mac) │ │ │ "ejecuta match_cv con {…}" ─────▶│ corres match_cv(...) de verdad │ │ → {"encaje_global": "medio", …} │◀───── "aquí tienes el resultado" ──│ │ │ sigue razonando con ese dato

El modelo propone; tu código dispone. Esto es function calling (o tool use): el estándar sobre el que están construidos absolutamente todos los agentes, con o sin framework.

Por eso un agente es seguro por diseño: el modelo no toca tu disco ni tu red. Solo puede pedir las funciones que tú le pusiste en el menú. Si no está en la lista, no existe para él.

03 El menú de herramientas

¿Cómo sabe el modelo qué herramientas tiene? Se lo entregas como un esquema (JSON Schema): una ficha por herramienta con su nombre, para qué sirve y qué argumentos acepta. El modelo elige leyendo la description — así que esa frase es prompt engineering, no un comentario decorativo.

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "analizar_oferta",
            "description": "Extrae los requisitos estructurados (imprescindibles vs "
                           "deseables, seniority, idiomas) de una oferta en texto libre.",
            "parameters": {
                "type": "object",
                "properties": {
                    "texto": {
                        "type": "string",
                        "description": "El texto completo de la oferta, tal cual copiado.",
                    }
                },
                "required": ["texto"],
            },
        },
    },
    # match_cv(requisitos: object) y redactar_borrador(oferta, encaje: object)
    # se describen igual: nombre + description honesta + parameters.
]

Fíjate: es la misma firma de tus funciones de la Fase 1, traducida a un formato que el modelo entiende. La función existe en Python; el esquema es su carta de presentación ante el LLM.

Conexión con el Artefacto 2: allí forzabas response_format=json_object para que tú recibieras JSON limpio. Aquí es el modelo quien te manda JSON (los argumentos de la llamada). Es la misma disciplina de "datos estructurados entre piezas", ahora en la otra dirección.

04 El bucle: piensa → actúa → observa

Este es el corazón, y cabe en una pantalla. Lo escribimos a mano (no LangGraph) precisamente para verlo girar. Es el patrón ReAct — Reason + Act— que estudiaste en el curso de LangChain, pero sin la caja que lo envuelve:

def agente(peticion_usuario: str, max_turnos: int = 6) -> str:
    mensajes = [
        {"role": "system", "content": SYSTEM},          # quién es y cómo actúa
        {"role": "user",   "content": peticion_usuario},# la oferta pegada
    ]

    for _ in range(max_turnos):                          # guardarraíl: nunca infinito
        resp = cliente.chat.completions.create(
            model=MODELO, temperature=0,
            messages=mensajes, tools=TOOLS,              # ← le pasas el menú
        )
        msg = resp.choices[0].message
        mensajes.append(msg)                             # 1) guarda lo que dijo

        if not msg.tool_calls:                           # 2) ¿no pide herramientas?
            return msg.content                           #    → ha terminado. Fin.

        for tc in msg.tool_calls:                        # 3) ejecuta lo que pidió
            mensajes.append(ejecutar_herramienta(tc))    #    y devuélvele el resultado
    # si agota los turnos sin cerrar, cortamos (ver §08)
    return "Se alcanzó el máximo de turnos sin respuesta final."

Piensa (la llamada al modelo) → actúa (ejecutas la herramienta) → observa (el resultado vuelve a la lista) → y otra vuelta. El bucle termina solo cuando el modelo responde sin pedir herramientas: esa es su forma de decir «ya tengo todo, aquí está la respuesta».

05 El despachador

Cuando el modelo pide match_cv, ¿quién corre la función de verdad? Un despachador (dispatcher): un mapa de nombre → función de Python. Recibe la petición, saca los argumentos (que llegan como texto JSON), llama a la función real y empaqueta el resultado como un mensaje de rol tool.

DISPATCH = {                       # el mapa: lo que el modelo nombra → lo que se ejecuta
    "analizar_oferta":   analizar_oferta,
    "match_cv":          match_cv,
    "redactar_borrador": redactar_borrador,
}

def ejecutar_herramienta(tc) -> dict:
    nombre = tc.function.name
    args   = json.loads(tc.function.arguments)   # el modelo manda JSON como TEXTO → a dict
    funcion = DISPATCH[nombre]
    resultado = funcion(**args)                  # ← se ejecuta TU código de la Fase 1
    return {
        "role": "tool",
        "tool_call_id": tc.id,                   # ata la respuesta a la pregunta exacta
        "content": json.dumps(resultado, ensure_ascii=False),
    }

El detalle que engaña: tc.function.arguments no es un diccionario, es un string con JSON dentro. Por eso el json.loads. Y el tool_call_id es el hilo que cose cada respuesta con su pregunta: si el modelo pidió dos herramientas a la vez, es lo que evita que se crucen los cables.

06 La memoria: una lista que crece

La API de OpenAI no recuerda nada entre llamadas — es stateless. En cada vuelta le reenvías toda la conversación hasta ese punto. La "memoria" del agente no es magia: es una lista de Python (mensajes) a la que vas añadiendo. Cuatro tipos de entrada conviven en ella:

RolQuién lo escribeQué lleva
systemtú, una vezlas instrucciones: quién es el agente y cómo orquesta
usertú / Denysla oferta pegada
assistantel modeloo bien texto final, o bien una petición de herramienta(s)
tooltu despachadorel resultado de haber ejecutado la función

El orden importa y la API lo vigila: un mensaje tool solo es válido justo después de un assistant que pidió esa herramienta. Por eso en el bucle siempre añadimos el mensaje del modelo (paso 1) antes de ejecutar nada.

07 Una vuelta completa, mensaje a mensaje

Así se ve la lista mensajes creciendo en una ejecución real: pegas una oferta y el agente encadena las tres herramientas solo, sin que tú le digas el orden.

system "Eres el asistente de empleo de Denys. Analiza la oferta, contrástala con su CV y redacta un borrador. Ese orden." user "Te paso esta oferta de AI Engineer (Mid) en Madrid: …" ── turno 1 ────────────────────────────────────────────── assistant (sin texto) → pide: analizar_oferta(texto="…") tool {"puesto":"AI Engineer","imprescindibles":[…], …} ── turno 2 ────────────────────────────────────────────── assistant (sin texto) → pide: match_cv(requisitos={…}) tool {"encaje_global":"medio","que_faltan":[], …} ── turno 3 ────────────────────────────────────────────── assistant (sin texto) → pide: redactar_borrador(oferta={…}, encaje={…}) tool {"asunto":"Candidatura AI Engineer","cuerpo":"Hola, …"} ── turno 4 ────────────────────────────────────────────── assistant "Encajas bien: cumples los imprescindibles. Aquí tu borrador…" └─ sin tool_calls → EL BUCLE PARA. Fin.

Tres herramientas, cuatro turnos, y el orden lo eligió el modelo leyendo el system prompt y los resultados. Tú solo pegaste la oferta.

08 Guardarraíles

Un bucle que llama a un LLM puede, en teoría, no parar nunca — y cada vuelta cuesta dinero (tokens). Dos frenos, baratos y obligatorios:

09 Diccionario del mundo real

Los términos que acabas de construir (no solo leer), con dónde aparecen fuera de este proyecto:

ConceptoQué es aquíDónde vive en el mundo real
Function calling / Tool useel modelo pide, tú ejecutasel cimiento de OpenAI, Anthropic, Gemini… todo agente lo usa
Tool schemael menú JSON Schema de §03lo que registras en cualquier framework para exponer una capacidad
Agent loopel while/for de §04lo que LangGraph, AutoGPT o CrewAI envuelven y esconden
ReActrazonar y actuar por turnosel paper/patrón detrás de casi todos los agentes de herramientas
Orchestrationcoordinar las 3 tools hacia una metael trabajo real de un "AI Engineer" de agentes
Dispatcher / tool routerel mapa nombre→función de §05toda plataforma de agentes tiene uno por dentro
Stateless / conversation statela lista mensajes que acarreaspor qué existen las "sesiones" y las bases de datos de chat
Guardrailsmax_turnos + revisión humanacontrol de coste y seguridad en agentes de producción

Para tu CV / entrevista, en una frase:

«Implementé un agente con tool use escribiendo el agent loop a mano sobre el SDK de OpenAI —esquemas de herramientas, despachador y guardarraíles— en vez de con un framework, para dominar la mecánica que LangGraph abstrae.» Eso te separa de quien solo sabe importar una librería.

→ Y ahora, la Fase 2

Con este plano en la cabeza, la implementación es casi transcribir: un fichero agente.py que importe las tres herramientas de la Fase 1, defina TOOLS, DISPATCH, el SYSTEM y el bucle, y lo veamos girar sobre una oferta real en la terminal. Un cacho de código a la vez, verificando cada pieza — como siempre.