本节摘要:上一节讲了输入这一半,本节讲输出——返回类型注解如何变成输出 Schema(结构化输出)。这是「类型即契约」的另一面:你写
-> int,SDK 生成输出 Schema;你写-> SearchResponse(BaseModel),SDK 用整个模型当 Schema。结构化输出的价值在于让应用(不只是模型)能拿到类型化数据做后续逻辑——模型拿到文本能继续对话,应用拿到结构化数据能直接用。我们会讲透两种返回形态(标量与模型)、content与structured_content的分工、以及如何控制output_schema。读完本节,你能让工具返回既能给模型看、又能给程序用的数据。
工具的返回值要同时服务两个受众,它们的「读取方式」完全不同:
| 受众 | 怎么读 | 需要什么形态 |
|---|---|---|
| 模型 | 读文本(把它当对话内容) | 文本(content) |
| 应用(宿主/你的代码) | 解析结构化数据(字段访问) | 结构化数据(structured_content) |
SDK 的做法是同时返回两份:
工具返回 add(1, 2) = 3 │ ▼ SDK 包装成: { "content": [{"type": "text", "text": "3"}], ← 给模型的文本 "structuredContent": {"result": 3} ← 给应用的结构化数据 }
content 让模型能「读懂」结果继续对话;structured_content 让应用能「直接用」结果(如把 3 存进数据库、传给下一个工具)。这两份内容同时存在,各取所需。
最简单的返回是标量(int、str、float、bool)。它的结构化输出会被包裹:
@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"]取实际值。记住这层包裹,避免「为什么多了一层」的困惑。
当返回类型是 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 |
对应数组/对象 | 集合结果 |
有时候你想更精细地控制输出 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 |
应用 | 类型化对象 | 应用字段访问,做后续逻辑 |
两者不冲突,SDK 自动都生成。模型读 content,应用读 structured_content,互不干扰。
💡 技巧:
content是文本化的,适合模型;structured_content是结构化的,适合程序。设计工具返回时,先想「应用需要哪些字段」(决定structured_content),再想「模型需要怎样的文本描述」(决定content)。两者分开设计,不要混为一谈。
content(文本,给模型)与 structured_content(结构化,给应用)。{"result": ...},因为结构化输出必须是对象。structured_output 标志控制是否生成结构化输出,除非返回无法 Schema 化,否则保持开启。content 与 structured_content 分工不冲突,前者给模型后者给程序,分开设计。输入与输出契约都清楚了,下一节讲 ToolAnnotations——声明工具的行为特征。