Saltar al contenido

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 haceSu aplicación hace
guardar las conversaciones guionizadas como conjunto de datos, idénticas para todo sistemaconducir 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íticaregistrar 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
ArchivoQué es
assistant.pyla aplicación evaluada: un asistente de planes con estado de sesión
systems.pyel adaptador: reproduce un guion y registra conversation/v1
judge_offline.pyel sustituto guionizado del modelo juez
evaluate.pyejecuta la evaluación, la comparación y la alternativa por turnos
release.yamlla política para una ejecución
comparison.yamlla política para el candidato frente a la línea base
turns_release.yamlla política para la alternativa por turnos
data/conversations.jsonl40 conversaciones guionizadas
data/turns.jsonllas 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, ...}]}
CampoSignificado
turns[].indexa qué turno guionizado responde; consecutivo desde 1
turns[].answerlo que devolvió el asistente, cualquier JSON
turns[].askedopcional, solo para leer; el motor empareja por index
turns[].retrievalopcional, lo que recuperó ese turno, con la forma retrieval/v1
declared_turnscuántos turnos declaraba el guion
truncatedel 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.py
baseline 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_00
output: {
  "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: False

El 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ónUn caso por turno, agrupado
Unidad de la tasaconversaciones que fueron bienturnos respondidos correctamente
Tamaño muestral efectivoel número de conversacionessigue siendo el número de conversaciones, no de turnos
Qué turno fallóleer la transcripcióncada turno tiene su propio veredicto
Historial que ve cada turnolas respuestas anteriores del propio asistentelos turnos de usuario anteriores del guion, reproducidos
Detecta la deriva causada por sus propias respuestas anterioressíno, cada turno parte de un historial guionizado
EvaluadoresConversationCompleted, 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 --live

El 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íntomaCausa y solución
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0El 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 falseLa 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 1Numere 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 themAñada records=(CONVERSATION,) al decorador @system.
Input tag 'conversation_completed' found using 'type' does not match any of the expected tags desde oloproof runLos evaluadores de conversación son solo del SDK. Use un script como aquí.
Las respuestas se filtran entre conversacionesUna sesión se comparte entre casos. Cree una por caso.
Las reglas de coherencia nunca decidenEl 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.