Ir al contenido
mcpd
sin daemon
Código

Pruebas de regresión

La ejecución que se rompió el martes pasa a ser el caso que toda versión posterior tiene que aprobar.

El ciclo que esto existe para cerrar

Un agente falla en una pregunta real. Alguien lo reproduce a mano, cambia un prompt, y parece que mejora. Tres semanas después vuelve a fallar en la misma pregunta, porque nadie guardó nada.

Un dataset sí lo guarda. Captura la ejecución fallida y deja de ser una anécdota: pasa a ser un caso con una entrada, una respuesta esperada cuando la tienes, y un puntero a la traza de la que salió. Toda versión posterior se ejecuta contra él.

Capturar una ejecución

Busca la ejecución en la interfaz o mediante la API, y luego entrega su id de traza al dataset.

capturar una ejecución fallida como caso

curl -X POST localhost:4317/v1/api/datasets/$DATASET/capture \ -H 'content-type: application/json' \ -d '{"trace_ids": ["b3d9e71a4c0f428eb6a5d2317f90c684"]}'

El caso capturado conserva source_trace_id, así que meses después puedes abrir la ejecución que lo motivó y leer lo que ocurrió de verdad en vez de deducirlo de la entrada.

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. tool returns_policy

The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.

La respuesta de la propia ejecución no es la respuesta esperada

use_output_as_expected está desactivado por defecto, y ese valor por defecto es lo importante. Las ejecuciones que vale la pena capturar suelen ser las que salieron mal, y guardar una respuesta equivocada como referencia consagraría el fallo: a partir de ahí, cada versión posterior se mediría por su capacidad de reproducirlo.

Actívalo cuando captures ejecuciones que salieron bien, para fijar un comportamiento que ya te gusta. Déjalo desactivado, y escribe tú la respuesta esperada, cuando captures un fallo.

Los evaluadores integrados

mcpd trae trece evaluadores deterministas: código corriente que lee una ejecución guardada y devuelve un número. No cuestan nada, nunca llaman a un modelo, y dan la misma respuesta dos veces, que es la propiedad que necesita una prueba de regresión y que un modelo juez no tiene.

EvaluadorPasa cuando
answeredLa ejecución produjo una respuesta no vacía.
no_errorsNingún span de la ejecución falló.
exact_matchLa respuesta es igual a la esperada.
containsLa respuesta contiene una cadena obligatoria.
json_validLa respuesta se analiza como JSON.
latency_budgetLa ejecución terminó dentro de un presupuesto en milisegundos.
cost_budgetLa ejecución no costó más de una cantidad fijada.
no_tool_loopNinguna herramienta se llamó dos veces con la misma entrada.
tool_efficiencyLa ejecución se mantuvo dentro de un presupuesto de llamadas a herramientas.
not_a_refusalLa respuesta no es una negativa ni una disculpa.
no_leaked_secretNo aparece ninguna clave, token ni credencial en la respuesta.
span_presentLa ejecución registró un span del tipo o el nombre exigido.
tool_succeededLa herramienta exigida se ejecutó y al menos una llamada terminó bien.

GET /v1/api/scorers devuelve esa misma lista con una marca needs_config. Un evaluador que necesita una respuesta esperada y no la recibe se abstiene en lugar de adivinar, y esa abstención se informa.

Cuando quien califica es un modelo

Hay preguntas que ningún código determinista responde. Si una respuesta atendió de verdad a la pregunta, si un resumen conservó el sentido, si el tono era el correcto: eso necesita un lector, y un modelo es el lector barato.

La puntuación con juez está apagada hasta que la configuras, y un evaluador con juez no se ejecuta nunca si no lo nombras. Los dos valores por defecto existen por la misma razón: calificar gasta dinero en cada ejecución que lee, y una función que hace eso en silencio es una función de la que te enteras por una factura.

{
  "judge": {
    "endpoint": "http://localhost:11434/v1",
    "model": "qwen2.5:14b",
    "api_key": "",
    "timeout_seconds": 30,
    "criteria": {
      "answer_relevance": "Does the answer address the question that was asked?",
      "no_hedging": "Does the answer commit, rather than hedge?"
    }
  }
}

La solicitud tiene la forma de chat-completions de OpenAI, y eso no es una preferencia por un proveedor: Ollama, vLLM, llama.cpp, LM Studio, OpenRouter y los proveedores alojados la aceptan todos, así que la misma configuración llega a un modelo en tu portátil y a una pasarela en producción. Deja api_key vacío para un modelo local que no la pida. La temperatura es cero, porque un juez que responde distinto cada vez no se puede comparar consigo mismo, que es lo único para lo que sirven las puntuaciones.

Los criterios se configuran en vez de pasarse en cada solicitud. El identificador de un evaluador queda guardado en cada puntuación que produce y es aquello por lo que agrupa una comparación posterior, así que dejar que cada llamada inventara a la vez el identificador y la pregunta construiría una columna en la que nadie puede confiar.

Los criterios configurados no aparecen en GET /v1/api/scorers. Ese endpoint lista los trece evaluadores integrados y nada más, así que un cliente que arme su menú de evaluadores a partir de ahí no ofrecerá tus criterios. El único sitio donde el daemon te los nombra de vuelta es el error que recibes por un identificador de evaluador desconocido, que lista juntos los integrados y los criterios configurados.

ejecutar un evaluador con juez sobre la ejecución más reciente

curl -X POST localhost:4317/v1/api/score \ -H 'content-type: application/json' \ -d '{"limit": 1, "scorers": ["answer_relevance"]}'

timeout_seconds es cuánto puede tardar una llamada de evaluación, desde que conecta hasta el último byte de la respuesta: 30 por defecto, de 1 a 300. Si una llamada se pasa de ese tiempo, el evaluador se abstiene y nombra la clave, y mcpd lee como mucho 1 MiB de una respuesta.

Cada evaluador con juez es una llamada al modelo por ejecución, y la solicitud tiene que terminar dentro de --request-timeout-seconds. Así que una solicitud evalúa como mucho las ejecuciones que caben en ese plazo si cada llamada tarda todo su timeout_seconds. Con los valores por defecto, 30 y 30, y un evaluador con juez, eso es una ejecución. Una solicitud que pide más, en trace_ids o en limit, se rechaza con un 400 que nombra el tope, antes de llamar a ningún modelo. Si omites limit, la solicitud evalúa tantas ejecuciones recientes como quepan. Sube --request-timeout-seconds para evaluar más en una solicitud. Cada puntuación con juez se guarda en cuanto el modelo responde, así que una solicitud cortada a medias conserva las que terminó.

Se abstiene en lugar de adivinar

Un juez que devuelve un número que no dedujo es peor que ningún juez, porque parece evidencia. Por eso cada uno de estos casos se informa en skipped con su motivo, y ninguno produce una puntuación.

Qué pasóQué hace mcpd
La ejecución no registró ninguna respuesta Se abstiene. Una ejecución sin respuesta no ha puntuado mal en relevancia: no se ha medido.
El modelo respondió en prosa Se abstiene, y cita la respuesta. Un JSON envuelto en un bloque de código o en una frase sí se lee.
El modelo respondió fuera de la escala Se abstiene. Un siete en una escala de uno no es un nueve décimos, y recortarlo sería inventar un número.
No se pudo contactar con el modelo Se abstiene, con el estado y el cuerpo. Un límite de tasa es un motivo, no un cero.
La respuesta pasaba de 1 MiB Se abstiene sin leer el resto. Un veredicto ocupa unos cientos de bytes, así que una respuesta de ese tamaño no lo es.
El plazo de la solicitud no dejó tiempo para la llamada Se niega sin llamar al modelo. La pasada responde con las puntuaciones que terminó, dentro del plazo, en vez de con un 503.

Toda puntuación con juez guarda el modelo como su scorer_version. Dos puntuaciones de dos jueces distintos no son comparables, y ese campo es lo que lo dice en el registro.

Puntuar ocurre a posteriori

POST /v1/api/score pasa los evaluadores sobre ejecuciones que ya están guardadas. Nada se evalúa en línea. Una evaluación en la ruta de la solicitud suma su latencia a aquello que juzga, y una evaluación que lanza un error nunca debería tumbar la solicitud de un usuario.

puntuar las últimas cien ejecuciones

curl -X POST localhost:4317/v1/api/score \ -H 'content-type: application/json' \ -d '{"project": "support-desk", "limit": 100, "config": {"max_ms": 4000}}'

La respuesta trae las puntuaciones que escribió y, por separado, cada evaluador que se abstuvo con el motivo por el que lo hizo. Un evaluador que no pudo responder es más interesante que uno que devolvió un número, así que mcpd nunca se lo traga. El techo de ingesta del proyecto también llega hasta esta puerta: una puntuación calculada por la pasada que llevaría al proyecto por encima de --quota-per-minute no se guarda, y aparece en skipped diciéndolo, en vez de escribirse pasando del techo o tirarse en silencio.

Envía "dry_run": true para ver qué produciría una pasada sin guardar nada.

Qué ejecuciones lee una pasada lo deciden trace_ids, from y limit. Nombra las ejecuciones en trace_ids para puntuar exactamente esas. Déjalo vacío y la pasada toma las limit ejecuciones más recientes del proyecto, de más nueva a más antigua, y from acota esa página: una pasada enviada con from lee solo las ejecuciones que empezaron en ese momento o después, que es como puntuas todo lo ocurrido desde un despliegue sin listar identificadores.

Comparar dos versiones

Registra un experimento por cada versión que quieras comparar, etiqueta sus puntuaciones con el identificador de ese experimento, y pídele a mcpd la diferencia.

¿ayudó el prompt nuevo?

curl 'localhost:4317/v1/api/experiments/compare?baseline=v3&candidate=v4'

La respuesta es una diferencia por evaluador con el tamaño de muestra de cada lado, y una marca decidable. Por debajo de veinte observaciones mcpd dice que la comparación no se puede decidir, en vez de informar de un movimiento.

Es una regla deliberadamente tosca, no una prueba de significación. El fallo que evita es el habitual: una media que se movió sobre cuatro ejecuciones, impresa sin su recuento, leída como un resultado y llevada a producción. Imprimir los recuentos junto a la diferencia es lo que impide confundir el ruido con un hallazgo.

Dónde se ejecuta esto

Todo esto es la API HTTP, así que el ciclo pertenece a CI: captura los fallos según ocurren, puntúa las ejecuciones que produce tu suite de pruebas, registra la compilación como un experimento, y haz fallar el trabajo cuando regressed vuelva como verdadero en una comparación decidible.

A continuación