Artefacto 4 de 6 · Mes 3–4 Desplegado · demo con token

El camino no lo fija el código: lo traza el modelo.

Un agente que lee una oferta de empleo, la contrasta con mi CV y redacta un borrador de candidatura — orquestando tres herramientas en un bucle de function calling escrito a mano sobre el SDK de OpenAI, sin LangGraph. Desplegado en Cloud Run, protegido por token. Cuarto artefacto de la ruta Data Science → AI Engineering.

3herramientas, un bucle
~4llamadas LLM por consulta
2interfaces, un motor
401sin token · fail-closed

La idea

Del RAG de un paso al agente de un bucle

El RAG del Artefacto 2 es de un solo salto: pregunta → recupera → responde. Un agente es otra cosa: tiene varias herramientas y un bucle que decide cuál usar, en qué orden y cuándo ha terminado. El camino no está fijado en el código — lo traza el modelo, leyendo el resultado de cada paso.

Herramienta 1 analizar_oferta texto crudo → requisitos
→
Herramienta 2 match_cv requisitos vs CV → encaje
→
Herramienta 3 redactar_borrador encaje → carta de candidatura

Ese orden no está escrito en ningún if: el modelo lo elige solo, consulta a consulta. Tú pegas la oferta; el bucle piensa, actúa, observa el resultado y repite hasta entregar la respuesta.

Definición de «hecho» Pegas una oferta y el agente decide solo qué herramientas usar, la contrasta con el CV y te entrega un borrador — vivo en internet, detrás de tu token.
La decisión de diseño

El bucle, a mano — no una caja opaca

Podría haber usado LangGraph. Elegí escribir el agent loop yo mismo sobre el SDK de OpenAI: esquemas de herramientas en JSON Schema, despachador nombre→función y guardarraíles. El motivo es de criterio, no de nostalgia — quiero entender la mecánica que los frameworks abstraen, y el bucle entero cabe en una pantalla.

agente.py · el corazón del agentefunction calling puro
for turno in range(1, max_turnos + 1):   # guardarraíl: nunca infinito
    resp = cliente.chat.completions.create(
        model=MODELO, messages=mensajes, tools=TOOLS)   # PIENSA
    msg = resp.choices[0].message
    mensajes.append(msg)

    if not msg.tool_calls:        # ¿no pide herramientas? → terminó
        return msg.content

    for tc in msg.tool_calls:      # ACTÚA + OBSERVA
        mensajes.append(ejecutar_herramienta(tc))
El instrumento

La traza, en vivo

Así se ve una consulta real desde dentro. Cada turno son las tres fases del bucle: piensa (el modelo lee el hilo y decide), actúa (llama una herramienta) y observa (el resultado vuelve al hilo). Cuando ya no pide nada, el bucle termina.

una consulta al agente · replay de la traza
» oferta pegada: «Data Scientist · Madrid · Python, SQL, GCP, Inglés B2…»
 
turno 1 · PIENSA   el modelo lee el hilo y el menú de herramientas
          ACTÚA    analizar_oferta(texto)         # temperatura 0
          OBSERVA  → requisitos extraídos: imprescindibles + deseables
 
turno 2 · PIENSA   con los requisitos ya en el hilo, toca contrastar
          ACTÚA    match_cv(requisitos)           # temperatura 0
          OBSERVA  → encaje "alto" · Inglés C1 ≥ B2 ✓
 
turno 3 · PIENSA   tiene el encaje: falta la carta
          ACTÚA    redactar_borrador(encaje)      # temperatura 0,7
          OBSERVA  → borrador listo, reglas de estilo horneadas
 
turno 4 · PIENSA   nada más que pedir → msg.tool_calls vacío
          FIN      → devuelve la carta + la traza de pasos   # exit del bucle

Nadie fijó ese orden. No hay un if que diga «primero analiza»: el modelo lee el menú (JSON Schema), decide, observa y repite. Por eso el agente devuelve la traza de pasos y no solo el texto final — para poder auditar qué decidió y por qué.

Por qué probar en serio importa

La interfaz cazó un error de criterio

Al probar el CLI con una oferta nueva (no la de ejemplo), el agente marcó «Inglés B2» como requisito que faltaba… teniendo el CV un C1. Comparaba los niveles de forma literal, sin entender que el marco europeo está ordenado (A1 < A2 < B1 < B2 < C1 < C2) y que un nivel superior cumple uno inferior.

la regla, horneada en la herramienta match_cvfix de una frase
# IDIOMAS — marco CEFR, niveles ORDENADOS A1<A2<B1<B2<C1<C2.
# Un nivel IGUAL O SUPERIOR cumple el requisito
# (un CV con C1 CUMPLE una oferta que pide B2). nativo/bilingüe = C2.

# antes:  encaje "medio", "Inglés B2" → BLOQUEANTE  ✗
# después: encaje "alto",  "Inglés B2" → cumplido    ✓

Un agente que dice «no cumples» cuando sí cumples te haría descartar buenas ofertas. Se detectó, se corrigió con una regla en el prompt y se verificó re-ejecutando el mismo caso — antes de desplegar. Y el fallo no se quedó en anécdota: hoy es el caso of_bug_cefr_b2 de la báscula del Artefacto 5 — el bug real convertido en prueba de regresión permanente.

Cómo está construido

Un motor, varias interfaces

herramientas herramientas.py — tres funciones (analizar_oferta, match_cv, redactar_borrador); extracción y juicio a temperatura 0, redacción a 0,7. El CV es conocimiento, cargado por context stuffing: cabe entero, sin RAG.
agente agente.py — el bucle de function calling: menú de herramientas (JSON Schema), despachador nombre→función y guardarraíl de turnos. Devuelve la traza de pasos, no solo texto.
interfaces cli.py + api.py — el mismo motor servido por dos bocas: un chat de terminal (REPL) y una API REST con FastAPI. Una lógica, dos puertas.
portero token — cada consulta son ~4 llamadas al LLM; sin token válido → 401 y el agente ni se ejecuta (fail-closed). Protege la API key.
despliegue Docker + Cloud Run — imagen con lista blanca de ficheros, secretos en Secret Manager, escala a cero. Verificado antes de subir que el CV viaja pero el .env no.
Documentación viva

Los cuadernos del artefacto

Cada concepto nuevo del recorrido se escribe como un cuaderno propio, no como un comentario en el código. Estos son cuatro de los seis de este artefacto; la colección de la ruta llega a dieciocho.

Lo que se entregó

Los criterios, cumplidos

Agente con tool use real, escrito a manoBucle de function calling sobre el SDK de OpenAI: esquemas, despachador y guardarraíles, sin framework que lo esconda.
Tres herramientas orquestadas por el modeloAnaliza, contrasta con el CV y redacta; el modelo elige el orden. Reglas de estilo horneadas en la redacción.
Dos interfaces sobre un mismo motorCLI de terminal y API REST con FastAPI; separación motor/interfaz que permitió desplegar sin tocar la lógica.
Desplegado en Cloud Run tras un tokenContenerizado con Docker, secretos en Secret Manager, escala a cero. Demo pública protegida para no exponer la API key.

Demo protegida por token. La página carga para cualquiera (prueba de que es real), pero ejecutar el agente requiere un token de acceso: cada consulta gasta llamadas al LLM y el portero evita el uso indebido de la API key. Es una decisión de coste y seguridad, no un adorno.

OpenAI function calling agent loop a mano JSON Schema tools FastAPI + token Docker + Cloud Run Secret Manager context stuffing (CV)
Por qué este artefacto importa

Cualquiera importa un framework de agentes. Lo que distingue es haber escrito el bucle: saber qué hace por debajo el tool use, dónde poner los guardarraíles y cómo protegerlo en producción. No usar un agente — entenderlo.