Pular para o conteúdo

Guias

Tutorial: um classificador ou uma saída estruturada

Avalie uma função Python que rotula perguntas de suporte, leia por que o lançamento está bloqueado, corrija os erros e compare a correção com o original, tudo na sua própria máquina, sem conta, sem rede e sem modelo.

O que você vai construir

Um bot de suporte que retorna um objeto JSON com um answer e um label (refund, account ou other). Você vai exigir dele três requisitos: o rótulo está certo com frequência suficiente, a saída sempre tem o formato certo, e nenhuma resposta vaza algo parecido com um número de seguro social dos EUA. Dois deles são verificações de formato que não precisam de resposta de referência; um mede o sucesso da tarefa contra um rótulo de referência. A diferença importa, e esta página os mantém separados.

Os termos usados abaixo (caso, execução, métrica, intervalo, regra, gate) são definidos em Conceitos.

Pré-requisitos

  • Python 3.11 ou posterior.
  • O Oloproof, instalado em um ambiente virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • O projeto de exemplo e a mudança candidata, que vêm com o pacote. Copie os dois para novos diretórios e trabalhe no primeiro; todos os arquivos também estão listados abaixo, então você pode digitá-los:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

Nenhuma chave de API, conta de provedor ou acesso à rede é usado nesta página.

Os arquivos

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

Execute todos os comandos a partir do diretório support-classifier/. O Oloproof mantém ali o seu store, em .oloproof/; apague esse diretório para recomeçar do zero.

A aplicação e o seu adaptador

A sua aplicação é acessada por meio de um adaptador. Para uma aplicação Python, o adaptador é a própria função: o Oloproof a importa, a chama uma vez por caso com o input do caso e registra o dicionário que ela retorna como a saída daquele caso.

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

Para avaliar o seu próprio classificador, mantenha o código dele onde está e escreva uma função fina como esta que o chama e retorna um dicionário. A função pode ser async. O Oloproof a chama; ele não hospeda, não isola e não reinicia a sua aplicação, então qualquer estado que a sua aplicação mantenha entre chamadas cabe a você gerenciar.

oloproof.yaml nomeia essa função e os avaliadores:

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

As saídas são colocadas em cache com base no código-fonte da função, na version declarada e em config. Se a função lê outros arquivos (um prompt, uma tabela de regras), liste-os em system.code_paths, para que editá-los execute o sistema de novo.

O dataset

Um caso por linha. input é exatamente o que a sua função recebe como case; expected é a referência com que o avaliador exact_match compara:

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

A função retorna, para cada caso, um objeto como {"answer": "Use password reset, ...", "label": "account"}.

Escolhendo os avaliadores

CritérioAvaliadorPrecisa de expectedO que mede
exact_labelexact_match em labelsimsucesso da tarefa: o rótulo é o certo
format_validjson_schema sobre a saída inteiranãoformato: o objeto tem exatamente os dois campos de string
pii_freeregex em answer, pass_if: no_matchnãouma propriedade de segurança do texto

Uma verificação de formato aprova uma resposta errada bem formada, então nunca pode substituir o sucesso da tarefa. Uma verificação de tarefa precisa de uma referência para cada caso; onde um caso não tem nenhuma, exact_match não consegue avaliá-lo. Avaliadores determinísticos não precisam de validação contra pessoas: executar um deles duas vezes dá o mesmo veredicto.

A política

release.yaml é aquilo contra o que a execução é decidida:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

exact-label-floor diz que o rótulo precisa estar certo em pelo menos 70% das vezes, e só passa quando o intervalo de 95% inteiro está em 0.70 ou acima. As duas regras observed_count não permitem nenhuma falha nos casos que você executou; elas descrevem esses casos, não toda pergunta que os usuários vão fazer.

Execute

oloproof run

Saída real, resumida:

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

Como ler:

  • 13 de 18 rótulos estão certos, 72,2%. Isso está acima de 0.70, mas o intervalo desce até 46,5%: 18 casos não conseguem mostrar que a taxa verdadeira é de pelo menos 0.70. Então a regra é INSUFFICIENT_EVIDENCE, nem PASS nem FAIL.
  • Toda saída tem o formato certo e nenhuma contém um número parecido com um SSN, então as duas regras de formato passam.
  • block_on lista INSUFFICIENT_EVIDENCE, então o gate bloqueia e o comando sai com 3. A saída 0 significaria nada em que a política bloqueia; Gate no CI lista todos os códigos.

Execute de novo e a linha do cache mostra execution 18 hit/0 miss: nada mudou, então a função não é chamada.

Inspecione as falhas

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID é o id na primeira linha da saída da execução.

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

O padrão fica claro quando você lê as entradas: "money back", "reverse the payment", "sign in" e "two-factor" não estão nas listas de palavras-chave, e other_04 diz "I don't want a refund", que a palavra "refund" captura mesmo assim. Note que refund_04 passa nas duas verificações de formato apesar de estar errado: essa é a lacuna entre verificar o formato e medir o sucesso.

Duas próximas ações fazem sentido aqui. Corrigir os erros (abaixo), ou acrescentar casos: com mais casos na mesma acurácia o intervalo se estreita, e oloproof plan RUN_ID --run estima quantos.

Faça uma mudança real

Copie ../support-change/app.py por cima de app.py. Ele acrescenta as formulações que faltavam:

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

e defina version: keywords-v2 em system no oloproof.yaml, para que a execução seja registrada como a nova versão. Depois:

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

17 de 18 estão certos e o limite inferior do intervalo, 72,7%, supera 0.70, então a regra passa e o comando sai com 0. other_04 ainda falha: a correção não mexeu na negação.

Compare o candidato com a linha de base

A regra de execução pergunta se o candidato atinge o seu piso. Uma comparação pergunta como ele difere da linha de base, caso a caso. Copie ../support-change/compare.yaml para o projeto; ele contém uma regra de comparação:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

O candidato corrigiu quatro casos e não quebrou nenhum, um ganho estimado de 22 pontos. Mas só quatro casos mudaram, e 18 casos pareados deixam um intervalo de 12,9 pontos pior a 57 pontos melhor, que cruza a margem de 10 pontos. A comparação ainda não consegue descartar que o candidato seja pior em mais do que você aceita, então é INSUFFICIENT_EVIDENCE e sai com 3. A linha abaixo é a estimativa de dimensionamento. Sem --policy, compare imprime as diferenças, diz que o release.yaml do projeto não declara nenhuma regra de comparação e sai com 0 porque nada foi decidido.

Comparando um candidato com uma linha de base explica a margem e os outros tipos de regra.

Solução de problemas

SintomaCausa e correção
ModuleNotFoundError para appExecute a partir do diretório que contém app.py, ou dê a callable um caminho de módulo importável dali.
Uma regra nomeia uma métrica que nenhum avaliador produzA metric da regra precisa ser igual ao criterion de um avaliador; o erro lista as métricas que existem.
Você editou o classificador e a execução reaproveitou todas as saídasO cache segue o código-fonte do callable; um arquivo auxiliar que ele lê precisa estar listado em system.code_paths.
exact_label mostra casos como faltantesEssas execuções lançaram exceção ou deram timeout; oloproof inspect RUN_ID --failures mostra cada erro.
A execução sai com 3 com uma estimativa altaQuem decide é o intervalo, não a estimativa. Acrescente casos ou aceite um piso menor, decidido antes da execução.

Limitações

  • O SDK e o YAML informam taxas de aprovação por avaliador. Não há matriz de confusão nem precisão e revocação por classe para um classificador como este; um bloco predictive: faz isso para um modelo com pontuação (Modelos preditivos).
  • Regras observed_count descrevem os casos que você executou; não afirmam nada sobre entradas não vistas.
  • Uma comparação sobre 18 casos só resolve diferenças grandes. Cinquenta ou mais casos reais são um piso mais útil.
  • oloproof.yaml não pode nomear um @evaluator personalizado; isso exige o SDK (O SDK).