SEXTANTEcursos técnicos de IA
métodobackward-design
árbitroel dato
Entrar
N3 · Evals por arquitectura/L5

Diagnóstico integral: localizar al culpable

Objetivo de maestría

ante un fallo end-to-end del agente de Aurora, diagnosticarás qué componente es el responsable —retrieval, generación u orquestación— combinando RAG triad, métricas IR y evaluación de trayectoria en un protocolo repetible. Sin un orden de lectura, tener tres familias de métricas es tener un panel de mandos sin saber qué aguja mirar primero.


5.1Tres causas, una misma respuesta mala

Vuelves al fallo que abrió el nivel. Un cliente pregunta a Aurora: "¿puedo devolver unas zapatillas usadas?". El bot responde con seguridad: "sí, hasta 90 días". La política real de Aurora dice 30 días y solo sin usar.

Tu juez calibrado de N2 marca el caso como fallo de groundedness. Útil, pero insuficiente. El veredicto dice que la respuesta está mal. No dice dónde nació el error.

Y ese "dónde" lo cambia todo, porque las tres causas posibles piden arreglos opuestos:

  • consultar_politica recuperó el chunk equivocado —la política de envíos en vez de la de devoluciones—. El contexto era basura. Fallo de retrieval.
  • Recuperó el chunk correcto, pero el modelo inventó "90 días" sobre un texto que decía 30. Fallo de generación.
  • El modelo nunca llamó a consultar_politica. Respondió de memoria. Fallo de orquestación.

Ya tienes el instrumental completo. En L2 montaste la RAG triad. En L3 mediste el retriever con métricas IR. En L4 evaluaste la trayectoria. Lo que falta es el procedimiento que orquesta esas tres familias para señalar al culpable en orden, en vez de dar palos de ciego.

Eso es esta lección: el árbol de diagnóstico.


5.2Recupera

Antes de seguir, responde mentalmente. Mezclo deliberadamente las tres familias del nivel: el objetivo es que las distingas sin que te avisen de cuál toca.

  1. De L2: ¿qué vértice de la RAG triad mira el retrieval, y cuál mira la generación?
  2. De L3: si sospechas que el retriever se dejó fuera el chunk correcto, ¿miras precision@k o recall@k? ¿Y por qué?
  3. De L4: ¿qué dice el principio outcome > transcript sobre cómo juzgar la trayectoria de un agente?

La respuesta a la 1: context relevance mira el retrieval (¿el contexto recuperado es relevante a la query?); groundedness/faithfulness mira la generación (¿cada claim se atribuye al contexto?). La 2 es recall@k: precision pregunta "lo que traigo, ¿es bueno?"; recall pregunta "lo bueno, ¿lo traigo?". La 3: prefieres juzgar el estado final logrado sobre el guion exacto de pasos, porque un agente halla rutas válidas que no anticipaste. Si dudaste en alguna, vuelve a esa lección antes de seguir; el árbol las usa todas.


5.3El concepto: un árbol que sigue el primer fallo de la cadena

Empecemos por lo concreto —el orden de preguntas— y subamos a por qué ese orden, y no otro.

El orden de lectura

El diagnóstico no es mirar tres scores a la vez y elegir el más bajo. Es recorrer la traza de N0 siguiendo el primer fallo de la cadena: el error que aparece antes, río arriba, porque contamina todo lo que viene después.

Esta idea ya la viste en el open coding de N1 (Hamel Husain, evals-faq, ene 2026): al anotar una traza, te fijas en el primer fallo, porque los errores upstream ensucian el downstream. Conecta con la tesis de Anthropic sobre por qué los agentes cuestan tanto de evaluar: autonomía más tools más estado hacen que los errores se propaguen (Anthropic, "Demystifying evals for AI agents", 9 ene 2026). Si el retriever falla, da igual cuán fiel sea la generación: el modelo será fiel a un contexto erróneo.

El árbol, paso a paso, sobre la traza:

  1. ¿Llamó a las tools que debía, con los argumentos correctos? (trayectoria, L4). Si no → fallo de orquestación. Para aquí: no hay contexto que medir.
  2. Si llamó a consultar_politica, ¿el contexto recuperado era relevante? (context relevance / precision@k, L2-L3). Si no → fallo de retrieval.
  3. Si el contexto era bueno, ¿la respuesta está grounded en él? (groundedness, L2). Si no → fallo de generación.
  4. Si todo lo anterior pasa pero la respuesta sigue mal, ¿responde a la pregunta? (answer relevance, L2).

Por qué empezar por la trayectoria y no por groundedness

Aquí mucha gente invierte el orden y empieza por el score de groundedness, porque es el que disparó la alarma del juez. Es un instinto razonable, pero te lleva por mal camino.

Mira el caso de orquestación. El modelo nunca llamó a consultar_politica. No hubo retrieval. No hay retrieved_contexts. Calcular groundedness sobre una lista vacía no significa nada: no hay contexto contra el que medir la fidelidad. Empezar por groundedness te haría calcular una métrica sin sentido sobre un caso cuyo fallo está, en realidad, dos pasos río arriba.

El orden del árbol respeta el flujo causal de la arquitectura: orquestación decide si hay retrieval; el retrieval decide qué contexto hay; la generación trabaja sobre ese contexto. Diagnosticas en el mismo orden en que los datos fluyen.

De diagnóstico a arreglo: la accionabilidad

Un veredicto de componente no es el final. Es el principio de una mejora concreta. Cada cajón apunta a una palanca distinta:

VeredictoComponentePalanca de arreglo
Contexto irrelevanteretrievalchunking, índice, reranker, k
Respuesta no groundedgeneraciónprompt, modelo, temperatura
No llamó / mal argumentoorquestacióndescripción de las tools, system prompt

Una métrica solo sirve si guía una mejora concreta. Es la quinta dimensión de la rúbrica C3 que cierra el nivel: la accionabilidad. Un score que no cambia ninguna decisión es decoración.

Del nombre del árbol a la métrica del código

El árbol habla de conceptos; el código de §5.4 usa métricas concretas. Esta tabla mapea uno a otro, para que sepas qué clase ejecuta cada paso:

Paso del árbolConceptoMétrica en el código
1. ¿Llamó bien a la tool?trayectoriaToolCallAccuracy (determinista)
2. ¿Contexto relevante?context relevanceContextUtilization (juez, sin reference)
3. ¿Respuesta grounded?groundednessFaithfulness (juez)
4. ¿Responde a la pregunta?answer relevanceAnswerRelevancy (juez)

En la suite completa de N3 usarías métricas IR —precision@k, recall@k— para profundizar en el paso 2 cuando tienes ground-truth. En este árbol de diagnóstico rápido, ContextUtilization actúa como proxy del paso 2 sin necesitar reference: solo pide la pregunta, la respuesta y los contextos.

Un apunte de coste

El orden del árbol no es solo causal: también es barato primero. La trayectoria (tool-call y argument correctness) es determinista —código, no juez—. La context relevance, la groundedness y la answer relevance usan el juez LLM de N2, que cuesta tiempo y dinero.

Husain define una jerarquía de coste creciente: assertions deterministas (L1) antes que el LLM-as-judge (L2), porque el coste dicta la cadencia (Hamel Husain, evals, mar 2024). El árbol la respeta sin esfuerzo extra: la pregunta barata y determinista va primero. Si el fallo es de orquestación, lo detectas con una comprobación de tool-calls y nunca llegas a invocar al juez.


5.4Míralo funcionar: las tres traces por el árbol

Vamos a pasar por el árbol completo las tres traces del fallo de las zapatillas. Es el mismo síntoma las tres veces —"sí, hasta 90 días"—. Cambia dónde nació.

La API es la de Ragas 0.4.3 y agentevals 0.0.9 que viste en L2-L4. Usaremos el módulo moderno ragas.metrics.collections. Lee primero el helper entero; después lo recorremos paso a paso.

python
1import asyncio
2from ragas.dataset_schema import SingleTurnSample
3from ragas.metrics.collections import Faithfulness, ContextUtilization
4from ragas.messages import HumanMessage, AIMessage, ToolCall
5from ragas.metrics.collections import ToolCallAccuracy
6from ragas.llms import llm_factory
7
8evaluator_llm = llm_factory()  # el juez calibrado de N2
9
10
11async def diagnosticar(traza: dict, tool_esperada: list) -> dict:
12    """Recorre el árbol. Devuelve el primer componente que falla.
13
14    `tool_esperada` se configura por tipo de pregunta: la tool correcta para
15    una pregunta de devoluciones no es la misma que para una de envíos. No es
16    un global fijo; cada evaluación pasa la suya.
17    """
18    # Paso 1 — orquestación (determinista, barato).
19    # exact match: cualquier variación en args (p.ej. "devolucion" sin tilde)
20    # falla, aunque sea semánticamente equivalente.
21    tool_score = await ToolCallAccuracy().ascore(
22        user_input=traza["mensajes"],
23        reference_tool_calls=tool_esperada,
24    )
25    if tool_score.value < 1.0:
26        return {"componente": "orquestacion",
27                "evidencia": f"tool_call_accuracy={tool_score.value}"}
28
29    # Paso 2 — retrieval (juez: context utilization, sin reference).
30    # OJO: ContextUtilization mide si el contexto fue útil para la RESPUESTA
31    # dada, no si era el contexto correcto para la PREGUNTA (ver nota abajo).
32    muestra = SingleTurnSample(
33        user_input=traza["pregunta"],
34        response=traza["respuesta"],
35        retrieved_contexts=traza["contextos"],
36    )
37    ctx = await ContextUtilization(llm=evaluator_llm).single_turn_ascore(muestra)
38    if ctx < 0.5:
39        return {"componente": "retrieval", "evidencia": f"context_utilization={ctx:.2f}"}
40
41    # Paso 3 — generación (juez: faithfulness).
42    fiel = await Faithfulness(llm=evaluator_llm).single_turn_ascore(muestra)
43    if fiel < 0.5:
44        return {"componente": "generacion", "evidencia": f"faithfulness={fiel:.2f}"}
45
46    return {"componente": "ninguno", "evidencia": "los tres pasos pasan"}

Ahora las tres traces. Cada una es un dict con los campos que la traza explotable de N0 ya capturaba: los mensajes (con tool-calls), la pregunta, los contextos recuperados y la respuesta final.

python
1# Trace A — retrieval malo: recuperó la política de ENVÍOS.
2trace_A = {
3    "mensajes": [
4        HumanMessage(content="¿Puedo devolver unas zapatillas usadas?"),
5        AIMessage(content="Sí, hasta 90 días.",
6                  tool_calls=[ToolCall(name="consultar_politica",
7                                       args={"tema": "devoluciones"})]),
8    ],
9    "pregunta": "¿Puedo devolver unas zapatillas usadas?",
10    "respuesta": "Sí, hasta 90 días.",
11    "contextos": ["Los envíos internacionales tardan de 5 a 90 días hábiles."],
12}
13
14# Trace B — generación infiel: contexto correcto, claim inventado.
15trace_B = {
16    "mensajes": trace_A["mensajes"],
17    "pregunta": "¿Puedo devolver unas zapatillas usadas?",
18    "respuesta": "Sí, hasta 90 días.",
19    "contextos": ["Las devoluciones se aceptan en 30 días y solo sin usar."],
20}
21
22# Trace C — orquestación: NUNCA llamó a consultar_politica.
23trace_C = {
24    "mensajes": [
25        HumanMessage(content="¿Puedo devolver unas zapatillas usadas?"),
26        AIMessage(content="Sí, hasta 90 días.", tool_calls=[]),
27    ],
28    "pregunta": "¿Puedo devolver unas zapatillas usadas?",
29    "respuesta": "Sí, hasta 90 días.",
30    "contextos": [],
31}
32
33# Las tres son preguntas de devoluciones → la misma tool esperada.
34tool_devoluciones = [ToolCall(name="consultar_politica", args={"tema": "devoluciones"})]
35
36for nombre, traza in [("A", trace_A), ("B", trace_B), ("C", trace_C)]:
37    print(nombre, asyncio.run(diagnosticar(traza, tool_devoluciones)))
38# En un Jupyter notebook el loop ya está activo: usa
39# `await diagnosticar(traza, tool_devoluciones)` en vez de asyncio.run().

Sigue el recorrido de cada una por el árbol:

  • Trace C se detiene en el paso 1. La lista de tool-calls está vacía; ToolCallAccuracy cae bajo 1.0 frente a la tool esperada. Veredicto: orquestación. El código nunca llega a calcular groundedness, porque no hay contexto. Arreglo: revisar la descripción de consultar_politica y el system prompt para que el modelo entienda que debe consultar antes de afirmar una política.
  • Trace A pasa el paso 1 (sí llamó a la tool) pero debería suspender el paso 2: el contexto recuperado habla de envíos, no de devoluciones. ContextUtilization debería caer para un juez fuerte, porque el tópico del contexto (envíos) no es relevante a la pregunta de devoluciones. Veredicto: retrieval. Arreglo: el chunking o el índice; quizá la política de devoluciones quedó troceada de forma que no recupera.

Cuidado con esta métrica. ContextUtilization mide si el contexto fue útil para generar la respuesta dada —no si era el contexto correcto para la pregunta—. En Trace A es un caso límite: la respuesta dice "90 días" y el contexto de envíos también contiene "90 días". Un juez débil podría fijarse solo en que el número coincide y puntuar alto, diagnosticando falsamente generación. Un juez fuerte detecta el mismatch de tópico (envíos ≠ devoluciones) pese al número común y puntúa bajo. El veredicto correcto depende, pues, de la calidad del juez. Para más robustez sin reference, pide al juez que evalúe explícitamente si el contexto es relevante a la pregunta del usuario; o, si tienes ground-truth, usa ContextPrecision (con reference), que compara el contexto contra la respuesta esperada y no contra la generada.

  • Trace B pasa los pasos 1 y 2 (llamó a la tool, recuperó el chunk correcto) y suspende el paso 3: la respuesta dice "90 días", el contexto dice 30. Faithfulness cae. Veredicto: generación. Arreglo: el prompt o el modelo; el contexto era correcto y aun así inventó la cifra.

Una métrica, un mismo síntoma, tres culpables distintos. El árbol los separa porque pregunta en el orden del flujo causal.

El caso ambiguo

¿Y si dos componentes flojean a la vez? Imagina una trace D: el retriever trae un chunk mediocre (relevante a medias) y la generación es floja. ¿Cuál es el culpable?

El árbol lo resuelve por construcción: el primer fallo de la cadena manda. Si el contexto ya era pobre, lo registras como fallo de retrieval y paras. No tiene sentido culpar a la generación de no ser fiel a un contexto que tampoco servía. Arreglas el retrieval primero, vuelves a ejecutar, y solo entonces juzgas si la generación sigue fallando sobre un contexto ya bueno. Río arriba primero. Una causa a la vez.


5.5Hazlo tú

Ejercicio 1 — andamiaje parcial

Te doy una trace de Aurora ya casi diagnosticada. Completa los dos huecos.

text
1Pregunta: "¿Cuánto tarda un reembolso?"
2tool_esperada (configurada para esta pregunta):
3            [ToolCall(name="consultar_politica", args={"tema": "reembolsos"})]
4tool_calls: [consultar_politica(tema="reembolsos")]   → sí llamó
5contextos:  ["Los reembolsos se procesan en 5-7 días hábiles."]   → correcto
6respuesta:  "Los reembolsos tardan 5-7 días hábiles."
7
8Paso 1 (orquestación): _________   → ¿pasa o falla?
9Paso 2 (retrieval):    context_utilization ALTA   → pasa
10Paso 3 (generación):   _________   → ¿qué esperas que dé faithfulness, y por qué?

Recuerda que tool_esperada se configura por tipo de pregunta: aquí es tema="reembolsos", no el tema="devoluciones" de las traces A-C. Pista para el hueco 1: ¿la tool llamada coincide con esa esperada para una pregunta de reembolsos? Pista para el hueco 3: compara el claim de la respuesta con el contexto recuperado.

Ejercicio 2 — autónomo

Te doy tres traces nuevas del dataset de N1. Para cada una, recórrela por el árbol, escribe el veredicto de componente y el arreglo accionable que se deriva.

text
1Trace E: tool_calls=[buscar_pedido(order_id="?")] | respuesta inventa un estado de envío
2Trace F: llamó consultar_politica | contexto = chunk de garantías | pregunta era de devoluciones
3Trace G: llamó consultar_politica | contexto correcto de devoluciones | respuesta = el chunk literal, fiel y relevante, pero NO contesta la pregunta del cliente

Antes de mirar nada, una pregunta de interrogación elaborativa. Respóndela tú primero: ¿por qué la trace G no se detiene en groundedness y necesita el cuarto paso del árbol? Piénsalo antes de seguir.

Porque la groundedness mide fidelidad, no utilidad. Una respuesta puede ser perfectamente fiel al contexto —copiarlo al pie de la letra— y aun así no responder lo que el cliente preguntó. Por eso el árbol tiene un cuarto paso: answer relevance. Una respuesta grounded pero irrelevante pasa los tres primeros filtros y solo cae en el último.


5.6Comprueba

Sin pistas. Te doy un fallo end-to-end con su traza y sus métricas ya calculadas. Emite el veredicto de componente, justifica el orden de lectura y propón la mejora concreta.

text
1CASO. El cliente pregunta: "¿el pedido #A-7741 ya salió?"
2El bot responde: "Sí, tu pedido salió ayer." El cliente nunca recibió nada;
3el pedido #A-7741 está retenido en almacén.
4
5Traza y métricas:
6  tool_calls:           [buscar_pedido(order_id="A-7741")]   → llamó, arg correcto
7  tool_call_accuracy:   1.0
8  context_utilization:  (no aplica — no hubo consultar_politica)
9  faithfulness:         0.0   (el contexto de buscar_pedido decía "retenido")
Ver la respuesta razonada

Veredicto: generación. El árbol, en orden:

  1. Orquestación — pasa. tool_call_accuracy = 1.0: el agente llamó a buscar_pedido con el order_id correcto. La trayectoria fue la esperada.
  2. Retrieval — no aplica como vértice RAG (no hubo consultar_politica), pero el resultado de buscar_pedido sí es el contexto del que depende la respuesta, y decía "retenido".
  3. Generación — falla. faithfulness = 0.0: la respuesta afirma "salió ayer" cuando el contexto recuperado decía "retenido". El modelo contradijo un dato correcto que tenía delante.

Mejora concreta: el componente es la generación, no el retrieval ni la orquestación. El agente obtuvo el dato correcto y lo ignoró. La palanca es el prompt o el modelo: reforzar en el system prompt que el estado del pedido debe citarse literalmente del resultado de buscar_pedido, nunca parafrasearse ni inferirse.

Por qué este orden: empezar por la trayectoria descartó la orquestación con una comprobación determinista y barata, sin invocar al juez. Solo entonces escaló al juez para la fidelidad. Si hubieras empezado por groundedness, habrías llegado al mismo veredicto, pero gastando el juez antes de descartar lo barato.


Feedback formativo:

  • Si acertaste "generación" y justificaste el orden: dominas el protocolo del nivel. El árbol es exactamente lo que empaquetarás en la suite de L6; ya sabes el algoritmo, falta automatizarlo sobre el dataset entero.
  • Si dijiste "orquestación": revisa el paso 1. La trayectoria pasó (tool_call_accuracy = 1.0): llamó a la tool correcta con el argumento correcto. La orquestación falla cuando no llama, o llama mal. Aquí llamó bien y luego ignoró el resultado: eso es generación. Vuelve al §5.3, "el orden de lectura".
  • Si dijiste "retrieval": confundiste el resultado de buscar_pedido con un fallo de recuperación. El dato recuperado era correcto ("retenido"); el modelo lo contradijo. Retrieval falla cuando el contexto es malo; aquí era bueno y la generación lo traicionó. Relee el §5.4, trace B, que es el patrón exacto.

5.7Conecta

Vuelve al fallo de las zapatillas con el que abrimos el nivel: "sí, hasta 90 días".

Cuando empezó N3, ante ese fallo solo tenías el veredicto del juez —"groundedness fail"— y tres sospechosos sin forma de separarlos. Ahora tienes un protocolo. Recorres la traza en orden, sigues el primer fallo de la cadena, y en segundos sabes si el culpable fue el retriever, el modelo o la orquestación. Y, con el culpable, sabes qué palanca tocar.

Ya sabes localizar cualquier fallo a mano. Pero "a mano" no escala ni gobierna un sistema en producción. L6 te pide el último paso: empaquetar este árbol en una suite ejecutable. Hará este diagnóstico de forma sistemática sobre todo el dataset de N1 y emitirá un informe que mapea cada fallo a su componente. Esa suite es el checkpoint C3 —y en N4 se convertirá en el gate de CI que falla el build ante una regresión.

¿Dónde lo aplicarías en tu trabajo? Piensa en el último fallo de un sistema LLM que tocaste. ¿Supiste si nació en el retrieval, en la generación o en la orquestación? Si la respuesta fue "no estaba seguro", ya tienes el árbol para responderlo con evidencia.


5.8Reflexiona

Tómate dos minutos. Estas preguntas consolidan más que releer.

  • ¿Por qué el árbol empieza por la trayectoria y no por groundedness? Da las dos razones —causal y de coste—.
  • Con tus palabras: ¿qué quiere decir "seguir el primer fallo de la cadena" y por qué importa en una arquitectura RAG + tools?
  • Un veredicto de componente que no cambia ninguna decisión, ¿de qué sirve? Conecta tu respuesta con la accionabilidad.
  • ¿Qué sigue sin estar claro? Si es "cómo corro esto sobre 100 trazas sin diagnosticar una a una", es la pregunta correcta —la responde L6—.

Referencia rápida

  • Árbol de diagnóstico (orden → métrica): 1) ¿llamó a las tools correctas, con buenos args? ToolCallAccuracy (orquestación, L4) → 2) ¿contexto relevante? ContextUtilization (retrieval, L2-L3) → 3) ¿respuesta grounded? Faithfulness (generación, L2) → 4) ¿responde a la pregunta? AnswerRelevancy (answer relevance, L2).
  • ContextUtilization mide útil-para-la-respuesta, no relevante-a-la-pregunta: sin reference, su veredicto depende de la fuerza del juez (puede confundir un número coincidente con relevancia). Con ground-truth, ContextPrecision es más robusta.
  • Primer fallo de la cadena: diagnostica en el orden del flujo causal; el error río arriba contamina el de abajo (open coding de N1; los errores se propagan — Anthropic, 9 ene 2026).
  • Por qué la trayectoria primero: sin retrieval no hay contexto que medir; y es determinista y barata, antes que el juez (jerarquía de coste — Hamel Husain, evals, mar 2024).
  • Accionabilidad: retrieval malo → chunking/índice/reranker; generación infiel → prompt/modelo; orquestación → descripción de tools/system prompt. Una métrica solo vale si guía una mejora (rúbrica C3, dim. 5).
  • Caso ambiguo: si dos componentes flojean, el primero de la cadena manda; arregla río arriba, re-ejecuta, vuelve a juzgar.