Structured Output 看起来只是“让 Agent 返回 JSON”,但一旦 Agent 还需要调用工具,问题就不再只是 Schema 怎么写。
有一种很常见、也很容易让人困惑的现象:
代码里明明已经配置了 Structured Output 和 Schema,模型还是返回普通文本、漏字段或不合法 JSON;在提示词里再写一遍输出格式后,反而生效了。
这往往意味着,Schema 没有成功变成当前 provider 的原生生成约束。可能是当前模型或 provider 不支持原生 Structured Output;也可能是它分别支持 Tools 和 Structured Output,却不能在同一次 Agent 执行中组合两者。
把 JSON 结构写进提示词之所以有效,是因为约束路径从原生 response_format 退回到了模型能直接看到的指令。Mastra 提供 jsonPromptInjection 正是为了显式处理这种兼容性问题:它把 Schema 说明注入 user message 或 system message,避免在业务 prompt 里手工维护另一份格式定义。
但两者的保证强度不同:提示词约束只是让模型更可能遵守格式,不等于 provider 在生成阶段强制 Schema。对数据库写入、支付、审核结果等严格业务,仍然需要校验、错误策略,或独立 structuring model。
真正的兼容性边界是:
模型支持 Function Calling
+ 模型支持 Structured Output
≠ 底层 API 支持两者在同一次 Agent 执行中组合这个区别决定了我们应该使用 Mastra 的哪一种路径:原生 response_format、jsonPromptInjection、独立 structuring model,还是 prepareStep。
Structured Output 约束的是什么
在 Mastra 中,可以用 Zod、Valibot、ArkType 或标准 JSON Schema 定义最终输出。例如:
import { z } from 'zod'
const resultSchema = z.object({
title: z.string(),
summary: z.string(),
tags: z.array(z.string()),
})
const response = await agent.generate('分析这篇文章', {
structuredOutput: {
schema: resultSchema,
},
})
console.log(response.object)得到的不再是普通文本,而是符合 Schema 的对象:
{
"title": "文章标题",
"summary": "文章摘要",
"tags": ["AI", "Agent"]
}这种输出适合 API 响应、数据库写入、UI 渲染、信息抽取、分类评分,以及 Workflow 节点之间的数据传递。
默认路径:Provider 原生 Response Format
省略 jsonPromptInjection,或者显式设为 false时,Mastra 会把 Schema 通过 provider 的 response_format 参数传给模型:
structuredOutput: {
schema: resultSchema,
jsonPromptInjection: false,
}可以把它理解为底层请求类似:
{
response_format: {
type: 'json_schema',
json_schema: {
// 由 Schema 转换而来
},
},
}原生结构化输出的优势是约束更强。它不只是在提示词里请模型“尽量写对 JSON”,而是由 provider 在生成阶段应用结构约束。
因此,没有工具时,原生 Structured Output 通常是最直接的选择。
为什么加上 Tools 后会出问题
Tools 和 Structured Output 分别约束 Agent 运行的不同阶段。
Tools 约束中间过程:
用户请求
→ 模型生成 tool call
→ Mastra 执行工具
→ 工具结果返回模型Structured Output 约束最终交付:
{
"temperature": 32,
"condition": "sunny",
"suggestion": "注意防晒"
}底层 API 需要正确处理“现在应该产生工具调用”和“现在应该产生符合 Schema 的结果”两种状态。某些模型 API 不能在同一请求里组合 function calling 与 response_format,可能导致:
- 工具不再被调用。
- Provider 返回参数冲突。
- 工具成功,但最终对象不符合 Schema。
- 模型跳过工具,直接猜测字段。
Mastra 文档特别指出,Gemini 2.5 在有 Tools 时不能组合 function calling 与原生 response_format。这是底层模型 API 的限制,不是 Mastra 自身的限制。
方案一:JSON Prompt Injection
jsonPromptInjection 不使用 provider 的原生 response_format,而是把 Schema 说明加入模型上下文。底层 API 只需正常处理 Tools,工具调用完成后,模型根据提示词生成 JSON。
它避开了 tools + response_format 的组合冲突,代价是结构更依赖模型遵循提示词,约束强度弱于原生方式。
Mastra 提供四种配置:
// 原生 Structured Output
jsonPromptInjection: false
// 把 Schema 加入最近一条用户消息
jsonPromptInjection: 'inline'
// 把 Schema 加入 System Message
jsonPromptInjection: true
jsonPromptInjection: 'system'
// 根据 Mastra 的模型能力数据选择原生方式或 inline 注入
jsonPromptInjection: 'auto''auto' 适合作为能力差异不明确时的起点:已知模型支持原生 Structured Output 时使用 response_format;不支持或没有能力数据时改用 inline 注入。
对新模型、自定义 Provider 或 OpenAI-compatible 接口,如果不希望依赖能力推断,可以显式使用 'inline'。Gemini 2.5 与 Tools 组合时,官方文档则要求使用 true 避开错误。
方案二:独立 Structuring Model
如果主 Agent 不擅长结构化输出,或者工具循环和 Schema 都比较复杂,可以在 structuredOutput 中指定另一个模型:
const response = await agent.generate('分析 TypeScript,并在必要时调用工具', {
structuredOutput: {
schema: z.object({
overview: z.string(),
strengths: z.array(z.string()),
weaknesses: z.array(z.string()),
}),
model: 'openai/gpt-5.5',
},
})此时 Mastra 会执行两次 LLM 调用:
用户请求
→ 主 Agent:理解任务、调用工具、读取结果、生成自然语言响应
→ Structuring Agent:读取当前响应,按 Schema 提取结构化对象这是很清晰的职责分离:
主 Agent 负责把事情做对
Structuring Model 负责把格式做对代价也很明确:额外的模型调用会增加延迟和成本。它更适合多工具、多轮 tool loop、复杂 Schema,或最终对象将直接进入 API 和数据库的场景。
Mastra 官方文档对这个机制的说明和示例见 Use a separate structuring model。
useAgent 要不要开
默认情况下,独立 structuring model 只根据主 Agent 当前生成的响应进行转换。
如果结构化过程确实需要当前 Agent 的对话历史和只读 Memory 上下文,可以增加:
structuredOutput: {
schema: resultSchema,
model: 'openai/gpt-5.5',
useAgent: true,
}不需要历史信息时,不要开启 useAgent。让 structuring model 只看本次响应,可以减少无关上下文的干扰。
方案三:用 prepareStep 分离执行阶段
当工具调用顺序明确时,可以在同一 Agent loop 里按步骤切换配置:
const result = await agent.stream('查询温哥华天气', {
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
tools: { weatherTool },
toolChoice: 'required',
}
}
return {
tools: undefined,
structuredOutput: {
schema: z.object({
temperature: z.number(),
humidity: z.number(),
windSpeed: z.number(),
}),
},
}
},
})这个流程的分工是:
Step 0:只开启 Tools,完成数据获取
Step 1:关闭 Tools,启用原生 Structured Output每个生成步骤都不需要同时处理 tools + response_format。和提示词注入相比,最终步骤仍然可以使用 provider 的原生结构约束。
三种兼容方案怎么选
| 方案 | 模型调用 | 结构约束 | 主要代价 | 适合场景 |
|---|---|---|---|---|
jsonPromptInjection | 不额外增加独立调用 | 中等 | 更依赖模型遵循提示词 | 简单 Schema、第三方模型、优先解决兼容问题 |
| 独立 structuring model | 额外一次 | 强 | 增加延迟和成本 | 多工具、复杂 Schema、生产 API |
prepareStep | 复用原 Agent loop 的分步调用 | 强 | 需要明确设计阶段 | 工具顺序可控、执行流程明确 |
可以按下面的顺序决策:
- 没有 Tools:直接使用原生 Structured Output。
- 有少量工具,Schema 简单:先尝试
'auto';能力推断不可信时显式使用'inline'。 - 有多个工具或多轮 Agent loop:考虑独立 structuring model。
- 工具调用顺序明确:用
prepareStep将工具阶段和结构化阶段分开。
工具已经返回结构化数据时,不要让模型重抄
假设天气工具已经返回:
const weather = {
temperature: 32,
humidity: 70,
condition: 'sunny',
}能由程序确定的字段,应该直接组装和校验:
const result = resultSchema.parse({
temperature: weather.temperature,
humidity: weather.humidity,
condition: weather.condition,
})只有建议类字段需要生成时,只把那一小部分交给模型。这会减少重抄错误,也能降低 token 和校验成本。
原则可以浓缩为一句话:
能由程序确定的数据,不要让模型重新抄写和推断。
生产环境的分层
复杂 Agent 可以把职责拆成四层:
Conversation Layer:自然语言交互
→ Agent Layer:推理、调用工具、获取信息
→ Structuring Layer:将结果转换成稳定对象
→ Validation Layer:Zod 或 JSON Schema 校验
→ Application Layer:API、数据库、UI 或后续 Workflow结构化输出不应被理解为“给整个 Agent 永久开启 JSON 模式”,它更像是在明确交付节点对输出施加 Schema 约束。
根据业务风险,还需要选择合适的错误策略:
strict:校验失败时抛错,适合数据库写入、支付、审核结果等严格业务。warn:记录警告并继续。fallback:校验失败时返回预设值,可用于非关键的推荐文案。
最后的判断
原生 Response Format 解决的是强约束,JSON Prompt Injection 解决的是兼容性,独立 structuring model 解决的是职责分离,prepareStep 则用显式阶段换取更强的控制。
对复杂 Agent,最稳定的思路不是让一个模型在一个阶段里承担所有责任,而是:
Agent 负责把事情做对
Structuring Model 负责把格式做对
Schema Validation 负责确保结果可信