Reescribir el prompt es el reflejo equivocado

João Rangel12 de agosto de 202611 min de lectura

En este artículo

Yo Claude

El capítulo 04 del tutorial oficial de prompt engineering de Anthropic comienza con un prompt que cualquiera ha escrito en un día apresurado:

  Yo Claude. {EMAIL} <----- Haz este correo más educado...

La respuesta que vuelve comienza así:Dear Claude. El cuaderno señala el diagnóstico:“Claude thinks ‘Yo Claude’ is part of the email it’s supposed to rewrite! You can tell because it begins its rewrite with ‘Dear Claude’.”El saludo que era instrucción fue leído como dato, porque nada en el texto decía dónde comenzaba el correo y dónde terminaba.

Reescribir es el reflejo equivocado. El síntoma indica qué pieza estructural falta.

La pieza aquí es una etiqueta. Las mismas tres líneas, una marcado más:

  Yo Claude. <email>{EMAIL}</email> <----- Haz este correo más educado...

Después de eso, la salida deja de saludarte. La doc de buenas prácticas recomienda envolver cada tipo de contenido en su propia etiqueta, con nombres como<instructions>,<context>y<input>, porque el modelo no tiene forma de inferir una frontera que nadie ha dibujado. Quien se encarga de la gobernanza debería leer este defecto con más aprecio. Instrucción y contenido de terceros en el mismo bloque de texto, sin delimitador, es la superficie en la que entrada no confiable se convierte en comando. En un tutorial, esto cuesta un correo mal formado, pero en un flujo que procesa documentos de clientes, es por esa misma rendija que el contenido de terceros se convierte en inyección.

Setenta por ciento

El caso que lleva el resto del post está en el cookbook oficial , un clasificador que encaja tickets de soporte de aseguradora en 10 categorías, algunas confundibles intencionalmente entre sí, comoBilling InquiriesyBilling Disputes. El cuaderno mide el mismo problema tres veces sin cambiar el modelo (claude-haiku-4-5), la temperatura (temperature=0.0) ni el test set, el conjunto de 68 tickets etiquetados a mano que sirve de plantilla. Lo que cambia de una ronda a otra es una pieza estructural.

La primera versión no tiene ejemplo alguno y ni siquiera tiene system prompt, ese bloque de instrucción que queda fuera de la conversación y vale para toda la sesión. Ninguna de ellas pasa este parámetro, y todo vive en el turno de usuario, dentro del mensaje que el código envía:

  You will classify a customer support ticket into one of the following categories:
<categories>
    {{categories}}
</categories>
...
Respond with just the label of the category between category tags.

En la llamada entran dos piezas más, fuera del texto del prompt. Una es el prefill, en el que comienzas la respuesta del modelo por él, y el pedazo escrito en su lugar es el"<category>"que obliga a la continuación a ser una etiqueta. La otra esstop_sequences=["</category>"], la cadena que hace que la generación se detenga en cuanto aparece, cortando cualquier cosa que venga después. Solo con eso, la exactitud llega a ~70%, contra un baseline aleatorio de ~10%, que es el suelo de una conjetura uniforme entre 10 categorías.

La última línea de la plantilla complica una lectura perezosa de la tesis, porque“Respond with just the label of the category between category tags”es una restricción de salida escrita en palabras comunes y está ahí desde el inicio. La prosa sirve. Lo que no hace por sí sola es fijar la forma de la respuesta, y quien fija es el prefill junto con la stop sequence. Estas dos piezas viven en el código de la llamada, entran en un diff y alguien las aprueba en un PR, mientras “pedimos ser más directos” no es revisable.

Vale registrar el límite del caso, porque el cuaderno declara que“the problem definition, data, and labels used in this example were synthetically generated by Claude 3 Opus”. El material generado así suele ser más bien comportado que tickets reales y da un techo de exactitud más generoso. La progresión entre las rondas sigue siendo informativa, ya que todo lo que cambia entre ellas es la pieza, pero el número absoluto no es una promesa para tu dominio.

¿Y si solo se trata de reescribir?

La objeción es justa, y es la primera que surge a cualquiera. El prompt no necesita una pieza nueva, solo necesita ser más claro. Quizás, y el cookbook tiene cómo medir eso.

RondaLo que cambió en el promptExactitudbaseline aleatorioNinguna, conjetura en 10 categorías~10%simple_classifyetiquetas, prefill, stop sequence~70%rag_classify+ 5 ejemplos etiquetados en<examples>94%rag_chain_of_thought_classify+<scratchpad>97%

Entre la segunda y la tercera línea de la tabla no cambian modelo, temperatura, test set ni cantidad de instrucción en prosa. Lo que entra son cinco ejemplos etiquetados, pares de ticket y categoría correcta pegados dentro del propio prompt para que el modelo vea cómo se resuelve la tarea antes de intentarlo, lo que se llama few-shot. Y no son cinco cualquiera. Son recuperados por similitud: el sistema devuelve los ejemplos más parecidos con el ticket a clasificar. Elegidos así, valen más que cinco fijos por la razón más banal, que es que el ticket de cobro llega acompañado de casos de cobro y no de siniestros. Este cambio vale 24 puntos de exactitud .

El cuaderno registra el salto así,“RAG boosted our accuracy from ~70% to 94%”, y luego“Chain-of-thought reasoning pushed our accuracy to 97%”. Este último salto proviene del<scratchpad>, un espacio en el prompt donde el modelo escribe el razonamiento antes de la respuesta final, lo que se llama chain-of-thought y cuesta tokens en toda llamada. La recomendación de la doc de incluir de tres a cinco ejemplos tiene ese número detrás.

En el directorioevaluation/del mismo caso, una configuración de Promptfoo varre la temperatura en cinco puntos entre 0.0 y 0.8, para ver cuánto del resultado depende de ese botón. Temperatura es cuánto el modelo se permite variar; en 0.0 se vuelve lo más predecible posible. El simple oscila entre 69,1% y 72,1%, el chain-of-thought entre 94,1% y 95,6%, y el RAG cae de 94,12% en T=0.0 a 89,71% en T=0.8. La temperatura afecta el número, sí, pero 4,4 puntos en el peor caso contra los 24 que la pieza compró deciden dónde vale gastar la tarde.

Para quienes necesitan rendir cuentas de lo que ha cambiado, “mejoramos el prompt” no pasa la primera pregunta de seguimiento, mientras que “agregamos cinco ejemplos etiquetados y la exactitud pasó de ~70% a 94% en el mismo test set de 68 tickets” sobrevive a una auditoría. Y sobrevive sobre todo a la situación inversa, cuando la exactitud cae y alguien necesita saber qué revertir.

Tres correcciones, un síntoma

En el ejercicio 7.1 del mismo tutorial, cuatro correos de soporte necesitan convertirse en A, B, C o D, y quien verifica la respuesta correcta es una sola línea de código:

  re.search(ans, response[-1])

Parser es el fragmento de código que lee la respuesta del modelo y extrae de ella el dato que interesa. Este solo mira el último carácter, que debe ser la letra de la categoría. Cualquier “¡Claro! La categoría es B, porque el cliente…” rompe el parser cargando la clasificación correcta dentro de la frase. En los ejercicios 6.1 y 6.2, del capítulo anterior, el corrector es más tolerante y busca el patrón en toda la respuesta; la implacabilidad que interesa aquí es la del 7.1.

Elhints.pyalmacena dos soluciones oficiales para este conjunto, separadas por la causa que atacan. La del 6.1 resuelve por restricción de salida, y son dos piezas, no una frase.

  No incluyas ninguna palabra extra excepto la categoría.
  # prefill del turno del asistente
"("

La del 7.1 resuelve por few-shot, con un bloque<examples>trayendo tres pares de pregunta y respuesta completos, más el prefill"La categoría correcta es:". Comparada con la del 6.1, es otro prompt entero. El 6.2 queda en el medio sin solución redactada, solo con el enunciado, que pide“tu técnica favorita de formato de salida para hacer que Claude envuelva JUST the letter… en<answer></answer>tags”.

Observa lo que la solución 6.1 tiene de incómoda, porque comienza con “Do not”, el encuadre que la doc desaconseja (“tell Claude what to do instead of what not to do”). El material oficial resuelve el ejercicio desobedeciendo a la recomendación de la propia Anthropic y el output sale correcto de todos modos, porque lo que salvó la respuesta fue el prefill(abriendo la forma en lugar del modelo. De ahí surge la regla, que es leer la salida incorrecta e instalar la pieza que cierra la frontera que el modelo no tenía cómo ver. Cuando cada corrección se etiqueta por la causa, se puede escribir un changelog de prompt que alguien aún entiende seis meses después, y sin eso, toda modificación se convierte en “ajuste de redacción” en la descripción del commit.

¿Y la factura?

Sobre el prompt inflado que infla la respuesta y el costo por llamada, no hay número público. El apoyo documental es cualitativo y nada más. La doc observa que el estilo de formateo del prompt puede influir en el estilo de la respuesta y da como ejemplo que eliminar markdown del prompt tiende a reducir el volumen de markdown en la salida. De ahí en adelante vale como impresión de quien opera prompts en producción, con el peso que una impresión tiene. Mide en tu propia cuenta antes de tomarlo como un hecho.

El número duro queda del otro lado, en los structured outputs. La doc lo declara sin eufemismos. Usar un schema, la descripción formal del formato que la respuesta necesita tener, hace que la API inyecte un system prompt adicional explicando ese formato al modelo. Esto aumenta el conteo de tokens de input en toda llamada. Y la primera solicitud con un schema nuevo aún paga la latencia de compilar la gramática, que luego se guarda en caché durante 24 horas desde el último uso. Medir tokens de salida por llamada junto con la exactitud es el mínimo defensable, porque costo que nadie mide solo aparece en el cierre del mes.

¿Structured outputs no resuelve esto?

Vale reconstruir la objeción en su mejor versión, la de la propia documentación . Structured outputs restringe la generación token a token contra una gramática compilada, es decir, la API traduce el schema en un conjunto de reglas y, a cada paso, solo deja que el modelo elija entre las continuaciones que mantienen la respuesta dentro del formato. La promesa es literal en la página,“Always valid: No more JSON.parse() errors”y“No retries needed for schema violations”. Si la salida ya se garantiza en esta capa, escribir instrucciones de formato a mano se convierte en trabajo manual para un problema que la API resuelve en la capa correcta, y quien hace ese argumento tiene la doc de su lado.

El contra-argumento vive en la misma página, y es por eso que vale. Dos líneas de output, ambas válidas contra el mismo schema:

  {"category": "Billing Disputes"}
{"category": "Billing disputes"}

“Structured outputs don’t guarantee the capitalization of string enum and const values.”El enum es la lista cerrada de valores que el campo acepta. La doc es específica sobre dónde suele caer la divergencia,“typically in the first letter of a word following a space”, lo que afecta etiquetas como las de este clasificador, y recomienda comparar valores de enum de forma case-insensitive. Un enrutador aguas abajo que compare cadenas exactas falla con las dos líneas anteriores parseando perfectamente. Hay aún un segundo agujero. La API devuelve enstop_reasonel motivo por el cual dejó de generar, y cuando esrefusalomax_tokensla doc advierte que la salida puede no coincidir con el schema. Schema y pieza de prompt operan en capas diferentes. Y como “el schema garantiza” es la frase que hace que un equipo desactive la validación aguas abajo, continuar verificandostop_reasondespués de adoptar structured outputs es código necesario, no celo.

Al final de cuentas

Diagnosticar por causa solo funciona si existe un test set y alguien dispuesto a ejecutar la medición. El cookbook tiene 68 tickets etiquetados exactamente por eso, y montar un conjunto de estos es el trabajo tedioso que el método exige por adelantado. Por debajo de un puñado de llamadas por día, o en un prototipo que todavía va a cambiar de forma la semana que viene, reescribir a simple vista es más barato y probablemente correcto. La regla vale cuando existe algo aguas abajo que se rompe, un enrutador o un informe que alguien firma. Y el caso conductor tiene el límite ya declarado, con datos y etiquetas sintéticas y un techo más generoso que el real.

¿Cuántos ejemplos etiquetados tendrías hoy, si necesitas probar mañana que el último cambio en tu prompt mejoró algo?

Sigue aprendiendo con Hcode

Regístrate en la plataforma de cursos y sigue las próximas guías del blog.