Incident s109: Contaminación de contexto en Oráculo por respuestas degradadas
Resumen
El Oráculo de la UI (chat multi-turno en SearchInline.tsx) presentaba un bucle de realimentación de contexto: respuestas degradadas (fallback genérico ‘tu pregunta es muy amplia’) se reenviaban como historia válida, lo que hacía que en la misma conversación cacheada, al repetir la pregunta, el modelo veía el fallback anterior y lo repetía indefinidamente.
Impacto: usuario reutiliza conversación en caché → hace otra pregunta → Oráculo repite fallback anterior aunque debería responder bien.
Causa raíz confirmada: Edu (s109) verificó que en caja limpia (pestaña recargada) el Oráculo responde perfecto. El bucle solo ocurría reutilizando la conversación cacheada.
Causa raíz (análisis)
El flujo es:
- Usuario pregunta → Oráculo falla en síntesis o retrieval → devuelve fallback genérico HTTP 200.
- Cliente (
SearchInline.tsx) filtraba errores de red (error?: boolean) pero no diferenciaba fallbacks. - El fallback se guardaba como turno asistente válido en
turns[]. - Al hacer otra pregunta,
historyPayloadincluía el fallback. - El modelo LLM veía que “el Oráculo dijo X fallback antes” → reproducía el patrón en la respuesta nueva.
El fallback es técnicamente un HTTP 200 (no un error), pero semánticamente es una no-respuesta que contamina el contexto.
Fix aplicado (PR#97, 3 archivos)
1. archivo-core.ts — añade campo degraded
export interface AskResult {
// ... campos existentes ...
degraded?: boolean; // true cuando se devuelve el fallback genérico
}
En synthesizeAnswer(), cuando no se puede extraer respuesta útil del modelo:
if (!answer) {
degraded = true; // marcar como degradada
answer = '...fallback genérico...';
}
Retorna { ..., degraded }.
2. ask.ts — propaga degraded en respuestas
Dos casos más donde se devuelve fallback sin respuesta útil (low-relevance, no-match):
// No-match (retrieval falló)
return Response.json({
degraded: true,
// ...
});
// Low-relevance score
return Response.json({
degraded: true,
// ...
});
// Caso normal: propagar desde synthesizeAnswer
degraded: result.degraded ?? false,
3. SearchInline.tsx — excluye degraded del historyPayload
Antes:
const historyPayload = priorTurns
.filter((t) => !('error' in t) || !t.error) // solo exluye errores de red
.map((t) => ({ role: t.role, content: t.content }));
Después:
const historyPayload = priorTurns
.filter((t) => t.role === 'user' || (!t.error && !t.degraded))
// explícitamente: incluye users, excluye assistants con error O degraded
.map((t) => ({ role: t.role, content: t.content }));
Guarda degraded en el turno:
degraded: data.degraded,
Verificación
- En caja limpia (pestaña recargada): Oráculo responde bien sin fallback reciente.
- En conversación cacheada reutilizada: fallback anterior ahora NO contamina; el modelo no lo ve como contexto válido.
- Typecheck: verde en los tres archivos.
Impacto técnico
| Aspecto | Detalle |
|---|---|
| Cambio de schema | AskResult + OracleResponse incluyen degraded?: boolean |
| Filtrado en cliente | historyPayload ahora excluye tanto error como degraded |
| Propagación | archivo-core → ask.ts → SearchInline.tsx |
| Compatibilidad | Campo opcional (?); clientes viejos ignoran degraded (degrada a incluir fallback, menos malo que antes) |
Contexto: cadena de urgencia del Oráculo
Este es el tercer y final escalón de la cadena de arreglos urgentes:
- PR#95: Mejorar retrieval (búsqueda de chunks).
- PR#96: Síntesis robusta (manejo de respuestas vacías/ininteligibles).
- PR#97 (este): History limpio (no realimentar fallbacks).
Las tres partes verificadas individualmente y en conjunto.
Véase también
- [[entity—functions—handler—archivo-core]]
- [[entity—functions—handler—oraculo-ask]]
- [[entity—src—component—search-inline]]