Open coding: leer trazas como investigador
conducirás open coding sobre trazas reales del agente de Aurora, anotando con notas concretas el primer fallo de cada traza, sin saltar todavía a categorías. Sin esta disciplina, tus notas confunden causa con síntoma y el análisis se contamina desde el primer minuto.
2.1La traza que falla por partida doble
Abres una traza del agente de Aurora —la tienda online ficticia que es tu banco de pruebas todo el curso—. La respuesta final es un desastre.
El cliente preguntó algo simple: "¿puedo devolver unas zapatillas que me quedan grandes?". El agente hizo dos cosas mal:
1TRACE conv_1d92 session=user_8830
2├─ retrieval consultar_politica(tema="devoluciones") 0.5s
3│ docs: [{chunk_id: "devol-electronica-04", score: 0.29}] ← política de ELECTRÓNICA
4├─ llm claude finish_reason=tool_use 2.1s
5│ tool_calls: [escalar_a_humano(motivo="caso de devolución")]
6├─ tool escalar_a_humano -> "ticket #7781 creado"
7└─ respuesta "No puedo ayudarte con esto. He abierto un ticket #7781."Cuentas dos errores. Uno: recuperó la política de devoluciones de electrónica para una consulta de calzado. Dos: escaló a un humano un caso que la base de conocimiento responde sin problema.
Vas a anotar el fallo. ¿Cuál de los dos apuntas?
Si los anotas como dos fallos iguales, tu análisis se contamina. Porque uno causó el otro: el agente escaló porque el contexto que recuperó no servía para responder. El escalado no es un fallo independiente. Es el coletazo de un fallo anterior.
Hoy aprendes a leer una traza para distinguir esas dos cosas.
2.2Qué vas a poder hacer
Al terminar esta lección sabrás:
- Conducir open coding sobre trazas del agente de Aurora: escribir una nota abierta y concreta por cada traza que falla.
- Identificar el primer fallo de una traza con errores encadenados, y justificar por qué los fallos posteriores no se anotan como causa.
- Distinguir una nota útil ("recuperó la política de electrónica para una consulta de calzado") de una nota inservible ("la respuesta está mal").
Necesitas saber antes:
- De N1·L1: por qué miramos datos antes de medir, y por qué las métricas genéricas no detectan los fallos reales de tu producto.
- De N0: cómo leer una traza explotable —qué campos te dejan reconstruir el flujo retrieval → LLM → tool—. Si una traza no te deja ver eso, vuelve al Nivel 0 antes de seguir.
- Python a nivel de leer un
DataFramede pandas y recorrer filas.
Esta lección es el paso 2 de 4 del error analysis. Hoy no agrupamos ni contamos —eso es L3—. Hoy solo leemos y anotamos, una traza a la vez.
2.3Recupera
Antes de seguir, responde mentalmente. No mires lo de abajo hasta tener una respuesta.
- En N1·L1 viste un score "3,72 hoy frente a 4,2 mañana". ¿Por qué ese número no te decía si el sistema mejoró?
- Tienes una traza de Aurora. ¿Qué tres pasos del flujo te deja reconstruir, y en qué campo vive cada uno?
- El agente devolvió una respuesta fluida y educada que, aun así, era inútil. ¿Por qué no basta con leer la respuesta final para saber qué falló?
La respuesta a la 1: el número no se refería a ningún fallo concreto del producto. Medía sin haber mirado. La respuesta a la 2: reconstruyes tres pasos. El paso de retrieval vive en qué documento recuperó consultar_politica y con qué score. El paso del LLM vive en su finish_reason y en si llamó a una tool. El paso de tool vive en qué herramienta ejecutó y qué devolvió. La respuesta a la 3: la respuesta final es el último eslabón. Un eslabón anterior pudo romperse mientras todo lo demás parecía correcto. Leer solo el final es leer el síntoma, no la causa.
2.4El concepto: anotar el primer fallo
Empecemos por el método entero y bajemos al detalle.
El error analysis tiene cuatro pasos
El proceso de error analysis que enseña este curso tiene cuatro pasos (Hamel Husain, evals-faq, ene 2026):
- Reunir un dataset representativo de trazas — ya lo tienes: las ≥100 del checkpoint C0.
- Open coding — escribir notas abiertas por traza, al estilo de un diario de investigación.
- Axial coding — agrupar esas notas en una taxonomía y contar instancias. Es L3.
- Refinar hasta saturación. También es L3.
Una nota sobre el origen del método. El open coding y el axial coding vienen de metodologías cualitativas de investigación; Hamel las describe como "adaptadas de metodologías de investigación cualitativa". No las llamaremos por el nombre académico de su disciplina de origen: lo que importa aquí es el procedimiento, no la etiqueta.
Hoy trabajas el paso 2. Y el paso 2 tiene una regla que lo gobierna todo.
La regla: el primer fallo
Defino el término que sostiene la lección.
El open coding es el acto de leer una traza y escribir una nota abierta: una descripción libre, concreta y en tus palabras de qué salió mal en esa traza. "Abierta" significa que aún no la fuerzas dentro de una categoría predefinida. Es como las notas al margen de un investigador que lee transcripciones: primero describe lo que ve, sin clasificar.
La regla que dirige el open coding es esta: fíjate en el primer fallo de la traza (Hamel Husain, evals-faq, ene 2026). No en el más visible. No en el último. El primero en el tiempo.
El motivo es la cascada de errores: en un agente multi-turno, un fallo upstream contamina todo lo que viene después (downstream). La analogía: una receta donde el primer paso usa sal en vez de azúcar. Cada paso siguiente —mezclar, hornear, emplatar— se ejecuta "bien", pero el plato sale mal. No tiene sentido anotar "el emplatado falló". El emplatado fue fiel a un error anterior. (Donde la analogía falla: en una receta el orden es fijo; en un agente, el modelo elige el siguiente paso, así que a veces el primer fallo es una mala decisión de qué herramienta llamar.)
Vuelve a la traza de apertura. El primer fallo fue el retrieval: trajo la política de electrónica. El LLM respondió fiel a ese contexto malo y escaló. El escalado es downstream. Si lo anotas como fallo, estás registrando un síntoma y te perderás la causa raíz cuando, en L3, cuentes cuántas veces falla cada cosa.
Concreto, no etiqueta prematura
Una nota de open coding describe; no clasifica. Esa es la diferencia entre una nota útil y una inservible.
Comparemos dos notas para la misma traza de apertura:
- Nota prematura: "alucinación". O peor: "la respuesta está mal".
- Nota útil: "recuperó la política de devoluciones de electrónica para una consulta de calzado; por eso escaló en vez de responder".
La nota prematura te traiciona de dos formas. Primero, "alucinación" es una etiqueta que decides antes de haber mirado lo suficiente —exactamente el error de fijar criterios a priori que viste en L1 con el criteria drift—. Segundo, "la respuesta está mal" no se puede contar ni agrupar: dentro de un mes no recordarás qué estaba mal.
La nota útil es concreta: nombra el documento equivocado, el tema real de la consulta y la consecuencia. En L3 podrás agrupar diez notas así en una categoría ("RAG recupera política equivocada") y contarlas. Una nota concreta es materia prima de una taxonomía. Una etiqueta prematura es ruido que tendrás que volver a investigar.
Es común querer saltar directo a la categoría —"esto es una alucinación, siguiente"—. Casi todo el mundo lo hace la primera vez; clasificar se siente productivo. La diferencia clave: clasificar antes de mirar fija tus prejuicios sobre los datos; describir antes de clasificar deja que los datos te enseñen las categorías.
2.5Míralo funcionar: tres trazas anotadas en vivo
Vamos a anotar tres trazas del agente de Aurora, una a una. Lee primero cada traza entera; luego razonamos la nota.
El agente combina cuatro funciones, las mismas de todo el curso:
1def buscar_pedido(order_id: str) -> dict: ... # estado/tracking de un pedido
2def consultar_politica(tema: str) -> str: ... # recupera de la KB (paso RAG)
3def escalar_a_humano(motivo: str) -> str: ... # abre un ticket humano
4def responder(mensaje: str, historial: list[dict]) -> str: ... # bucle que orquestaTraza 1 — el fallo en cascada
1TRACE conv_1d92 session=user_8830
2user_query: "¿puedo devolver unas zapatillas que me quedan grandes?"
3├─ retrieval consultar_politica(tema="devoluciones") docs: [devol-electronica-04, score=0.29]
4├─ llm claude finish_reason=tool_use -> escalar_a_humano(motivo="caso de devolución")
5├─ tool escalar_a_humano -> "ticket #7781 creado"
6└─ respuesta "No puedo ayudarte con esto. He abierto un ticket #7781."Nota: recuperó la política de devoluciones de electrónica (devol-electronica-04) para una consulta de calzado; con ese contexto inservible, escaló en vez de responder. Primer fallo = retrieval.
Traza 2 — el pedido inventado
1TRACE conv_2a07 session=user_4471
2user_query: "¿dónde está mi pedido 5582? hace días que espero"
3├─ llm claude finish_reason=end_turn (NO llamó a buscar_pedido)
4└─ respuesta "Tu pedido 5582 fue entregado ayer a las 14:30. ¡Gracias por tu paciencia!"Nota: afirmó un estado de entrega concreto ("entregado ayer 14:30") sin llamar a buscar_pedido; se inventó el dato. Primer fallo = el LLM responde sin la tool necesaria.
Traza 3 — el escalado innecesario
1TRACE conv_3f55 session=user_119
2user_query: "¿a qué hora cerráis los sábados?"
3├─ retrieval consultar_politica(tema="horarios") docs: [horarios-tienda-01, score=0.88]
4├─ llm claude finish_reason=tool_use -> escalar_a_humano(motivo="consulta de horario")
5├─ tool escalar_a_humano -> "ticket #7790 creado"
6└─ respuesta "He pasado tu consulta a un agente humano."Nota: recuperó el horario correcto (score 0.88) y aun así escaló una pregunta trivial que tenía respuesta en el contexto. Primer fallo = el LLM escala teniendo la respuesta delante.
Ahora la pregunta de auto-explicación. Antes de leer el análisis, respóndela: en la Traza 1, ¿por qué anotar "escaló mal" como el fallo sería un error de análisis?
Porque en la Traza 1 el escalado es un efecto, no una causa. El agente escaló porque el retrieval le dio un contexto que no respondía la pregunta. Si anotas "escaló mal", registrarás esa traza en el mismo cubo que la Traza 3 —donde el escalado sí es el primer fallo, con el contexto correcto delante—. En L3 esas dos trazas acabarían en la misma categoría, inflando el conteo de "escalado innecesario" y ocultando el verdadero culpable de la Traza 1: el retrieval.
Fíjate en el contraste entre la Traza 1 y la Traza 3. Ambas terminan en un escalado. Pero el primer fallo es distinto: retrieval en una, decisión del LLM en la otra. La respuesta final no te lo dice; el flujo de la traza sí. Por eso lees la traza entera y de izquierda a derecha, no solo el último eslabón.
2.6Hazlo tú
Empezarás con dos trazas con una nota de ejemplo al lado (andamiaje), y luego anotarás tres en limpio. El consejo de L1 sigue vigente: arranca con 10-20 trazas en una hoja antes de escalar a las 100 del C0. El método se aprende despacio y luego se acelera.
Ejercicio 1 — con andamiaje
Aquí tienes dos trazas. La primera ya está anotada como modelo. Anota la segunda con el mismo estilo: concreta, identificando el primer fallo.
1TRACE conv_a1 user_query: "quiero cambiar la talla de mi pedido 9001"
2├─ retrieval consultar_politica(tema="cambios") docs: [cambios-talla-02, score=0.91]
3├─ llm claude finish_reason=end_turn (NO llamó a buscar_pedido)
4└─ respuesta "Tu pedido 9001 ya salió de almacén, no se puede cambiar."Nota modelo: afirmó que el pedido 9001 "ya salió de almacén" sin llamar a buscar_pedido; inventó el estado para justificar la negativa. El retrieval fue correcto (cambios-talla-02). Primer fallo = el LLM afirma un estado de pedido sin la tool.
Ahora la tuya:
1TRACE conv_a2 user_query: "¿el abrigo de lana es lavable a máquina?"
2├─ retrieval consultar_politica(tema="cuidado prendas") docs: [garantia-devol-09, score=0.34]
3├─ llm claude finish_reason=end_turn
4└─ respuesta "Sí, puedes lavarlo a máquina a 30 grados sin problema."Tu nota: _______________________________________________
Ejercicio 2 — autónomo
Anota estas tres trazas en limpio. Para cada una, identifica el primer fallo.
1TRACE conv_b1 user_query: "no me llegó el pedido 3320, ¿qué hago?"
2├─ llm claude finish_reason=tool_use -> buscar_pedido(order_id="3320")
3├─ tool buscar_pedido -> {"estado": "en reparto", "eta": "mañana"}
4├─ llm claude finish_reason=end_turn
5└─ respuesta "Tu pedido 3320 se ha perdido. Te haremos un reembolso completo."1TRACE conv_b2 user_query: "¿cuánto tarda un reembolso?"
2├─ retrieval consultar_politica(tema="envios") docs: [plazos-envio-03, score=0.41]
3├─ llm claude finish_reason=end_turn
4└─ respuesta "Los envíos tardan entre 3 y 5 días laborables."1TRACE conv_b3 user_query: "tengo el pedido 7012 dañado, está roto"
2├─ retrieval consultar_politica(tema="productos dañados") docs: [danados-15, score=0.87]
3├─ llm claude finish_reason=tool_use -> escalar_a_humano(motivo="producto dañado")
4├─ tool escalar_a_humano -> "ticket #8001 creado"
5└─ respuesta "He abierto un ticket para gestionar tu producto dañado."Antes de mirar las soluciones, una pregunta de interrogación elaborativa. Respóndela tú primero: ¿por qué el campo finish_reason te ayuda a localizar el primer fallo?
Porque distingue cómo terminó cada turno del modelo. finish_reason=tool_use te dice que el modelo eligió llamar a una herramienta —y cuál—; end_turn te dice que decidió responder sin más tools. En la Traza 2 del worked example, end_turn sin una llamada previa a buscar_pedido es la prueba de que el modelo respondió sin verificar. El campo convierte "se lo inventó" de sospecha en evidencia.
Ver las notas razonadas del Ejercicio 2
conv_b1 — primer fallo = el LLM contradice la tool. El retrieval no se usó; la tool buscar_pedido devolvió "en reparto, eta mañana", dato correcto. El modelo lo ignoró y afirmó lo contrario ("se ha perdido, reembolso"). Nota útil: llamó a buscar_pedido y obtuvo "en reparto", pero respondió "se ha perdido" y prometió reembolso, contradiciendo el resultado de la tool. Primer fallo = el LLM ignora/contradice el dato que recuperó.
conv_b2 — primer fallo = retrieval. La consulta era sobre reembolsos; el retrieval trajo plazos-envio-03 (envíos) con score bajo (0.41). El modelo respondió fiel a ese contexto equivocado. Nota útil: recuperó la política de plazos de envío para una pregunta sobre reembolsos; respondió sobre el tema equivocado. Primer fallo = retrieval trae la política equivocada.
conv_b3 — no hay fallo claro. El retrieval fue correcto (danados-15, score 0.87) y escalar a un humano un producto dañado puede ser la conducta deseada. Nota honesta: retrieval correcto; escalado de un producto dañado, plausiblemente correcto. Sin fallo evidente — marcar como "pasa" o revisar contra la política de daños. Una traza que no falla es una observación válida: no toda traza esconde un error.
2.7Comprueba
Sin pistas. Aquí tienes una traza con dos fallos encadenados. Señala cuál es el primer fallo y justifica por qué el segundo no se anota como causa.
1TRACE conv_c9 session=user_550
2user_query: "¿puedo devolver este libro que compré la semana pasada?"
3├─ retrieval consultar_politica(tema="devoluciones") docs: [devol-perecederos-22, score=0.27]
4├─ llm claude finish_reason=end_turn
5└─ respuesta "Lo siento, los productos perecederos no admiten devolución."Ver la respuesta razonada
Primer fallo = el retrieval. consultar_politica(tema="devoluciones") recuperó devol-perecederos-22 —la política de productos perecederos— con un score muy bajo (0.27), para una consulta sobre un libro. El paso de búsqueda trajo el documento equivocado.
El segundo "fallo" no se anota como causa. El modelo respondió "los perecederos no admiten devolución": una respuesta fiel al contexto que recibió. Dado un libro etiquetado como perecedero por un retrieval roto, la generación hizo lo correcto con datos malos. El fallo de generación es downstream; si arreglas el retrieval, ese segundo "fallo" desaparece solo.
Nota útil: recuperó la política de perecederos (score 0.27) para una consulta sobre un libro; el modelo respondió fiel a ese contexto erróneo. Primer fallo = retrieval.
Feedback formativo:
- Si señalaste el retrieval y supiste explicar la fidelidad de la generación al contexto malo: dominas el criterio central de la lección —el primer fallo, no el síntoma—. Esta misma lógica es la que evita conteos inflados en L3; la reutilizarás en cada traza del C1.
- Si señalaste la generación como el fallo: tu instinto vio el problema correcto (la respuesta es falsa), pero te quedaste en el síntoma. La pregunta que cierra la brecha: si el retrieval hubiera traído la política de libros, ¿qué habría respondido el modelo? Una respuesta correcta. Eso prueba que la generación no es la causa raíz. Siguiente paso: relee la sección 2.4, "La regla: el primer fallo".
- Si anotaste los dos como fallos separados: mezclaste causa y efecto. En L3 esa traza contaría doble y repartiría la culpa entre dos categorías, una de ellas falsa. La diferencia clave: un fallo downstream que desaparece al arreglar el upstream no es un fallo independiente. Vuelve al worked example de la Traza 1 y compáralo con la Traza 3.
2.8Conecta
Vuelve a la traza de apertura: las zapatillas, la política de electrónica, el escalado innecesario.
Antes de esta lección, habrías anotado "escaló mal y citó mal una política" —dos fallos, igual de graves—. Ahora sabes leerla distinto: un único primer fallo, el retrieval, con un escalado que es su consecuencia. Esa distinción no es un tecnicismo. Es lo que decidirá, en L3, si tu taxonomía dice la verdad sobre dónde falla Aurora.
Porque ahora mismo tienes un montón de notas abiertas heterogéneas: "recuperó política de electrónica", "se inventó el tracking", "ignoró el dato de la tool", "escaló teniendo la respuesta". Eso todavía no es un análisis. Es una lista. En L3 las agruparás, contarás y priorizarás —el axial coding— hasta que dejen de ser ruido y se vuelvan una taxonomía con un fallo nº1 y un porcentaje.
¿Cómo guardas esas notas para que L3 pueda agruparlas? En una hoja, una fila por traza. Con pandas, cada nota vive en una columna junto a su traza:
1import pandas as pd
2
3# Una fila por traza; la nota de open coding en su propia columna.
4df = pd.DataFrame(data=[
5 {"trace_id": "conv_1d92", "user_query": "devolver zapatillas",
6 "nota_open_coding": "recuperó política de electrónica para calzado; escaló en vez de responder",
7 "primer_fallo": "retrieval"},
8 {"trace_id": "conv_2a07", "user_query": "dónde está pedido 5582",
9 "nota_open_coding": "afirmó entrega sin llamar a buscar_pedido; inventó el dato",
10 "primer_fallo": "llm-sin-tool"},
11 # ... una fila por cada una de tus ≥100 trazas
12])
13
14df[["trace_id", "nota_open_coding", "primer_fallo"]].head(20)La columna primer_fallo la rellenas a ojo hoy, en lenguaje libre. En L3 se convertirá en la categoría axial que cuentas con value_counts(). No la fuerces todavía: hoy describes, mañana clasificas.
¿Dónde lo aplicarías en tu trabajo? Toma cualquier sistema LLM que hayas tocado y una respuesta concreta que salió mal. Lee la traza de izquierda a derecha. ¿Cuál fue el primer eslabón que se rompió? Si tu respuesta apunta al final de la cadena, mira más atrás.
2.9Reflexiona
Tómate dos minutos. Estas preguntas consolidan más que releer.
- Con tus palabras: ¿qué hace que una nota de open coding sea útil frente a una etiqueta prematura como "alucinación"?
- ¿Por qué el primer fallo, y no el más visible? Explica la cascada con un ejemplo propio.
- ¿Qué sigue sin estar claro? Si es "cómo decido cuándo dejar de anotar trazas", es la pregunta correcta —la responde L3 con el criterio de saturación—.
Referencia rápida
- Error analysis, 4 pasos: (1) dataset representativo → (2) open coding → (3) axial coding → (4) refinar hasta saturación (Hamel Husain, evals-faq, ene 2026). Pasos 3-4 en L3.
- Open coding: una nota abierta y concreta por traza, en tus palabras, antes de clasificar. Adaptado de metodologías de investigación cualitativa.
- La regla: anota el primer fallo de la traza, no el más visible ni el último.
- Cascada de errores: un fallo upstream contamina lo downstream; el síntoma posterior desaparece al arreglar la causa. No lo anotes como fallo aparte.
- Nota útil vs prematura: "recuperó política de electrónica para una consulta de calzado" (concreta, contable) ≠ "la respuesta está mal" / "alucinación" (etiqueta sin materia prima).
- Campos clave para localizar el primer fallo: el orden retrieval → LLM → tool, el
finish_reason(tool_usevsend_turn) y si se llamó (o no) a la tool necesaria.