Aller au contenu
mcpd
aucun daemon
Source

Scores et retours

Un score est ce qu’une machine a pensé. Un retour est ce qu’une personne a pensé. mcpd en fait deux signaux et ne fond jamais l’un dans l’autre par une moyenne.

Deux signaux, pas un chiffre

Un score est une évaluation automatique : un modèle juge, une expression régulière, un test unitaire, tout ce qui peut produire un nombre au sujet d’une exécution. Un enregistrement de retour est le jugement d’une personne : un pouce vers le bas dans votre produit, la note d’un relecteur, une réponse corrigée.

Les deux se rattachent à un trace_id ou à un span_id, les deux arrivent comme des signaux à part entière, et chacun est stocké dans sa propre table. Ils ne sont jamais combinés. Un juge qui note 0.9 en pertinence et un client qui appuie sur le pouce vers le bas disent tous les deux quelque chose de vrai, et leur désaccord est ce qu’il y a de plus utile sur l’exécution. Une moyenne effacerait précisément cela et vous laisserait un nombre sur lequel personne ne peut agir.

  • Spans

    What ran, nested, with timings

  • Logs

    Lines the run emitted

  • Metrics

    Numbers, with exemplars back to a run

  • Scores

    Machine evaluation

  • Feedback

    What a person thought

  • Aspect notes

    A sentence a model wrote about the run

trace_id Store Search Export

Un score déclare son échelle

min et max font partie du score. Sans eux, une valeur ne s’interprète pas : 0.86 peut être un bon résultat sur une échelle de 0 à 1, un mauvais sur une échelle de 0 à 5, ou une distance où plus bas vaut mieux. Six mois plus tard, avec deux scorers dans le même projet, personne ne peut trancher. La spécification leur donne 0 et 1 par défaut, et chaque SDK les exige à l’appel pour qu’une valeur par défaut ne soit jamais acceptée par accident.

ChampÀ quoi il sert
scorer_id Quel scorer a produit ce score, par exemple answer_relevance. Obligatoire.
scorer_version Un score ne se compare qu’à un autre score de la même version. Changez le prompt d’un juge et vous avez un nouveau scorer, pas la suite de l’ancien.
value Le nombre.
min, max L’échelle déclarée sur laquelle le nombre se place.
label Un résultat catégoriel, quand le scorer en produit un à côté du nombre.
reason Pourquoi. Pour un modèle juge, c’est son explication, et c’est ce qui rend un score discutable plutôt que magique.
source Live, offline, experiment ou manual.
score_trace_id La trace de l’exécution de notation elle-même, pour qu’un score suspect s’ouvre et se lise comme n’importe quelle autre exécution.
cost Ce que la production du score a coûté.

En enregistrer un

La fonction utilitaire du SDK met un score en file d’attente comme elle y met un span : l’appel ne bloque pas et ne peut pas lever d’exception. Sans traceId, il se rattache à la trace du span actif.

score et retour

// A machine evaluation of the run. `min` and `max` are required: 0.86 on an
// undeclared scale cannot be compared to anything.
mcpd.score({
  traceId,
  scorerId: 'answer_relevance',
  value: 0.86,
  min: 0,
  max: 1,
  reason: 'Answered the question and gave the delivery date.',
})

// A human evaluation. Kept separate from a score, and never averaged with
// one, because the two answer different questions.
mcpd.feedback({
  traceId,
  kind: 'thumbs',
  value: 'up',
  comment: 'Exactly what the customer asked for.',
  authorKind: 'end_user',
})

// Anything you already count can travel with the trace.
mcpd.metric('support.tickets_handled', 1, 'counter', { labels: { queue: 'tier-1' } })

En HTTP, c’est POST /v1/api/scores, avec un Score pour corps. Un score doit nommer le trace_id ou le span_id qu’il concerne ; un score rattaché à rien est refusé par un 400 plutôt que stocké là où on ne le retrouvera jamais. Envoyez score_id comme chaîne vide et le serveur en génère un.

D’où vient un score

source est distinct de scorer_id parce que le même scorer tourne dans plusieurs contextes, et que ces contextes ne se comparent pas. Renoter le trafic de la semaine dernière avec le juge d’aujourd’hui ne prouve pas que la qualité s’est améliorée.

SourceSignification
liveProduit en ligne, pendant que l’exécution tracée avait lieu.
offlineProduit plus tard, en notant des traces déjà stockées.
experimentProduit par une expérience lancée sur un dataset.
manualSaisi par une personne, par l’API ou par l’interface.

Un score écrit par un SDK vaut live par défaut. Réglez-le vous-même pour les trois autres.

Noter coûte de l’argent, donc c’est enregistré

Une évaluation par juge est en général un autre appel de modèle. Noter chaque exécution de production avec un gros juge peut coûter plus cher que n’a coûté le trafic servi, et c’est une facture que l’on découvre tard. Un Score porte donc un cost de la même forme que celui d’un span, avec une devise, une répartition entre entrée et sortie, et la grille tarifaire qui a produit le chiffre. Renseignez-le quand vous écrivez le score par l’API HTTP ou par un lot d’ingestion natif.

score_trace_id est l’autre moitié de la même idée. Tracez l’exécution de notation elle-même et son identifiant se pose sur le score, de sorte qu’un juge qui s’est mis à tout dévaluer peut être ouvert, lu, et montré comme défectueux.

Les retours humains

Un retour s’écrit depuis votre propre produit. Appelez l’endpoint depuis le contrôle sur lequel la personne a réellement appuyé et le jugement se pose sur la trace exacte qui a produit la réponse à laquelle elle réagissait. kind décide de la forme de value.

Typevalue
thumbsup ou down.
ratingUn nombre, sur une échelle que le producteur déclare.
correctionLa bonne réponse, en texte.
tagUne étiquette choisie dans un ensemble.
commentDu texte libre, sans score attaché.

un pouce vers le bas depuis votre produit

// Sans traceId : il se rattache à la trace du span actif.
mcpd.feedback({
  kind: 'thumbs',
  value: 'down',
  comment: 'Named the wrong depot.',
  authorKind: 'end_user',
})

Le masquage s’applique aux noms de clés, et il passe sur les metadata que vous attachez à un jugement. author_id est stocké tel que vous l’envoyez : envoyez donc un identifiant pseudonyme plutôt qu’une adresse e-mail. Voir Masquage et conservation.

La requête qui trouve les mauvaises réponses

C’est à cela que servent les scores. has_score nomme un scorer et score_below place la barre ; la liste des traces renvoie les exécutions que ce scorer a mises en dessous. La comparaison est stricte, donc score_below=0.5 exclut une exécution notée exactement 0.5.

trouver les exécutions mal notées

// The same client reads. This is the query that finds bad answers: every run
// that a scorer put below the bar.
const suspect = await mcpd.listTraces({
  hasScore: 'answer_relevance',
  scoreBelow: 0.5,
  limit: 10,
})
console.log(`runs scoring under 0.5: ${suspect.traces.length}`)

const recent = await mcpd.listTraces({ limit: 5, sort: 'start_time', order: 'desc' })
for (const trace of recent.traces) {
  const seconds = ((trace.durationNs ?? 0) / 1e9).toFixed(2)
  console.log(
    `${trace.traceId.slice(0, 8)}  ${trace.name.padEnd(16)} ${trace.status.padEnd(6)} ` +
      `${seconds}s  ${trace.spanCount} spans  $${(trace.cost?.totalCost ?? 0).toFixed(6)}`,
  )
}

// One run, with every span in it, ordered so a waterfall renders directly.
const detail = await mcpd.getTrace(traceId, { include: ['scores', 'feedback'] })
for (const span of detail.spans) {
  console.log(`  ${span.kind.padEnd(18)} ${span.name}`)
}

score_below ne veut dire quelque chose qu’avec has_score à côté : une barre sans scorer nommé est une barre sur rien. Les deux sont des paramètres de GET /v1/api/traces, donc ils se combinent avec tous les autres filtres de cet endpoint. Les mauvaises réponses d’un modèle donné, en production, sur la dernière journée : une seule requête.

Les relire

GET /v1/api/scores liste les évaluations automatiques, filtrées par scorer_id, source, trace_id, experiment_id, min_value, max_value et une fenêtre temporelle, avec pagination par curseur. source accepte live, offline, experiment ou manual, ce qui vous permet de relire les scores du juge sans ceux qu’une personne a saisis. GET /v1/api/feedback prend un trace_id et renvoie les jugements portés sur cette exécution. Lister les retours d’un projet sans nommer d’exécution n’est pas implémenté dans cette version, et l’endpoint le dit : sans trace_id, il refuse par un 400 qui nomme le paramètre. Il répondait autrefois une liste vide, ce qui se lit comme si personne n’avait rien jugé.

La liste des scores n’accepte pas order : elle va du plus récent au plus ancien, et son curseur ne compare que dans un sens. Les listes d’exécutions et de spans en acceptent un, donc il est ici refusé nommément plutôt qu’accepté puis ignoré. Il en va de même pour les filtres kind, from, to et cursor que plusieurs clients SDK vous laissent poser sur une requête de retours. Aucun n’a jamais été appliqué, et les quatre sont désormais refusés.

Pour lire une exécution et tout ce qui y est attaché en une fois, demandez les suppléments par leur nom : GET /v1/api/traces/{trace_id}?include=scores,feedback, ou include=all pour avoir aussi les logs.

Ce que porte la liste des traces, et ce qu’elle ne porte pas

Chaque trace stockée conserve deux agrégats pour que la liste n’ait besoin d’aucune jointure. Le premier est la valeur moyenne par scorer : une moyenne à l’intérieur d’answer_relevance, et une moyenne distincte à l’intérieur de groundedness. Le second est un décompte des retours par type : trois pouces, une correction. Des décomptes, pas une moyenne, parce que la moyenne d’un pouce levé et d’une correction n’est pas une quantité.

Il n’y a pas de score de qualité global, et il n’y en aura pas. Un chiffre unique combinant un modèle juge, une expression régulière et le pouce baissé d’un client serait un nombre sans unité et sans prise pour le contester. mcpd vous montre les pièces et vous laisse décider.

Ensuite