4.2 结构化输出:返回类型注解即输出 Schema


4.2 结构化输出:返回类型注解即输出 Schema

本节摘要:上一节讲了输入这一半,本节讲输出——返回类型注解如何变成输出 Schema(结构化输出)。这是「类型即契约」的另一面:你写 -> int,SDK 生成输出 Schema;你写 -> SearchResponse(BaseModel),SDK 用整个模型当 Schema。结构化输出的价值在于让应用(不只是模型)能拿到类型化数据做后续逻辑——模型拿到文本能继续对话,应用拿到结构化数据能直接用。我们会讲透两种返回形态(标量与模型)、contentstructured_content 的分工、以及如何控制 output_schema。读完本节,你能让工具返回既能给模型看、又能给程序用的数据。

一、两个受众,两份内容

工具的返回值要同时服务两个受众,它们的「读取方式」完全不同:

受众 怎么读 需要什么形态
模型 读文本(把它当对话内容) 文本(content)
应用(宿主/你的代码) 解析结构化数据(字段访问) 结构化数据(structured_content)

SDK 的做法是同时返回两份:

工具返回 add(1, 2) = 3 │ ▼ SDK 包装成: { "content": [{"type": "text", "text": "3"}], ← 给模型的文本 "structuredContent": {"result": 3} ← 给应用的结构化数据 }

content 让模型能「读懂」结果继续对话;structured_content 让应用能「直接用」结果(如把 3 存进数据库、传给下一个工具)。这两份内容同时存在,各取所需。

二、标量返回:被包裹成

最简单的返回是标量(intstrfloatbool)。它的结构化输出会被包裹:

@mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b

返回 3 时:

content(给模型): "3" ← 文本 structured_content(给应用): {"result": 3} ← 包了一层

为什么要包一层 {"result": ...}?因为结构化输出必须是对象(JSON object),这是协议规定。裸标量不是合法的对象,所以 SDK 自动用一个固定键 result 把它包起来。

💡 技巧:写客户端代码(第 9 章)取结构化输出时,标量返回要 result["structured_content"]["result"] 取实际值。记住这层包裹,避免「为什么多了一层」的困惑。

三、模型返回:直接作为 Schema

当返回类型是 Pydantic BaseModel,SDK 用整个模型当输出 Schema,不再包裹:

from pydantic import BaseModel class CalcResult(BaseModel): value: int operation: str is_success: bool @mcp.tool() def add(a: int, b: int) -> CalcResult: """Add two numbers with metadata.""" return CalcResult(value=a + b, operation="add", is_success=True)

返回时:

content(给模型): "value=3 operation=add is_success=True" ← 文本化 structured_content(给应用): {"value": 3, "operation": "add", "is_success": True} ← 直接是模型

两种返回形态的差别对照:

返回类型 结构化输出形态 何时用
标量(int / str 等) {"result": <标量>}(包裹) 简单结果,一个值
Pydantic 模型 模型本身(不包裹) 复杂结果,多个字段
list / dict 对应数组/对象 集合结果

四、output_schema:显式控制输出 Schema

有时候你想更精细地控制输出 Schema——比如声明「这个工具可能返回多种结构之一」。SDK 让你通过 output_schema 参数或返回类型注解来控制:

# 方式一:用返回类型注解(推荐,自动推断) @mcp.tool() def add(a: int, b: int) -> CalcResult: ... # 方式二:用结构化输出标志精细控制 @mcp.tool(structured_output=True) # 启用结构化输出 def add(...): ...

structured_output 标志的常见取值:

取值 含义
True(默认,有返回注解时) 生成并返回结构化输出
False 不返回结构化输出,只返回文本
不传 由 SDK 根据返回注解判断

⚠️ 注意:关掉结构化输出(structured_output=False)会让应用拿不到类型化数据,只能从文本里解析——这通常是个倒退。除非你的返回形态无法用 Schema 表达(如自由格式的长文本),否则建议保持开启。

五、结构化输出的实战价值

结构化输出最大的价值,在于让工具链成为可能。考虑这个场景:

工具 A:query_db(sql) → {"rows": [...], "count": 10} (结构化) 工具 B:format_table(data) → "..." (文本) 应用编排: result_a = await client.call_tool("query_db", {...}) rows = result_a.structured_content["rows"] ← 直接拿结构化数据 result_b = await client.call_tool("format_table", {"data": rows})

如果 query_db 只返回文本,应用要从文本里解析出 rows——脆弱、易错。而结构化输出让应用直接字段访问,稳定可靠。这是「工具不只是给模型用,也给程序用」的关键。

六、content 与 structured_content 的分工

最后强调两者的分工,避免混用:

字段 给谁 形态 怎么用
content 模型 文本(可多段) 模型读文本,继续对话
structured_content 应用 类型化对象 应用字段访问,做后续逻辑

两者不冲突,SDK 自动都生成。模型读 content,应用读 structured_content,互不干扰。

💡 技巧:content 是文本化的,适合模型;structured_content 是结构化的,适合程序。设计工具返回时,先想「应用需要哪些字段」(决定 structured_content),再想「模型需要怎样的文本描述」(决定 content)。两者分开设计,不要混为一谈。

本节要点回顾

  1. 工具返回同时服务两个受众:模型(读文本)与应用(读结构化数据)。
  2. SDK 同时返回两份:content(文本,给模型)与 structured_content(结构化,给应用)。
  3. 标量返回被包裹成 {"result": ...},因为结构化输出必须是对象。
  4. Pydantic 模型返回直接作为 Schema,不再包裹,字段就是结构。
  5. structured_output 标志控制是否生成结构化输出,除非返回无法 Schema 化,否则保持开启。
  6. 结构化输出的最大价值是让工具链成为可能,应用能稳定地字段访问而非文本解析。
  7. contentstructured_content 分工不冲突,前者给模型后者给程序,分开设计。

输入与输出契约都清楚了,下一节讲 ToolAnnotations——声明工具的行为特征。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U