Mastra 核心原理与工程实践

# Mastra 核心原理与工程实践

本篇目标

先从一个可运行项目建立 Mastra 的整体认识,再理解 Agent、Tool、Runtime Context、Memory、RAG、Workflow、Multi-Agent、MCP 与评测怎样协作,最后把运行状态安全地接到前端并部署。代码用于验证概念,不把记住 API 当成掌握框架。

跑通最小项目理解一次运行完成 RAG 与编排连接并部署

# 先记住一句话

Mastra 是面向 TypeScript 应用的 AI 工程框架:Agent 负责根据上下文动态决策,Tool 提供有类型的行动能力,Workflow 编排确定性流程,Memory 与 Storage 保存跨步骤状态,宿主应用负责身份、权限、事务和最终验收。

真正需要掌握的不是 new Agent() 的参数,而是下面这条边界:

模型负责提出决策
        ↓
Mastra 负责组织模型、工具、记忆和流程
        ↓
宿主应用负责可信身份、权限、真实副作用和业务正确性
1
2
3
4
5

# Mastra 解决什么问题

直接调用模型 API 只能得到一次生成结果。一个可运行的 Agent 系统还需要解决:

  1. 如何把工具描述交给模型,并执行模型提出的调用;
  2. 如何保存会话、用户信息和长任务状态;
  3. 如何将固定流程与模型的动态决策组合;
  4. 如何暂停审批、失败恢复和避免重复副作用;
  5. 如何追踪轨迹、评测质量并控制成本;
  6. 如何把 MCP、Skill、文件系统或沙箱等能力接入同一个运行时。

Mastra 把这些通用能力组织成一组可组合的基础模块,但它不会自动替你定义业务完成标准,也不会因为使用了 Schema 就自动获得权限安全和事务一致性。

# 从零跑起一个最小 Mastra 项目

如果简历里写了 Mastra,至少要知道项目怎样创建、核心代码放在哪里,以及 Agent 怎样被框架发现。使用官方脚手架创建项目后,开发服务器会同时启动 Mastra Server 和本地 Studio:Server 负责真正运行 Agent、Tool 与 Workflow,Studio 用来聊天测试、查看执行过程和调试组件。

# 创建项目;openai 表示选择 OpenAI 作为初始模型提供方。
npm create mastra@latest interview-assistant --default --llm openai

# 进入脚手架生成的项目目录。
cd interview-assistant

# OPENAI_API_KEY 由模型提供方后台获取,应写入本地环境变量,不能提交到 Git。
export OPENAI_API_KEY='替换为自己的密钥'

# 启动本地 Mastra Server 与 Studio,默认可通过 http://localhost:4111 调试。
npm run dev
1
2
3
4
5
6
7
8
9
10
11

脚手架生成的核心目录可以这样理解:

interview-assistant/
├── src/
│   └── mastra/
│       ├── agents/
│       │   └── interview-agent.ts   # 定义 Agent 的模型、Instructions 和 Tools
│       ├── tools/
│       │   └── explain-term.ts      # 定义可被 Agent 调用的具体能力
│       ├── workflows/
│       │   └── interview-flow.ts    # 定义顺序、分支、并行和暂停恢复
│       └── index.ts                 # 注册组件,是 Mastra 项目的入口
├── package.json                     # 依赖与 dev、build、start 等脚本
└── .env                             # 本地密钥;必须加入 .gitignore
1
2
3
4
5
6
7
8
9
10
11
12

下面这个 Tool 使用内存对象代替数据库,因此复制到脚手架项目后即可理解和调试。真实项目只需要把 termNotes 换成笔记服务或数据库查询,输入输出契约保持不变。

// src/mastra/tools/explain-term.ts
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

// 这里故意使用内存数据,让最小示例不依赖数据库。
// 生产项目应改为调用只返回当前用户有权读取内容的笔记服务。
const termNotes: Record<string, string> = {
  Agent: '能够根据目标和上下文动态决定下一步,并调用工具完成任务。',
  RAG: '先检索外部证据,再让模型根据证据生成答案。'
}

export const explainTerm = createTool({
  // id 是日志、Trace 和模型工具调用中使用的稳定标识。
  id: 'explain-term',
  // description 帮助模型判断什么时候应该调用这个 Tool。
  description: '从技术笔记中查询 Agent 开发术语的简要解释',
  // inputSchema 校验模型生成的工具参数。
  inputSchema: z.object({
    term: z.string().min(1).describe('需要解释的技术术语')
  }),
  // outputSchema 约束返回给 Agent 的结果,避免调用方猜测字段。
  outputSchema: z.object({
    found: z.boolean(),
    explanation: z.string()
  }),
  // execute 的第一个参数已经通过 inputSchema 校验。
  execute: async ({ term }) => {
    const explanation = termNotes[term]

    return {
      found: Boolean(explanation),
      explanation: explanation ?? '当前笔记暂未收录该术语。'
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/mastra/agents/interview-agent.ts
import { Agent } from '@mastra/core/agent'
import { explainTerm } from '../tools/explain-term'

export const interviewAgent = new Agent({
  // id 用于通过 API 或 Mastra 注册表稳定定位这个 Agent。
  id: 'interview-agent',
  // name 是 Studio、日志和 Trace 中展示的可读名称。
  name: 'Interview Agent',
  // Instructions 说明职责、使用工具的时机和证据不足时的行为。
  instructions: `
    你是技术面试学习助手。
    遇到术语解释问题时,先调用 explainTerm 查询笔记。
    如果笔记没有收录,明确说明未知,不要编造答案。
  `,
  // provider/model 是 Mastra Model Router 的模型标识;运行时会读取 OPENAI_API_KEY。
  model: 'openai/gpt-5-mini',
  // 只有注册到 tools 中的能力才会暴露给当前 Agent。
  tools: { explainTerm }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { interviewAgent } from './agents/interview-agent'

// Mastra 入口负责注册组件;注册后 Studio 和 Server 才能发现这个 Agent。
export const mastra = new Mastra({
  agents: { interviewAgent }
})
1
2
3
4
5
6
7
8

Studio、模板和示例分别解决什么问题

Studio 是本地或团队使用的调试与观测界面,可以直接测试 Agent、Tool 和 Workflow,并查看 Trace;它不是给最终用户使用的业务前端。Templates / Examples 是可以参考或合并进项目的示例代码,适合快速了解推荐目录和 API,但接入真实项目后仍要重新检查权限、数据来源和完成标准。

# 核心组成与职责

组成 它负责什么 不应该负责什么
Mastra 实例 注册和组装 Agent、Workflow、Storage、Logger 等运行组件 具体业务规则
Agent 读取目标和上下文,选择模型或工具,循环到完成或停止 直接持有数据库超级权限
Model / Router 完成推理、结构化输出和工具选择,可按场景路由模型 判断业务操作是否被授权
Tool 用 Schema 描述一个边界清晰的外部能力 包办整个开放式任务
Runtime Context 携带本次请求的租户、用户、权限、模型策略等可信运行信息 充当长期记忆或数据库
Memory 保存 thread、resource、历史消息、工作记忆或可召回信息 保存所有业务事实
Workflow 编排 step、分支、并行、循环、暂停与恢复 让模型随意改写关键流程
Storage / Snapshot 持久化记忆、Workflow 状态和恢复点 替代领域数据库事务
Processor / Guardrail Processor 在输入输出生命周期中检查或转换内容;Guardrail 泛指阻止不合规行为的防线 仅靠 Prompt 保证安全
Workspace / Skill 提供文件、沙箱和可复用任务方法 自动成为有自主决策能力的 Agent
MCP 以标准协议接入或暴露工具、资源和提示模板 替代 Tool 的权限校验
Scorer / Observability 记录 Trace、评估质量、发现回归 只看最终答案就判断系统健康
Client SDK / AI SDK Adapter 从浏览器或服务端调用 Agent、Workflow、Memory 并消费流式事件 把服务端权限下放给浏览器

# Mastra 中一次 Agent 运行经历什么

Agent 开发总览 已经说明不绑定框架的通用运行链路。下面是在 Mastra 中的具体落地,其中 Runtime Context、Memory、Processor 和 Trace 是 Mastra 对通用步骤的实现方式;整体仍然遵循 “模型决策 → 工具执行 → 观察结果 → 继续决策” 的 Agent Loop。

1. 宿主应用接收请求并完成鉴权
2. 创建本次 Runtime Context
3. Agent 解析 instructions、model、tools 和 memory
4. 加载与当前 thread / resource 相关的记忆
5. 输入 Processor 做清洗、裁剪和安全检查
6. 模型决定直接回答,或提出一个 Tool Call
7. 宿主应用校验参数、权限、预算和审批条件后执行 Tool
8. Tool 的真实结果作为 observation 回到上下文
9. 模型基于新状态继续决策,直到完成、暂停或触发上限
10. 输出 Processor 检查结果,随后持久化状态并记录 Trace
1
2
3
4
5
6
7
8
9
10

其中第 7 步非常关键。模型只是在非可信数据中提出 “我想调用这个工具”,真正执行前必须由宿主应用重新校验。

更容易与通用链路对照的五阶段版本

下面没有减少实际工作,只是把 Mastra 的十个步骤归入五个通用阶段:

1. 接收请求与初始化运行环境
├── 宿主应用验证身份与权限
└── 创建本次 Runtime Context

2. 组装本轮上下文
├── 读取 Agent 的 instructions、model、tools 和 memory 配置
├── 加载与当前 thread / resource 相关的记忆
└── 输入 Processor 清洗、裁剪并检查输入

3. 模型决定下一步
├── 已经可以完成 → 生成最终回答
└── 还需要行动 → 提出 Tool Call

4. 验证并执行工具
├── 宿主应用校验参数、权限、预算和审批条件
└── 执行 Tool,把真实结果作为 observation 返回上下文

5. 更新状态,继续或结束
├── 未完成 → 模型根据新状态继续决策
└── 已完成或暂停 → 输出 Processor 检查结果,保存状态并记录 Trace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

五阶段版本适合理解和面试复述;十步骤版本适合查看 Mastra 的组件分别在哪个环节参与运行。

# Agent:动态决策的执行主体

一个 Agent 的核心配置通常包括:

  • instructions:角色、目标、边界和完成标准;
  • model:默认模型或根据运行上下文动态选择的模型;
  • tools:本次允许发现和调用的能力集合;
  • memory:如何读取、裁剪和保存相关上下文;
  • processors:进入模型前、输出给用户前的检查和转换;
  • scorers:对结果或轨迹进行质量评估。
import { Agent } from '@mastra/core/agent'

// buildInstructions、selectModel、Tool、Memory 和 Processor 均由当前应用实现或注入。
// 这个示例用于说明配置之间的关系,不是一个可独立运行的完整文件。
const interviewAgent = new Agent({
  // Agent 在 Mastra 注册表和 Trace 中使用的稳定名称。
  name: 'interview-notes-agent',
  // 根据服务端写入的可信上下文动态组装 Instructions。
  instructions: ({ requestContext }) => buildInstructions({
    role: requestContext.get('role'), // 当前用户经过鉴权后的角色。
    knowledgeScope: requestContext.get('knowledgeScope') // 本次允许检索的知识范围。
  }),
  // 根据质量档位选择模型,不能让用户直接提交任意模型名称。
  model: ({ requestContext }) => selectModel(
    requestContext.get('qualityTier')
  ),
  // 只暴露当前任务需要的 Tool,减少误调用和权限面。
  tools: { searchNotes, previewLearningGap },
  // memory 是应用配置好的 Mastra Memory 实例。
  memory,
  // 输入 Processor 在调用模型前检查注入风险并限制上下文长度。
  inputProcessors: [promptInjectionDetector, contextLimiter],
  // 输出 Processor 在结果返回给用户前检查引用。
  outputProcessors: [citationChecker]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这段代码表达的是依赖关系,不要求死记某个版本的参数名。真正稳定的设计是:Agent 只获得本次任务需要的最小能力,配置可以读取可信运行上下文,但不能让模型伪造身份和权限。

# Runtime Context、Memory 与 Workflow State

这三个概念最容易混淆:

状态 生命周期 示例 读取者
Runtime Context 单次请求或运行 userId、tenantId、权限、模型档位、请求来源 Agent 配置、Tool、Processor
Memory 跨轮次或跨会话 历史消息、已确认偏好、用户学习目标 Agent 与记忆处理器
Workflow State 一次流程从开始到完成 当前 step、草稿、分数、重试次数、审批结果 Workflow 各步骤
领域数据 长期业务事实 笔记正文、订单、账户余额、发布记录 宿主应用的领域服务与数据库

Runtime Context 决定 “这次运行是谁、能做什么”,Memory 决定 “过去有什么值得取回”,Workflow State 决定 “任务进行到哪里”。

不要把 userId 作为 Tool 参数交给模型填写;应从 Runtime Context 读取。Mastra 当前 API 中承载这类信息的参数名通常是 requestContext,本文的 Runtime Context 指的是它代表的运行时概念。也不要把账户余额、发布状态等权威事实只保存在消息历史里。

# Tool:模型与真实世界之间的契约

一个可靠 Tool 至少包含五部分:

  1. 单一且明确的职责;
  2. 足以帮助模型正确选择的描述;
  3. 严格的输入与输出 Schema;
  4. 执行前的权限、预算和风险检查;
  5. 结构化成功结果与可行动的错误结果。
import { createTool } from '@mastra/core/tools'
// z 是 Zod 导出的 Schema 构造器,下面用它校验模型生成的工具参数和工具结果。
import { z } from 'zod'

export const searchNotes = createTool({
  // Tool 的稳定标识,供模型选择、日志记录和 Trace 展示。
  id: 'search-notes',
  // description 会提供给模型,用于判断什么时候应调用该 Tool。
  description: '在当前用户允许访问的技术笔记中检索相关段落',
  // inputSchema 校验模型生成的调用参数。
  inputSchema: z.object({
    query: z.string().min(2), // 至少两个字符的检索词。
    limit: z.number().int().min(1).max(8).default(5) // 最多返回 8 条,默认 5 条。
  }),
  // noteExcerptSchema 由应用定义,用来约束每条笔记片段的字段。
  outputSchema: z.object({
    items: z.array(noteExcerptSchema)
  }),
  // 第一个参数已经通过 inputSchema 校验;requestContext 由可信宿主应用传入。
  execute: async ({ query, limit }, { requestContext }) => {
    // userId 必须来自服务端运行上下文,不能让模型或浏览器自行填写。
    const userId = requestContext.get('userId')
    // 在真正检索前再次执行服务端权限检查。
    await permissionService.assertCanReadNotes(userId)
    // noteService 是应用自己的领域服务,负责访问数据库或搜索索引。
    return noteService.search(userId, query, limit)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Schema 解决的是 “数据长什么样”,权限解决的是 “谁能调用”,幂等键解决的是 “重试会不会重复执行”。三者不能相互替代。

对于发送邮件、删除文件、转账、发布内容等高风险 Tool,推荐先生成预览和 actionId,进入审批节点后再执行;同时为副作用设置幂等约束。

Tool Streaming 是什么

Tool Streaming 是在 Agent 流式运行期间,把工具参数生成、工具开始执行、工具结果或错误等生命周期事件及时传给调用方。它的价值不是让数据库查询本身变成逐字输出,而是让前端知道 Agent 正在调用什么、是否等待审批以及何时拿到结果。页面仍应把底层事件转换成自己的稳定 UI 状态,不能只拼接文本。

# Memory:不是把聊天记录无限塞回模型

Mastra 的 Memory 可以围绕不同范围组织信息:

  • Thread:一次连续会话或任务的消息与上下文;
  • Resource:跨多个 thread 共享的用户或实体级信息;
  • Working Memory:当前工作中需要持续维护的结构化事实;
  • Conversation History:近期对话;
  • Semantic Recall:按相关性取回较早信息;
  • Processors:负责截断、摘要、筛选或写入策略。

Memory 的正确流程是:

候选信息产生 → 判断是否值得保存 → 按作用域持久化
      ↓ 下一次运行
按任务检索 → 去除过期或冲突内容 → 放入有限 Context
1
2
3

只有用户确认且未来确实有复用价值的信息才适合长期保存。模型推测、临时计划和过期状态不能直接升级为长期事实。

# RAG:把外部知识作为可追溯证据

Memory 主要解决过去交互中哪些信息值得再次取回,RAG 主要解决怎样从外部知识库找到与当前问题相关的原文证据。在 Mastra 中,完整 RAG 仍然分为离线索引和在线检索两条链路:

离线索引
原始 Markdown
└── MDocument 读取文档
    └── chunk() 切成语义片段
        └── Embedding 模型生成向量
            └── Vector Store 保存向量、原文和来源元数据

在线检索
用户问题
└── createVectorQueryTool 生成查询向量
    └── Vector Store 相似度检索与权限过滤
        └── 可选 rerank 重新排序
            └── 原文片段作为 Context 交给 Agent 回答
1
2
3
4
5
6
7
8
9
10
11
12
13

先安装 RAG、Embedding 和本地向量库需要的依赖:

# @mastra/rag 提供文档切分和向量检索工具;ai 提供批量 Embedding 调用。
# @mastra/libsql 只用于这个本地示例,生产环境可以替换成团队实际使用的向量库。
npm install @mastra/rag@latest @mastra/libsql@latest ai@latest
1
2
3

下面是一次性离线索引脚本。它负责把笔记切分并写入向量库,不应该在每次用户提问时重复执行。

// src/index-notes.ts
import { readFile } from 'node:fs/promises'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'
import { MDocument } from '@mastra/rag'
import { embedMany } from 'ai'
import { mastra } from './mastra'

// 笔记路径由索引任务指定;真实项目通常会遍历经过权限筛选的文档目录。
const notePath = 'docs/ai-fullstack/agent/overview.md'
const noteText = await readFile(notePath, 'utf8')

// MDocument 把原始文本包装为可切分、可携带元数据的文档对象。
const document = MDocument.fromText(noteText)
const chunks = await document.chunk({
  strategy: 'recursive', // 优先沿自然分隔符递归切分,减少语义被截断。
  maxSize: 512, // 单个片段的最大长度;最终值应由检索评测决定。
  overlap: 50, // 相邻片段保留少量重叠,避免边界处信息丢失。
  separators: ['\n\n', '\n', ' ']
})

// 文档和查询必须使用兼容的 Embedding 模型与相同向量维度。
const embeddingModel = new ModelRouterEmbeddingModel(
  'openai/text-embedding-3-small'
)
const { embeddings } = await embedMany({
  model: embeddingModel,
  values: chunks.map(chunk => chunk.text)
})

// libSqlVector 是在 src/mastra/index.ts 中注册的 Vector Store。
const vectorStore = mastra.getVector('libSqlVector')

// 1536 是当前示例模型的向量维度;更换模型时必须同步修改。
// 生产项目通常通过 migration 创建索引,不在重复执行的业务请求中创建。
await vectorStore.createIndex({
  indexName: 'agent-notes',
  dimension: 1536
})

await vectorStore.upsert({
  indexName: 'agent-notes',
  vectors: embeddings,
  // metadata 保存真正需要返回给模型和用户的原文与来源。
  metadata: chunks.map(chunk => ({
    text: chunk.text,
    source: notePath,
    knowledgeBase: 'agent-development'
  }))
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

在线阶段把向量检索包装成 Tool,由 Agent 根据问题决定是否调用:

// src/mastra/agents/rag-interview-agent.ts
import { Agent } from '@mastra/core/agent'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'
import { createVectorQueryTool } from '@mastra/rag'

const searchKnowledgeBase = createVectorQueryTool({
  // 名称必须与 Mastra 入口中 vectors 的注册键一致。
  vectorStoreName: 'libSqlVector',
  // 在线查询必须检索离线脚本写入的同一个索引。
  indexName: 'agent-notes',
  // 查询向量与文档向量使用同一个 Embedding 模型。
  model: new ModelRouterEmbeddingModel('openai/text-embedding-3-small')
})

export const ragInterviewAgent = new Agent({
  // id 是 API、注册表和 Trace 使用的稳定标识。
  id: 'rag-interview-agent',
  // name 是 Studio 中展示的可读名称。
  name: 'RAG Interview Agent',
  // Instructions 规定检索时机、证据边界和证据不足时的行为。
  instructions: `
    回答 Agent 开发问题前先检索知识库。
    只把检索结果中的原文当作证据,并在回答中给出 source。
    证据不足时明确说明笔记暂未覆盖,不得自行补造来源。
  `,
  // 回答模型与 Embedding 模型职责不同,这里配置的是生成回答的模型。
  model: 'openai/gpt-5-mini',
  // Agent 会根据 Instructions 和 Tool 描述决定何时检索。
  tools: { searchKnowledgeBase }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/mastra/vector-store.ts
import { LibSQLVector } from '@mastra/libsql'

export const libSqlVector = new LibSQLVector({
  // id 是这个 Vector Store 实例在日志和内部组件中的稳定标识。
  id: 'agent-notes-vector',
  // 本地开发可把 VECTOR_DB_URL 设置为 file:/绝对路径/vector.db。
  // 生产环境应换成可持久化、可备份并支持权限隔离的远程存储。
  url: process.env.VECTOR_DB_URL!
})
1
2
3
4
5
6
7
8
9
10

这个示例重点说明代码放置和数据流。真实项目还要在检索阶段加入用户权限与知识库过滤,召回后可用 reranker 重排,并用 Recall@K、引用准确率等指标分别评估检索和回答。更完整的原理见 RAG 原理与工程实践。

# Workflow:把确定性控制放回代码

Workflow 适合明确的步骤、分支、并行、循环、重试和人工介入。例如笔记与面试助手可以这样编排:

validateRequest
      ↓
retrieveNotes ──并行── loadInterviewRubric
      ↓
runAgent
      ↓
evaluateDraft ──不通过且有预算──→ runAgent
      ↓通过
previewLearningGap
      ↓ suspend
saveLearningGap
1
2
3
4
5
6
7
8
9
10
11

Workflow 的关键能力不只是 “按顺序调用函数”:

  • step 显式声明输入输出,降低隐藏依赖;
  • branch / parallel / loop 分别表达条件分支、并行执行和循环;
  • suspend / resume 让流程暂停等待外部输入,并在收到审批或补充信息后继续;
  • snapshot 是保存下来的完整执行状态,可供另一个进程恢复;
  • rewind 是回到较早状态重新运行,主要用于调试或重新处理后续步骤。

恢复并不意味着所有代码都只执行一次。暂停之前或失败步骤中的副作用仍应满足幂等,关键写入仍由数据库事务保证。

# 一个最小可运行的 Workflow

下面的示例把生成草稿和检查完成标准固定为两个步骤。它展示的是最稳定的主干:createStep() 定义步骤契约,createWorkflow() 定义整条流程,.then() 连接顺序,最后必须 .commit() 固化执行图。

// src/mastra/workflows/interview-flow.ts
import { createStep, createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
import { interviewAgent } from '../agents/interview-agent'

const generateAnswer = createStep({
  // id 会出现在 Workflow Trace 和 Studio 的执行图中。
  id: 'generate-answer',
  // inputSchema 定义本步骤允许接收的数据。
  inputSchema: z.object({
    question: z.string().min(1)
  }),
  // outputSchema 同时也是下一步骤的输入契约。
  outputSchema: z.object({
    question: z.string(),
    draft: z.string()
  }),
  execute: async ({ inputData }) => {
    // Agent 只负责需要语义判断的回答生成。
    const response = await interviewAgent.generate(inputData.question)

    return {
      question: inputData.question,
      draft: response.text
    }
  }
})

const checkAnswer = createStep({
  // 这个步骤只检查结果,不再让 Agent 自己判断是否合格。
  id: 'check-answer',
  // 输入与 generateAnswer 的输出保持一致,连接时才能通过类型检查。
  inputSchema: z.object({
    question: z.string(),
    draft: z.string()
  }),
  // passed 供后续分支决定完成、重试还是转人工检查。
  outputSchema: z.object({
    answer: z.string(),
    passed: z.boolean()
  }),
  execute: async ({ inputData }) => {
    // 这里只演示确定性完成标准;真实项目可以换成独立 Scorer。
    const passed = inputData.draft.length >= 40

    return {
      answer: inputData.draft,
      passed
    }
  }
})

export const interviewFlow = createWorkflow({
  id: 'interview-flow',
  // Workflow 输入来自 API、上层 Workflow 或另一个 Agent。
  inputSchema: z.object({
    question: z.string().min(1)
  }),
  // Workflow 输出必须与最后一个步骤的结果一致。
  outputSchema: z.object({
    answer: z.string(),
    passed: z.boolean()
  })
})
  // then 表示前一步成功后,把输出交给下一步。
  .then(generateAnswer)
  .then(checkAnswer)

// commit 会完成图校验并固定 Workflow 定义;漏掉后不能正常运行。
interviewFlow.commit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { interviewAgent } from './agents/interview-agent'
import { ragInterviewAgent } from './agents/rag-interview-agent'
import { libSqlVector } from './vector-store'
import { interviewFlow } from './workflows/interview-flow'

// 这是把前面 Agent、RAG 和 Workflow 合并后的最终入口。
export const mastra = new Mastra({
  // agents 中注册需要由 Server 和 Studio 对外提供的 Agent。
  agents: { interviewAgent, ragInterviewAgent },
  // 注册键 interviewFlow 供 Server、Studio 和其他组件查找这条 Workflow。
  workflows: { interviewFlow },
  // 注册键 libSqlVector 必须与检索 Tool 的 vectorStoreName 一致。
  vectors: { libSqlVector }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

真实流程可以继续用 .parallel() 表达互不依赖的并行步骤,用 .branch() 表达互斥条件,用 .doWhile() / .doUntil() 表达有上限的循环。需要等待审批时,在 Step 中 suspend();收到可信的审批结果后再通过对应 Run 恢复。不要为了展示所有 API 把简单流程强行写成复杂执行图。

# Processor 与 Guardrail:把约束放进生命周期

Prompt 中写 “不要泄漏隐私” 只是行为提示,不是可靠安全边界。Processor 适合承担多次调用都会经过的通用检查和转换:

  • 输入阶段:检测提示注入、PII、恶意文件和上下文超限;
  • 模型阶段:裁剪消息、路由模型、限制工具集合;
  • 输出阶段:检查敏感内容、引用、结构和完成条件;
  • 记忆阶段:决定哪些内容可写入、需要过期或必须删除。

稳定做法是分层防护:Processor 负责早期发现与阻断,Tool 和宿主应用的领域服务仍进行最终授权,数据库负责最后的一致性约束。

# Workspace、Skill 与 Supervisor

Workspace 为 Agent 提供可控的文件系统、工作目录或沙箱。Skill 则把一类任务需要的指令、资源和脚本组织成可复用方法。

  • Agent 决定当前目标和下一步;
  • Skill 告诉 Agent 某类任务应遵循什么方法;
  • Workspace 提供完成任务所需的工作环境;
  • Tool 执行具体动作。

Supervisor Agent 用于把任务委派给专业 Agent,并汇总结果。只有当角色确实拥有不同工具、上下文或权限时才值得拆分;如果只是为了 “看起来智能” 而增加多个 Agent,只会提高成本、延迟和调试难度。

# Mastra 中怎样编排多个 Agent

Mastra 的多 Agent 编排不是只有一种写法,应先根据控制权选择:

方式 谁决定执行路径 更适合什么场景
Workflow 调用多个 Agent 代码中的执行图 顺序、并行和验收规则已知,要求稳定可审计
Supervisor Agent 主 Agent 主控需要动态选择专家,但最终仍由主控汇总
.network() 路由 Agent 的模型循环 路径事先难以确定,需要动态组合 Agent、Tool 和 Workflow

下面以笔记生产为例:研究 Agent 负责找证据,写作 Agent 负责组织表达,主 Agent 根据任务决定先调用谁。子 Agent 的 description 很重要,因为主 Agent 会根据它判断是否委派。

// src/mastra/agents/note-network.ts
import { Agent } from '@mastra/core/agent'

const researchAgent = new Agent({
  // id 是委派、日志和 Trace 中使用的稳定标识。
  id: 'note-researcher',
  // name 是 Studio 中展示的可读名称。
  name: 'Note Researcher',
  // description 面向主 Agent,说明这个子 Agent 适合接收什么任务。
  description: '负责检索官方资料并返回带来源的已验证事实,不负责润色文章。',
  // Instructions 约束研究过程和输出边界。
  instructions: '只返回有来源支持的事实;无法核验时明确标记未知。',
  // 每个子 Agent 可以选择适合自己任务的模型。
  model: 'openai/gpt-5-mini'
})

const writingAgent = new Agent({
  // 写作 Agent 使用独立 id,避免与研究 Agent 的运行记录混淆。
  id: 'note-writer',
  name: 'Note Writer',
  // description 告诉主 Agent 何时应该把任务交给它。
  description: '负责把已有证据改写成通俗的中文面试笔记,不负责外部检索。',
  // Instructions 规定写作 Agent 只能使用已经交付的证据。
  instructions: '根据收到的证据写作,不得补充来源中不存在的技术结论。',
  model: 'openai/gpt-5-mini'
})

export const noteSupervisor = new Agent({
  // 主 Agent 也需要独立 id,方便定位整条编排 Trace。
  id: 'note-supervisor',
  name: 'Note Supervisor',
  // 主 Agent 负责拆分任务、选择子 Agent、检查结果并决定何时结束。
  instructions: `
    先判断是否缺少可靠证据,缺少时委派给 researchAgent。
    获得证据后再委派给 writingAgent,最后检查来源和完成标准。
  `,
  // 主 Agent 的模型负责路由、验收和停止判断。
  model: 'openai/gpt-5-mini',
  // agents 中注册的是可被主 Agent 委派的专业 Agent,不是普通 Tool。
  agents: { researchAgent, writingAgent }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/run-network.ts
import { noteSupervisor } from './mastra/agents/note-network'

// network() 让主 Agent 在循环中动态选择子 Agent、Tool 或 Workflow。
// 调用结果仍需由应用按来源、格式和完成标准验收。
const result = await noteSupervisor.network(
  '核验 Mastra Workflow 的最新 API,并生成一段面试笔记。'
)

// result 中包含网络运行的最终输出;生产环境还应记录 Trace 和成本。
console.log(result)
1
2
3
4
5
6
7
8
9
10
11

`.network()` 为什么要谨慎使用

.network() 当前属于实验性能力,版本变化速度可能快于 Agent 和 Workflow 的稳定 API。项目使用时应锁定 Mastra 版本并做回归测试。若执行顺序已知,优先用 Workflow 显式编排多个 Agent;只有路径确实需要模型动态决定时,再使用 Network。

# MCP:统一能力接入,不替代业务边界

Mastra 可以消费 MCP Server 提供的工具、资源和提示模板,也可以把自身能力暴露给其他 MCP Host。它解决的是跨应用的能力发现与调用协议。

Mastra Agent → MCP Client → MCP Server → 外部系统
外部 Host   → MCP Client → Mastra MCP 能力
1
2

MCP Server 返回的描述和内容都属于外部输入。连接成功不代表可信,仍需做 Server 白名单、Tool 白名单、参数校验、超时、结果裁剪与写操作审批。

# Scorer、Eval 与 Observability

生产闭环要同时回答两个问题:

  • Observability:这次运行经过了哪些模型、Tool 和 Workflow step,在哪里变慢或失败?
  • Eval:这条轨迹和最终结果是否足够好?

Mastra 的 Trace 可以记录模型调用、工具调用、步骤、Token、延迟和错误,并通过 OpenTelemetry 等方式接入观测系统。Scorer 可评估答案正确性、引用覆盖、工具选择、安全和任务完成度。

推荐闭环:

生产 Trace 发现问题
        ↓
脱敏后加入固定评测集
        ↓
比较 Prompt / Model / Tool / Workflow 版本
        ↓
通过质量、成本和延迟门槛后灰度发布
1
2
3
4
5
6
7

# Client SDK:Mastra 怎样对接前端

@mastra/client-js 是 Mastra Server 的类型化客户端。它不仅能调用 Agent,还能访问 Workflow、Memory Thread、Tool、Trace 等服务端资源。

React / Vue / Mobile
        ↓ @mastra/client-js 或 AI SDK UI
宿主应用自己的 API Route / BFF(推荐用于用户系统)
        ↓ 身份认证、权限映射、限流、审计
Mastra Server
        ↓
Agent / Workflow / Memory / Storage
1
2
3
4
5
6
7

# 初始化 Client

import { MastraClient } from '@mastra/client-js'

export const mastraClient = new MastraClient({
  // 浏览器可见的 Mastra Server 或 BFF 地址,不应包含任何 Secret。
  baseUrl: import.meta.env.VITE_MASTRA_BASE_URL,
  // 跨请求携带由服务端设置的登录 Cookie;服务端仍需完成鉴权。
  credentials: 'include'
})
1
2
3
4
5
6
7
8

浏览器里的 baseUrl 本来就是公开信息,不能把 API Key、数据库凭据或其他服务端 Secret 写进前端环境变量。Cookie 或 Token 应由 Mastra Server / BFF 验证,并在服务端把登录用户映射为可信 Runtime Context。

# 同步生成与流式生成怎样选择

// 从上面初始化的 Client 中取得服务端注册的 Agent。
const agent = mastraClient.getAgent('interview-notes-agent')

// 适合短任务:等待完整结果后一次更新页面。
const result = await agent.generate('解释 MCP 和 Tool Calling 的区别', {
  memory: {
    thread: threadId, // 当前会话 ID,来自页面状态或路由参数。
    resource: resourceId // 记忆所属用户或资源 ID,服务端必须校验归属。
  }
})

// 适合聊天和长回答:逐块更新页面,并显示 Tool 与运行状态。
const response = await agent.stream('模拟一道 Agent 系统设计面试题', {
  memory: {
    thread: threadId, // 让流式运行继续写入同一会话。
    resource: resourceId // 决定从哪个资源范围读取 Memory。
  }
})

// 把 Mastra 的原始 chunk 转成页面自己的稳定事件,再更新 UI 状态。
await response.processDataStream({
  onChunk: chunk => dispatch(toUiEvent(chunk))
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

前端传来的 threadId 和 resourceId 都是不可信标识。服务端必须验证它们是否属于当前登录用户,不能因为 Client SDK 的类型检查通过就直接读取对应 Memory。

# 不要只拼接 text-delta

Mastra 的数据流包含多类 chunk。前端应先转换成自己的稳定事件,再更新 UI:

Mastra 事件族 页面状态 推荐展示
start、step-start、step-finish running、当前步骤 “正在检索笔记” 之类的明确进度
text-start、text-delta、text-end assistant message 流式正文
tool-call tool activity 工具名、目的和安全参数摘要
tool-result、tool-error tool result 成功摘要或可重试错误
tool-call-approval、tool-call-suspended awaitingApproval 审批卡片,而不是一直显示加载中
finish completed 最终答案、引用、耗时
abort、error cancelled、failed 失败原因与重试入口
type ChatRunState = {
  runId?: string // 当前服务端运行 ID,用于断线重连和查询状态。
  status: 'idle' | 'running' | 'awaitingApproval' | 'completed' | 'failed' // 页面主状态。
  text: string // 已收到并准备展示的回答正文。
  activities: Array<{ id: string; type: string; status: string }> // 工具和步骤的可视化记录。
  approval?: { toolCallId: string; summary: string } // 等待用户确认的工具调用摘要。
  error?: string // 可以展示给用户的失败原因,不应包含服务端敏感信息。
}
1
2
3
4
5
6
7
8

SDK 的 chunk 类型可以随框架升级,页面自己的 ChatRunState 才应该是稳定契约。建议在适配层完成版本兼容、顺序校验和未知事件降级,不要让每个组件直接判断底层 chunk。

# 三种前端接入方式

方式 适合场景 主要取舍
浏览器直接使用 @mastra/client-js 内部工具、同域部署、服务端已完整鉴权 最简单,但必须正确配置 CORS、认证和资源隔离
Next.js / Nuxt / Node BFF 调用 Client SDK 有 Cookie Session、私有权限或需要聚合领域数据 多一层接口,但安全边界和业务契约最清晰
@mastra/ai-sdk + useChat 等 UI Hook 已使用 Vercel AI SDK 的聊天界面 UI 集成快,但仍要理解 Mastra 原始事件和审批状态

面向真实用户的笔记与面试助手,推荐 BFF:浏览器只提交消息和页面需要的公开 ID;BFF 根据登录态确定 userId、resourceId 和允许使用的 Agent,再调用 Mastra。

// app/api/chat/route.ts
// requireSession、chatInputSchema、threadService 和 streamInterviewAgent 均由应用提供。
export async function POST(request: Request) {
  // 先从 Cookie 或 Token 恢复可信登录身份。
  const session = await requireSession(request)
  // 再校验浏览器提交的消息和 threadId 是否符合输入契约。
  const input = await chatInputSchema.parseAsync(await request.json())

  // 防止用户读取或写入其他人的会话。
  await threadService.assertOwner(input.threadId, session.userId)

  // 只把服务端确认后的身份和资源范围传给 Agent。
  return streamInterviewAgent({
    message: input.message,
    threadId: input.threadId,
    resourceId: session.userId
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如果项目采用 AI SDK UI,可以通过 @mastra/ai-sdk 的 Route Handler 或 Stream Handler,把 Mastra Agent / Workflow 的输出转换为 useChat、useCompletion 或 useObject 能消费的协议。它是协议适配层,不会替代鉴权和领域校验。

# Thread、断线重连与 Durable Agent

普通 HTTP Stream 与当前连接绑定。网络中断或浏览器刷新后,前端至少要保存:

  • threadId:会话与 Memory 的逻辑标识;
  • runId:某次 Agent 执行的标识;
  • 已接收事件的 sequence 或去重键;
  • 当前是否处于审批、完成或失败状态。

对于需要跨刷新继续观察的长任务,可使用 Durable Agent:服务端缓存或持久化运行事件,Client SDK 通过 runId 调用 observe() 重新订阅。Thread 级交互还可以使用 subscribeToThread(),配合 sendMessage()、queueMessage() 或 Signal 接收活跃运行和后续轮次的事件。

// mastraClient 在“初始化 Client”一节创建;userMessage 和 runId 来自页面状态。
const durableAgent = mastraClient.getAgent('durable-interview-agent')

// 页面首次发起运行。
const response = await durableAgent.stream(userMessage, { runId })
// handleChunk 把服务端事件合并进页面状态,并按事件 ID 或序号去重。
await response.processDataStream({ onChunk: handleChunk })

// 刷新或断线后根据同一 runId 重新观察。
const resumed = await durableAgent.observe({ runId })
await resumed.processDataStream({ onChunk: handleChunk })
1
2
3
4
5
6
7
8
9
10
11

Durable Stream 解决 “客户端重新连接后还能观察运行”,Workflow Snapshot 解决 “流程暂停或失败后从保存状态恢复”。二者关注点不同,不要混为一谈。

# 前端提交审批时发生什么

收到 tool-call-approval / suspended
        ↓
页面展示工具、影响范围和参数摘要
        ↓ 用户批准、修改或拒绝
BFF 再次检查用户、runId、toolCallId 与权限
        ↓
调用 resume / approval API
        ↓
继续消费新的 Stream,直到 completed 或 failed
1
2
3
4
5
6
7
8
9

审批按钮需要防重复点击;服务端需要一次性审批记录和幂等键。不能只在前端把按钮禁用,因为用户可以绕过 UI 重放请求。

# 从本地 Studio 到生产部署

本地执行 npm run dev 时,Mastra Server 提供 Agent、Tool 和 Workflow 的运行接口,Studio 提供调试和观测界面。到了生产环境,这两部分可以一起托管,也可以分开部署:

部署方式 Mastra Server 在哪里 Studio / Observability 在哪里 适合场景
全托管 Mastra 平台 Mastra 平台 小团队快速上线,不想维护 Agent Runtime
自托管 Server 自己的容器、虚拟机或云平台 托管 Studio 已有基础设施,希望数据面由自己控制
全自托管 自己的基础设施 自己的基础设施 合规、数据隔离或内网环境要求高
用户浏览器
└── Next.js / Node BFF
    ├── 登录认证、权限、限流与业务 API
    └── Mastra Client
        └── Mastra Server
            ├── Agent / Workflow / Tool
            ├── Memory / Storage / Vector Store
            └── Trace → Studio / Observability
1
2
3
4
5
6
7
8

Server 是生产运行时,Studio 是开发与观测界面,二者不是同一个职责。前端可以和 Mastra Server 部署在同一个 Web 应用中,也可以通过 BFF 调用独立的 Mastra Server。需要独立扩缩容、服务多个客户端或隔离 Agent 运行资源时,拆成单独服务更合适;原型和小项目可以先同进程部署。

无论部署在哪里,都要同时配置模型密钥、持久化 Storage、向量库、认证中间件、日志与 Trace 导出。生产环境不能继续依赖进程内 Memory 或本地临时文件,否则重启、扩容或迁移实例时会丢失状态。Mastra Cloud 可以减少 Server 和 Studio 的运维工作,但不会自动替代应用自己的用户权限、领域数据库和数据合规设计。

# 用笔记与面试助手验证这些知识

这个项目不需要堆满 API,只要验证核心抽象:

  1. Runtime Context 携带用户身份和知识范围;
  2. searchNotes Tool 只返回允许访问的片段;
  3. Agent 判断检索、追问或回答;
  4. Workflow 固定检索、回答、评测和审批;
  5. thread 保存本轮面试,resource 保存用户确认的长期目标;
  6. 保存薄弱点前 suspend,恢复后用 actionId 幂等写入;
  7. RAG 离线索引笔记,在线按权限检索并返回可追溯原文;
  8. 多 Agent 场景由 Supervisor 分配研究与写作任务,固定步骤仍交给 Workflow;
  9. Trace 记录模型、工具和步骤,Scorer 检查引用与知识点覆盖;
  10. Client SDK 将 chunk 转换成页面事件,刷新后可根据 threadId / runId 恢复界面;
  11. Mastra Server 使用持久化 Storage 部署,前端通过 BFF 安全访问。

做完后应该能解释每个状态放在哪里、一次失败怎样恢复、为什么不会越权,而不只是展示 “模型成功调用了一个工具”。

# 高频面试题与回答

回答顺序:先说结论,再解释原因,最后补一个例子或工程边界。不要逐字背诵,记住这条表达主线即可。

1. 2 分钟回答:你怎样理解 Mastra 的核心设计参考答案

我理解 Mastra 的核心是把 TypeScript Agent 应用里的 动态决策 和 确定性流程 分开。Agent 根据 Instructions、Context 和工具结果决定下一步;固定分支、校验、重试和人工审批放进 Workflow,并通过 Snapshot 支持暂停和恢复。Tool 的 Schema 只定义模型与代码之间的数据契约,真实身份、权限、幂等和事务仍由应用负责。状态也要分清:Runtime Context 是本次请求的可信配置,Memory 是跨轮次可能复用的信息,Workflow State 是任务进度。生产环境再用 Processor 检查输入输出、用 Trace 排查执行过程、用 Scorer 评估结果质量。

2. Mastra Agent 和 Workflow 怎样选择?参考答案

如果下一步必须由模型根据语义和工具结果临时判断,就用 Agent;如果步骤固定、风险高,或者需要稳定重试和人工审批,就用 Workflow。常见做法不是二选一,而是让外层 Workflow 控制关键阶段,把 Agent 放在其中一个需要动态判断的节点里。

3. Runtime Context 和 Memory 有什么区别?参考答案

Runtime Context 是应用为这一次运行传入的可信信息,例如当前用户、租户、权限范围和模型策略;Memory 保存过去交互中以后可能还要取回的信息。可以记成:Runtime Context 回答 “这次以什么身份和配置运行”,Memory 回答 “过去有什么值得记住”。

4. Schema 为什么不能保证 Tool 安全?参考答案

因为 Schema 只检查参数格式,例如金额是不是数字,不能证明用户有权转账,也不能防止同一笔转账重复执行。Tool 真正执行前,服务端仍要验证可信身份、业务规则和审批,并用幂等键控制重复请求。

5. Mastra 的 Memory、Workflow Snapshot 和业务数据库怎样分工?参考答案

Memory 保存 Agent 以后可能复用的历史和偏好;Workflow Snapshot 保存这一次流程执行到哪一步,供暂停后恢复;业务数据库保存订单、账户和笔记等最终权威事实。三者用途不同,不能因为框架能保存 Memory 或 Snapshot,就把业务数据库省掉。

6. 怎样判断自己真正掌握了 Mastra?参考答案

不只是能运行一个 Agent Demo,而是能讲清一次请求怎样经过 Agent、Tool 和 Workflow,各类状态分别存在哪里;还能实现安全 Tool、可暂停恢复的 Workflow、前端流式交互和评测追踪。更重要的是,能说明什么时候原生 SDK 或普通 Workflow 已经够用。

7. Mastra 怎样与前端页面对接?参考答案

我通常让前端先请求自己的 BFF,也就是专门服务前端的后端接口层。BFF 根据登录态确定用户和可访问资源,再用 Mastra Client SDK 调用 Agent,避免把敏感凭据放到浏览器。前端把流里的文本、工具、审批、完成和错误事件转换成自己的 UI 状态;threadId 标识会话,runId 标识一次运行。长任务要把状态持久化并支持重新订阅,这样页面刷新后还能恢复观察;审批和资源权限始终在服务端复核。

8. 你在项目中怎样组织 Mastra 代码?参考答案

我把 Agent、Tool 和 Workflow 分目录定义,再在 src/mastra/index.ts 统一注册。Agent 保存模型、Instructions 和允许使用的能力;Tool 封装单个外部动作并校验输入输出;Workflow 固定关键步骤、分支和审批。开发时用 Studio 调试执行图和 Trace,生产请求则经过 BFF 完成身份与权限校验后再进入 Mastra Server。

9. Mastra 中的 RAG 是怎样接入 Agent 的?参考答案

我把它拆成离线和在线两部分。离线使用 MDocument 切分笔记,通过 Embedding 模型生成向量,再把向量、原文和来源写入 Vector Store;在线把向量查询封装成 Tool,Agent 需要证据时再调用。召回结果还要经过权限过滤和必要的重排,最终回答携带真实来源。这样向量库负责找候选资料,Agent 负责阅读证据并组织回答,两者职责不会混在一起。

10. Mastra 的 Workflow、Supervisor 和 Network 怎样选择?参考答案

执行路径已知、需要审计和恢复时,我优先用 Workflow;需要主 Agent 根据语义选择专业 Agent 时,可以使用 Supervisor;只有路径事先难以确定、确实需要动态组合多个 Agent、Tool 和 Workflow 时,才考虑 Network。Network 更灵活,但成本、失败路径和版本风险也更高,所以固定业务规则仍然放在 Workflow 和宿主代码里。

11. Mastra 项目怎样从本地部署到生产?参考答案

本地用 Mastra Server 运行组件、用 Studio 调试和查看 Trace。生产环境可以使用托管平台,也可以把 Server 部署在自己的容器或云平台,并单独选择托管或自托管的 Studio。无论采用哪种方式,浏览器都不直接持有模型和数据库密钥;BFF 负责认证、权限和限流,Memory、Workflow Snapshot 与向量索引使用可持久化存储,同时接入日志、Trace、成本和延迟监控。

# 接下来学什么

下一篇学习 LangChain 生态核心,重点理解 create_agent 的执行框架、LangGraph 的状态与持久化模型,以及 LangSmith 的追踪评测闭环。

# 参考资料

上次更新时间: 2026年09月10日 00:05:23