Guías
Tutorial: una conversación de varios turnos
Un recorrido ejecutable para evaluar un asistente conversacional con el SDK de Python: su aplicación reproduce una conversación guionizada en una sesión nueva, registra lo que respondió como un artefacto conversation/v1, y dos evaluadores juzgan cada conversación completa. Funciona sin conexión con un sustituto guionizado del modelo juez, después compara una corrección candidata y termina con la alternativa de evaluar los turnos como casos agrupados.
Términos como intervalo, estado de decisión y acción de publicación se definen en Conceptos; Registrar lo que hizo un sistema trata los artefactos.
Lo que Oloproof hace y no hace aquí
| Oloproof hace | Su aplicación hace |
|---|---|
| guardar las conversaciones guionizadas como conjunto de datos, idénticas para todo sistema | conducir la conversación: plantear cada turno guionizado en orden |
| comprobar que se respondió a cada turno guionizado (ConversationCompleted) | ser dueña del estado de sesión, iniciar una sesión nueva por caso y restablecerla |
| juzgar la transcripción completa con un modelo (ConversationJudge) | decidir qué ocurre cuando no puede continuar, y registrar que se detuvo |
| calcular intervalos, comparar dos sistemas y decidir según una política | registrar el artefacto conversation/v1 |
Oloproof no tiene simulador de usuario: nunca escribe un turno de usuario, así que el lado del usuario es lo que el conjunto de datos tenga guionizado. No tiene ninguna métrica por turno dentro de una conversación registrada, y no puede reproducir una conversación registrada contra un sistema nuevo. Los evaluadores de conversación solo existen en el SDK de Python: ConversationCompleted y ConversationJudge no son tipos de evaluador en oloproof.yaml, así que este tutorial usa un script en lugar de oloproof run.
Requisitos previos
- Python 3.11 o posterior y pip install oloproof, como en la guía rápida.
- Los archivos de ejemplo, que vienen con el paquete. Cópielos en un directorio nuevo para que el almacén de la ejecución quede allí:
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation| Archivo | Qué es |
|---|---|
| assistant.py | la aplicación evaluada: un asistente de planes con estado de sesión |
| systems.py | el adaptador: reproduce un guion y registra conversation/v1 |
| judge_offline.py | el sustituto guionizado del modelo juez |
| evaluate.py | ejecuta la evaluación, la comparación y la alternativa por turnos |
| release.yaml | la política para una ejecución |
| comparison.yaml | la política para el candidato frente a la línea base |
| turns_release.yaml | la política para la alternativa por turnos |
| data/conversations.jsonl | 40 conversaciones guionizadas |
| data/turns.jsonl | las mismas conversaciones, un caso por turno |
Sin clave, sin red y sin coste de proveedor, hasta el paso opcional en vivo del final.
La aplicación
assistant.py responde preguntas sobre tres planes de precios. Guarda un único dato de estado, el plan del que trata la conversación, para que una pregunta de seguimiento como "Does that include SSO?" pueda resolver "that". La línea base tiene un defecto deliberado: no recuerda el plan, así que un seguimiento se responde sobre el plan por defecto. Un usuario que pide hablar con una persona termina la conversación con HandoffRequested.
class PlanAssistant:
def __init__(self, *, remembers_plan):
self.remembers_plan = remembers_plan
self.reset()
def reset(self):
"""Forget everything, so one conversation never leaks into the next."""
self.current_plan = None
def ask(self, question): ...Esta es la parte que usted sustituye por su propia aplicación: un cliente de chatbot, una sesión de agente, una sesión HTTP con su servicio. Sea lo que sea, es dueña de su estado y de su restablecimiento; Oloproof solo ve lo que registra el adaptador.
El conjunto de datos: el guion es la entrada
Una línea de data/conversations.jsonl es una conversación:
{"expected": {"plan": "enterprise"}, "id": "conv_00", "input": {"turns": ["What does the enterprise plan cost?", "Does that include SSO?"]}, "metadata": {"pattern": "pronoun_followup"}}Los turnos del usuario son contenido del conjunto de datos, cubiertos por el digest de la suite e idénticos para todo sistema medido con ellos; eso es lo que hace comparables dos sistemas. expected es la referencia que se muestra al juez. Las 40 conversaciones son 24 con un seguimiento que no nombra ningún plan, 12 que nombran el plan en cada turno y 4 que piden una persona en el turno dos de tres.
El adaptador
systems.py inicia una sesión nueva por caso, plantea cada turno guionizado en orden y registra lo que se devolvió:
from oloproof import CONVERSATION, current_case, system
def replay(case, *, remembers_plan):
script = [str(turn) for turn in case["turns"]]
session = PlanAssistant(remembers_plan=remembers_plan) # a new session per case
turns = []
truncated = False
for index, question in enumerate(script, start=1):
try:
reply = session.ask(question)
except HandoffRequested:
truncated = True # the recording stops here and says so
break
turns.append({"index": index, "asked": question, "answer": reply["answer"]})
current_case().artifact(
CONVERSATION,
{"turns": turns, "declared_turns": len(script), "truncated": truncated},
)
last = turns[-1]["answer"] if turns else None
return {"answer": last, "turns_answered": len(turns)}
@system(name="plan-assistant", version="baseline", records=(CONVERSATION,))
def baseline(case):
return replay(case, remembers_plan=False)
@system(name="plan-assistant", version="candidate-remembers-plan", records=(CONVERSATION,))
def candidate(case):
return replay(case, remembers_plan=True)Una sesión nueva por caso importa: Oloproof ejecuta los casos de forma concurrente y sin un orden fijo, y una sesión compartida entre casos dejaría que el estado de una conversación se filtrara a otra. records= declara que el sistema registra el artefacto; sin él, los evaluadores de conversación se rechazan antes de que se ejecute nada, en lugar de contar todos los casos como faltantes.
El artefacto conversation/v1
Lo que la línea base registró para conv_00, según oloproof export RUN_ID (el cases.jsonl del paquete exportado):
{"declared_turns": 2, "truncated": false, "turns": [{"answer": "The enterprise plan costs a price agreed per contract.", "asked": "What does the enterprise plan cost?", "index": 1, ...}, {"answer": "The starter plan does not include SSO.", "asked": "Does that include SSO?", "index": 2, ...}]}Y para una conversación que pidió una persona:
{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}| Campo | Significado |
|---|---|
| turns[].index | a qué turno guionizado responde; consecutivo desde 1 |
| turns[].answer | lo que devolvió el asistente, cualquier JSON |
| turns[].asked | opcional, solo para leer; el motor empareja por index |
| turns[].retrieval | opcional, lo que recuperó ese turno, con la forma retrieval/v1 |
| declared_turns | cuántos turnos declaraba el guion |
| truncated | el registro se detiene antes del final del guion, sea cual sea la causa |
El artefacto se comprueba cuando se registra: un registro con menos turnos de los declarados debe indicar truncated: true, los índices deben ser consecutivos y un registro no puede responder a más turnos de los que se le plantearon. Un artefacto mal formado detiene la ejecución con un SystemContractError.
Los dos evaluadores, y por qué ambos
- ConversationCompleted es determinista: ¿respondió el asistente a cada turno guionizado? Se ejecuta primero porque cualquier otra afirmación sobre una conversación que se detuvo en el turno uno de tres es una afirmación sobre otra conversación. Una conversación truncada no lo supera; es un resultado, no un caso faltante.
- ConversationJudge es un juez modelo sobre la transcripción completa, cada turno del usuario y del asistente, porque los fallos que se reprochan a un producto conversacional son relacionales: una respuesta que contradice otra del turno anterior solo es incorrecta junto a ella. Una conversación truncada se juzga por lo que se registró, y la transcripción indica al juez dónde se detuvo.
evaluators = [
ConversationCompleted(),
ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]Juzgar sin conexión
Un juez necesita un modelo. Para ejecutar sin red, judge_offline.py pasa un proveedor guionizado, el mismo auxiliar que usan las propias pruebas de Oloproof (FakeProvider de los internos del motor, no API pública). Responde a cada prompt del juez con una única regla fija: aprueba cuando cada turno del asistente nombra el plan que nombra la referencia. Eso hace que los juicios sean deterministas y el tutorial reproducible. No mide nada sobre cómo se comporta un modelo juez real.
La política
release.yaml:
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
- {id: completion-floor, metric: conversation_completed, min: 0.75}
- {id: coherence-floor, metric: plan_coherent, min: 0.80}Ejecútelo
python evaluate.pybaseline run run_...
conversation_completed: 0.900 [0.763, 0.972] over 40 conversations
plan_coherent: 0.400 [0.249, 0.567] over 40 conversations
gate BLOCK (exit 3)
completion-floor: PASS (lower_bound_meets_minimum)
coherence-floor: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
first failing conversation:
conversation_completed: passed=True {'turns_recorded': 2, 'turns_declared': 2, 'truncated': False}
plan_coherent: passed=False {'provider_model': 'offline-rule'}Cómo leerlo:
- La compleción es de 36 sobre 40, las cuatro derivaciones a una persona. Su límite inferior supera 0.75, así que esa regla es PASS.
- La coherencia es del 40%, y su regla indica INSUFFICIENT_EVIDENCE con evaluator_not_validated, no FAIL. Las reglas de un juez no deciden hasta que el juez se ha medido frente a etiquetas humanas (require_validated_evaluators está activado por defecto; Jueces lo explica). La estimación se sigue mostrando, y sigue siendo evidencia: simplemente no puede permitir ni bloquear una publicación por sí sola.
- gate BLOCK (exit 3): la política bloquea con INSUFFICIENT_EVIDENCE. La salida 3 es ese estado, no un fallo.
Validar el sustituto sin conexión no tendría sentido, ya que es una regla escrita para este ejemplo. Con un juez real, etiquete una muestra de la ejecución con oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20 y después ejecute oloproof evaluators validate EVALUATOR_ID --by YOU.
Inspeccionar una conversación que falla
El SDK escribe en el mismo almacén que lee la CLI, .oloproof/ en el directorio desde el que ejecutó:
oloproof inspect RUN_ID --case conv_00output: {
"answer": "The starter plan does not include SSO.",
"turns_answered": 2
}
judgments:
conversation_completed: passed
plan_coherent: failed
judge text, not verified:
every answer is about enterprise: FalseEl usuario preguntó por el plan enterprise y el seguimiento se respondió sobre el plan starter. oloproof inspect RUN_ID --failures enumera todas las conversaciones que fallan; los 24 seguimientos sin nombre de plan fallan de la misma manera. La siguiente acción está en la aplicación: conservar el plan en el estado de sesión.
Un cambio candidato, y la comparación
candidate en systems.py fija remembers_plan=True. evaluate.py ejecuta ambos sistemas sobre los mismos guiones y los compara caso a caso según comparison.yaml:
rules:
- {id: coherence-better, kind: superiority, metric: plan_coherent}
- {id: completion-no-worse, kind: non_inferiority, metric: conversation_completed, margin: 0.05}comparison, candidate minus baseline
conversation_completed: +0.000 [-0.127, +0.127] over 40 pairs
plan_coherent: +0.600 [+0.337, +0.817] over 40 pairs
gate BLOCK (exit 3)
coherence-better: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
completion-no-worse: INSUFFICIENT_EVIDENCE (interval_overlaps_margin)La diferencia de coherencia es grande y su intervalo excluye el cero, pero el juez no está validado, así que su regla sigue sin decidir. La compleción no cambia, y 40 pares no pueden demostrar que esté dentro de cinco puntos: el intervalo llega a 12.7 puntos en ambos sentidos. Ambas cosas apuntan al mismo paso siguiente: validar el juez y añadir conversaciones.
La alternativa: turnos como casos, analizados como clústeres
Un veredicto por conversación dice con qué frecuencia una conversación fue bien, no qué turno salió mal. Como Oloproof no tiene ninguna métrica por turno dentro de una conversación registrada, la otra vía es hacer de cada turno su propio caso y unir los turnos de una conversación con group_id:
{"expected": {"plan": "enterprise"}, "group_id": "conv_00", "id": "conv_00_t2", "input": {"history": ["What does the enterprise plan cost?"], "question": "Does that include SSO?"}}El sistema reproduce el historial guionizado en una sesión nueva y después responde al turno:
@system(name="plan-assistant-turns", version="baseline")
def turn_baseline(case):
session = PlanAssistant(remembers_plan=False)
for earlier in case["history"]:
session.ask(str(earlier))
return session.ask(str(case["question"]))Los turnos de una conversación no son independientes, así que en cuanto algún caso tiene un group_id la suite se analiza por clúster con un método aproximado que la política debe aceptar (turns_release.yaml fija allow_approximate_methods: true; Casos agrupados lo explica):
turns as cases: turn_plan 0.667 [0.588, 0.749] over 72 turns
gate BLOCK (exit 1)
turn-plan-floor: FAIL (upper_bound_below_minimum)El compromiso:
| Un caso por conversación | Un caso por turno, agrupado | |
|---|---|---|
| Unidad de la tasa | conversaciones que fueron bien | turnos respondidos correctamente |
| Tamaño muestral efectivo | el número de conversaciones | sigue siendo el número de conversaciones, no de turnos |
| Qué turno falló | leer la transcripción | cada turno tiene su propio veredicto |
| Historial que ve cada turno | las respuestas anteriores del propio asistente | los turnos de usuario anteriores del guion, reproducidos |
| Detecta la deriva causada por sus propias respuestas anteriores | sí | no, cada turno parte de un historial guionizado |
| Evaluadores | ConversationCompleted, ConversationJudge (solo SDK) | cualquier evaluador, en YAML o en el SDK |
La alternativa por turnos excluye las cuatro conversaciones derivadas a una persona, así que sus 72 turnos proceden de 36 conversaciones. Aquí puede decidir donde el juez no pudo, porque ExactMatch es determinista y no necesita validación.
Opcional: un modelo juez en vivo
Este paso necesita un modelo servido en su máquina. Ni el tutorial sin conexión ni su prueba lo ejecutan. Con Ollama en marcha y llama3.1 descargado:
python evaluate.py --liveEl juez llama entonces a http://localhost:11434/v1 con provider="openai_compatible". Un servidor en loopback no necesita clave y no envía nada fuera de la máquina. Un proveedor en la nube necesita su clave en el entorno, envía cada transcripción a ese proveedor y cuesta dinero por juicio. Los veredictos de un modelo real difieren de los del sustituto, así que las cifras de arriba cambiarán, y sus reglas seguirán indicando evaluator_not_validated hasta que lo valide.
Solución de problemas
| Síntoma | Causa y solución |
|---|---|
| this evaluator needs exactly one conversation/v1 artifact; the case recorded 0 | El adaptador no llamó a current_case().artifact(CONVERSATION, ...), o lanzó una excepción antes. Registre incluso cuando la conversación se detiene antes de tiempo. |
| malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is false | La ejecución se detiene con un SystemContractError. Un registro con menos turnos que declared_turns debe fijar truncated: true. |
| conversation turn indexes must be contiguous starting at 1 | Numere los turnos 1, 2, 3 según el turno guionizado al que responden. |
| evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records them | Añada records=(CONVERSATION,) al decorador @system. |
| Input tag 'conversation_completed' found using 'type' does not match any of the expected tags desde oloproof run | Los evaluadores de conversación son solo del SDK. Use un script como aquí. |
| Las respuestas se filtran entre conversaciones | Una sesión se comparte entre casos. Cree una por caso. |
| Las reglas de coherencia nunca deciden | El juez no está validado. Valídelo, o fije require_validated_evaluators: false a sabiendas. |
Limitaciones
- Sin simulador de usuario: cada turno de usuario procede del guion del conjunto de datos, así que la conversación no puede ramificarse según lo que dijo el asistente.
- Sin métrica por turno dentro de una conversación registrada; use en su lugar turnos como casos agrupados, con el compromiso descrito arriba.
- Sin reproducción de conversaciones: una conversación registrada no puede volver a ejecutarse contra otro sistema. Comparar dos sistemas significa que cada uno reproduce el mismo guion.
- ConversationCompleted y ConversationJudge son solo del SDK de Python.
- Solo texto: a un juez se le muestra texto JSON, nunca imágenes ni audio.
- El juez sin conexión es una regla guionizada. Sus veredictos muestran la mecánica, no la exactitud de un juez real.
Siguientes pasos
- Jueces trata los proveedores, la validación y la recalibración.
- Casos agrupados trata group_id y la aceptación explícita de métodos aproximados.
- La API de Python trata evaluate y evaluate_comparison.
- Agentes trata la misma frontera para el bucle de herramientas de un agente.