se refiere al diseño cuidadoso y la optimización de las entradas (por ejemplo, consultas o instrucciones) para guiar el comportamiento y las respuestas de los modelos de IA generativos. Las indicaciones generalmente se estructuran utilizando el paradigma declarativo o imperativo, o una mezcla de ambos. La elección del paradigma puede tener un gran impacto en la precisión y relevancia de la salida del modelo resultante. Este artículo proporciona una visión general conceptual de la solicitud declarativa e imperativa, analiza las ventajas y limitaciones de cada paradigma y considera las implicaciones prácticas.
El que y el como
En términos simples, las indicaciones declarativas expresan qué debe hacerse, mientras las indicaciones imperativas especifiquen cómo se debe hacer algo. Supongamos que estás en una pizzería con un amigo. Le dices al camarero que tendrás al napolitano. Dado que solo menciona el tipo de pizza que desea sin especificar exactamente cómo desea que se prepare, este es un ejemplo de un aviso declarativo. Mientras tanto, tu amigo, que tiene algunas preferencias culinarias muy particulares y está de humor para una pizza a medida Alle quattro stagioni – procede a decirle al camarero exactamente cómo le gustaría que se hiciera; Este es un ejemplo de un mensaje imperativo.
Los paradigmas de expresión declarativos e imperativos tienen una larga historia en la informática, con algunos lenguajes de programación que favorecen un paradigma sobre el otro. Un lenguaje como C tiende a usarse para la programación imperativa, mientras que un lenguaje como Prolog está orientado a la programación declarativa. Por ejemplo, considere el siguiente problema de identificar a los antepasados de una persona llamada Charlie. Sabemos que sabemos los siguientes hechos sobre los parientes de Charlie: Bob es la madre de Charlie, Alice es la madre de Bob, Susan es la madre de Dave y John es la madre de Alice. Según esta información, el siguiente código muestra cómo podemos identificar a los antepasados de Charlie usando Prolog.
parent(alice, bob).
parent(bob, charlie).
parent(susan, dave).
parent(john, alice).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
get_ancestors(Person, Ancestors) :- findall(X, ancestor(X, Person), Ancestors).
?- get_ancestors(charlie, Ancestors).
Aunque la sintaxis del prólogo puede parecer extraña al principio, en realidad expresa el problema que deseamos resolver de manera concisa e intuitiva. Primero, el código establece los hechos conocidos (es decir, quién es cuyo padre). Luego define recursivamente el predicado ancestor(X, Y)que evalúa a verdadero si X es un antepasado de Y. Finalmente, el predicado findall(X, Goal, List) desencadena al intérprete Prolog para evaluar repetidamente Goal y almacene todas las fijaciones exitosas de X en List. En nuestro caso, esto significa identificar todas las soluciones a ancestor(X, Person) y almacenarlos en la variable Ancestors. Observe que no especificamos los detalles de implementación (el “cómo”) de ninguno de estos predicados (el “qué”).
En contraste, la implementación de C a continuación identifica a los antepasados de Charlie describiendo con detalles minuciosos exactamente cómo se debe hacer esto.
#include <stdio.h>
#include <string.h>
#define MAX_PEOPLE 10
#define MAX_ANCESTORS 10
// Structure to represent parent relationships
typedef struct {
char parent[20];
char child[20];
} ParentRelation;
ParentRelation relations[] = {
{"alice", "bob"},
{"bob", "charlie"},
{"susan", "dave"},
{"john", "alice"}
};
int numRelations = 4;
// Check if X is a parent of Y
int isParent(const char *x, const char *y) {
for (int i = 0; i < numRelations; ++i) {
if (strcmp(relations[i].parent, x) == 0 && strcmp(relations[i].child, y) == 0) {
return 1;
}
}
return 0;
}
// Recursive function to check if X is an ancestor of Y
int isAncestor(const char *x, const char *y) {
if (isParent(x, y)) return 1;
for (int i = 0; i < numRelations; ++i) {
if (strcmp(relations[i].child, y) == 0) {
if (isAncestor(x, relations[i].parent)) return 1;
}
}
return 0;
}
// Get all ancestors of a person
void getAncestors(const char *person, char ancestors[][20], int *numAncestors) {
*numAncestors = 0;
for (int i = 0; i < numRelations; ++i) {
if (isAncestor(relations[i].parent, person)) {
strcpy(ancestors[*numAncestors], relations[i].parent);
(*numAncestors)++;
}
}
}
int main() {
char person[] = "charlie";
char ancestors[MAX_ANCESTORS][20];
int count;
getAncestors(person, ancestors, &count);
printf("Ancestors of %s:\n", person);
for (int i = 0; i < count; ++i) {
printf("%s\n", ancestors[i]);
}
return 0;
}
Hoy en día, la creciente disponibilidad de bibliotecas y API ricas en características, y la consolidación de las mejores prácticas arquitectónicas (por ejemplo, programación para interfaces, usando la configuración sobre código), nos permite centrarnos en el “qué” sin tener que preocuparnos por el “cómo” en muchos casos. De hecho, como se sugiere en el breve video a continuación, mucho código puede, y tal vez debería, escribirse de manera declarativa.
Desde la programación hasta la solicitud
Si pensamos en la ingeniería rápida como el próximo paso evolutivo en la programación, rastreando el viaje desde idiomas centrados en la máquina, como el ensamblaje y la C, a idiomas más centrados en los humanos como Java y Python, entonces la relevancia de los paradigmas declarativos e imperativos para impulsar ingeniería se hace evidente. Aunque ahora podemos instruir un modelo de IA generativo simplemente usando un lenguaje natural, aún debemos decidir en la medida en que debe expresar la instrucción qué se debe lograr versus cómo debe lograrse.
Considere el escenario de un desarrollador que desea usar un asistente de IA para generar código, específicamente, código Python para calcular la distancia coseno entre dos vectores. Las siguientes son tres versiones de un aviso que el desarrollador podría escribir para lograr su objetivo. La versión 1 es declarativa, la versión 2 es imperativa y la versión 3 es un híbrido de los dos. También se proporciona el código generado por el modelo GPT-4 de OpenAI en respuesta a cada una de estas indicaciones.
Versión rápida 1 (declarativa)
Escriba una función corta y rápida en Python para calcular la distancia de coseno entre dos vectores de entrada.
Resultado:
import numpy as np
def cosine_distance(u, v):
u = np.array(u)
v = np.array(v)
return 1 - np.dot(u, v) / (np.linalg.norm(u) * np.linalg.norm(v))
Versión rápida 2 (imperativo)
Escriba una función de pitón cosine_distance (v1, v2) que calcula la distancia coseno entre dos vectores. Siga estos pasos utilizando solo funciones incorporadas:
1. Elevar una excepción de ValueError si V1 o V2 están vacíos, o si V1 y V2 son de diferentes longitudes.
2. Calcule el producto DOT de V1 y V2.
3. Calcule la magnitud de cada vector y levante un ValueError si cualquiera de las magnitud es cero.
4. Divida el producto DOT por el producto de las dos magnitudes para obtener la similitud de coseno.
5. Haga 1 menos la similitud de coseno para obtener la distancia del coseno.
6. Devuelva la distancia resultante.
Resultado:
import math
def cosine_distance(v1, v2):
# Step 1: Input validation
if not v1 or not v2:
raise ValueError("Input vectors must not be empty.")
if len(v1) != len(v2):
raise ValueError("Vectors must be of the same length.")
# Step 2: Compute dot product
dot_product = sum(a * b for a, b in zip(v1, v2))
# Step 3: Compute magnitudes
mag_v1 = math.sqrt(sum(a ** 2 for a in v1))
mag_v2 = math.sqrt(sum(b ** 2 for b in v2))
if mag_v1 == 0 or mag_v2 == 0:
raise ValueError("Vectors must not have zero magnitude.")
# Step 4 & 5: Compute cosine similarity and distance
cosine_similarity = dot_product / (mag_v1 * mag_v2)
cosine_distance = 1 - cosine_similarity
# Step 6: Return result
return cosine_distance
Versión rápida 3 (híbrido)
Escriba una función de Python rápida que calcule la distancia de coseno entre dos vectores V1 y V2 utilizando funciones optimizadas relevantes de Numpy y SciPy. La función debe validar los vectores de entrada (no vacíos, igual longitud) y manejar con gracia entradas no válidas con excepciones.
Resultado:
import numpy as np
from scipy.spatial.distance import cosine
def cosine_distance(v1, v2):
# Convert inputs to NumPy arrays
v1 = np.asarray(v1, dtype=np.float64)
v2 = np.asarray(v2, dtype=np.float64)
# Validation checks
if v1.size == 0 or v2.size == 0:
raise ValueError("Input vectors must be non-empty.")
if v1.shape != v2.shape:
raise ValueError("Vectors must have the same shape.")
if not np.any(v1) or not np.any(v2):
raise ValueError("Vectors must not be zero vectors.")
# Compute cosine distance using optimized function
return cosine(v1, v2)
Caballos para cursos
Las tres indicaciones y sus respectivas implementaciones generadas por IA que se muestran anteriormente implican diferentes compensaciones y pueden ser adecuadas para diferentes personajes y escenarios en la práctica.
El aviso declarativo (versión 1) es corto y simple. No especifica detalles del enfoque algorítmico exacto a adoptar, expresando en su lugar la tarea de alto nivel solamente. Como tal, promueve la creatividad y la flexibilidad en la implementación. La desventaja de tal aviso, por supuesto, es que el resultado no siempre puede ser reproducible o robusto; En el caso anterior, el código generado por el aviso declarativo podría variar significativamente entre las llamadas de inferencia, y no maneja los casos de borde, lo que podría ser un problema si el código está destinado a su uso en la producción. A pesar de estas limitaciones, las personas típicas que pueden favorecer el paradigma declarativo incluyen gerentes de productos, diseñadores de UX y expertos en dominios comerciales que carecen de experiencia en codificación y pueden no necesitar respuestas de IA de grado de producción. Los desarrolladores de software y los científicos de datos también pueden utilizar las indicaciones declarativas para generar rápidamente un primer borrador, pero se espera que revisen y refinen el código después. Por supuesto, uno debe tener en cuenta que el tiempo necesario para mejorar el código generado por IA puede cancelar el tiempo ahorrado escribiendo un breve aviso declarativo en primer lugar.
Por el contrario, el indicador imperativo (versión 2) deja muy poco al azar: cada paso algorítmico se especifica en detalle. Se evitan explícitamente las dependencias de los paquetes no estándar, lo que puede evitar ciertos problemas en la producción (p. Ej., Rompiendo cambios o deprecaciones en paquetes de terceros, dificultad para depurar un comportamiento de código extraño, exposición a vulnerabilidades de seguridad, sobrecarga de instalación). Pero el mayor control y robustez vienen a costa de un mensaje detallado, que puede ser casi tan esfuerzos como escribir el código directamente. Personas típicas que optan por la solicitud imperativa pueden incluir desarrolladores de software y científicos de datos. Si bien son bastante capaces de escribir el código real desde cero, pueden encontrar más eficiente alimentar al pseudocódigo a un modelo de IA generativo. Por ejemplo, un desarrollador de Python podría usar pseudocódigo para generar rápidamente código en un lenguaje de programación diferente y menos familiar, como C ++ o Java, reduciendo así la probabilidad de errores sintácticos y el tiempo dedicado a depurarlos.
Finalmente, el indicador híbrido (versión 3) busca combinar lo mejor de ambos mundos, utilizando instrucciones imperativas para corregir los detalles de implementación clave (por ejemplo, estipular el uso de numpy y scipy), al tiempo que emplea formulaciones declarativas para mantener el aviso general conciso y fácil de seguir. Las indicaciones híbridas ofrecen libertad dentro de un marco, guiando la implementación sin encerrarla por completo. Personas típicas que pueden inclinarse hacia un híbrido de indicaciones declarativas e imperativas incluyen desarrolladores senior, científicos de datos y arquitectos de soluciones. Por ejemplo, en el caso de la generación de código, un científico de datos puede desear optimizar un algoritmo utilizando bibliotecas avanzadas que un modelo de IA generativo podría no seleccionar de forma predeterminada. Mientras tanto, un arquitecto de solución puede necesitar alejar explícitamente la IA de ciertos componentes de terceros para cumplir con las pautas arquitectónicas.
En última instancia, la elección entre la ingeniería rápida declarativa e imperativa para la IA generativa debe ser deliberada, sopesando los pros y los contras de cada paradigma en el contexto de aplicación dado.