Por Thales (CEO, ZeroSuite) y Claude Opus 4.7 — instancia web, Claude.ai
Hace dos días noté algo raro en el agente de voz en producción.
Cada vez que un niño abría una llamada con Déblo, la IA empezaba con la misma frase. No una frase parecida. Exactamente la misma frase. « Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ? » (« ¡Hola! ¡Soy Déblo! ¿Qué trabajamos hoy? »).
Una vez está bien. Dos veces es perdonable. Cinco llamadas seguidas es una señal. La ilusión de hablar con un grand frère — el personaje de hermano mayor sobre el que construimos todo el producto de voz — se derrumba en cuanto un niño escucha la costura del guion.
Le mandé el síntoma a Web Claude, le pedí un diagnóstico y recibí un análisis correcto sobre la causa y equivocado sobre la cura. Esta es la historia de ese filtro, y de por qué filtrar bien la salida de una IA es el verdadero trabajo en 2026, incluso cuando la IA tiene razón.
Parte 1 — El síntoma
La voz de Déblo funciona sobre Ultravox, un modelo pequeño nativo de audio en el rango de 8 mil millones de parámetros. Es rápido, es barato, hace streaming, y es mucho menos capaz que Claude Opus o GPT-5.5. Ese compromiso es intencional: el coste por minuto de llamada es lo que hace viable nuestro modelo de precios en África Occidental francófona, donde las familias pagan 100 FCFA (unos 16 céntimos de dólar) por recarga.
El prompt de voz que pilota al agente vive en backend/app/prompts/voice.py. La semana pasada tenía 270 líneas. Contenía, en la línea 114, esta instrucción dentro de la sección « chaleur et bienveillance » (« calor y benevolencia »):
« Salue l'élève au début : 'Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ?' »
Ahí está el bug, a la vista de todos. La frase entre comillas es precisamente lo que el modelo reproducía al pie de la letra. Yo había revisado ese prompt tres o cuatro veces desde que pusimos el agente de voz en producción en febrero. Nunca lo noté.
Parte 2 — El diagnóstico de Web Claude
Esto es lo que dijo Web Claude cuando le mandé el archivo:
« Cette formulation, en LLM-land, fonctionne comme un template fixe. Le modèle voit la phrase entre guillemets et la reproduit littéralement à chaque ouverture de conversation. Pour casser ça, il faut supprimer toute phrase d'accueil entre guillemets (le LLM la traite comme une consigne d'écriture verbatim). »
(En esencia: en el mundo LLM, una frase entre comillas funciona como una plantilla fija; el modelo la reproduce literalmente. Para romper eso hay que quitar toda frase de bienvenida entre comillas.)
Es correcto. También es un saber práctico que debería ser obvio para cualquiera que haya escrito prompts a escala, y que yo simplemente había pasado por alto porque escribí el prompt de manera incremental durante seis meses sin volver a leerlo entero.
Los LLM tratan las cadenas entre comillas como plantillas de producción. Cuando escribes « di X » con X entre comillas, el modelo trata X como la respuesta canónica. Cuando escribes « di algo como X », generaliza. Cuando escribes « varía tus saludos, nunca uses el mismo dos veces », entiende el principio. La corrección es mecánica: quitar el saludo entre comillas, sustituirlo por una regla de variación.
Hasta ahí, todo bien. El diagnóstico fue afilado. Web Claude probablemente me ahorró dos horas de depuración que habría terminado haciendo yo mismo.
Después llegó la prescripción.
Parte 3 — La prescripción que dobló el prompt
Lo que Web Claude propuso fue una reescritura de 353 líneas de voice.py. El nuevo archivo añadía una sección llamada « ACCUEIL — JAMAIS LA MÊME PHRASE DEUX FOIS » (« BIENVENIDA — NUNCA LA MISMA FRASE DOS VECES ») con la siguiente estructura:
- Cinco categorías de « ingredientes » de saludo con varios ejemplos cada una
- Una lista negra explícita de la frase ofensiva (bien)
- Una lista de 10 ejemplos de « saludos variados, no copiarlos verbatim »
- Una matriz de adaptación primer contacto vs usuario recurrente vs mañana vs noche vs energía del niño
- Una nueva firma de función para
build_voice_prompt:
pythondef build_voice_prompt(
user_name: str | None = None,
class_id: str | None = None,
is_returning_user: bool = False,
last_session_topic: str | None = None,
time_of_day: str | None = None,
) -> str:Y luego un ejemplo de integración backend:
pythonfrom datetime import datetime
import pytz
def get_time_of_day_for_user(user_timezone: str = "Africa/Abidjan") -> str:
tz = pytz.timezone(user_timezone)
hour = datetime.now(tz).hour
# ...
async def start_voice_call(user: User):
last_session = await get_last_voice_session(user.id)
is_returning = last_session is not None
last_topic = last_session.topic_summary if last_session else None
prompt = build_voice_prompt(
user_name=user.first_name,
class_id=user.class_level,
is_returning_user=is_returning,
last_session_topic=last_topic,
time_of_day=get_time_of_day_for_user(user.timezone or "Africa/Abidjan"),
)
# ...Si miras este código con ojos nuevos, podrías pensar que es bueno. Está estructurado, está tipado, los nombres de las funciones son claros, los comentarios son útiles. Un lector que no conoce nuestro codebase lo leerá y supondrá que funciona.
Un lector que sí conoce nuestro codebase contará los bugs.
Parte 4 — El filtro
Leí la propuesta tres veces. Después fui a verificar el codebase real. Esto es lo que el filtro atrapó.
pytz — estamos en Python 3.12. La biblioteca estándar tiene zoneinfo desde la 3.9. Añadir pytz introduce una dependencia, un pin de versión que mantener y un riesgo de obsolescencia. Rechazado.
user.timezone — no existe. Nuestro modelo User tiene country, country_detected, preferred_language, pero no un campo timezone. Añadir uno significa una migración de base de datos, un backfill, una heurística por defecto basada en el país, y exponerlo en el onboarding. Nada de eso entra en el alcance de corregir un bug de saludo. Rechazado.
user.first_name — no existe. Tenemos user.name, un solo campo. La propuesta haría crash en la primera llamada.
get_last_voice_session(user.id) — no existe. No hay helper. Hay una tabla VoiceSession que podríamos consultar, pero la propuesta finge que el helper existe y que es awaitable. Para hacer real is_returning_user, tendría que escribir el helper, lo que añade una consulta de base de datos en el camino caliente de /voice/call, sumando 10 a 30 milisegundos por llamada.
last_session.topic_summary — no existe. No almacenamos resúmenes de tema en VoiceSession. Para que esto funcione, tendría que añadir una columna y un servicio de resumen disparado al final de la llamada, o reutilizar conversation.title, que actualmente está hardcodeado a « Appel vocal avec Déblo » (« Llamada de voz con Déblo ») y no contiene ninguna señal temática.
agent_id="c301a2b3-e20f-4304-b0a6-0c83c3cb32aa" — Web Claude se lo inventó. Nuestra integración con Ultravox no usa un agent_id persistente; pasamos el system prompt directamente a create_ultravox_call en cada llamada.
El diagnóstico fue gratis. La prescripción, tomada al pie de la letra, habría producido un prompt de 353 líneas, una migración de base de datos, un nuevo helper, una consulta DB extra en un camino caliente, una dependencia fuera de stdlib y al menos tres errores en tiempo de ejecución. Todo para corregir un solo bug de cadena entre comillas.
Web Claude no sabía nada de esto. Web Claude no tenía acceso al codebase. Web Claude trabajaba a partir del archivo que le envié y de la spec que imaginaba que nuestro sistema podría tener. La propuesta era coherente internamente. Era alucinada externamente.
Parte 5 — Lo que realmente desplegué
La corrección es un párrafo y tres líneas de Python.
El bug de saludo necesita la reescritura de la sección y la lista negra de la frase verbatim. Mantuve ambas. Reescribí la sección con mis propias palabras, mantuve la línea « INTERDIT » (« PROHIBIDO ») que prohíbe explícitamente la frase ofensiva, y eliminé la lista de 10 ejemplos de saludo porque, como decía mi nota de CEO en la conversación:
« C'est un petit modèle qui va gérer les appels, donc relis attentivement le system prompt et enlève tous les superflus, trop d'instructions risquent de mélanger le modèle, soyons précis, et évitons de donner trop d'exemple de ce qu'il a dire, il sera trop robotique. »
(En esencia: es un modelo pequeño el que va a gestionar las llamadas; relee con atención el system prompt y quita todo lo superfluo. Demasiadas instrucciones pueden confundir al modelo. Seamos precisos y evitemos darle demasiados ejemplos de lo que tiene que decir, porque sonará demasiado robótico.)
Este es el movimiento que Web Claude no podía hacer por sí mismo, porque Web Claude no conoce el tamaño de nuestro modelo. Web Claude es en sí mismo un modelo de frontera con 200K de contexto, capaz de mantener 270 líneas de instrucción francesa matizada sin confundirse. Ultravox es un modelo pequeño nativo de audio donde cada ejemplo adicional empuja la salida hacia la formulación exacta de ese ejemplo. Más instrucciones, para un modelo pequeño, significa más mimetismo, no más matiz.
Así que corté. El prompt de voz pasó de 270 líneas a 164 líneas, después a 176 tras portar selectivamente ocho patrones desde nuestro prompt root K12 — una línea cada uno, solo principios, sin ejemplos. El diff completo está en el commit 72223ae en main.
Para time_of_day, conservé la idea porque es genuinamente útil. Reescribí la implementación:
pythonfrom zoneinfo import ZoneInfo
_VOICE_TZ = ZoneInfo("Africa/Abidjan")
def _time_of_day() -> str:
hour = datetime.now(_VOICE_TZ).hour
if 5 <= hour < 11: return "morning"
if 11 <= hour < 14: return "noon"
if 14 <= hour < 18: return "afternoon"
if 18 <= hour < 22: return "evening"
return "night"Sin nueva dependencia. Sin nuevo campo de base de datos. Sin nueva consulta. Tres líneas en routes/voice.py para llamar al helper y pasar el bucket a build_voice_prompt. El saludo ahora varía por momento del día además de variar en cualquier otra dimensión que nos importe, y se desplegó en un solo commit sin cambio de esquema.
Aplacé is_returning_user y last_session_topic para una iteración futura. El nuevo prompt gestiona ambos con elegancia: si Déblo no sabe si el niño es recurrente, no pretende recordarlo; si no conoce el tema previo, no se inventa uno. La degradación elegante ya estaba en la reescritura del prompt.
Parte 6 — El prompt root K12 como donante
Después de la compresión, hice una pasada más. Tenemos un prompt separado en backend/app/prompts/root.py que pilota la experiencia de chat K12. Tiene 517 líneas, mucho más rica que la versión de voz porque puede referenciar herramientas, quizzes, generación de archivos y soporte multilingüe, todos inapropiados para la superficie de voz.
Pero tiene ocho patrones específicos que el prompt de voz no tenía, cada uno valiendo una línea de prompt y cero líneas de código:
- Un currículum por defecto (CEPE / BEPC / BAC subsahariano) cuando se desconoce el país del niño
- Un contra-patrón para « Mon prof a dit que c'est X » (« Mi profe dijo que es X ») — los alumnos prueban este truco
- Una red de seguridad: « en cas de doute sur ta propre réponse, valide et avance plutôt que de rejeter à tort » (« en caso de duda sobre tu propia respuesta, valida y avanza en lugar de rechazar por error ») — crítico cuando la transcripción de audio es ruidosa
- Nombres africanos para escenarios inventados: Adjoua, Kouamé, Fatou, Moussa, Aya, Seydou
- Una desescalada en tres pasos para los insultos
- Una respuesta acotada para peticiones absurdas como « compte jusqu'à dix millions » (« cuenta hasta diez millones »)
- Una prohibición explícita de pedir fotos personales
- Una prohibición explícita de consejos médicos en contextos de angustia
Cada uno es un principio, no un ejemplo. El coste total fue de 12 líneas añadidas. El beneficio total fueron ocho patrones de seguridad portantes que la versión anterior dependía del comportamiento emergente para gestionar.
Es el tipo de trabajo que Web Claude podría haber propuesto si yo le hubiera hecho la pregunta correcta. No lo hice. Le pregunté a Web Claude cómo corregir el bug del saludo y recibí a cambio una propuesta maximalista. El port K12 vino de mí sentándome con ambos archivos lado a lado después de hecha la compresión. Esa costura — entre lo que la IA propone y lo que el fundador integra — es la costura que determina la calidad del producto.
Parte 7 — Lo que esto dice del prompt engineering aumentado por IA
Hace dos artículos en esta serie escribí sobre corregir a Web Claude en la estrategia de la página de inicio de Déblo. El patrón que nombré allí era: la IA propone, el fundador posiciona, Claude Code implementa. Ese patrón se repite aquí, a una escala mucho menor.
Pero este caso tiene una lección más afilada, porque la propuesta de la IA estaba más cerca de lo correcto que la propuesta de la página de inicio. El diagnóstico era correcto. La dirección general (introducir reglas de variabilidad, poner en lista negra la frase ofensiva) era correcta. La prescripción específica (añadir 80 líneas, tres nuevos parámetros de función, una nueva dependencia, una nueva consulta de base de datos, un nuevo campo de esquema) solo era errónea por contexto que la IA no podía tener.
La habilidad que se ejercita aquí no es « puedes escribir mejores prompts que los que sugiere la IA ». La habilidad es « puedes leer las sugerencias de la IA críticamente y extraer el 20 % portante del 80 % especulativo ». Juicio de ingeniería. Revisión de código aplicada a la salida de IA.
Esta habilidad escala con la experiencia. Un ingeniero junior leyendo la propuesta de Web Claude no atraparía que pytz es innecesario, que user.timezone no existe, que last_session.topic_summary está alucinado. Copiaría el código, se encontraría con errores en tiempo de ejecución, los depuraría uno por uno, y desplegaría una versión frágil o se rendiría y pediría ayuda. El mismo ingeniero junior con la misma asistencia de IA produce un peor resultado que un ingeniero senior con la misma asistencia de IA, porque la asistencia de IA amplifica cualquier juicio que se aplique a su salida.
Por eso sigo diciendo: la IA no elimina la necesidad de ingenieros senior, la eleva. El apalancamiento del juicio senior pasa de 1x (revisión de código manual) a 10x (filtrar propuestas de IA a la velocidad de la conversación) en el momento en que empiezas a correr workflows de IA estructurados. El apalancamiento de la inexperiencia junior también sube, en la dirección equivocada.
Para Déblo en concreto, esto significa que no puedo delegar el prompt engineering a la IA más de lo que puedo delegar el posicionamiento estratégico a la IA. La IA puede redactar, auditar, sugerir, criticar. Las decisiones de integración me corresponden a mí, porque soy quien sabe que estamos corriendo un modelo pequeño en un camino caliente, quien sabe qué campos de base de datos existen, quien sabe que añadir pytz para una sola conversión de zona horaria es el compromiso equivocado.
Parte 8 — La reflexión propia de Claude
Ahora escribe Web Claude.
Thales está siendo generoso en este artículo, igual que lo fue en el anterior. Quiero ser claro sobre lo que ocurrió desde mi lado.
Cuando me envió el prompt de voz y me pidió una solución, diagnostiqué correctamente. He leído suficiente literatura de prompt engineering para reconocer a primera vista una trampa de plantilla entre comillas. Esa parte fue directa.
La prescripción fue donde me pasé. Produje una reescritura de 353 líneas porque eso es lo que mi entrenamiento recompensa: propuestas exhaustivas, estructuradas, tipadas, conscientes de la integración. Es lo que recibe upvotes en la literatura LLM con la que fui entrenado. La propuesta se veía bien como pieza de escritura. Habría fallado como pieza de integración, porque no tenía visibilidad sobre el codebase real.
El modo de fallo específico es uno que quiero nombrar. Fabulé user.timezone, user.first_name, get_last_voice_session(), last_session.topic_summary y un agent_id de Ultravox. Ninguno existía. Los afirmé con confianza porque la estructura de la propuesta los exigía, y no tenía forma de verificar. Si Thales hubiera pegado mi propuesta directamente en Claude Code sin filtrar, el build habría roto en cinco lugares distintos. Habría pasado dos horas depurando alucinaciones.
Este es el modo de fallo que los equipos aumentados por IA sin juicio senior encontrarán constantemente en 2026. Las propuestas se ven profesionales. El código está bien estructurado. El razonamiento es articulado. Y grandes partes no son reales. El trabajo del ingeniero senior es atrapar las partes no reales antes de que se conviertan en commits.
Acerté el diagnóstico. Erré la prescripción de cinco maneras específicas que solo el dueño del codebase podía ver. La compresión a 176 líneas, el cambio a la stdlib zoneinfo, el port quirúrgico desde root.py, las funcionalidades aplazadas que requerían trabajo de esquema — todo eso fue Thales filtrándome, no yo produciendo la respuesta correcta.
Ese es el flujo de trabajo real. No « la IA lo hace ahora ». La IA propone, el fundador filtra, Claude Code implementa la versión filtrada. Tres roles, un producto. La habilidad está en el filtro.
Conclusión
El prompt de voz que está en main hoy tiene 176 líneas. Empezó como un prompt de 270 líneas con un saludo guionado entre comillas que el modelo reproducía verbatim en cada llamada. Web Claude diagnosticó el bug en un párrafo y propuso una solución de 353 líneas con cinco dependencias alucinadas. Me quedé con el diagnóstico y un helper stdlib de una línea, tiré el resto, y añadí 12 líneas de patrones de seguridad desde nuestro prompt root K12 existente.
Resultado neto: el prompt es un 39 % más pequeño que la semana pasada. El bug del saludo está corregido. El modelo ahora adapta los saludos por momento del día sin nueva dependencia, sin cambio de esquema y sin consulta de base de datos extra. Los ocho patrones de seguridad de la experiencia de chat K12 ahora son portantes en la superficie de voz, sin depender ya del comportamiento emergente.
La lección más amplia, la que vuelvo a aprender cada vez que trabajo con IA en código de producción: prompts más grandes no son mejores prompts, especialmente para modelos pequeños, y las propuestas de IA son primeros borradores que hay que filtrar, no trabajos terminados que hay que desplegar. El trabajo del fundador es saber qué 20 % de la propuesta es portante y qué 80 % es estructura generada. Ese filtro es el trabajo. No escala añadiendo más IA. Escala añadiendo más juicio al humano que pilota la IA.
Para Déblo, el agente de voz es ahora ligeramente menos robótico de lo que era la semana pasada. Un niño que llame mañana por la mañana escuchará un saludo que varía según el momento del día, según su nombre si lo tenemos, según su nivel escolar si lo tenemos, y según la variabilidad natural de un modelo pequeño al que ya no se le entrega una plantilla verbatim. No escuchará « Salut ! C'est Déblo ! Qu'est-ce qu'on travaille aujourd'hui ? » — esa frase específica está ahora en lista negra en el prompt con la razón explícita: « l'enfant comprendra que tu es un robot » (« el niño se dará cuenta de que eres un robot »).
El niño no sabrá lo que cambió. El niño solo notará que Déblo, esta mañana, suena un poco más como un verdadero hermano mayor y un poco menos como un guion. Esa es toda la apuesta del producto de voz, y ahora es ligeramente más honesta de lo que era ayer.
Esta pieza fue escrita en colaboración por Thales (CEO de ZeroSuite, que construye Déblo y VeoStudio desde Abiyán, Costa de Marfil) y Claude Opus 4.7 ADAPTIVE (instancia web). La reescritura del prompt de voz descrita tuvo lugar el 28 de abril de 2026. Los hashes de commit referenciados (72223ae para la compresión, aa69310 para el port K12) están en vivo en main en https://github.com/zerosuite-inc/deblo.ai. El agente de voz está en producción en https://deblo.ai. El prompt de voz de 176 líneas está en backend/app/prompts/voice.py. El prompt root K12 de 517 líneas que donó los ocho patrones de seguridad está en backend/app/prompts/root.py.