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.
El Artefacto 2 (RAG DOBE) es una cadena: un camino recto y fijo que tú programaste. Siempre pasa lo mismo, en el mismo orden:
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.
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.»
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.
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.
¿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.
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».
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.
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:
| Rol | Quién lo escribe | Qué lleva |
|---|---|---|
| system | tú, una vez | las instrucciones: quién es el agente y cómo orquesta |
| user | tú / Denys | la oferta pegada |
| assistant | el modelo | o bien texto final, o bien una petición de herramienta(s) |
| tool | tu despachador | el 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.
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.
Tres herramientas, cuatro turnos, y el orden lo eligió el modelo leyendo el system prompt y los resultados. Tú solo pegaste la oferta.
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:
max_turnos — el for del bucle tiene tope. Si el modelo se enrosca,
cortamos con un mensaje honesto en vez de sangrar la tarjeta. Es el mismo espíritu del
umbral del Artefacto 2: preferir "no sé" a un desastre caro.Los términos que acabas de construir (no solo leer), con dónde aparecen fuera de este proyecto:
| Concepto | Qué es aquí | Dónde vive en el mundo real |
|---|---|---|
| Function calling / Tool use | el modelo pide, tú ejecutas | el cimiento de OpenAI, Anthropic, Gemini… todo agente lo usa |
| Tool schema | el menú JSON Schema de §03 | lo que registras en cualquier framework para exponer una capacidad |
| Agent loop | el while/for de §04 | lo que LangGraph, AutoGPT o CrewAI envuelven y esconden |
| ReAct | razonar y actuar por turnos | el paper/patrón detrás de casi todos los agentes de herramientas |
| Orchestration | coordinar las 3 tools hacia una meta | el trabajo real de un "AI Engineer" de agentes |
| Dispatcher / tool router | el mapa nombre→función de §05 | toda plataforma de agentes tiene uno por dentro |
| Stateless / conversation state | la lista mensajes que acarreas | por qué existen las "sesiones" y las bases de datos de chat |
| Guardrails | max_turnos + revisión humana | control 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.
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.