4.5 富 Schema:用 pydantic.Field 表达约束 本节摘要:这是第 4 章的收尾。前面讲了类型注解能表达基础契约(类型、必填性),但现实里的约束更细——年龄必须在 0-150、邮箱要符合正则、状态只能是几个枚举值、每个参数要有给模型看的描述。光靠类型注解表达不了这些,这就是 的用武之地。本节讲清它如何在参数上叠加描述、范围、正则、枚举等约束,生成更精确的 JSON Schema,从而减少模型的错误调用。读完本节,你能把工具契约写到「模型几乎不会调错」的程度。 一、类型注解的局限 先看类型注解表达不了的几种约束: 光看 ,模型只知道「这是个整数」。
本节摘要:这是第 4 章的收尾。前面讲了类型注解能表达基础契约(类型、必填性),但现实里的约束更细——年龄必须在 0-150、邮箱要符合正则、状态只能是几个枚举值、每个参数要有给模型看的描述。光靠类型注解表达不了这些,这就是
pydantic.Field的用武之地。本节讲清它如何在参数上叠加描述、范围、正则、枚举等约束,生成更精确的 JSON Schema,从而减少模型的错误调用。读完本节,你能把工具契约写到「模型几乎不会调错」的程度。
先看类型注解表达不了的几种约束:
@mcp.tool() def set_age(age: int) -> str: """Set user age.""" ...
光看 age: int,模型只知道「这是个整数」。但它不知道:
age 必须在 0-150 之间(传 -5 或 999 是错的)age 应该是成年(传 10 可能业务不允许)结果模型可能传 age=-5,你的工具执行出错或产生荒谬结果。类型注解只表达了「类型」,没表达「值的约束」。这就是 pydantic.Field 要补的缺口。
pydantic.Field 让你在参数上叠加多种约束,SDK 会把它们全部翻译进 JSON Schema:
| 能力 | 写法 | Schema 体现 |
|---|---|---|
| 描述 | Field(description="...") |
description 字段 |
| 范围 | Field(ge=0, le=150) |
minimum、maximum |
| 长度 | Field(min_length=1, max_length=100) |
minLength、maxLength |
| 正则 | Field(pattern=r"^\w+$") |
pattern |
| 默认值 | Field(default=10) |
default |
| 示例 | Field(examples=[...]) |
examples |
| 枚举 | Literal["a", "b"](类型层) |
enum |
用法示例:
from typing import Annotated, Literal from pydantic import Field @mcp.tool() def set_age( age: Annotated[int, Field(ge=0, le=150, description="用户年龄,0-150")], ) -> str: """Set user age.""" ... @mcp.tool() def create_user( name: Annotated[str, Field(min_length=1, max_length=50, description="用户名")], email: Annotated[str, Field(pattern=r"^[\w.]+@[\w.]+$", description="邮箱")], role: Literal["admin", "user", "guest"], # 枚举 ) -> str: """Create a user.""" ...
模型看到的 Schema 会包含所有这些约束,据此构造合法的调用参数。
富 Schema 的价值,在于让模型在调用前就知道边界,从而避免越界调用:
没有 Field 约束 模型:set_age(age=-5) ← 不知道下界,瞎填 工具:报错或产生荒谬结果 有 Field(ge=0, le=150) 约束 模型看到 Schema:age 的 minimum=0, maximum=150 模型:set_age(age=25) ← 知道范围,填合法值 工具:正常执行
几种约束各自阻止的错误:
| 约束 | 阻止的错误 |
|---|---|
ge / le(范围) |
越界数值(负年龄、超大数) |
min_length / max_length |
过短或过长字符串 |
pattern(正则) |
格式错误(非法邮箱、非法 ID) |
Literal(枚举) |
非法选项(传了不存在的角色) |
description |
误解参数含义(填错语义) |
description 特别值得单独讲——它直接发给模型,是「参数级」的说明。区别于工具的文档字符串(描述整个工具),description 描述单个参数:
@mcp.tool() def search( query: Annotated[str, Field( description="搜索关键词,支持中文,最长 100 字" )], limit: Annotated[int, Field( ge=1, le=50, default=10, description="返回结果数,1-50,默认 10" )], ) -> list: """Search items by query.""" ...
模型看到 Schema 时,会读到这些参数级描述,从而更准确地填值。好的 description 直接提升调用准确率——它告诉模型「这个参数该填什么、有什么限制」。
💡 技巧:写
description时,说清三件事:是什么(数据类型/含义)、限制(范围/格式)、默认(可省略时默认值)。例:「搜索关键词,支持中文,最长 100 字」同时说了含义、格式、限制。这种描述让模型填参数时几乎不会错。
当参数只能取几个固定值,用 Literal 类型(而不是 str)。这会被翻译成 Schema 的 enum:
from typing import Literal @mcp.tool() def set_env( env: Literal["dev", "staging", "prod"], # 只能这三个值 ) -> str: """Set deployment environment.""" ...
模型看到 Schema 里 env 的 enum: ["dev", "staging", "prod"],只会填这三个值之一,绝不会传 production 或 test。这比让模型猜「环境名该叫啥」可靠得多。
枚举适合的场景:
富 Schema 这么好,是不是越多越好?不是。过度的约束会让 Schema 臃肿,反而让模型困惑。平衡原则:
| 原则 | 说明 |
|---|---|
| 约束要反映真实业务规则 | 没有业务意义的约束不要加 |
| description 要简洁有用 | 长篇大论不如一句精准描述 |
| 枚举要完整 | 别漏掉合法取值,否则模型没法填 |
| 别用正则表达过于复杂的格式 | 模型可能理解不了,反而出错 |
⚠️ 注意:约束是「告诉模型边界」,不是「强制模型听话」。模型仍可能尝试越界(尤其指令强烈时),你的工具仍要做参数校验(第 4.1 节的 Schema 校验)。约束减少错误调用,但不替代运行时校验——两者配合才安全。
把本章学的合起来,看一个契约精准的工具:
from typing import Annotated, Literal from pydantic import BaseModel, Field from mcp.server import MCPServer from mcp.server.mcpserver import ToolAnnotations mcp = MCPServer("UserOps") class User(BaseModel): id: int name: str role: Literal["admin", "user", "guest"] @mcp.tool(annotations=ToolAnnotations(read_only=False, destructive=False)) def create_user( name: Annotated[str, Field( min_length=1, max_length=50, description="用户名,1-50 字符" )], email: Annotated[str, Field( pattern=r"^[\w.]+@[\w.]+$", description="合法邮箱" )], age: Annotated[int, Field( ge=0, le=150, description="年龄,0-150" )], role: Literal["admin", "user", "guest"], ) -> User: """创建用户,返回新建的用户对象。""" if email_exists(email): raise ValueError(f"邮箱已存在:{email}") return User(id=gen_id(), name=name, role=role)
这个工具的契约包含:
User 模型)ToolAnnotations)模型看到这份契约,几乎不可能调错——这就是「类型即契约」做到极致的样子。
pydantic.Field 表达值的约束(范围、长度、正则、枚举)。Literal)。description 是参数级说明,说清「是什么、限制、默认」三件事,提升准确率。Literal 限定枚举取值,模型只会填合法值,适合有限取值集合。第 4 章结束。你已经吃透了「类型即契约」的全部:输入 Schema、结构化输出、行为声明、错误处理、富 Schema。第 5 章转向资源与提示词这两类「非模型驱动」的原语。