Aller au contenu principal

Exécuter des Experiments

Ajouté dans : @mastra/core@1.4.0

Un Experiment fait passer chaque élément d’un Dataset par une cible (un Agent, un Workflow ou un Scorer), puis évalue éventuellement les sorties. Utilisez un Scorer comme cible lorsque vous souhaitez évaluer le juge LLM lui-même. Par défaut, les résultats sont conservés dans le stockage afin que vous puissiez comparer les exécutions entre différents prompts, modèles ou changements de code.

Experiment de base
Lien direct vers Experiment de base

Appelez startExperiment() avec une cible et des Scorers :

src/mastra/experiments/basic.ts
import { mastra } from '../index'

const dataset = await mastra.datasets.get({ id: 'translation-dataset-id' })

const summary = await dataset.startExperiment({
name: 'gpt-5.1-baseline',
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy', 'fluency'],
})

console.log(summary.status) // 'completed' | 'failed'
console.log(summary.succeededCount) // number of items that ran successfully
console.log(summary.failedCount) // number of items that failed

startExperiment() bloque l’exécution jusqu’à ce que tous les éléments soient terminés. Pour une exécution sans attendre le résultat, consultez les Experiments asynchrones.

Studio
Lien direct vers Studio

Vous pouvez également exécuter des Experiments dans Studio. Après avoir ajouté un élément au Dataset, ouvrez-le, sélectionnez Run Experiment, puis configurez la cible, les Scorers et les options.

Après l’exécution d’un Experiment, l’onglet Experiments affiche toutes les exécutions de ce Dataset, avec leur état, leurs décomptes et leurs horodatages. Sélectionnez un Experiment pour consulter les résultats, scores et Traces d’exécution de chaque élément.

Dans l’onglet Experiments, sélectionnez Compare, puis choisissez au moins deux Experiments afin de comparer côte à côte leurs scores et leurs résultats.

Cibles d’Experiment
Lien direct vers Cibles d’Experiment

Vous pouvez diriger un Experiment vers un Agent, un Workflow ou un Scorer enregistré.

Agent enregistré
Lien direct vers Agent enregistré

Ciblez un Agent enregistré dans votre instance Mastra :

src/mastra/experiments/agent-target.ts
const summary = await dataset.startExperiment({
name: 'agent-v2-eval',
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy'],
})

La valeur input de chaque élément est transmise directement à agent.generate() ; elle doit donc être de type string, string[] ou CoreMessage[].

Agents dotés de mémoire
Lien direct vers Agents dotés de mémoire

Lorsque l’Agent cible possède sa propre mémoire et que le contexte de requête contient un identifiant de ressource (MASTRA_RESOURCE_ID_KEY, défini par le middleware d’authentification, le requestContext de l’Experiment ou de l’élément, ou le formulaire Run Experiment de Studio), le moteur d’Experiment injecte un nouveau thread de mémoire pour chaque élément. Un identifiant de ressource dans le contexte de requête signifie « exécuter en tant que cette ressource » : la conversation de chaque élément est conservée sous forme de thread associé à cette ressource, et les éléments relancés reçoivent un nouveau thread à chaque tentative afin que les tentatives précédentes ayant échoué ne puissent pas contaminer le contexte de la nouvelle tentative.

Les threads injectés sont étiquetés afin que vous puissiez les rattacher à l’exécution : leurs métadonnées contiennent l’experimentId ainsi que l’identifiant de l’élément du Dataset sous la clé experimentItemId. Aucun titre de thread n’est généré pour eux.

Comme les threads appartiennent à la ressource de l’appelant, les fonctionnalités de mémoire limitées à cette ressource lisent et modifient toutes deux son état pendant l’exécution :

  • Les mises à jour de la mémoire de travail limitée à la ressource sont conservées dans celle-ci, et les éléments traités ultérieurement voient les mises à jour effectuées par les éléments précédents.
  • Le rappel sémantique limité à la ressource peut exposer à l’Experiment les conversations antérieures de cette ressource, tandis que les transcriptions de l’Experiment deviennent accessibles au rappel dans les conversations ultérieures de la même ressource.

Cette approche est utile lorsque vous souhaitez évaluer un Agent à partir du contexte accumulé d’un utilisateur réel. Si vous ne voulez pas que les exécutions d’Experiment modifient l’état d’un utilisateur réel, exécutez plutôt l’Experiment avec un identifiant de ressource dédié à l’évaluation.

L’injection de threads est ignorée dans les cas suivants :

  • Si le contexte de requête définit également MASTRA_THREAD_ID_KEY, le moteur utilise ce thread tel quel ; tous les éléments, ainsi que toutes leurs nouvelles tentatives, partagent donc la même conversation.
  • Si l’Agent n’a pas de mémoire ou si le contexte de requête ne contient aucun identifiant de ressource, l’exécution s’effectue sans mémoire et rien n’est conservé.

Workflow enregistré
Lien direct vers Workflow enregistré

Ciblez un Workflow enregistré dans votre instance Mastra :

src/mastra/experiments/workflow-target.ts
const summary = await dataset.startExperiment({
name: 'workflow-eval',
targetType: 'workflow',
targetId: 'translation-workflow',
scorers: ['accuracy'],
})

Le Workflow reçoit la valeur input de chaque élément comme données de déclenchement.

Scorer enregistré
Lien direct vers Scorer enregistré

Ciblez un Scorer pour évaluer un juge LLM par rapport à la vérité terrain :

src/mastra/experiments/scorer-target.ts
const summary = await dataset.startExperiment({
name: 'judge-accuracy-eval',
targetType: 'scorer',
targetId: 'accuracy',
})

Le Scorer reçoit les valeurs input et groundTruth de chaque élément. Les juges fondés sur un LLM peuvent dériver au fil du temps lorsque les modèles sous-jacents évoluent ; il est donc important de les réaligner régulièrement sur des libellés de référence fiables. Un Dataset fournit un benchmark stable permettant de détecter cette dérive.

Évaluer les résultats
Lien direct vers Évaluer les résultats

Les Scorers s’exécutent automatiquement après l’exécution de la cible pour chaque élément. Transmettez des instances de Scorer ou des identifiants de Scorers enregistrés :

// Reference scorers registered on the Mastra instance
const summary = await dataset.startExperiment({
name: 'with-registered-scorers',
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy', 'fluency'],
})

Les résultats de chaque élément comprennent les scores de chaque Scorer :

for (const item of summary.results) {
console.log(item.itemId, item.output)
for (const score of item.scores) {
console.log(` ${score.scorerName}: ${score.score}${score.reason}`)
}
}

Consultez la présentation des Scorers pour en savoir plus sur les Scorers disponibles et personnalisés.

Contrôler la persistance pour chaque exécution
Lien direct vers Contrôler la persistance pour chaque exécution

Utilisez persistence pour ignorer les écritures dans le stockage lors d’une exécution donnée. Les enregistrements d’Experiment et ceux des scores peuvent être désactivés indépendamment :

src/mastra/experiments/no-persistence.ts
const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy'],
persistence: {
experiments: 'none',
scores: 'none',
},
})

La cible et les Scorers continuent de s’exécuter, et startExperiment() renvoie toujours les résultats et les scores des éléments dans summary. Ces paramètres sont indépendants. Par exemple, définissez uniquement scores: 'none' pour conserver l’Experiment et les résultats de ses éléments sans créer d’enregistrements de scores.

Les paramètres omis prennent par défaut la valeur 'default', qui conserve le comportement de stockage standard. Cette politique contrôle uniquement les enregistrements d’Experiment et de scores créés par l’exécution. Elle ne désactive pas le stockage utilisé par la cible, comme la mémoire de l’Agent, les vecteurs, l’observabilité ou le stockage personnalisé des Tools.

Lorsque startExperimentAsync() s’exécute avec experiments: 'none', aucun enregistrement d’Experiment, aucune mise à jour de progression ni aucun résultat d’élément ne sont conservés. La persistance des scores reste contrôlée séparément par persistence.scores. Sans observateur d’événements d’Experiment, l’exécution se déroule sans attendre le résultat et l’API des Experiments ne peut pas indiquer si elle a réussi ou échoué.

Utilisez la méthode synchrone startExperiment() lorsque l’appelant a besoin du récapitulatif renvoyé. Un observateur d’événements d’Experiment peut recevoir les événements du cycle de vie et le récapitulatif final.

Observer les événements d’un Experiment
Lien direct vers Observer les événements d’un Experiment

Utilisez onEvent pour recevoir des événements de cycle de vie versionnés et sérialisables en JSON pendant l’exécution d’un Experiment. Cette option fonctionne avec startExperiment(), startExperimentAsync() et runExperiment().

src/mastra/experiments/observe.ts
import type { ExperimentEvent } from '@mastra/core/datasets'

const events: ExperimentEvent[] = []

await dataset.startExperimentAsync({
task: async ({ input }) => processItem(input),
persistence: { experiments: 'none' },
onEvent: async event => {
events.push(event)
await publishEvent(event)
},
})

L’observateur reçoit les types d’événements suivants :

  • experiment.run.started : identifie l’exécution, la cible, la version résolue du Dataset et le nombre d’éléments.
  • experiment.item.completed : signale le résultat enregistré d’un élément après son évaluation, notamment les scores, les erreurs, le nombre de nouvelles tentatives, les détails des mocks de Tools et l’identité stable de l’élément.
  • experiment.run.finished : signale le résultat final et les compteurs du récapitulatif.

Mastra attend la fin de chaque appel de l’observateur avant de transmettre l’événement suivant. Cette transmission sérialisée applique une contre-pression et garantit que les valeurs sequence des événements correspondent à leur ordre de transmission, tandis que l’exécution des éléments peut rester concurrente.

Si l’observateur lève une exception ou rejette la promesse, Mastra interrompt le reste de l’exécution et rejette runExperiment() avec une MastraError dont l’id vaut EXPERIMENT_EVENT_OBSERVER_FAILED. Aucun événement final n’est envoyé par l’intermédiaire de l’observateur défaillant. Avec startExperimentAsync(), la méthode a déjà renvoyé sa valeur lorsqu’un observateur détaché échoue ; gérez donc les échecs de transmission au sein de l’observateur lorsque l’appelant a besoin d’un signalement direct des erreurs.

Mastra attend l’événement experiment.run.finished avant de conserver l’état final de l’Experiment. Considérez cet événement comme le signal final faisant autorité lorsque la persistance de l’Experiment est désactivée, mais ne l’utilisez pas comme signal de lecture après écriture pour le stockage.

Les types d’événements exportés sont ExperimentEvent, ExperimentRunStartedEvent, ExperimentItemCompletedEvent et ExperimentRunFinishedEvent. Utilisez le champ discriminant type pour préciser le type d’un événement avant de lire ses propriétés spécifiques.

Mocks de Tools
Lien direct vers Mocks de Tools

Lorsqu’un Experiment exécute un Agent qui appelle des Tools produisant des effets de bord, associez des mocks statiques de Tools aux différents éléments du Dataset afin de rendre l’exécution déterministe. Pendant l’Experiment, un Tool mocké renvoie la sortie déclarée au lieu de s’exécuter. Par défaut, les Tools qui ne disposent d’aucun mock sur l’élément s’exécutent réellement.

Les mocks sont stockés sur l’élément du Dataset ; ils sont donc versionnés avec la ligne et accompagnent le cas de test. Chaque mock déclare le nom d’un Tool, les arguments attendus et la sortie à renvoyer :

src/mastra/experiments/tool-mocks.ts
await dataset.addItem({
input: 'What is the weather in Seattle?',
toolMocks: [
{
toolName: 'getWeather',
args: { city: 'Seattle' },
output: { temperature: 60, conditions: 'rainy' },
},
],
})

Les mocks de Tools sont pris en charge uniquement pour les cibles agent.

Bloquer les Tools non déclarés
Lien direct vers Bloquer les Tools non déclarés

Définissez unmockedToolPolicy: 'deny' sur un Experiment pour bloquer tout appel de Tool dépourvu de mock. Cette option est utile lorsqu’un appel réel risque de produire des effets de bord :

const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'weather-agent',
unmockedToolPolicy: 'deny',
})

La politique par défaut est 'allow'. Vous pouvez remplacer la politique de l’Experiment sur un élément individuel stocké ou défini en ligne :

await dataset.addItem({
input: 'What is the weather in Seattle?',
unmockedToolPolicy: 'allow',
})

La valeur de l’élément prévaut sur celle de l’Experiment. Un appel refusé échoue avec TOOL_MOCK_NOT_DECLARED avant l’exécution du Tool. Cet échec ne fait l’objet d’aucune nouvelle tentative et n’est pas ajouté à liveCalls.

Correspondance et consommation
Lien direct vers Correspondance et consommation

La correspondance des arguments est stricte : l’ordre des clés d’objet est ignoré, l’ordre des éléments de tableau est significatif et aucune coercition de type n’est effectuée. Un mock n’est fourni que lorsque l’Agent appelle le Tool avec des arguments profondément égaux aux args du mock.

Lorsqu’un élément déclare plusieurs mocks pour le même Tool et les mêmes arguments, ils sont consommés dans l’ordre : le premier appel reçoit le premier mock, l’appel suivant reçoit le deuxième, et ainsi de suite. L’ordre est suivi séparément pour chaque groupe (toolName, args) et reste indépendant entre des arguments différents.

Mode de correspondance
Lien direct vers Mode de correspondance

Par défaut, chaque mock établit une correspondance stricte avec ses args. Définissez matchArgs: 'ignore' pour établir la correspondance uniquement sur le nom du Tool : les args du mock ne sont alors pas comparés et le prochain mock non consommé de ce Tool est fourni, quels que soient les arguments avec lesquels l’Agent l’a appelé :

const subAgentMock = {
toolName: 'agent-balanceAgent',
args: { prompt: 'look up the balance for YJ' },
output: { text: "YJ's balance is $100." },
matchArgs: 'ignore',
}

Cette option est utile lorsque les arguments d’un Tool sont instables ou générés par le modèle. Le cas le plus courant consiste à mocker la réponse d’un sous-Agent : un sous-Agent délégué est exposé au parent comme un Tool agent-<name>, et ses arguments comprennent un prompt rédigé par un LLM ainsi que des champs injectés à l’exécution. Mocker agent-<name> renvoie la réponse prédéfinie au lieu d’exécuter le sous-Agent et ses Tools internes. Lorsque vous créez un mock à partir d’une Trace, les appels de délégation aux sous-Agents sont automatiquement dérivés avec matchArgs: 'ignore'. Vous pouvez remplacer cette valeur par 'strict' pour imposer les arguments exacts.

Échecs
Lien direct vers Échecs

Un appel de Tool fait échouer l’élément lorsqu’il enfreint la configuration des mocks :

  • TOOL_MOCK_MISMATCH : le Tool a été appelé avec des arguments ne correspondant à aucun mock.
  • TOOL_MOCK_EXHAUSTED : tous les mocks correspondants ont déjà été consommés.
  • TOOL_MOCK_NOT_DECLARED : le Tool ne possède aucun mock et la valeur effective de unmockedToolPolicy est 'deny'.

En cas de l’un de ces échecs, l’exécution de l’Agent est immédiatement interrompue. Le modèle ne peut donc plus appeler d’autres Tools, notamment des Tools non mockés produisant des effets de bord qui, autrement, s’exécuteraient réellement. Ces échecs étant déterministes, ils ne font l’objet d’aucune nouvelle tentative. Les mocks déclarés mais jamais utilisés ne font pas échouer l’élément ; ils sont signalés comme non consommés.

Lorsque l’interception des mocks est active, les Tools de l’Agent s’exécutent séquentiellement afin que les mocks (toolName, args) répétés soient consommés dans l’ordre des appels du Provider. L’interception est active lorsque l’élément déclare des mocks ou que sa valeur effective de unmockedToolPolicy est 'deny'.

Diagnostic
Lien direct vers Diagnostic

Le résultat de chaque élément contient un toolMockReport décrivant la manière dont l’exécution a traité les mocks de cet élément :

for (const item of summary.results) {
const report = item.toolMockReport
if (!report) continue

console.log(report.served) // mocks matched and returned
console.log(report.unconsumed) // mocks declared but never used
console.log(report.liveCalls) // undeclared tools allowed to run live
console.log(report.failure) // the first deterministic mock failure, if any
}

Dans Studio, modifiez un élément du Dataset pour définir les mocks de Tools sous forme de tableau JSON, puis ouvrez le résultat d’un Experiment pour consulter le même rapport.

Limites
Lien direct vers Limites

  • Aucun span de Tool pour les appels mockés. Un appel mocké renvoie sa sortie avant l’exécution du Tool ; il ne crée donc aucun span de Tool. Par conséquent, les Scorers de trajectoire fondés sur des Traces stockées peuvent ne pas voir les appels de Tools mockés. L’extraction de trajectoire qui se rabat sur la sortie du message de l’Agent les voit toujours ; l’évaluation de la trajectoire peut donc varier selon votre configuration d’observabilité.
  • Prise en charge par le stockage. Les mocks de Tools et leurs rapports sont conservés par les adaptateurs LibSQL, PostgreSQL, MongoDB et Spanner. L’adaptateur MySQL ne les prend pas en charge et rejette les écritures contenant des mocks de Tools ou un rapport de mocks de Tools. Tous les adaptateurs de stockage de Datasets conservent unmockedToolPolicy.

Experiments asynchrones
Lien direct vers Experiments asynchrones

startExperiment() bloque l’exécution jusqu’à ce que chaque élément soit terminé. Pour les Datasets dont l’exécution est longue, utilisez startExperimentAsync() afin de lancer l’Experiment en arrière-plan :

src/mastra/experiments/async.ts
const { experimentId, status } = await dataset.startExperimentAsync({
name: 'large-dataset-run',
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy'],
})

console.log(experimentId) // UUID
console.log(status) // 'pending'

Interrogez régulièrement getExperiment() jusqu’à la fin de l’exécution :

let experiment = await dataset.getExperiment({ experimentId })

while (experiment.status === 'pending' || experiment.status === 'running') {
await new Promise(resolve => setTimeout(resolve, 5000))
experiment = await dataset.getExperiment({ experimentId })
}

console.log(experiment.status) // 'completed' | 'failed'

Options de configuration
Lien direct vers Options de configuration

Concurrence
Lien direct vers Concurrence

Contrôlez le nombre d’éléments exécutés en parallèle (valeur par défaut : 5) :

const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'translation-agent',
maxConcurrency: 10,
})

Délais d’expiration et nouvelles tentatives
Lien direct vers Délais d’expiration et nouvelles tentatives

Définissez un délai d’expiration par élément, en millisecondes, ainsi qu’un nombre de nouvelles tentatives :

const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'translation-agent',
itemTimeout: 30_000, // 30 seconds per item
maxRetries: 2, // retry failed items up to 2 times
})

Les nouvelles tentatives utilisent un délai exponentiel. Les erreurs d’interruption ne font jamais l’objet d’une nouvelle tentative.

Interrompre un Experiment
Lien direct vers Interrompre un Experiment

Transmettez un AbortSignal pour annuler un Experiment en cours d’exécution :

const controller = new AbortController()

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000)

const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'translation-agent',
signal: controller.signal,
})

Les éléments restants sont marqués comme ignorés dans le récapitulatif.

Épingler une version du Dataset
Lien direct vers Épingler une version du Dataset

Exécutez l’Experiment sur un instantané précis du Dataset :

const summary = await dataset.startExperiment({
targetType: 'agent',
targetId: 'translation-agent',
version: 3, // use items from dataset version 3
})

Consulter les résultats
Lien direct vers Consulter les résultats

Répertorier les Experiments
Lien direct vers Répertorier les Experiments

const { experiments, pagination } = await dataset.listExperiments({
page: 0,
perPage: 10,
})

for (const exp of experiments) {
console.log(`${exp.name}${exp.status} (${exp.succeededCount}/${exp.totalItems})`)
}

Détails d’un Experiment
Lien direct vers Détails d’un Experiment

const experiment = await dataset.getExperiment({
experimentId: 'exp-abc-123',
})

console.log(experiment.status)
console.log(experiment.startedAt)
console.log(experiment.completedAt)
📹 À regarder

Regardez le Workflow des Datasets et Experiments Mastra pour découvrir comment les Datasets et les Experiments contribuent à améliorer la fiabilité.

Résultats par élément
Lien direct vers Résultats par élément

const { results, pagination } = await dataset.listExperimentResults({
experimentId: 'exp-abc-123',
page: 0,
perPage: 50,
})

for (const result of results) {
console.log(result.itemId, result.output, result.error)
}

Comprendre le récapitulatif
Lien direct vers Comprendre le récapitulatif

startExperiment() renvoie un ExperimentSummary contenant des décomptes et les résultats de chaque élément :

  • completedWithErrors vaut true lorsque l’Experiment s’est terminé, mais que certains éléments ont échoué.
  • Les éléments annulés via signal sont comptabilisés dans skippedCount.

Consultez la référence de startExperiment pour obtenir la documentation complète sur les paramètres et le type de retour.