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 baseLien direct vers Experiment de base
Appelez startExperiment() avec une cible et des Scorers :
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.
StudioLien 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’ExperimentLien 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 :
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émoireLien 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 :
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 :
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ésultatsLien 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 :
- Identifiants de Scorers
- Instances de Scorer
// Reference scorers registered on the Mastra instance
const summary = await dataset.startExperiment({
name: 'with-registered-scorers',
targetType: 'agent',
targetId: 'translation-agent',
scorers: ['accuracy', 'fluency'],
})
import { createAnswerRelevancyScorer } from '@mastra/evals/scorers/prebuilt'
const relevancy = createAnswerRelevancyScorer({ model: 'openai/gpt-5-mini' })
const summary = await dataset.startExperiment({
name: 'with-scorer-instances',
targetType: 'agent',
targetId: 'translation-agent',
scorers: [relevancy],
})
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écutionLien 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 :
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 ExperimentLien 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().
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 ToolsLien 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 :
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ésLien 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 consommationLien 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 correspondanceLien 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.
ÉchecsLien 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 deunmockedToolPolicyest'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'.
DiagnosticLien 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.
LimitesLien 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 asynchronesLien 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 :
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 configurationLien direct vers Options de configuration
ConcurrenceLien 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 tentativesLien 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 ExperimentLien 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 DatasetLien 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ésultatsLien direct vers Consulter les résultats
Répertorier les ExperimentsLien 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 ExperimentLien 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)
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émentLien 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écapitulatifLien direct vers Comprendre le récapitulatif
startExperiment() renvoie un ExperimentSummary contenant des décomptes et les résultats de chaque élément :
completedWithErrorsvauttruelorsque l’Experiment s’est terminé, mais que certains éléments ont échoué.- Les éléments annulés via
signalsont comptabilisés dansskippedCount.
Consultez la référence de startExperiment pour obtenir la documentation complète sur les paramètres et le type de retour.