Creación de un bucle de mejora de agentes con las herramientas de evaluación de LangGraph y LangSmith.
Un bucle paso a paso de LangGraph y LangSmith: recolección de datos, tres capas de puntuación, agrupamiento de fallos, conjuntos de datos de regresión sin conexión y evaluaciones previas al envío.
Fuentes: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines
Colocar un agente en producción requiere el menor esfuerzo. Mejorarlo posteriormente es lo que diferencia una construcción casual de un sistema que funciona de manera estable. Muchos equipos lanzan una primera versión y luego pasan meses respondiendo a quejas una por una. Cada parche es una reacción a un caso concreto, cada corrección se queda limitada a ese caso, y el agente nunca mejora en su conjunto. Solo se acumulan soluciones temporales.
La causa habitual es tratar al agente como un software convencional. En los programas normales, el código explica qué hace el sistema, por lo que un error implica leer el código fuente hasta encontrarlo. Con los agentes, esa suposición falla. El código solo describe lo que el sistema está autorizado a hacer, mientras que los registros registran lo que realmente hizo. Esa brecha constituye la base de todo proceso de evaluación serio, incluidos aquellos que Anthropic describe en sus escritos técnicos y LangChain en la documentación de LangSmith.
Lo que sigue es un ciclo completo de mejora para un agente LangGraph. Comienza con un agente ReAct sencillo, registra sus huellas en LangSmith, añade tres capas de evaluación, identifica patrones recurrentes de fallos, convierte esos fallos en un conjunto de pruebas offline permanente y realiza una evaluación de regresión antes de lanzar cualquier corrección. Cada etapa incluye código, una explicación y el resultado esperado.
El ciclo funciona de la siguiente manera:
- El agente maneja una consulta real de un usuario y genera una huella que abarca cada llamada al LLM, cada llamada a una herramienta y todos los resultados intermedios.
- Las verificaciones basadas en código evalúan cada huella en cuanto a formato y corrección de la herramienta, un evaluador basado en LLM califica su utilidad y relevancia, y los humanos revisan los fallos sutiles.
- Las huellas con puntuaciones bajas se agrupan en patrones de fallo como el uso incorrecto de una herramienta, desviaciones en el razonamiento o la falta de comprensión de la intención del usuario.
Así es como un agente mejora con el tiempo en lugar de simplemente acumular parches. Las seis etapas son:
- Etapa 1, base: un agente LangGraph ReAct con seguimiento mediante LangSmith.
- Etapa 2, recolección de registros: extracción de las ejecuciones en producción como registros estructurados.
- Etapa 3, evaluación: verificaciones de código, evaluación por un modelo LLM y revisión humana.
- Etapa 4, descubrimiento de patrones: conversión de los registros evaluados en categorías de fallos.
- Etapa 5, conjuntos de pruebas sin conexión: transformación de los fallos en un conjunto de datos seguro contra regresiones.
Etapa 1: el agente fundamental
La primera etapa crea un pequeño agente de soporte al cliente que responde preguntas sobre una cuenta. Cuenta con dos herramientas: una para buscar detalles de la cuenta y otra para verificar pedidos recientes, y utiliza el agente ReAct preconstruido de LangGraph, que es el patrón estándar para los agentes que utilizan herramientas. Está activado el seguimiento de LangSmith, por lo que cada ejecución, llamada al LLM y llamada a herramienta se registran automáticamente.
Tres componentes hacen que esto funcione:
- El LangGraph StateGraph es la estructura subyacente que dirige la comunicación entre un nodo que decide qué hacer y un nodo que ejecuta una herramienta. La función auxiliar
create_react_agentencapsula dicho patrón.
LANGSMITH_TRACING=true. A partir de entonces, cada llamada a LangChain o LangGraph genera un registro sin cambios en el código.@tool de LangChain, lo que las expone al agente junto con un nombre y una descripción que guían la selección de herramientas.Configuración del entorno
Primero se deben instalar las bibliotecas:
# Install the libraries we need
pip install langgraph langchain langchain-openai langsmith
Se instalan cuatro paquetes. langgraph proporciona el entorno de ejecución del grafo y el agente ReAct ya preconstruido. langchain ofrece el decorador @tool y los tipos de mensaje. langchain-openai envuelve el modelo de chat de OpenAI en la interfaz de LangChain. langsmith es la biblioteca de cliente utilizada para acceder al backend de LangSmith con el fin de realizar seguimiento y evaluaciones.
A continuación vienen las variables de entorno, las cuales activan el seguimiento sin modificar el código:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=your_langsmith_key
export LANGSMITH_PROJECT=agent-improvement-loop
export OPENAI_API_KEY=your_openai_key
LANGSMITH_TRACING=true es el parámetro de activación. LANGSMITH_PROJECT designa el proyecto que agrupa las trazas, y es así como se filtran posteriormente. Los otros dos son las credenciales para LangSmith y el proveedor del modelo.
Con esas cuatro variables configuradas, cualquier llamada a LangChain o LangGraph en el proceso envía una traza a LangSmith. No es necesario envolver ni modificar nada, ya que el seguimiento de trazas se ejecuta en segundo plano.
Creación del agente
Primero, las importaciones:
# Step 1: Import the pieces we need
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
Las importaciones incluyen tres componentes. ChatOpenAI encapsula la API de completación de chat de OpenAI para LangChain. El decorador tool convierte una función normal de Python en un elemento llamable que el agente puede utilizar. create_react_agent es una herramienta lista para usar de LangGraph que crea un grafo al estilo ReAct para llamar a las herramientas.
Luego están las dos herramientas:
# Step 2: Define two tools the agent can call
@tool
def get_account_info(account_id: str) -> str:
"""Look up basic information for an account by account_id."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
@tool
def get_recent_orders(account_id: str) -> str:
"""Return the three most recent orders for a given account_id."""
fake_orders = {
"A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
"A200": "Orders for A200: no orders in the last 90 days",
}
return fake_orders.get(account_id, f"No orders for {account_id}")
Cada herramienta acepta un account_id y devuelve una cadena de texto. La documentación de cada función es importante, ya que el LLM lee ese texto al decidir qué herramienta llamar. El decorador obtiene el nombre de la función, sus parámetros y su documentación para construir el esquema que ve el modelo. En un sistema real, estos componentes consultarían una base de datos o una API, pero la demostración devuelve cadenas de texto predefinidas para que sea autónoma y reproducible.
Finalmente, se ensambla el agente:
# Step 3: Wire the tools into a ReAct agent
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [get_account_info, get_recent_orders]
agent = create_react_agent(model, tools)
El modelo de chat se crea con una temperatura de cero para garantizar la reproducibilidad. Las herramientas se incluyen en una lista y se pasan junto con el modelo a create_react_agent, el cual devuelve un StateGraph de LangGraph compilado que ya cuenta con un paso de razonamiento, un paso de ejecución de herramientas y los enlaces condicionales que los conectan.
Prueba del agente
Una consulta sencilla lo pone a prueba:
# Step 4: Invoke the agent with a simple query
result = agent.invoke({
"messages": [{"role": "user", "content": "What plan is account A100 on?"}]
})
for message in result["messages"]:
print(message.type, ":", message.content)
La llamada pasa un diccionario que contiene una clave messages. Esto se debe a que create_react_agent depende del esquema preconstruido MessagesState, cuyo estado es simplemente una lista de mensajes de chat que el agente amplía a medida que piensa y utiliza herramientas. Posteriormente, el bucle imprime cada tipo de mensaje junto con su contenido, de modo que todo el rastro queda visible.
El resultado esperado:
human : What plan is account A100 on?
ai :
tool : Account A100: plan=pro, status=active, email=ada@example.com
ai : Account A100 is on the pro plan.
El agente recibió el mensaje del usuario y luego emitió un mensaje de IA que activó una llamada a herramienta (su contenido está vacío porque contiene solo la llamada a herramienta y no texto). La herramienta devolvió la cadena de la cuenta, y el agente finalizó con una respuesta. Al abrir LangSmith ahora se muestra un nuevo seguimiento en el proyecto agent-improvement-loop, con cada paso registrado.
Nada en ese código capturó el seguimiento. Tan pronto como LANGSMITH_TRACING=true está en el entorno, cada llamada envía entradas, salidas y pasos intermedios a LangSmith. Esa capacidad de observabilidad ambiental es de la que depende el resto del bucle.
La primera etapa deja que un agente que realiza llamadas a herramientas funcione bajo LangGraph, emitiendo un seguimiento en cada ejecución. Sin embargo, un seguimiento que aparece en un panel de control aún no constituye una señal de mejora; es necesario extraerlo y tomar medidas al respecto, tarea que corresponde a la siguiente etapa.
Etapa 2: recopilación de registros
En esta etapa se extraen los registros de LangSmith y se introducen en el código de análisis. Un registro corresponde a una ejecución completa del agente, que incluye entradas, salidas, llamadas a herramientas y tiempos de latencia. El cliente Python de LangSmith enumera las ejecuciones del proyecto, las filtra según el rango de tiempo y el estado, y las convierte en objetos que puede utilizar la etapa de puntuación.
Algunos términos facilitan la lectura del código:
- Una ejecución es un único fragmento de trabajo grabado, ya sea una llamada completa al agente o una llamada a un LLM anidada dentro de ella. Una invocación del agente forma un árbol de ejecuciones.
- Una ejecución raíz es la ejecución de nivel superior de una llamada al agente; es la que se muestra cuando se desea un registro por cada consulta del usuario.
- Un proyecto es el espacio de nombres al que pertenece una ejecución, definido por
LANGSMITH_PROJECT. Filtrar por proyecto permite separar las ejecuciones en entorno de producción de las ejecuciones de desarrollo local.
Obtención de ejecuciones recientes
Primero se crea el cliente:
# Step 1: Create a LangSmith client
from langsmith import Client
from datetime import datetime, timedelta
client = Client()
Client se importa del paquete langsmith y se crea sin argumentos. Lee automáticamente LANGSMITH_API_KEY del entorno, por lo que no se transmiten credenciales alguna.
Luego se enumeran las ejecuciones recientes como root:
# Step 2: List recent root runs from our project
recent_runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
print(f"Found {len(recent_runs)} root runs in the last 24 hours")
client.list_runs se llama con tres filtros. project_name limita la consulta al proyecto, is_root=True mantiene solo las llamadas de los agentes de nivel superior y no todas las llamadas anidadas a LLMs y herramientas, mientras que start_time restringe los resultados a las últimas 24 horas. El iterador es perezoso, por lo que list() lo fuerza a convertirse en una lista real.
El resultado esperado:
Found 42 root runs in the last 24 hours
Conservar solo los campos importantes
Una ejecución bruta de LangSmith incluye muchos campos, pero para calcular la puntuación se necesitan solo cinco elementos: la entrada, la salida, la latencia, cualquier error y las ejecuciones secundarias que indican qué herramientas se utilizaron.
# Step 3: Build a compact trace record from each run
def compact_trace(run):
return {
"run_id": str(run.id),
"inputs": run.inputs,
"outputs": run.outputs,
"error": run.error,
"latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
if run.end_time else None,
"tool_calls": [
child.name for child in client.list_runs(
trace_id=run.trace_id, run_type="tool"
)
],
}
traces = [compact_trace(run) for run in recent_runs]
print(f"Collected {len(traces)} compact traces")
print("Example tool calls:", traces[0]["tool_calls"])
Este asistente acepta un ejecución en bruto y la reduce a un pequeño diccionario. Las entradas y salidas (run.inputs, run.outputs) contienen la consulta del usuario y la respuesta final, mientras que run.error es None en caso de éxito y una cadena de texto en caso de error. La latencia se obtiene restando start_time de end_time. Las llamadas a herramientas requieren una segunda consulta de LangSmith utilizando run_type="tool" junto con el mismo trace_id, lo que solo muestra las ejecuciones de herramientas de ese árbol de ejecución.
El resultado esperado:
Collected 42 compact traces
Example tool calls: ['get_account_info']
La idea no es copiar las trazas de LangSmith y abandonarlo. La versión compacta es simplemente más fácil de evaluar, mientras que la traza completa permanece en LangSmith para un depuración más profunda. Mantener una vista detallada en el panel de control y otra más simplificada para el análisis es una forma sensata de separar la observabilidad de la evaluación.
Verificación del recolector
# Step 4: Inspect the first trace end to end
import json
print(json.dumps(traces[0], indent=2, default=str))
La primera huella se genera como JSON con sangría mediante json.dumps. El argumento default=str hace que json convierta en cadena todo aquello que no puede serializar por sí mismo, lo cual incluye los tipos datetime y UUID que devuelve LangSmith.
El resultado esperado:
{
"run_id": "b1d2e3f4-...",
"inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
"outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
"error": null,
"latency_ms": 1423.51,
"tool_calls": ["get_account_info"]
}
Cada serie de producción ahora cuenta con un registro claro y estructurado, que la etapa de puntuación enriquecerá. Recopilar registros de seguimiento cuesta poco, y el esfuerzo radica en hacerlos útiles. Muchos equipos se detienen en el panel de control y se preguntan por qué la observabilidad nunca se traduce en mejoras. Los registros de seguimiento sin puntuaciones son ruido, y un registro sin puntuación no es más que una línea de registro.
Etapa 3: puntuación de los registros de seguimiento
Tres niveles de puntuación alimentan un único registro enriquecido. La publicación técnica de Anthropic sobre las evaluaciones describe esta misma estructura: calificadores basados en código que son rápidos, objetivos y reproducibles; calificadores basados en modelos que son flexibles y escalables; y calificadores humanos que representan el estándar de oro pero son costosos. Aquí se desarrolla uno de cada tipo.
- Las verificaciones basadas en código son funciones deterministas y rápidas que comprueban hechos estructurales, como si el agente utilizó la herramienta adecuada o si el resultado tiene el formato esperado. Detectan fallos evidentes sin costo alguno.
- Un juez basado en LLM es un segundo modelo que evalúa cualidades subjetivas como la utilidad, la relevancia y el tono. Se puede escalar ya que se ejecuta en cada registro, pero debe calibrarse con el juicio humano antes de que alguien confíe en él.
- La revisión humana pone a un experto en el área frente a los registros que la automatización pasó por alto o evaluó de manera inconsistente. Es la verdad de referencia contra la cual se calibran los otros dos niveles.
Nivel 1: corrección de la herramienta
# Step 1: Define a rule that says which tool SHOULD be called for a given query
import re
def expected_tool_for_query(query: str) -> str:
q = query.lower()
if re.search(r"\b(order|shipment|shipped|return)\b", q):
return "get_recent_orders"
if re.search(r"\b(plan|account|status|email)\b", q):
return "get_account_info"
return "any"
def score_tool_correctness(trace):
query = trace["inputs"]["messages"][0]["content"]
expected = expected_tool_for_query(query)
actual = trace["tool_calls"][0] if trace["tool_calls"] else None
if expected == "any":
return 1.0
return 1.0 if actual == expected else 0.0
Se definen dos funciones. expected_tool_for_query aplica una expresión regular al texto de la consulta para adivinar qué herramienta debería haberla manejado. Se trata de una heurística rudimentaria, adecuada para detectar errores de enrutamiento evidentes pero no para entornos de producción. score_tool_correctness toma la consulta del usuario del rastro, determina la herramienta esperada y la compara con la primera herramienta que realmente llamó el agente. Una discrepancia obtiene una puntuación de 0.0 y una coincidencia, de 1.0.
Una versión para producción sustituiría la expresión regular por un clasificador más potente o un motor de reglas basado en intenciones. El principio permanece: se realiza una verificación determinística antes de los costosos evaluadores basados en LLM.
Capa 2: un juez basado en LLM para la utilidad
# Step 2: Score qualitative properties with an LLM judge
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
judge_prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert reviewer of AI agent responses. "
"Rate the response on a scale of 1 to 5 for helpfulness. "
"Respond with only a single integer, nothing else."),
("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
])
def score_helpfulness(trace):
question = trace["inputs"]["messages"][0]["content"]
response = trace["outputs"]["messages"][-1]["content"]
chain = judge_prompt | judge_model
result = chain.invoke({"question": question, "response": response})
try:
return int(result.content.strip()) / 5.0
except ValueError:
return 0.0
Se construye una cadena pequeña de LangChain al pasar un modelo de prompt al modelo juez. El mensaje del sistema limita al juez a devolver un único número entero del 1 al 5, de modo que el resultado se analiza de manera determinista. Dentro de score_helpfulness, se obtienen la pregunta del usuario y la respuesta final del agente del registro de ejecución, se ejecuta la cadena y el número entero se escala a un valor flotante entre 0 y 1 para que se combine adecuadamente con la puntuación de la herramienta. gpt-4o-mini funciona como juez porque es económico y rápido.
Los jueces basados en LLM son estocásticos, por lo que dos ejecuciones sobre el mismo registro de ejecución pueden devolver puntuaciones ligeramente diferentes. La guía de Anthropic recomienda calibrar un juez con un conjunto etiquetado por humanos antes de confiar en él, y la tercera capa se encarga precisamente de crear ese conjunto.
Capa 3: revisión humana como cola de anotaciones
LangSmith cuenta con una cola de anotaciones nativa donde las personas etiquetan los registros en el panel de control. Para que el ejemplo sea autónomo, se modela la misma estructura en código.
# Step 3: Model a human review signal
def score_human(trace, human_labels: dict) -> float | None:
run_id = trace["run_id"]
return human_labels.get(run_id)
human_labels = {
"b1d2e3f4-...": 1.0,
"c2e3f4g5-...": 0.0,
}
La función busca un identificador de ejecución en un diccionario con etiquetas proporcionadas por humanos. Si existe una etiqueta, esta se devuelve; de lo contrario, la función retorna None, lo que indica que aún no ha sido revisada. En entornos de producción, dicho diccionario estaría detrás de la API de cola de anotaciones de LangSmith. Lo importante es cómo se comportan las puntuaciones: son escasas porque solo algunas ejecuciones las reciben, son autoritativas porque anulan a las demás cuando hay desacuerdos, y sirven para calibrar al juez basado en LLM.
Combinando las tres capas
# Step 4: Run all three scorers and attach scores to each trace
def enrich(trace):
trace["scores"] = {
"tool_correctness": score_tool_correctness(trace),
"helpfulness": score_helpfulness(trace),
"human": score_human(trace, human_labels),
}
return trace
enriched = [enrich(t) for t in traces]
print(json.dumps(enriched[0]["scores"], indent=2))
La función enrich agrega las tres puntuaciones a un registro y lo devuelve. Se aplica a cada registro recopilado durante la recolección, generando una lista de registros enriquecidos que cada uno contiene un diccionario scores estructurado. La expresión regular basada en reglas produce tool_correctness, el juez del LLM genera helpfulness, y human es un número flotante o None.
El resultado esperado:
{
"tool_correctness": 1.0,
"helpfulness": 0.8,
"human": null
}
Las tres puntuaciones difieren intencionadamente. La corrección del herramienta indica que se utilizó la herramienta adecuada, mientras que el juez basado en LLM afirma que la respuesta merece 4 de 5 puntos. Un revisor humano que intervenga posteriormente podría decir que la herramienta fue correcta, pero que la redacción no se ajustaba al tono de la marca. Cada capa detecta lo que las demás pasan por alto, por lo que su combinación permite obtener cobertura y información fiable a diferentes costos.
Ahora cada registro contiene tres puntuaciones: una basada en reglas, una evaluada por un LLM y, cuando está disponible, una realizada por un humano. Las puntuaciones sin estructura son casi tan inútiles como no tener ninguna, por lo que a continuación se analizan los patrones.
Etapa 4: identificación de patrones de fallo
Cuando los registros muestran errores, ese mismo fallo tiende a repetirse. El agente elige la herramienta incorrecta para una formulación específica. Su razonamiento se desvía en consultas de múltiples pasos. Sus respuestas son técnicamente correctas pero no satisfacen lo que quería el usuario. Cada patrón requiere un tipo diferente de solución, ya sea una descripción revisada de la herramienta, una actualización del prompt o un flujo de trabajo reestructurado. Nombrar el patrón es la parte difícil, mientras que corregirlo posteriormente suele ser sencillo.
LangChain ofrece un Insights Agent diseñado precisamente para esta tarea a gran escala. Este agente procesa los registros, los agrupa según el tipo de error que presentan y ayuda a los equipos a determinar qué requiere atención primero. Una versión simplificada del mismo concepto está implementada en el código a continuación.
Filtrado de fallos
# Step 1: Keep only traces where at least one score is low
def is_low_scoring(trace):
scores = trace["scores"]
if scores["human"] is not None and scores["human"] < 0.5:
return True
if scores["tool_correctness"] < 0.5:
return True
if scores["helpfulness"] < 0.6:
return True
return False
failures = [t for t in enriched if is_low_scoring(t)]
print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
is_low_scoring realiza tres verificaciones. Una puntuación humana inferior a 0.5 marca la traza como un fallo. De lo contrario, las puntuaciones automáticas deciden: una corrección del herramienta inferior a 0.5 significa que el agente enrutó incorrectamente, y una utilidad inferior a 0.6 significa que al juez del LLM no le gustó la respuesta. Los umbrales pueden ajustarse, y estos son valores por defecto razonables para una primera evaluación. El filtro procesa las trazas enriquecidas de la etapa anterior para generar el conjunto de fallos.
El resultado esperado:
9 of 42 traces flagged as low scoring
Clasificación de los fallos en categorías
Una sencilla heurística etiqueta los fallos aquí. En entornos de producción, los embeddings con clustering podrían hacer el trabajo, o bien el Insights Agent de LangChain, que realiza precisamente eso a partir de los registros de LangSmith.
# Step 2: Tag each failure with a category based on its scores
def categorize(trace):
scores = trace["scores"]
if scores["tool_correctness"] < 0.5:
return "wrong_tool"
if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
return "unhelpful_answer"
if scores["human"] is not None and scores["human"] < 0.5:
return "human_flagged"
return "other"
from collections import Counter
categories = Counter(categorize(t) for t in failures)
print(categories.most_common())
La función categorize verifica los elementos en orden de prioridad. Un herramienta incorrecta representa el problema más estructural, por lo que se etiqueta primero. Una respuesta inútil que utilizó la herramienta adecuada indica problemas en la formulación de la solicitud o en la generación de la respuesta. Un fallo detectado por un humano que la automatización pasó por alto se clasifica en su propia categoría. El Counter estándar de Python cuenta cada categoría, y se importa directamente allí porque es su primer uso.
El resultado esperado:
[('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
La categoría indica el remedio. Una serie de cinco fallos con herramientas incorrectas apunta a la descripción de la herramienta o a la instrucción de enrutamiento. Tres respuestas inútiles a pesar de utilizar la herramienta correcta indican que el mensaje que genera las respuestas es el problema. Un fallo detectado únicamente por un humano significa que los evaluadores automáticos pasaron por alto algo, por lo que probablemente sea necesario un nuevo evaluador.
Analizando un único patrón
# Step 3: Print the inputs and outputs for every wrong_tool failure
wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
for trace in wrong_tool[:3]:
print("QUERY:", trace["inputs"]["messages"][0]["content"])
print("TOOLS CALLED:", trace["tool_calls"])
print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
print("---")
El conjunto de fallos se reduce a los casos de wrong_tool, y el bucle imprime la consulta del usuario, las herramientas que realmente se llamaron y la respuesta final para los tres primeros. Aquí es donde una persona o un LLM más inteligente analiza los fallos y formula una hipótesis, por ejemplo, que toda consulta que contenga “subscription” activa get_recent_orders en lugar de get_account_info. Esa hipótesis se convierte en el objetivo para la corrección.
El resultado esperado:
QUERY: Is my subscription active?
TOOLS CALLED: ['get_recent_orders']
RESPONSE: I could not find any recent orders to confirm your subscription status.
---
QUERY: Tell me about my subscription to your product
TOOLS CALLED: ['get_recent_orders']
RESPONSE: There are no recent orders for this account.
---
Tres ejemplos en la pantalla dejan claro el patrón. “Suscripción” está semánticamente más relacionado con “estado de la cuenta” que con “pedidos”, sin embargo, el agente redirige la solicitud basándose en una superposición superficial de palabras clave. La solución probable es realizar un cambio en una sola línea en la documentación de get_account_info para incluir referencias a suscripciones, o añadir un breve mensaje del sistema que explique la diferencia.
La etapa finaliza con patrones de fallo que incluyen conteos, categorías y ejemplos. Un problema resuelto hoy tiene poco valor si un cambio realizado por otro ingeniero lo vuelve a causar la semana siguiente, por lo que la solución debe confirmarse mediante una prueba.
Etapa 5: conjuntos de pruebas sin conexión
Aquí los fallos se convierten en pruebas estáticas. Cada fallo real en producción se agrega a un conjunto de datos de LangSmith como ejemplo que incluye la entrada (la consulta del usuario), el comportamiento esperado (la herramienta adecuada o una respuesta aceptable) y metadatos opcionales como la categoría del fallo. Una vez almacenado, nunca se elimina, por lo que un fallo fijo no puede reaparecer silenciosamente.
La evaluación offline en LangSmith consta de tres partes:
- Un conjunto de datos es una colección nombrada de ejemplos, cada uno con entradas y, opcionalmente, salidas esperadas.
- La función objetivo es el código que se está probando; en el caso de un agente, suele ser su método
.invoke. - Los evaluadores son funciones que califican la salida del objetivo en relación con cada ejemplo, siguiendo el mismo formato que los calificadores de la etapa de puntuación.
Crear el conjunto de datos
# Step 1: Create a LangSmith dataset for our agent
dataset_name = "agent-production-failures"
dataset = client.create_dataset(
dataset_name=dataset_name,
description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
)
print(f"Created dataset: {dataset.id}")
El mismo client de la fase de recolección llama a create_dataset con un nombre y una descripción, y devuelve un objeto de conjunto de datos cuyo .id puede ser referenciado posteriormente. Una implementación real protegería esto con una verificación de creación solo si no existe, pero la demostración asume que se ejecuta por primera vez.
El resultado esperado:
Created dataset: 3f2a1b0c-...
Convierte los fracasos en ejemplos
# Step 2: Convert each failure trace into a dataset example
examples = []
for trace in failures:
query = trace["inputs"]["messages"][0]["content"]
expected_tool = expected_tool_for_query(query)
examples.append({
"inputs": {"question": query},
"outputs": {"expected_tool": expected_tool},
"metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
})
client.create_examples(dataset_id=dataset.id, examples=examples)
print(f"Added {len(examples)} examples to {dataset_name}")
El código recorre cada rastro de fallo y crea un diccionario de ejemplo con tres campos. inputs contiene la pregunta del usuario, outputs indica el comportamiento a probar, que en este caso es la herramienta que debería haberse llamado, y metadata incluye la categoría y el ID de ejecución original para que cada ejemplo pueda rastrearse hasta su origen. Una sola llamada batched a client.create_examples sube todo.
El resultado esperado:
Added 9 examples to agent-production-failures
Las implementaciones reales suelen permitir que un revisor decida el resultado esperado, ya que la heurística de expresiones regulares es solo un punto de partida. Una persona que aprueba cada ejemplo proporciona una verdad objetiva más sólida, y para eso existe la cola de anotaciones de LangSmith.
El evaluador de regresión
# Step 3: Write an evaluator that checks the agent's tool call against the expected tool
def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
actual_messages = outputs.get("messages", [])
tool_called = None
for m in actual_messages:
if getattr(m, "tool_calls", None):
tool_called = m.tool_calls[0]["name"]
break
expected = reference_outputs["expected_tool"]
score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
return {"key": "tool_match", "score": score}
tool_match_evaluator sigue la firma que espera LangSmith: recibe los inputs del ejemplo, los outputs reales del agente y los reference_outputs del ejemplo. Examina los mensajes de salida del agente, encuentra el primero que contiene una llamada a herramienta y extrae el nombre de dicha herramienta. Luego lo compara con la herramienta esperada y devuelve un diccionario con una key y una score. La key se convierte en el nombre de la métrica que se muestra en LangSmith. Su estructura sigue las indicaciones oficiales de inicio rápido para la evaluación en LangSmith.
La función objetivo
# Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
def run_agent(inputs: dict) -> dict:
result = agent.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
run_agent se mantiene deliberadamente mínimo. Dadas las inputs de un ejemplo de conjunto de datos, llama al agente y devuelve el diccionario en bruto que este devuelve, lo cual coincide con lo que espera la función evaluate de LangSmith. El agent al que se llama fue creado durante la fase de fundamentación.
Mantener el envoltorio ligero es intencional. Cualquier elemento colocado dentro de él se ejecuta en cada evaluación y aumenta el costo, mientras que una simple transmisión directa permite que el evaluador pruebe al agente y no al envoltorio.
Así, el conjunto de datos existe, el evaluador está conectado, y cada fallo en la producción queda registrado como un caso de prueba permanente. La fase final ejecuta ese evaluador antes de aplicar cualquier cambio.
Fase 6: cerrar el bucle
Este es el portal de regresión. El agente recibe una actualización, por ejemplo una descripción del herramienta revisada para solucionar el error de enrutamiento relacionado con las “suscripciones” detectado anteriormente. Antes del lanzamiento, todo el conjunto de datos de fallos anteriores se procesa con la nueva versión. LangSmith almacena ambas ejecuciones como experimentos y muestra, ejemplo por ejemplo, dónde la nueva versión mejoró y dónde empeoró. El lanzamiento solo tiene lugar cuando la nueva línea de referencia supera a la antigua y nada de lo ya corregido regresa a su estado anterior.
Ejecución de la evaluación de línea de referencia
# Step 1: Run the evaluator across the dataset with the current agent
from langsmith.evaluation import evaluate
experiment_results = evaluate(
run_agent,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v1-baseline",
max_concurrency=4,
)
evaluate se importa de langsmith.evaluation. Se le proporciona el destino run_agent, el nombre del conjunto de datos (que LangSmith asocia con el conjunto de datos de la etapa anterior), la lista de evaluadores, un prefijo de experimento que identifica la ejecución y una concurrencia de cuatro para que la evaluación se realice en paralelo. Devuelve un objeto con los resultados del experimento que hace referencia a la ejecución en LangSmith.
El resultado esperado:
View the evaluation results for experiment: 'agent-v1-baseline-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.33
De los nueve ejemplos fallidos, el agente actual sigue dirigiendo la mayoría de ellos incorrectamente, y esa es la situación base.
Envío de la corrección y medición nuevamente
La solución sugerida en la fase de patrones es reescribir la documentación de get_account_info utilizando el lenguaje específico para suscripciones.
# Step 2: Update the tool docstring to teach the agent about subscriptions
@tool
def get_account_info_v2(account_id: str) -> str:
"""Look up basic information for an account, including plan, status,
subscription tier, and contact email. Use this tool for any question
about the account itself, including subscription status."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
tools_v2 = [get_account_info_v2, get_recent_orders]
agent_v2 = create_react_agent(model, tools_v2)
def run_agent_v2(inputs: dict) -> dict:
result = agent_v2.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
La herramienta de información de cuentas se ha redefinido con una documentación más completa que menciona explícitamente el estado de la suscripción. Dado que es la documentación lo que lee el LLM al seleccionar una herramienta, ampliarla aborda directamente el problema de enrutamiento. El agente se ha reconstruido con la herramienta actualizada y se encuentra dentro de un nuevo objetivo run_agent_v2. Todo lo demás permanece sin cambios.
# Step 3: Run the evaluator on the v2 agent
experiment_results_v2 = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v2-docstring-fix",
max_concurrency=4,
)
La misma evaluación se ejecuta nuevamente con el mismo conjunto de datos y el mismo evaluador, cambiando únicamente el objetivo. Un nuevo experiment_prefix permite distinguir esta ejecución en la interfaz de LangSmith para poder compararla con el valor de referencia. LangSmith hace pasar cada ejemplo por el agente v2, almacena cada registro completo y califica cada resultado con el evaluador.
El resultado esperado:
View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.89
La puntuación aumentó de 0.33 a 0.89. Ocho de los nueve fallos ya se han corregido, y el noveno es una consulta realmente ambigua que podría pertenecer a la cola de anotación humana.
Before (v1): After (v2):
wrong_tool: 5 wrong_tool: 1
unhelpful: 3 unhelpful: 0
human: 1 human: 0
avg score: 0.33 avg score: 0.89
Varias cosas hicieron que esta comparación fuera fiable:
- La evaluación inicial arrojó una puntuación concreta antes de que se modificara cualquier código.
- La solución consistió en una sola edición precisa, un único documento de documentación.
- La reevaluación midió el efecto exacto de esa edición frente a un conjunto seleccionado de fallos reales.
- El conjunto de datos de pruebas permaneció inalterado, por lo que la comparación fue equitativa.
- Cada rastro de fallo original ahora está registrado como prueba, de modo que ningún cambio posterior pueda afectarlo silenciosamente.
Eso es lo que hace que el ciclo sea acumulativo. Cada solución agrega ejemplos al conjunto de datos, y cada versión futura se evalúa en relación con todos los fallos anteriores. El proceso de lanzamiento se vuelve más rápido con el tiempo y no más lento, ya que la confianza aumenta junto con el conjunto de datos.
Todo el ciclo ahora se ejecuta de principio a fin. Se recopilan los registros y se enriquecen con puntuaciones, surgen patrones, las fallas se convierten en pruebas fuera de línea, la evaluación precede al lanzamiento, y un cambio solo se promueve cuando la línea de referencia aumenta. Cada ciclo comienza desde un nivel más alto.
Una ejecución diaria completa
Un único conductor puede ejecutar todo el ciclo una vez con datos de producción frescos.
# Full loop driver
def run_improvement_loop():
# 1. Pull traces
runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
traces = [compact_trace(r) for r in runs]
# 2. Enrich with scores
enriched = [enrich(t) for t in traces]
# 3. Find failures and categorize them
failures = [t for t in enriched if is_low_scoring(t)]
categories = Counter(categorize(t) for t in failures)
# 4. Add failures to the dataset
new_examples = [{
"inputs": {"question": t["inputs"]["messages"][0]["content"]},
"outputs": {"expected_tool": expected_tool_for_query(
t["inputs"]["messages"][0]["content"])},
"metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
} for t in failures]
if new_examples:
client.create_examples(dataset_id=dataset.id, examples=new_examples)
# 5. Re-evaluate the current agent against the full dataset
result = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="daily-regression",
)
return {
"traces_collected": len(traces),
"failures_found": len(failures),
"by_category": dict(categories),
"examples_added": len(new_examples),
}
print(run_improvement_loop())
Esta función une las seis etapas. Recupera los registros de ayer, los califica, categoriza los fallos, agrega nuevos fallos como ejemplos de conjunto de datos y realiza una evaluación de regresión. Devuelve un pequeño diccionario con indicadores que se puede registrar o publicar en Slack cada día.
El resultado esperado:
{
"traces_collected": 186,
"failures_found": 12,
"by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
"examples_added": 12,
"regression_score": 0.87
}
Lo que muestra el resultado: ese día se obtuvieron 186 registros de producción sin cambios en el código del agente, y doce de ellos arrojaron al menos una calificación baja. Ahora hay más respuestas inútiles que llamadas incorrectas a herramientas, lo que indica que la corrección de enrutamiento surtió efecto y que la siguiente mejora debe aplicarse en la generación de respuestas. La puntuación de regresión se mantuvo en 0,87, por lo que ninguno de los cambios de la semana rompió un fallo anterior. Doce ejemplos nuevos se añadieron al conjunto de datos permanente, donde servirán para evitar repeticiones.
Cinco mejoras que vale la pena implementar próximamente
Cada una de estas se relaciona con una función de LangChain, LangGraph o LangSmith que puede activarse sin tener que reconstruir toda la arquitectura anterior.
- Sustituya el clasificador basado en expresiones regulares por embeddings.
OpenAIEmbeddingspuede incrustar cada consulta de modo que las trazas de fallos se agrupen según la similitud semántica y no según reglas basadas en palabras clave, lo que permite detectar problemas más sutiles como desajustes en el tono. - Cambie al juez LLM integrado por la biblioteca OpenEvals. La guía rápida de LangSmith utiliza
openevalscon prompts listos para usar comoCORRECTNESS_PROMPT, los cuales funcionan mejor que un juez creado manualmente y cuentan con variantes sin referencia para el monitoreo en línea. - Agregue evaluaciones de múltiples turnos. Las evaluaciones de múltiples turnos de LangSmith aplican un juez LLM a toda la conversación y no solo a las respuestas individuales, lo que permite detectar desviaciones en el razonamiento que los evaluadores de un solo turno pasan por alto en los agentes basados en chat.
LANGSMITH_PROJECT=agent-canary durante un día y luego comparar sus resultados en tiempo real con los del proyecto de control, solo se reemplazará la versión en producción si es claramente superior.Luego, el ciclo se repite cada vez desde una base más alta, con rastros mejores, evaluaciones más precisas y un agente más eficaz.
Lecturas relacionadas
- Harness, Loop, Graph: Tres capas detrás de la mayoría de los fallos en los agentes en producción — Por qué los agentes que fallan en producción rara vez son un problema del modelo: separar el conjunto de herramientas, la condición de detención del bucle y la topología del grafo, asignando un responsable para cada uno.
- Enrutamiento de múltiples agentes con LangGraph, Claude y seguimiento en LangSmith — Construir un enrutador que envíe solicitudes de soporte a agentes de facturación o técnicos en LangGraph con Claude, y luego inspeccionar cada decisión, prompt y conteo de tokens en los registros de LangSmith.