Multi-turn evals
多轮 evals 测试 agent 在对话中的行为。你提供的不是单个 input,而是 inputs 数组。每个条目会按顺序在同一线程上发送给 agent,scorer 会看到所有轮次累积的输出。
何时使用多轮 evals何时使用多轮 evals的直接链接
- agent 使用内存,并且必须回忆之前轮次的上下文
- agent 处理依赖先前回复的后续问题
- 你需要验证整段对话中的工具调用序列
- agent 运行多步骤工作流(搜索、确认、执行)
快速开始快速开始的直接链接
import { runEvals } from '@mastra/core/evals'
import { checks } from '@mastra/evals/checks'
import { weatherAgent } from '../agents'
const result = await runEvals({
data: [
{
inputs: [
'What is the weather in Brooklyn?',
'What about tomorrow?',
'Compare the two forecasts.',
],
},
],
target: weatherAgent,
scorers: [checks.calledTool('get_weather', { times: 2 }), checks.includes('Brooklyn')],
})
每一轮都使用相同的线程 ID 运行 agent.generate()。agent 会看到完整对话历史。Scorer 会接收所有轮次累积的输出消息。
预构建的 LLM judge scorer(rubric、answer relevancy、faithfulness,以及Scorer 兼容性中列出的其他 scorer)读取单条 assistant 消息,因此使用 inputs 时它们只评判最后一轮的响应。要对整段对话进行语义评分,请使用 createMultiTurnJudgeScorer(),它会根据同一标准评判每个 assistant 轮次;或使用按轮的 turns[].scorers,让每一轮评分自己的输出。
跨轮回忆需要内存跨轮回忆需要内存的直接链接
多轮回忆依赖 agent 配置了内存存储。共享线程 ID 让每一轮都能看到之前的轮次,但只有 agent 配置了内存,线程才能持久化历史。如果 agent 没有配置内存,轮次仍会按顺序运行,其输出仍会累积用于评分,但 agent 不会回忆之前的轮次(每个输入都在隔离环境中运行)。当你在没有内存的 agent 上使用 inputs 时,runEvals 会记录警告。
runEvals 通过生成共享的 threadId 并注入 resourceId 来管理对话身份。Mastra 内存按 resource 和 thread 划分消息作用域,因此回忆需要这两个值。默认情况下,从生成的线程派生 resource 会隔离每段对话。传入 targetOptions.memory.resource 可固定 resource,例如复用现有用户的内存时;runEvals 仍拥有线程,因此你无需提供线程:
await runEvals({
target: weatherAgent,
data: [{ inputs: ['What is the weather in Brooklyn?', 'What about tomorrow?'] }],
scorers: [checks.similarity('weather forecast')],
targetOptions: { memory: { resource: 'user-42' } },
})
如何配置内存存储请参阅内存。
工作原理工作原理的直接链接
当数据项包含 inputs 数组时,runEvals 会:
- 为对话创建一个新线程(唯一
threadId)和 resource(调用方提供的targetOptions.memory.resource会保留) - 在该线程上通过
agent.generate()按顺序发送每个输入 - 累积所有轮次的输出消息
- 将完整的累积输出传递给 scorer 进行评估
Scorer 会看到完整的对话输出,包括每一轮。
评分语义评分语义的直接链接
在为多轮项编写 scorer 时,以下细节很重要:
run.output是每一轮累积的输出。 基于输出的 scorer:checks.includes、checks.calledTool、checks.similarity等:会评估整段对话。例如,checks.calledTool('get_weather', { times: 2 })会统计所有轮次中的调用次数。run.input只是第一轮的输入。 将输入与输出进行比较的 scorer(faithfulness、answer relevancy 以及其他相对输入的 LLM scorer)只能看到第一条用户消息,而不是完整对话。对于多轮场景,优先使用基于输出的检查,或构建直接读取累积run.output的 scorer。
Scorer 兼容性Scorer 兼容性的直接链接
只有当 scorer 读取全部累积输出时,累积输出才有帮助:
| Scorer | 使用 inputs 时看到的内容 |
|---|---|
Quick Checks:checks.calledTool、checks.includes、checks.similarity、checks.noToolErrors 等 | 整段对话。工具调用检查会统计每一轮的调用,文本检查会搜索所有 assistant 文本 |
| Multi-turn Judge scorer | 每个 assistant 轮次,根据一条自然语言标准一起评分 |
| 预构建 LLM judge scorer:rubric、answer relevancy、answer similarity、faithfulness、hallucination、bias、toxicity、context precision、context recall、context relevance、noise sensitivity、prompt alignment、summarization | 只有最后一条包含文本的 assistant 消息。相对输入的 judge 也只会看到第一轮的输入 |
轨迹 scorer(AgentScorerConfig.trajectory) | 有 trace 时使用最后一轮的 span,否则使用所有累积消息中的工具调用 |
| 自定义 scorer | 它们从 run.output 中读取的内容 |
评分整段对话评分整段对话的直接链接
要一起评判每一轮,请使用 createMultiTurnJudgeScorer()。它会构建每个 assistant 轮次的记录,并询问 judge 模型对话是否满足一条自然语言标准,评分为 1 或 0:
import { runEvals } from '@mastra/core/evals'
import { createMultiTurnJudgeScorer } from '@mastra/evals/scorers/prebuilt'
import { weatherAgent } from '../agents'
const result = await runEvals({
data: [
{
inputs: [
"How's the weather in London?",
'And Paris?',
'Should I pack an umbrella for London?',
],
},
],
target: weatherAgent,
scorers: [
{
scorer: createMultiTurnJudgeScorer({
model: 'anthropic/claude-haiku-4-5',
criterion:
'The agent provided forecasts for London and Paris, and gave weather-appropriate packing advice.',
}),
threshold: 1,
},
],
})
要评判标准无法表达的内容,请编写自己的 scorer,从 run.output 中读取 assistant 消息。extractAgentResponseMessages() 会按顺序返回每条 assistant 消息的文本:
import { createScorer } from '@mastra/core/evals'
import { extractAgentResponseMessages } from '@mastra/evals/scorers/utils'
import { z } from 'zod'
export const conversationJudge = createScorer({
id: 'conversation-judge',
name: 'Conversation Judge',
description: 'Grades every assistant turn in a conversation against one criterion',
type: 'agent',
judge: {
model: 'anthropic/claude-haiku-4-5',
instructions: 'You grade multi-turn assistant transcripts against a single criterion.',
},
})
.analyze({
description: 'Judge the transcript as a whole',
outputSchema: z.object({ satisfied: z.boolean(), reason: z.string() }),
createPrompt: ({ run }) => {
const transcript = extractAgentResponseMessages(run.output)
.map((text, index) => `Turn ${index + 1}: ${text}`)
.join('\n\n')
return `Grade this conversation:\n\n${transcript}\n\nCriterion: the assistant keeps the forecast consistent across turns.`
},
})
.generateScore(({ results }) => (results.analyzeStepResult?.satisfied ? 1 : 0))
.generateReason(({ results }) => results.analyzeStepResult?.reason ?? '')
像其他 scorer 一样将其传给 runEvals。完整的 createScorer() 流水线请参阅自定义 scorer。
使用 turns 进行逐轮断言per-turn-assertions-with-turns的直接链接
inputs 形式会将累积输出作为一个整体评分,即对每一轮输出给出一个总分。这可能会掩盖逐轮失败:如果 任意 一轮提到了 Brooklyn,像 checks.includes('Brooklyn') 这样的基于输出的检查就会通过,即使后续轮次已经出错。
当你需要断言特定轮次行为正确时,请改用 turns。每个轮次是一个对象,拥有自己的 input 以及可选的 gates/scorers,它们只评估该轮的输入和输出:
import { runEvals } from '@mastra/core/evals'
import { checks } from '@mastra/evals/checks'
import { weatherAgent } from '../agents'
const result = await runEvals({
data: [
{
turns: [
{
input: 'What is the weather in Brooklyn?',
gates: [checks.calledTool('get_weather')],
},
{
// The follow-up must call the tool again — it can't be satisfied
// by the first turn's tool call.
input: 'What about tomorrow?',
gates: [checks.calledTool('get_weather')],
scorers: [{ scorer: checks.similarity('tomorrow forecast'), threshold: 0.5 }],
},
],
},
],
target: weatherAgent,
})
result.verdict // 'passed' | 'scored' | 'failed'
result.turnResults // per-turn gate/threshold/scorer outcomes
语义:
- 逐轮 gate 或 scorer 只接收该轮的
run.input和run.output,而不是累积对话。其他轮次的输出无法满足检查,并且run.input包含被评估轮次的输入。 - 逐轮结果会汇总到 verdict:某个轮次 gate 失败会使 verdict 为
failed。在 gates 通过的情况下,某个轮次阈值未达到会使 verdict 为scored。 result.turnResults[i]报告每一轮的gateResults、thresholdResults和scores。失败会指向确切的轮次。在多段对话中,轮次结果按轮次索引取平均值。- 没有
gates或scorers的轮次会推进对话。 - 顶层的
scorers/gates仍会针对累积输出整体运行,因此你可以将“这一轮必须调用工具”与“答案提到 Brooklyn”组合使用。 - 当 agent 配置了存储时,每个逐轮 scorer/gate 结果会像顶层得分一样持久化,因此逐轮结果会出现在你的得分存储中。每个存储的逐轮得分使用其轮次索引作为标签(
metadata.turnIndex),并共享对话的threadId。它还会包含指向该轮 trace span 的链接。
当整段对话的单一总分足够时,请使用 inputs。当正确性取决于各个轮次时,请使用 turns。turns 不能与同一数据项中的 input 或 inputs 组合使用。
与 gates 和 thresholds 组合与 gates 和 thresholds 组合的直接链接
多轮数据项可与 gates 和 verdicts 配合使用。请使用基于输出的 scorer,使门控反映完整对话:
import { runEvals } from '@mastra/core/evals'
import { checks } from '@mastra/evals/checks'
const result = await runEvals({
data: [
{
inputs: ['My favorite city is Brooklyn.', 'What is the weather in my favorite city?'],
},
],
target: weatherAgent,
gates: [checks.calledTool('get_weather')],
scorers: [{ scorer: checks.similarity('Brooklyn weather forecast'), threshold: 0.5 }],
})
result.verdict // 'passed' | 'scored' | 'failed'
混合单轮和多轮混合单轮和多轮的直接链接
单次 runEvals 调用可以同时包含单轮和多轮数据项:
const result = await runEvals({
data: [
{ input: 'What is the weather in Brooklyn?' },
{
inputs: ['My favorite city is Brooklyn.', 'What is the weather in my favorite city?'],
},
],
target: weatherAgent,
scorers: [checks.includes('Brooklyn')],
})
单轮项照常使用 input。多轮项使用 inputs,可以完全省略 input。
校验校验的直接链接
如果 inputs 存在但为空,runEvals 会抛出 MastraError:
// Throws: 'inputs' must be a non-empty array
await runEvals({
data: [{ inputs: [] }],
target: myAgent,
scorers: [myScorer],
})
相关内容相关内容的直接链接
runEvals()reference:runEvals参数和返回值的完整 API- Gates and verdicts:强制执行硬性要求和质量阈值
- Quick Checks:零 LLM 可组合微型 scorer
- Scorer utilities:从 scorer 运行中读取消息的辅助函数
- Multi-turn Judge scorer:一起评判每个 assistant 轮次的 LLM judge