un LLM puede ver antes de generar una respuesta. Esto incluye el mensaje en sí, instrucciones, ejemplos, documentos recuperados, resultados de herramientas e incluso el historial de conversaciones anteriores.
El contexto tiene un gran impacto en la calidad de las respuestas. Por ejemplo, si le pide a un LLM que escriba una consulta SQL sin proporcionar el esquema de datos, es casi seguro que el resultado no será óptimo. Peor aún, si el modelo no tiene ningún acceso a la base de datos, puede simplemente alucinar una consulta que no funciona. Incluso cuando hay herramientas disponibles, el modelo todavía necesita tiempo y esfuerzo extra para inferir el esquema antes de poder producir una respuesta correcta.
Debido a que el contexto juega un papel tan central en las aplicaciones basadas en LLM, la ingeniería de contexto ha surgido como una disciplina centrada en optimizar sistemáticamente la información que se incluye en el mensaje de un modelo. El objetivo es construir sistemas de “mejora personal” que aprendan de la experiencia sin depender de costosos ajustes (reentrenamiento de modelos y actualización de millones de parámetros).
La ingeniería de contexto tiene varias ventajas clave:
es más rentable y no requiere conocimientos especializados de ajuste; el contexto y las instrucciones siguen siendo transparentes, interpretables y fáciles de modificar para los humanos; los ciclos de iteración son mucho más rápidos, ya que las actualizaciones se pueden realizar instantáneamente sin volver a entrenar o implementar modelos; es más ágil, especialmente cuando es necesario olvidar información por motivos legales o de privacidad.
Con todas estas ventajas, no sorprende que la ingeniería contextual esté ganando tanta atención. Lo interesante, sin embargo, es la rapidez con la que están evolucionando los propios enfoques. En este artículo, analizaré esa evolución y luego experimentaré con uno de los marcos más nuevos para una optimización rápida: Ingeniería de contexto agente (ACE).
Evolución de los enfoques de ingeniería de contexto.
La ingeniería de contexto no apareció de la noche a la mañana. Ha evolucionado a través de varias etapas distintas.
La primera etapa fue la incitación estática. Aquí, las indicaciones eran instrucciones hechas a mano que nunca cambiaban. La mayor parte del esfuerzo se destinó a la ingeniería de indicaciones clásica: elegir cuidadosamente la redacción, la estructura y el formato para obtener un mejor rendimiento del modelo.
El siguiente gran paso fue la recuperación dinámica. En lugar de depender de un mensaje fijo, los sistemas comenzaron a extraer información relevante (documentos, ejemplos o hechos) en el momento de la inferencia. La generación aumentada de recuperación (RAG) se convirtió en uno de los enfoques más populares en esta categoría. Al basar las respuestas en datos externos, RAG mejoró significativamente la precisión y redujo las alucinaciones, especialmente en tareas que requieren mucho conocimiento.
Más recientemente, la atención se ha desplazado hacia contextos de mejora personal. En lugar de tratar el contexto como algo que simplemente se recupera o inyecta, estos enfoques permiten que el sistema actualice y refine su propio contexto basándose en el desempeño anterior. En otras palabras, el estímulo en sí se vuelve adaptativo y evoluciona a través de la reflexión y la retroalimentación.
En torno a esta idea han surgido varios marcos. A continuación se muestran algunos de los más influyentes.
Uno de los primeros y más significativos trabajos es “Reflexión: agentes del lenguaje con aprendizaje por refuerzo verbal” de Shinn et al. Esta investigación introdujo la idea de que los agentes del lenguaje pueden aprender de los errores a través de la reflexión del lenguaje natural en lugar de actualizaciones basadas en gradientes. Los agentes de reflexión analizan la retroalimentación de intentos anteriores, generan reflexiones verbales sobre lo que salió mal y almacenan estas reflexiones en un buffer de memoria episódica. Estas reflexiones almacenadas luego guían una mejor toma de decisiones en ensayos posteriores. Otra contribución importante es “TextGrad: Diferenciación automática vía texto” de Yuksekgonul et al. TextGrad toma prestados conceptos de la optimización del aprendizaje profundo (como gradientes, retropropagación y descenso de gradientes) pero reemplaza los derivados numéricos con retroalimentación del lenguaje natural. En este marco, los LLM generan críticas textuales que describen cómo debería cambiar una variable para mejorar el resultado. Estos "gradientes textuales" luego se propagan hacia atrás a través del sistema mediante indicaciones, realizando efectivamente una versión en lenguaje natural de propagación hacia atrás a través de un sistema de IA compuesto. El artículo “GEPA: La evolución rápida reflexiva puede superar el aprendizaje por refuerzo” de Agrawal et al. adopta un ángulo diferente al combinar algoritmos evolutivos con reflexión basada en el lenguaje. Las indicaciones se tratan como organismos: mutan, compiten y evolucionan bajo presión de selección. Con el tiempo, las indicaciones con mejor rendimiento sobreviven y se propagan. Este enfoque se implementa en DSPy y Hugging Face proporciona una guía práctica para aplicarlo en casos de uso del mundo real. Finalmente, “Dynamic Cheatsheet: Aprendizaje en el momento de la prueba con memoria adaptativa” de Suzgun et al. explora el aprendizaje en el momento de los exámenes a través de la memoria persistente. En esta configuración, un LLM de caja negra recibe un cuaderno donde puede escribir estrategias, patrones y fragmentos de código útiles durante la inferencia. En lugar de redescubrir repetidamente los mismos conocimientos, el modelo acumula y reutiliza conocimientos en todas las tareas. Esta memoria adaptativa mejora significativamente el rendimiento sin requerir etiquetas explícitas ni comentarios humanos.
Ingeniería de contexto agente
Ahora que hemos cubierto cómo ha evolucionado la ingeniería de contexto, echemos un vistazo más de cerca a la Ingeniería de Contexto Agentic (ACE), uno de los enfoques más recientes y el enfoque principal de este artículo. ACE se presenta en el artículo “Ingeniería de contexto agente: contextos en evolución para modelos de lenguaje de mejora automática” de Zhang et al., publicado en 2025.
El artículo comienza identificando dos problemas clave con los métodos contextuales de mejora personal existentes.
El sesgo de brevedad es la tendencia de los sistemas a simplificar demasiado los detalles importantes y colapsar gradualmente hacia indicaciones breves y genéricas. Si bien las indicaciones compactas son atractivas, a menudo pierden los matices que realmente impulsan un buen rendimiento. Colapso del contexto. Cuando los sistemas reescriben repetidamente todo el mensaje, tienden a olvidar el conocimiento útil acumulado anteriormente. Con el tiempo, esto conduce a inestabilidad y regresiones en lugar de una mejora constante.
Para abordar estos problemas, los autores proponen Agentic Context Engineering (ACE), un marco diseñado para una adaptación del contexto escalable y eficiente tanto en entornos fuera de línea (como la optimización de avisos del sistema) como en escenarios en línea (como la adaptación de la memoria en el momento de la prueba). En lugar de comprimir el conocimiento en un único mensaje estático, ACE permite que el modelo evolucione continuamente en su contexto acumulando estrategias exitosas, reflexionando sobre los fracasos y organizando el conocimiento de una manera estructurada. Conceptualmente, se asemeja a un asistente de inteligencia artificial que mejora con el tiempo al tomar notas detalladas y perfeccionar su propio manual.
En el centro de ACE hay un circuito de aprendizaje agente que refleja cómo los humanos aprenden a través de la experimentación: probar, reflexionar y consolidar. El marco consta de tres componentes:
Generador, que produce trayectorias de razonamiento mientras resuelve tareas; Reflector, que analiza éxitos y fracasos y extrae conocimientos prácticos; Curator, que integra esos conocimientos en el contexto compartido como pequeñas actualizaciones incrementales.
En lugar de mantener un único mensaje monolítico, ACE organiza el contexto como un manual compuesto por viñetas estructuradas. Cada viñeta contiene metadatos (como un identificador único y contadores que rastrean la frecuencia con la que ha sido útil o perjudicial), así como contenido que representa una pequeña unidad de conocimiento reutilizable. Podría ser una estrategia general, un concepto de dominio específico o un modo de falla común.
El flujo de trabajo de ACE consta de varias fases.
Fase de generación. El Generador aborda nuevos problemas utilizando el manual actual, marcando qué viñetas fueron útiles o engañosas. Fase de reflexión. El Reflector analiza la trayectoria completa, extrayendo lecciones tanto de los éxitos como de los fracasos mediante un refinamiento iterativo. Fase de curación. El curador convierte estos conocimientos en actualizaciones “delta” compactas: viñetas nuevas o modificadas que se fusionan en el manual existente utilizando una lógica ligera que no es LLM. Fase de crecimiento y refinamiento. Se añaden nuevas viñetas, las existentes se actualizan en su lugar y la deduplicación periódica elimina la redundancia mediante incorporaciones semánticas.
Este diseño permite el procesamiento paralelo de múltiples actualizaciones y admite la adaptación a múltiples épocas, donde las mismas consultas se pueden revisar para fortalecer progresivamente el contexto a lo largo del tiempo.
Empíricamente, ACE ofrece resultados sólidos. En las evaluaciones comparativas, supera a otros enfoques de contexto de mejora personal, logrando una mejora del +10,6 % en las tareas de los agentes de IA y una ganancia del +8,6 % en dominios especializados como las finanzas.
Más allá de la precisión, ACE también es más rentable gracias a su mecanismo de actualización incremental, que muestra costos de token un 83,6% más bajos en comparación con los métodos de referencia.
En conjunto, estos resultados posicionan a ACE como un paso adelante práctico y escalable en la construcción de sistemas LLM de mejora automática.
Uso de ACE para datos de intención bancaria
El marco ACE parece prometedor sobre el papel, por lo que el siguiente paso es ver cómo funciona en la práctica. Afortunadamente, los autores han compartido una implementación de código abierto en GitHub, lo que nos brinda un punto de partida sólido.
Cargando los datos
Para mantener enfocado el experimento, decidí aplicar ACE a una tarea de clasificación. Estoy usando un conjunto de datos disponible públicamente sobre intenciones bancarias publicado por PolyAI (). Este conjunto de datos refleja un problema muy común del mundo real: identificar la intención del cliente cuando alguien contacta con el servicio de atención al cliente. La clasificación precisa de la intención es fundamental para dirigir las solicitudes al equipo adecuado, activar respuestas semiautomáticas o simplemente monitorear problemas recurrentes.
En este conjunto de datos, cada mensaje de cliente (por ejemplo, "No estoy seguro de por qué mi tarjeta no funcionó") debe asignarse a una intención bancaria específica, como pago_con_tarjeta_rechazada. En total, hay 77 categorías de intención distintas.
Para que el experimento fuera manejable, tomé muestras de 500 ejemplos del conjunto de datos y los dividí en conjuntos de entrenamiento, prueba y validación. A continuación se muestra el código utilizado para cargar los datos y crear las divisiones.
full_df = pd.read_csv('./poly_ai_banking_data/train.csv') # params total_number_of_samples = 500 train_share = 0.5 test_share = 0.4 val_share = 0.1 sample_df = full_df.sample(n=total_number_of_samples, random_state=42) .reset_index(drop=True) random.seed(42) sample_df['group'] = random.choices(['train', 'test', 'val'], pesos=(train_share, test_share, val_share), k=total_number_of_samples) train_df = sample_df[sample_df['group'] == 'tren'].reset_index(drop=True) test_df = sample_df[sample_df['group'] == 'test'].reset_index(drop=True) val_df = sample_df[sample_df['group'] == 'val'].reset_index(drop=True)
Ampliar ACE a los datos de intención bancaria
El siguiente paso es ampliar el marco ACE para que pueda funcionar con nuestro conjunto de datos de intención bancaria. Afortunadamente, los autores proporcionan una guía detallada que hace que este proceso sea relativamente sencillo.
Además de conectar el nuevo conjunto de datos, hice un par de pequeñas modificaciones en el marco central para admitir modelos antrópicos y ajustes de temperatura configurables. Puede encontrar la versión completa y modificada del código en GitHub.
Preparando los datos
Lo primero que debemos hacer es preparar el conjunto de datos en el formato que espera ACE. Guardé las divisiones de capacitación, validación y prueba como archivos CSV en banca/datos. Cada ejemplo contiene:
texto: el mensaje de atención al cliente, categoría: la etiqueta de intención de destino que queremos predecir, grupo: un campo auxiliar que indica si el ejemplo pertenece al conjunto de entrenamiento, prueba o validación.
El campo de grupo no será utilizado más adelante por el propio marco, pero es conveniente para la gestión y reproducibilidad del conjunto de datos.
Así es como se ve el formato de datos.
text,category,group ¿Puedo cambiar mi número PIN?,change_pin,test ¿Cuál es la transacción de $1 en mi cuenta?,extra_charge_on_statement,test ¿Cuánto cuestan las tarifas de recarga?,top_up_by_card_charge,test Vivo en la UE, ¿puedo obtener una tarjeta?,country_support,test
A continuación, debemos indicarle a ACE dónde encontrar cada división. Esto se hace especificando las rutas del conjunto de datos en banking/data/task_config.json.
{ "banca": { "train_data": "./banking/data/train.csv", "val_data": "./banking/data/val.csv", "test_data": "./banking/data/test.csv" } }
Implementación del procesador de datos
Para integrar una nueva tarea, el marco requiere un módulo DataProcessor personalizado. Según la guía, esto implica implementar tres métodos principales: Process_task_data, respuesta_es_correct y evaluar_accuracy.
Además, necesitamos una función auxiliar para cargar los datos sin procesar desde el disco. Empecemos con eso.
A continuación se muestra la implementación de la función de carga de datos. Lee un archivo CSV, valida su existencia y convierte cada fila en un diccionario con el que puede trabajar el resto de la canalización.
def load_data(data_path: str) -> List[Dict[str, Any]]: """ Carga y procesa datos desde un archivo CSV. Formato CSV esperado: texto, categoría, grupo (con encabezado) Args: data_path: ruta al archivo CSV Devuelve: lista de diccionarios que contienen los datos """ si no os.path.exists(data_path): rise FileNotFoundError(f"Archivo de datos no encontrado: {data_path}") datos =[]con open(data_path, 'r', encoding='utf-8') como f: lector = csv.DictReader(f) para la fila en el lector: data.append({ 'text': fila['text'], 'categoría': fila['categoría'], 'grupo': fila.get('grupo', '') }) print(f"Muestras cargadas {len(data)} de {data_path}") devuelven datos
Con la función de carga de datos implementada, podemos pasar a implementar los métodos restantes del Procesador de datos.
El objetivo principal de Process_task_data es convertir el conjunto de datos sin procesar al formato de entrada estandarizado de ACE.
ACE espera que cada ejemplo contenga tres campos: contexto, pregunta y objetivo. En nuestro caso, el mapeo es bastante simple. Asignamos la categoría de intención directamente al objetivo y dejamos el contexto vacío, ya que no se necesita información adicional adicional para la clasificación.
La parte más importante aquí es la pregunta. Agregamos contexto adicional para dejar claro al LLM que debe clasificar la consulta en lugar de responder las preguntas directamente, al mismo tiempo que proporcionamos la lista de temas disponibles para guiar la respuesta de un LLM.
def Process_task_data(self, raw_data: List[Dict]) -> List[Dict]: """ Convierte datos CSV sin procesar a formato estandarizado para ACE. Args: raw_data: datos sin procesar cargados desde CSV (lista de dicts con 'texto', 'categoría') Devuelve: Lista de dicts con claves: 'contexto', 'pregunta', 'objetivo' """process_data =[]# Reúna la lista de temas para incluir en la pregunta issues_list = ", ".join(self.allowed_topics) para el elemento en raw_data: customer_query = item.get('text', '') ground_truth_topic = item.get('category', '') # La pregunta proporciona la instrucción de la tarea de clasificación question = ( f"Clasifique la siguiente consulta de atención al cliente bancaria en uno de los temas predefinidos.nn" f"Consulta del cliente: {customer_query}nn" f"Temas disponibles: {topics_list}nn" f"Responda SOLO con el nombre del tema, nada más." )process_item = { "context": "", # No se necesita contexto adicional "question": question, "target": ground_truth_topic, "others": { "original_text": customer_query, "task": self.task_name, } } datos_procesados.append(elemento_procesado) devuelve datos_procesados
El siguiente método, answer_is_correct, verifica si la predicción de un modelo coincide con la etiqueta de verdad fundamental. Dado que le indicamos explícitamente al LLM que responda solo con el nombre de la categoría, aquí es suficiente una simple comparación de cadenas que no distinga entre mayúsculas y minúsculas.
def respuesta_is_correct(self, predicted: str, ground_truth: str) -> bool: """ Comprueba si el tema predicho coincide con la verdad fundamental. Utiliza una comparación simple que no distingue entre mayúsculas y minúsculas. Args: predicted: Tema predicho del modelo ground_truth: Tema de verdad fundamental Devuelve: bool: Verdadero si la predicción es correcta, Falso en caso contrario """ return predicted.lower().strip() == ground_truth.lower().strip()
El último método que debemos implementar es evaluar_accuracy, que calcula la precisión de clasificación general en múltiples predicciones. No pasa nada especial aquí. Simplemente calculamos la fracción de casos en los que respuesta_is_correct(prediction, ground_truth) devuelve Verdadero.
def evaluar_accuracy(self, predicciones: Lista[str], ground_truths: Lista[str]) -> float: """ Calcula la precisión de la clasificación en múltiples predicciones. Args: predicciones: Lista de predicciones del modelo ground_truths: Lista de temas de verdad sobre el terreno Devuelve: Precisión como un valor flotante entre 0 y 1 """ if len(predictions) != len(ground_truths): rise ValueError("Las predicciones y las verdades fundamentales deben tener la misma longitud") si no son predicciones: devuelve 0.0 correcto = suma (1 para pred, verdad en zip(predicciones, verdades_fundamentales) si self.answer_is_correct(pred, verdad)) devuelve correcto/len(predicciones)
Armar el script del flujo de trabajo
Con el procesador de datos instalado, el siguiente paso es crear un script de ejecución completo para ACE. Creé un script run_ace_workflow que acepta varios argumentos clave:
api_provider selecciona la API del modelo de lenguaje que se utilizará ('anthropic', 'openai', 'together' o 'sambanova'), y el valor predeterminado es 'anthropic'. generador_modelo especifica el modelo para el agente Generador (predeterminado: 'claude-haiku-4-5'). reflector_model especifica el modelo del agente reflector (predeterminado: 'claude-sonnet-4-5'). curator_model especifica el modelo para el agente Curator (predeterminado: 'claude-sonnet-4-5'). max_train y max_test son límites opcionales en los tamaños del conjunto de prueba y tren, útiles para experimentos rápidos o depuración.
Analicemos cómo funciona realmente este script. El script comienza cargando los datos de intención bancaria e inicializando el procesador de datos. Aquí está la función auxiliar que escribí para esto.
def load_banking_data(max_train=None, max_test=None): """Cargar y procesar el conjunto de datos bancarios.""" from banking.data_processor import DataProcessor, load_data base_path = os.path.dirname(__file__) data_path = os.path.join(base_path, "data") # Cargar datos sin procesar train_raw = load_data(os.path.join(data_path, "train.csv")) val_raw = load_data(os.path.join(data_path, "val.csv")) test_raw = load_data(os.path.join(data_path, "test.csv")) # Limitar muestras si se especifica si max_train: train_raw = train_raw[:max_train] val_raw = val_raw[:max(max_train // 4, 10)] if max_test: test_raw = test_raw[:max_test] # Procesador de datos de proceso = DataProcessor(task_name="banking") train_samples = procesador.process_task_data(train_raw) val_samples = procesador.process_task_data(val_raw) test_samples = procesador.process_task_data(test_raw) devuelve train_samples, val_samples, test_samples, procesador train_samples, val_samples, test_samples, procesador = load_banking_data( max_train=args.max_train, max_test=args.max_test )
El siguiente paso es definir una plantilla de libro de jugadas. Esto es importante porque la implementación actual de ACE no puede crear nuevas secciones dinámicamente, por lo que predefinimos la estructura para guiar el modelo. Aquí está la plantilla que utilicé para el dominio bancario.
BANKING_PLAYBOOK_TEMPLATE = """ ## GENERAL ## PRINCIPIOS DE CLASIFICACIÓN ## DESAMBIGUACIÓN DE CATEGORÍAS ## CONOCIMIENTO DEL DOMINIO BANCARIO ## PATRONES COMUNES ## MANEJO DE CONSULTAS AMBIGUAS ## ERRORES COMUNES A EVITAR ## OTROS """
Con los datos y la plantilla listos, podemos inicializar el objeto ACE con los parámetros principales.
ace_system = ACE( api_provider=args.api_provider, generador_model=args.generator_model, reflector_model=args.reflector_model, curator_model=args.curator_model, max_tokens=4096, inicial_playbook=BANKING_PLAYBOOK_TEMPLATE, use_bulletpoint_analyzer=True, # habilitando la deduplicación de viñetas en el libro de estrategias generador_temperature=0.1, # priorizando la coherencia para el generador reflector_temperature=0.7, # priorizando la creatividad para el reflector y el curador curator_temperature=0.7,)
Finalmente, definimos una función para ejecutar el flujo de trabajo de capacitación de ACE, que incluye evaluación inicial, reflexión iterativa, curación y evaluación final.
def run_ace_training(ace_system, train_samples, val_samples, test_samples, procesador, results_dir): """Entrene a ACE para mejorar el libro de jugadas (incluye evaluaciones iniciales y finales).""" config = { 'num_epochs': 1, 'max_num_rounds': 3, # rondas de reflexión máximas por muestra 'curator_frequency': 5, # ejecute curador cada 5 pasos 'eval_steps': max(len(train_samples) // 10, 10), # evalúa 10 veces durante el entrenamiento 'save_steps': max(len(train_samples) // 10, 10), 'playbook_token_budget': 80000, 'task_name': 'banking_ace', 'json_mode': False, 'no_ground_truth': False, 'save_dir': os.path.join(results_dir, "training"), 'test_workers': 10, } resultados = ace_system.run( mode='offline', train_samples=train_samples, val_samples=val_samples, test_samples=test_samples, data_processor=processor, config=config ) # Extraer resultados inicial_acc = results.get('initial_test_results', {}).get('accuracy', 0) final_acc = results.get('final_test_results', {}).get('accuracy', 0) Training_results = results.get('training_results', {}) devuelve ace_system.best_playbook, resultados best_playbook, Training_results = run_ace_training( ace_system, train_samples, val_samples, test_samples, procesador, results_dir)
¡Y eso es todo! Esa es toda la lógica central que necesitamos para ejecutar ACE. Agregué algunos registros además del flujo de trabajo para mayor comodidad, pero no es esencial para la funcionalidad principal.
Resultados
Echemos un vistazo a los resultados y veamos cómo se combina todo. Primero, consulte el mejor libro de jugadas, que puede encontrar en results/banking_{dt}/best_playbook.txt. El manual está organizado en viñetas detalladas, agrupadas según las categorías que definimos en nuestra plantilla inicial. Cada viñeta contiene instrucciones y estrategias detalladas, junto con metadatos que muestran con qué frecuencia se marcó como útil o perjudicial. Esta estructura facilita ver qué temas y estrategias el sistema encontró más útiles durante la capacitación.
## GENERAL ## PRINCIPIOS DE CLASIFICACIÓN [cls-00001] útil=1 dañino=0 :: Indicadores temporales como "pudo hacerlo antes", "trabajó anteriormente" o "solía trabajar" son fuertes señales de que el problema es específico de la transacción actual en lugar de un problema general de capacidad del sistema. Estas frases sugieren un cambio de estado para una entidad específica (beneficiario, tarjeta, cuenta) en lugar de una funcionalidad general. [cls-00002] útil=18 dañino=4 :: Aplicar jerarquía de especificidad: cuando se puedan aplicar varias categorías, elija la más específica que coincida con las pistas contextuales. Por ejemplo, beneficiario_not_allowed (específico del destinatario) es más específico que declinado_transferencia (fallo general). [cls-00009] útil=0 dañino=3 :: La jerarquía de especificidad funciona bidireccionalmente: elija categorías específicas cuando las pistas contextuales apunten a un tipo de transacción en particular, pero use categorías generales (como 'extra_charge_on_statement') cuando la consulta carezca de contexto suficiente para determinar la naturaleza específica de la transacción. No fuerce la especificidad cuando la consulta del cliente es inherentemente general. [cls-00017] útil=5 dañino=1 :: Distinción orientada a procesos versus seguimiento de estado: diferencia entre preguntas sobre CÓMO obtener/adquirir algo (orientada a procesos) versus preguntas sobre CUÁNDO llegará algo o SI ha llegado (seguimiento de estado). Las preguntas sobre el proceso se centran en los pasos y componentes necesarios, mientras que las preguntas sobre el estado se centran en el tiempo y la confirmación de la entrega. Utilice esta distinción para elegir entre categorías de adquisición y categorías de seguimiento/llegada. ## DESAMBIGUACIÓN DE CATEGORÍA [dis-00003] útil = 1 dañino = 0 :: transferencia_declinada vs beneficiario_no_permitido: si el cliente menciona que podía transferir antes pero de repente no puede, esto indica claramente beneficiario_no_permitido (el destinatario está bloqueado/restringido) en lugar de transferencia_rechazada (fallo general de la transferencia debido a fondos, límites o errores del sistema). [dis-00011] útil=11 dañino=0 :: pendiente_* vs fallado_* vs rechazado_*: El estado de la transacción es crítico para la clasificación. "Aún no se ha completado" o "tarda demasiado" = estado pendiente. "No funcionó", "fue rechazado" o "fue rechazado" = estado fallido/rechazado. 'Se devolvió el dinero' o 'se devolvió' = estado revertido. Haga coincidir la categoría con el estado de transacción real descrito. [dis-00012] útil=0 dañino=1 :: soporte_país vs tarjetas_y_monedas_compatibles: Las consultas sobre disponibilidad geográfica ("qué países", "dónde puedo", "qué regiones") deben clasificarse como "soporte_país". Por el contrario, 'supported_cards_and_currencies' es para preguntas sobre tipos de tarjetas (Visa, Mastercard) y opciones de moneda, no sobre disponibilidad geográfica. [dis-00014] útil=2 dañino=0 :: Problemas con retiros de efectivo: distinga por estado y resultado de la transacción: 'pending_cash_withdrawal' (aún no completado, aún en proceso), 'declined_cash_withdrawal' (rechazado, no se recibió efectivo), 'cash_withdrawal_not_recognised' (el cliente no recuerda la transacción) y 'wrong_amount_of_cash_received' (transacción completada pero incorrecta cantidad dispensada). Si se recibió efectivo pero el monto era incorrecto, use la categoría más específica: monto_incorrecto_de_efectivo_recibido. [dis-00015] útil=3 dañino=3 :: card_arrival vs get_physical_card: Distinga entre preguntas de seguimiento de estado (card_arrival) y preguntas de adquisición de procesos (get_physical_card). 'card_arrival' sirve para rastrear pedidos existentes ('¿Ha llegado mi tarjeta?', '¿Dónde está mi tarjeta?'). 'get_physical_card' abarca todo el proceso de obtención de una tarjeta física, incluidos todos los componentes como el PIN ('¿Dónde puedo encontrar mi PIN?', '¿Cómo obtengo mi tarjeta y mi PIN?'). Las preguntas sobre PIN faltantes con "aún no lo he recibido" indican que el cliente está en el proceso de adquisición, no solo en el seguimiento de la entrega. [dis-00021] útil=1 dañino=0 :: card_paid_not_recognised vs extra_charge_on_statement: cuando un cliente menciona un 'pago' que no reconoce o no realizó ('pago que nunca envié', 'pago que no autoricé'), clasifíquelo como 'card_paid_not_recognised' porque 'pago' es un tipo de transacción específico. Utilice 'extra_charge_on_statement' solo cuando el cliente describa montos, tarifas o cargos inesperados SIN especificar el tipo de transacción (por ejemplo, 'Veo $5 adicionales en mi estado de cuenta', 'hay un cargo extraño' sin mencionar pago/transferencia/retiro). [dis-00024] útil=0 dañino=1 :: Especificidad de categoría de tarifa/cargo: cuando los clientes pregunten sobre tarifas o cargos, priorice las categorías de tarifas específicas del tipo de transacción sobre 'extra_charge_on_statement'. Si la consulta menciona un tipo de transacción específica (transferencia, pago, retiro, recarga), utilice la categoría de tarifa específica correspondiente: 'transfer_fee_charged' para tarifas de transferencia, 'card_paid_fee_charged' para tarifas de pago, 'atm_fee_charged' para tarifas de retiro, 'top_up_fee' para tarifas de recarga. Reserve 'extra_charge_on_statement' solo para consultas de tarifas en las que no se menciona ningún tipo de transacción específico (por ejemplo, '¿Por qué hay un cargo adicional de $5?' sin contexto). [dis-00026] útil=0 dañino=0 :: recibir_dinero vs transferir_a_cuenta: Distinga entre recibo pasivo y transferencia activa. 'receiving_money' es para consultas sobre la recepción de fondos DE otra parte (pasiva, iniciada por el remitente). 'transfer_into_account' es para consultas sobre el cliente que inicia una transferencia PARA agregar fondos a su propia cuenta (activa, autoiniciada). Pistas de contexto: saldo vacío/bajo + preguntar sobre transferencias = probable transferencia_a_cuenta. Preguntas sobre "¿puedo transferir fondos" en el contexto de la necesidad de agregar dinero = transferir_a_cuenta, no recibir_dinero? [dis-00029] útil=0 dañino=0 :: beneficiario_no_permitido vs transferencia_rechazada: cuando una consulta menciona explícitamente "beneficiario" o "destinatario" combinado con lenguaje de restricción ("no permitido", "bloqueado", "restringido", "no se puede agregar", "no se puede agregar"), se clasifica como "beneficiario_no_permitido" incluso sin indicadores temporales. La combinación del término específico de entidad bancaria (beneficiario/destinatario) con lenguaje de restricción es una fuerte señal directa de restricciones a nivel de destinatario en lugar de fallas generales en las transferencias. ## CONOCIMIENTO DEL DOMINIO BANCARIO [bank-00006] útil=0 dañino=0 :: En la banca, cuando una transferencia previamente exitosa falla repentinamente, las causas comunes incluyen: beneficiario marcado/bloqueado por sistemas de fraude, restricciones de la cuenta del beneficiario o beneficiario eliminado de la lista permitida. Estos son distintos de los rechazos generales de transferencias debido a fondos insuficientes o errores del sistema. [bank-00008] útil=0 dañino=6 :: Las pequeñas cantidades inesperadas (como £1, £0,01) que aparecen en los extractos a menudo indican retenciones de autorización, cargos de verificación o tarifas diversas. Cuando los clientes los cuestionan sin contexto adicional, deben clasificarse como 'extra_charge_on_statement' en lugar de tipos de transacciones más específicos. [bank-00018] útil=0 dañino=0 :: 'card_swallowed' es el término de la industria bancaria para escenarios de retención de tarjetas de cajero automático donde la máquina guarda/retiene la tarjeta del cliente. Esto se aplica cuando las tarjetas se atascan, no salen o quedan retenidas en el cajero automático, independientemente de la frase específica utilizada por el cliente. [bank-00020] útil=10 dañino=4 :: La terminología bancaria tiene una jerarquía de especificidad para las referencias de transacciones. Las palabras clave de tipos de transacciones específicas incluyen: 'pago' (pagos con tarjeta), 'transferencia' (transferencias de dinero), 'retiro' (retiros de efectivo), 'recarga' (financiación de cuenta), 'débito directo', 'orden permanente'. Los términos genéricos incluyen: "cargo", "monto", "transacción", "tarifa". Cuando un cliente utiliza una palabra clave de tipo de transacción específica, proporciona contexto suficiente para clasificarla en categorías específicas de tipo de transacción en lugar de categorías generales. ## PATRONES COMUNES [pat-00004] útil=0 dañino=0 :: Patrón: 'Antes funcionó, ahora no' + contexto de transferencia = probable restricción a nivel de beneficiario en lugar de disminución a nivel de sistema. El éxito anterior indica que la cuenta y el mecanismo de transferencia funcionan, lo que apunta a una restricción específica en el destinatario actual. [pat-00007] útil=3 dañino=6 :: Patrón: El cliente describe la transacción como "extraña", "inesperada", "inexplicable" o pregunta "cuál es este cargo" en su estado de cuenta sin proporcionar un contexto específico del tipo de transacción (transferencia, pago, retiro, etc.) = clasificar como "extra_charge_on_statement". Esta es la categoría general apropiada cuando la naturaleza del cargo no está clara. [pat-00010] útil=8 dañino=1 :: Patrón: Frases como "aún no se ha realizado", "aún esperando", "no completado" o "aún pendiente" indican una transacción en estado PENDIENTE, no en estado FALLADO. Elija las categorías 'pendiente_*' en lugar de las categorías 'fallido_*' o 'rechazado_*' cuando estas señales de idioma estén presentes. [pat-00013] útil=0 dañino=2 :: Patrón: preguntas con indicadores de alcance geográfico como "qué países", "dónde puedo", "qué regiones" o "en qué ubicaciones" preguntan sobre la disponibilidad del servicio por geografía = clasificar como "país_soporte". La intención principal es comprender el alcance geográfico de los servicios. [pat-00016] útil=2 dañino=9 :: Patrón: Las frases '¿Dónde puedo encontrar?' o '¿Cómo puedo obtenerlo?' indican preguntas orientadas al proceso que buscan información sobre cómo obtener o adquirir algo, no preguntas de seguimiento de estado. Por lo general, estos deberían asignarse a categorías de adquisición/configuración (como 'get_physical_card') en lugar de categorías de entrega/seguimiento (como 'card_arrival' o 'card_delivery_estimate'). [pat-00019] útil=0 dañino=0 :: Patrón: las frases que indican que una tarjeta está retenida físicamente en un cajero automático ('tarjeta atascada en el cajero automático', 'la tarjeta no sale', 'el cajero automático retuvo mi tarjeta', 'sacar mi tarjeta del cajero automático', 'recuperar la tarjeta de la máquina') deben clasificarse como 'tarjeta_tragada'. El indicador clave es que la máquina retiene físicamente la tarjeta en lugar de otros problemas de la tarjeta como daños, pérdida o problemas de funcionalidad. [pat-00022] útil=1 dañino=0 :: Patrón: palabra clave de tipo de transacción específica + 'no reconocido'/'no realizó'/'nunca enviado' = use la categoría 'no_reconocida' específica del tipo de transacción. Ejemplos: 'pago que no hice' → tarjeta_pago_no_reconocido; 'transferencia que no reconozco' → transferencia_no_recibida_por_el_destinatario o problema de transferencia relacionado; 'retiro que nunca hice' → cash_withdrawal_not_recognised. La presencia de una palabra clave de tipo de transacción específica (pago, transferencia, retiro) es contexto suficiente para evitar categorías generales. [pat-00025] útil=1 dañino=0 :: Patrón: palabra clave de tipo de transacción + pregunta de tiempo ('cuánto tiempo', 'cuándo', 'cuánto tiempo') + mención geográfica = priorizar la categoría de tiempo específica de la transacción (por ejemplo, 'transfer_timing', 'card_delivery_estimate'). Trate las menciones geográficas como información contextual sobre el origen/destino de la transacción, a menos que la consulta pregunte explícitamente sobre la disponibilidad del servicio ("qué países", "dónde puedo usarlo", "está disponible en"). Ejemplo: 'transferencia desde China, ¿cuánto tiempo?' → 'transfer_timing' (no 'country_support'). [pat-00027] útil=0 dañino=0 ::
Por supuesto, la métrica más interesante es la precisión. Puede encontrar los resultados de las pruebas iniciales y finales en results/banking_{datetime}/training/initial_test_results.json y results/banking_{datetime}/training/final_test_results.json.
# resultados iniciales { "test_results": { "accuracy": 0.7512437810945274, "correct": 151, "total": 201, "no_answer": 0 }, "error_log": { "accuracy": 0.7512437810945274, "errors": [ { "index": 2, "prediction": "declined_card_paid", "ground_truth": "declined_transfer" }, { "index": 9, "prediction": "top_up_limits", "ground_truth": "automatic_top_up" }, { "index": 7, "prediction": "transfer_not_received_by_recipient", "ground_truth": "balance_not_updated_after_cheque_or_cash_deposit" }, … ] } } # resultados finales { "test_results": { "accuracy": 0.736318407960199, "correct": 148, "total": 201, "no_answer": 0 }, "error_log": { "accuracy": 0.736318407960199, "errores": [ { "index": 9, "prediction": "top_up_limits", "ground_truth": "automatic_top_up" }, { "index": 2, "prediction": "declined_card_paid", "ground_truth": "declined_transfer" }, { "index": 7, "prediction": "pending_transfer", "ground_truth": "saldo_not_updated_after_cheque_or_cash_deposit" }, … ] } }
Es cierto que los resultados no son muy impresionantes. De hecho, la precisión disminuyó ligeramente después de la optimización, del 75,1 % al 73,6 %. Pero incluso los resultados negativos pueden enseñarnos algo valioso.
Hay algunas razones probables por las que ACE no proporcionó muchos beneficios en este caso:
Datos limitados por categoría. Solo teníamos 248 ejemplos de capacitación, 201 ejemplos de prueba y 51 ejemplos de validación. Sin embargo, nuestra tarea involucró 77 categorías diferentes. Con tan pocos ejemplos por clase, es posible que el modelo simplemente no haya tenido suficientes datos para aprender distinciones significativas. Conjunto de validación pequeño y no representativo. Con solo 51 ejemplos, es posible que el conjunto de validación no haya capturado toda la diversidad de consultas de los clientes, lo que dificulta que ACE genere reflexiones y mejoras útiles. Complejidad de la tarea. Nuestro caso de uso es relativamente sencillo. Como señalan los autores, ACE tiende a brillar en escenarios con grandes cantidades de conocimiento de dominio altamente especializado o flujos de trabajo agentes más complejos, donde la reflexión y el refinamiento iterativo del contexto pueden mejorar significativamente el rendimiento.
Usando ACE para la generación de código
Animado por el experimento anterior, decidí darle otra oportunidad a ACE. Esta vez en el conjunto de datos Mostly Basic Python Problems (disponible bajo licencia cc-by-4.0). Con suerte, los resultados serían más prometedores con una tarea de generación de código.
Resumen de datos
Cada ejemplo del conjunto de datos contiene tres componentes clave:
Pregunta, por ejemplo, "Escribe una función para invertir palabras en una cadena determinada". Implementación de verdad básica: código de referencia de Python. Por ejemplo, para la pregunta anterior def palabras_inversas(s): return ' '.join(reversed(s.split())) Los casos de prueba son afirmaciones para validar el código generado, como [ afirmar palabras_inversas("programa python")==("programa python"), afirmar palabras_inversas("lenguaje java")==("idioma java"), afirmar palabras_inversas("hombre indio")==("hombre indio") ]
Agregar una nueva tarea al marco ACE
Podemos seguir pasos similares para ampliar el marco ACE para manejar tareas de codificación. No entraré en todos los detalles de implementación aquí, ya que puedes encontrar el código completo en GitHub. Sin embargo, vale la pena resaltar las diferencias clave en comparación con el ejemplo de la intención bancaria.
Las tareas de codificación son inherentemente más complejas. En el caso de la intención bancaria, el modelo genera una única clase de 77, que es fácil de comparar directamente con la verdad básica. Sin embargo, en la generación de código, el LLM puede producir código arbitrario, por lo que no podemos simplemente verificar coincidencias exactas. En cambio, necesitamos ejecutar pruebas para determinar si la solución generada es correcta.
# banca def respuesta_es_correcta(self, predicha: str, ground_truth: str) -> bool: return predicted.lower() == ground_truth.lower() # codificación def respuesta_is_correct(self, predicha: str, ground_truth: str, test_list: Lista[str], idx: int, save_dir: str) -> bool: código = extraer_código_de_response(predicho) resultado = ejecutar_code_with_tests(código, lista_prueba, tiempo de espera = 5) devuelve resultado ['éxito']
Debido a esta complejidad adicional, tuve que implementar varias mejoras en el Procesador de datos para la generación de código:
Extracción de código. Los LLM a menudo incluyen contexto adicional alrededor del código, como el formato Markdown (“`python…“`). Necesitamos limpiar y extraer el código para asegurarnos de que se pueda compilar correctamente. Ejecución segura. Dado que ejecutamos el código generado para verificar que sea correcto, es importante implementar medidas de seguridad básicas, como tiempos de espera y entornos de ejecución aislados. Proporcionar contexto completo. Es fundamental incluir toda la información necesaria en la pregunta. Si simplemente le pedimos al LLM que genere código, es poco probable que pase las pruebas porque no quedará claro qué nombre de función o firma se espera. Por eso es crucial proporcionar todos los detalles necesarios en la pregunta al estandarizar los datos en la función Process_task_data. question = ( f"Escribe una función de Python para resolver el siguiente problema:nn" f"Problema: {problem_text}nn" f"Tu código debe pasar los siguientes casos de prueba:n" f"{test_cases_formatted}nn" f"Importante: Los casos de prueba se ejecutarán contra tu código. " f"Asegúrate de que el nombre y la firma de tu función coincidan con lo que esperan las pruebas.nn" f"Responde SÓLO con el Código Python, sin explicaciones." )
En la implementación original de ACE, Reflector comparó el código generado directamente con la verdad básica, que funciona para tareas de clasificación. Sin embargo, para la codificación, este enfoque no tiene sentido: pueden existir múltiples soluciones correctas y optimizar el código que "parece similar" a la referencia no garantiza que pasará las pruebas.
Para solucionar esto, implementé un nuevo método, get_test_feedback, que proporciona al Reflector resultados reales de ejecución de pruebas y mensajes de error. La salida de la prueba se convierte en la señal principal de corrección, brindando retroalimentación mucho más informativa que la simple comparación de códigos.
def get_test_feedback(self, predicted: str, ground_truth: str, test_list: List[str] = Ninguno) -> str: """ Obtenga comentarios detallados sobre la ejecución de la prueba para el reflector. Este método proporciona al reflector resultados de prueba reales y mensajes de error, lo cual es más informativo que simplemente comparar el código generado con la verdad del terreno. La salida de la prueba es la señal principal para la corrección en las tareas de generación de código. Args: predicted: Código predicho del modelo ground_truth: Código de verdad del terreno (solo referencia, no se usa para evaluación) test_list: Lista de aserciones de prueba para ejecutar Devuelve: str: Cadena de comentarios detallada con resultados de ejecución de prueba """ si test_list es Ninguno: devuelve "No se proporcionaron casos de prueba; no se puede evaluar el código". # Extraer código de la respuesta si es necesario code = extract_code_from_response(predicted) # Ejecutar código con el resultado de las pruebas = ejecutar_code_with_tests(code, test_list, timeout=self.timeout) # Crear comentarios detallados feedback_parts =[]if result['success']: feedback_parts.append(f" ✓ Todas las {resultado['total']} pruebas PASADAS") feedback_parts.append("nCasos de prueba ejecutados exitosamente:") para i, prueba en enumerate(test_list, 1): feedback_parts.append(f" {i}. {test} ✓") else: feedback_parts.append(f"✗ Las pruebas FALLARON: {resultado['pasado']}/{resultado['total']} pruebas aprobadas") if resultado['timeout']: feedback_parts.append("n⏱ TIEMPO DE ESPERA: La ejecución del código excedió el límite de tiempo") if resultado['errors']: feedback_parts.append("n— DETALLES DEL ERROR —") for error en el resultado['errors']: feedback_parts.append(f" • {error}") # Mostrar qué pruebas aprobado versus fallido feedback_parts.append("n— RESULTADOS DE LA PRUEBA —") for i, test in enumerate(test_list, 1): # Verifique si esta prueba específica aparece en errores test_failed = any(f"Test {i}" in err for err in result.get('errors',[])) status = "✗ FAILED" if test_failed else " ✓ aprobado" feedback_parts.append(f" {i}. {test} – {status}") # Agregar código extraído como referencia feedback_parts.append("n— CÓDIGO EXTRAÍDO —") feedback_parts.append(code) return "n".join(feedback_parts)
Además de este nuevo método, creé un mensaje Reflector dedicado diseñado para la generación de código. Se centra en los resultados de las pruebas, no en la comparación de códigos línea por línea.
Eres un educador y revisor de códigos experto. Su trabajo es analizar por qué el código generado pasó o falló los casos de prueba e identificar patrones que conducen a soluciones correctas o incorrectas. **IMPORTANTE: Los resultados de la ejecución de la prueba son la señal PRINCIPAL de corrección.** – El código es correcto si y solo si TODAS las pruebas pasan – NO compare las implementaciones línea por línea con la referencia – diferentes implementaciones pueden ser igualmente correctas – Concéntrese en comprender POR QUÉ las pruebas pasaron o fallaron según la lógica del código **Instrucciones:** – Primero, examine los resultados de la ejecución de la prueba para determinar si el código es correcto – Si las pruebas FALLARON: analice qué causó la falla (errores de sintaxis, errores lógicos, borde (casos, algoritmo incorrecto) – Si las pruebas PASARON: Identifique qué hizo bien el modelo que condujo al éxito – La "Posible implementación" es solo UNA forma de resolver el problema – El enfoque del modelo puede ser diferente pero igualmente válido – Proporcione información práctica para mejorar la generación de código en el futuro – Etiquete los puntos como útiles/dañinos/neutrales en función de si contribuyeron a pasar las pruebas Su salida debe ser un objeto json, que contenga los siguientes campos: – razonamiento: analice los resultados de las pruebas y la lógica del código, explique por qué las pruebas pasaron/fallaron – error_identification: si las pruebas fallaron, ¿qué problema específico causó la falla? Si se aprobaron las pruebas, indique "Sin errores, se aprobaron todas las pruebas" – root_cause_analysis: ¿qué concepto o patrón subyacente condujo al éxito o al fracaso? – correct_approach: ¿qué estrategia o patrón de codificación debería utilizarse para problemas similares? – key_insight: ¿qué principio se debe recordar para futuras tareas de generación de código? – Bullet_tags: una lista de objetos json con Bullet_id y etiqueta para cada viñeta **Pregunta:** {} **Seguimiento de razonamiento del modelo:** {} **Código generado por el modelo:** {} **Posible implementación (solo referencia, NO la única solución correcta):** {} **Resultados de la ejecución de la prueba (SEÑAL PRIMARIA):** {} **Parte del libro de jugadas que utiliza el generador para responder la pregunta:** {} **Respuesta exacta Formato JSON:** {{ "reasoning": "[Analizar los resultados de las pruebas y la lógica del código – ¿por qué las pruebas pasaron o fallaron?]", "error_identification": "[¿Qué causó las fallas en las pruebas? O 'Sin errores – todas las pruebas pasaron']", "root_cause_analysis": "[¿Qué concepto/patrón condujo al éxito o al fracaso?]", "correct_approach": "[¿Qué estrategia de codificación funciona para este tipo de problema?]", "key_insight": "[¿Qué principio se debe recordar para ¿generación futura de código?]", "bullet_tags": [ {{"id": "code-00001", "tag": "helpful"}}, {{"id": "code-00002", "tag": "harmful"}} ] }}
Este reflector específico de codificación se utiliza automáticamente siempre que el nombre de la tarea contiene "codificación".
Resultados
Finalmente, ejecuté el proceso de optimización rápida en un conjunto de datos de 500 muestras, divididas en conjuntos de entrenamiento, prueba y validación. Esta vez, los resultados son mucho más prometedores: la precisión mejoró significativamente del 71,1% al 87,1%. En este caso, ACE claramente ayudó a optimizar las indicaciones y guiar el modelo hacia las soluciones correctas.
Mirando el mejor libro de jugadas, es bastante extenso. Muchos de los patrones más útiles son principios generales, como por ejemplo:
Escriba primero la solución Pythonic correcta más simple. Trate los casos de prueba como la especificación verdadera. Verifique la corrección antes de cualquier optimización adicional.
Al mismo tiempo, el manual también incluye orientación muy específica, por ejemplo, instrucciones detalladas para tareas como cálculos de MCD.
En general, esto muestra que ACE puede capturar eficazmente tanto estrategias de alto nivel como consejos para tareas específicas.
## GENERAL ## ERRORES COMUNES A EVITAR [err-00003] útil=5 dañino=0 :: No agregue complejidad innecesaria a los algoritmos recursivos. Por ejemplo, en las implementaciones de GCD, la lógica mínima/máxima explícita o los casos especiales para comprobar si un valor es igual a 1 son redundantes cuando se utiliza el algoritmo euclidiano estándar. [err-00007] útil=0 dañino=0 :: No asuma que las restricciones del problema coinciden con los requisitos previos matemáticos de su algoritmo. Por ejemplo, el pequeño teorema de Fermat para el inverso modular requiere un módulo PRIME; verifique que el problema lo garantice antes de usar pow(a, p-2, p). Si no se especifican restricciones, elija algoritmos más generales. ## OTROS ## PRINCIPIOS DE GENERACIÓN DE CÓDIGO [cgp-00002] útil=41 dañino=2 :: Prefiere implementaciones mínimas y matemáticamente sólidas a las complejas. Evite agregar lógica de preprocesamiento innecesaria (como mín/máx) o verificaciones de casos especiales cuando el algoritmo central maneja naturalmente todos los escenarios. [cgp-00012] útil=91 dañino=2 :: Asegúrese siempre de que el código generado esté sintácticamente completo antes de finalizar el resultado. Verifique que todos los corchetes, llaves y paréntesis abiertos estén correctamente cerrados y que todas las declaraciones estén completamente formadas. La generación de código incompleto (truncamiento a mitad de declaración) provoca errores de sintaxis que impiden la ejecución independientemente de la corrección algorítmica. [cgp-00020] útil=6 dañino=0 :: Cuando un problema requiere explícitamente el uso de funciones lambda, intégrelas de forma natural con las herramientas de programación funcional de Python (asignar, filtrar, reducir, ordenar con parámetro clave). No fuerce el uso de lambda cuando sea incómodo: estas funciones integradas están diseñadas para funcionar perfectamente con lambda para operaciones como filtrado, transformación y conteo. [cgp-00024] útil=140 dañino=2 :: Priorice las soluciones Pythonic legibles que utilizan funciones integradas en lugar de algoritmos complejos con rendimiento optimizado, a menos que el problema requiera explícitamente optimización o involucre datos a gran escala. Una solución clara que utiliza bin(), métodos str o listas por comprensión suele ser preferible a la manipulación de bits o los bucles manuales. Optimice solo cuando sea necesario. [cgp-00047] útil=56 dañino=2 :: Siga una estrategia de desarrollo que dé prioridad a la corrección: (1) implemente el algoritmo sencillo que resuelve correctamente el problema, incluso si no es óptimamente eficiente, (2) verifique la corrección con casos de prueba, (3) solo luego considere la optimización si el rendimiento es inadecuado o el problema lo requiere explícitamente. Una solución O(n) correcta es infinitamente mejor que un intento O(log n) con errores. La optimización prematura a menudo introduce errores en la lógica, especialmente en problemas matemáticos o algorítmicos. [cgp-00050] útil=0 dañino=0 :: Cuando existen varias soluciones algorítmicamente correctas, prefiera la que tenga mejor complejidad temporal/espacial. Una solución correcta basada en la fórmula O(1) es superior a una solución iterativa O(n) correcta. Sin embargo, optimice solo si puede mantener la corrección: una solución O(n) que funcione es infinitamente mejor que un intento O(1) con errores. Verifique que el enfoque más eficiente pase todas las pruebas antes de comprometerse con él. [cgp-00053] útil=0 dañino=0 :: Al implementar optimizaciones matemáticas (especialmente para el recuento de pares/combinaciones), verifique el enfoque optimizado frente a casos de prueba mediante el cálculo manual ANTES de codificar. Para cada caso de prueba: (1) aplique su conocimiento matemático para predecir el resultado, (2) confirme que coincida con el resultado esperado, (3) solo luego implemente. Esto detecta errores en el razonamiento matemático temprano, evitando errores que son más difíciles de depurar en el código que en la aritmética. [cgp-00057] útil=0 dañino=0 :: Evite ocultar los nombres integrados de Python (dict, list, str, int, set, tuple, etc.) al nombrar variables o parámetros. Utilice alternativas descriptivas en su lugar: 'd' o 'data' en lugar de 'dict', 'lst' o 'items' en lugar de 'list', 's' o 'text' en lugar de 'str'. El sombreado integrado los hace inaccesibles en ese ámbito y reduce la claridad del código, aunque sea sintácticamente válido. [cgp-00059] útil=2 dañino=0 :: Incluir prácticas de programación defensiva (validación de entradas, verificación de límites, verificación de tipos) incluso cuando no se prueben explícitamente mediante casos de prueba visibles. Para la indexación de cadenas, valide los límites del índice antes del acceso. Para conversiones numéricas, verifique que la entrada sea un dígito válido. Para operaciones de lista, busque colecciones vacías. Estas salvaguardas aumentan la solidez del código y evitan errores de tiempo de ejecución en casos extremos que pueden existir en pruebas ocultas, lo que demuestra prácticas de codificación con calidad de producción. [cgp-00074] útil=0 dañino=0 :: Para operaciones que involucran potencias de 2, prefiera los operadores de desplazamiento bit a bit a las operaciones aritméticas para mayor claridad y eficiencia: use desplazamiento a la izquierda (1 << k) en lugar de 2**k o pow(2, k) para calcular 2^k, use desplazamiento a la derecha (n >> k) en lugar de n // (2**k) para dividir entre potencias de 2. Los operadores bit a bit hacen explícita la intención a nivel de bits y son idiomáticos enfoque en contextos de manipulación de bits. Esto es especialmente valioso cuando se trabaja con posiciones de bits y sus valores correspondientes. [cgp-00081] útil=0 dañino=0 :: Antes de usar constantes matemáticas de biblioteca estándar (math.pi, math.e, etc.), valide que los casos de prueba esperen valores de precisión total calculando un resultado de prueba y comparándolo con lo esperado. Si los resultados esperados sugieren constantes truncadas/simplificadas (pi=3,14, pi=3,1415, e=2,718), utilice valores codificados que coincidan con la precisión de la prueba en lugar de constantes de biblioteca. Patrón: (1) identificar la constante matemática necesaria, (2) calcular el resultado de la prueba con la constante estándar, (3) si existe una discrepancia, derivar el valor constante que produce los resultados esperados exactos, (4) utilizar un valor codificado. Las expectativas de los casos de prueba anulan la pureza matemática. ## PATRONES COMUNES DE PYTHON [cpp-00010] útil=23 dañino=0 :: Para buscar elementos con propiedades máximas/mínimas según un criterio, utilice las funciones integradas max()/min() con el parámetro clave. Ejemplo: max(list_of_lists, key=len) encuentra la lista más larga. Esto es más Pythonic y legible que la iteración manual con comparaciones. [cpp-00013] útil=17 dañino=0 :: Para contar o buscar operaciones en colecciones de Python (tuplas, listas, cadenas), priorice los métodos integrados: use .count() para contar apariciones, .index() para buscar posiciones, .find() para cadenas. Son más confiables, eficientes y pitónicas que la iteración manual con contadores o bucles. [cpp-00014] útil=3 dañino=0 :: Cuando trabaje con estructuras de datos de tipo mixto, use isinstance() para verificar tipos y distinguir entre diferentes tipos de elementos. Combine con comprobaciones len() para validar la estructura. Ejemplo: isinstance(item, list) y len(item) == 2 identifica de manera confiable listas de 2 elementos en colecciones mixtas. [cpp-00015] útil=3 dañino=0 :: Utilice extend() en lugar de append() al agregar varios elementos de una secuencia a una lista. extend() agrega elementos individualmente a la lista de destino, mientras que append() agregaría la secuencia completa como un único elemento anidado. Ejemplo: resultado.extend([valor] * recuento) frente a resultado.append([valor] * recuento). [cpp-00016] útil=2 dañino=0 :: Utilice la multiplicación de listas ([valor] * recuento) para repetir elementos de manera eficiente. Esto es más Pythonic y legible que los bucles manuales para crear elementos repetidos. Combínelo con extend() para agregar elementos repetidos a listas existentes. [cpp-00019] útil=2 dañino=0 :: Para contar elementos que coincidan con una condición con funciones lambda, use sum(map(lambda x: 1 if condition else 0, iterable)) como una alternativa elegante a len(list(filter(lambda x: condition, iterable))). El enfoque sum(map()) asigna elementos a 1/0 y los suma, a menudo más legible y eficiente que filtrar y luego contar. [cpp-00026] útil=14 dañino=0 :: Para convertir secuencias (tuplas, listas) de caracteres/cadenas en una sola cadena, use el método str.join(): ''.join(sequence) para concatenación de caracteres, o 'separator'.join(sequence) para unir con delimitadores. Este es el enfoque idiomático de Python: más legible y eficaz que los bucles manuales con += o patrones de acumulación. [cpp-00030] útil=1 dañino=0 :: Para la clasificación de caracteres con expresiones regulares, use re.findall() con patrones de clases de caracteres mutuamente excluyentes. Para las categorías de "todo lo demás" (como caracteres especiales), prefiera los patrones de negación [^…] en lugar de enumerar caracteres específicos; por ejemplo, [^A-Za-z0-9] captura todos los caracteres no alfanuméricos de manera integral, evitando la fragilidad de listas como [,.!?]. Asegúrese de que los patrones no se superpongan para evitar el doble conteo. [cpp-00031] útil=2 dañino=0 :: Para encontrar el máximo/mínimo global en iterables anidados (lista de tuplas, lista de listas, etc.), utilice expresiones generadoras anidadas con max()/min() integrado: `max(elemento para contenedor en contenedores para elemento en contenedor)`. Este patrón aplana naturalmente un nivel de anidamiento sin crear listas intermedias, lo que lo hace ideal para encontrar extremos en registros de tuplas o sublistas. Más eficiente y legible que la iteración manual. [cpp-00033] útil=2 dañino=0 :: Para acceso basado en índice a las claves del diccionario, utilice el patrón list(dict)[index] o list(dict.keys())[index]. Esto se basa en Python 3.7+ y garantiza que los diccionarios mantengan el orden de inserción. La conversión del diccionario en una lista extrae las claves en orden, lo que permite la indexación de listas estándar. Esta es la solución idiomática de Python para asignar índices numéricos a claves de diccionario. [cpp-00036] útil=27 dañino=2 :: Para operaciones matemáticas (GCD, LCM, factorial, verificación de primos, trigonometría), verifique PRIMERO el módulo matemático de Python antes de implementar algoritmos manualmente. Las funciones integradas como math.gcd(), math.factorial(), math.isqrt() están bien probadas, optimizadas y reducen los errores de implementación. Patrón: (1) Comprender la definición matemática, (2) Verificar si el módulo matemático proporciona la operación, (3) Úselo directamente o envuélvalo con la lógica específica del problema (por ejemplo, is_coprime = math.gcd(a,b) == 1). [cpp-00038] útil=0 dañino=0 :: Para verificar si un número es un cuadrado perfecto, use math.isqrt() en lugar de math.sqrt() para evitar errores de precisión de punto flotante. Patrón: b = math.isqrt(n); is_perfect_square = (b * b == n). La función isqrt() devuelve la raíz cuadrada del entero, y elevarla al cuadrado permite una comparación exacta de enteros sin problemas de redondeo de punto flotante. [cpp-00043] útil=0 dañino=0 :: Para problemas de filtrado de caracteres (eliminar/mantener caracteres según los criterios de membresía), utilice el patrón conjunto+comprensión+unión: (1) Convierta los criterios de filtro en un conjunto para la búsqueda O(1) (char_set = set(filter_string)), (2) Utilice la comprensión de listas o la expresión generadora para filtrar (char para char en la fuente si char no está en char_set), (3) Utilice ''.join() para reconstruir el cuerda. Este patrón es más pitónico, legible y mantenible que la manipulación manual de índices o los enfoques de conteo de caracteres, y al mismo tiempo es igualmente correcto y eficiente. [cpp-00049] útil=0 dañino=0 :: Al devolver tuplas o listas con tipos numéricos mixtos (enteros y flotantes), utilice operadores de división adecuados para cada componente: división entera (//) para resultados de números enteros, división regular (/) para resultados decimales. exa
Resumen
En este artículo, hemos explorado mucho sobre la ingeniería de contexto y el enfoque ACE, así que recapitulemos brevemente las conclusiones clave:
La ingeniería de contexto se ha convertido en un campo crítico porque nos permite mejorar el rendimiento del LLM sin ajustes largos y costosos. ACE (Agentic Context Engineering) es uno de los últimos enfoques para la optimización rápida, aprovechando guías detalladas con viñetas atomizadas que incluyen instrucciones y metadatos. Como mostraron nuestros ejemplos, la optimización rápida no es una solución milagrosa. No mejora el rendimiento en todos los casos. Según los autores, ACE es más eficaz para flujos de trabajo agentes o dominios altamente especializados. En nuestros experimentos, marcó una clara diferencia en la generación de código, pero tuvo un impacto limitado en la clasificación de la intención bancaria.
La conclusión principal para mí es que la optimización rápida no resolverá su tarea automáticamente. Aún necesita una comprensión integral de qué información tienen el LLM y los agentes durante el proceso de optimización y cuál es la mejor manera de estructurarla y refinarla. El contexto importa, y la ingeniería cuidadosa de ese contexto es lo que hace que enfoques como ACE sean efectivos.
Gracias por leer. Espero que este artículo haya sido revelador. Recuerda el consejo de Einstein: "Lo importante es no dejar de cuestionar. La curiosidad tiene su propia razón de existir". Que tu curiosidad te lleve a tu próxima gran idea.
Referencia
Este artículo se basó en el artículo y la investigación de Zhang et al., publicado en 2025, “Agentic Context Engineering: Evolving Contexts for Self-Improving Language Models”.