跳到主要内容
非官方简体中文翻译,内容版权归 Mastra 所有,遵循 Apache-2.0 许可。

Multi-turn evals

多轮 evals 测试 agent 在对话中的行为。你提供的不是单个 input,而是 inputs 数组。每个条目会按顺序在同一线程上发送给 agent,scorer 会看到所有轮次累积的输出。

何时使用多轮 evals
何时使用多轮 evals的直接链接

  • agent 使用内存,并且必须回忆之前轮次的上下文
  • agent 处理依赖先前回复的后续问题
  • 你需要验证整段对话中的工具调用序列
  • agent 运行多步骤工作流(搜索、确认、执行)

快速开始
快速开始的直接链接

src/evals/conversation-eval.ts
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 只评判一轮

预构建的 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 会:

  1. 为对话创建一个新线程(唯一 threadId)和 resource(调用方提供的 targetOptions.memory.resource 会保留)
  2. 在该线程上通过 agent.generate() 按顺序发送每个输入
  3. 累积所有轮次的输出消息
  4. 将完整的累积输出传递给 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:

src/evals/trip-planning-eval.ts
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 消息的文本:

src/evals/conversation-judge.ts
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,它们只评估该轮的输入和输出:

src/evals/per-turn-eval.ts
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,使门控反映完整对话:

src/evals/memory-eval.ts
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 调用可以同时包含单轮和多轮数据项:

src/evals/mixed-eval.ts
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],
})

相关内容
相关内容的直接链接