小节二:从 Python 类型标注自动生成 Schema——真正把重复劳动交给程序

假设我们有:

def read_file(
    path: str,
    encoding: str = "utf-8",
) -> str:
    """读取指定文本文件。"""

Python 已经包含很多信息:

函数名:
read_file

描述:
读取指定文本文件。

参数:
path
encoding

类型:
str
str

必填:
path

可选:
encoding

我们完全可以通过 inspect 自动提取。

这里其实已经出现了自动生成 Schema 的核心思路:函数签名里本来就同时放着“参数名、类型、默认值”三类信息,我们做的只是把它们翻译成模型侧的结构。下面这张图可以把“Python 世界”和“Schema 世界”的对应关系先建立起来。

图片[1]-小节二:从 Python 类型标注自动生成 Schema——真正把重复劳动交给程序-大赛人网

尤其要分清两个维度:类型标注决定值允许是什么,默认值决定参数能不能省略。 所以 Optional[str] 和“有默认值”不是一回事,这也是后面生成 required 时最容易写错的地方。

这里有一个 Python 3.9 下很容易忽略的细节。代码如果启用了:

from __future__ import annotations

注解可能会以字符串形式保存。此时 parameter.annotation is str 不一定成立,即使源码里明明写了 path: str。因此后面的完整实现会先调用 typing.get_type_hints(),把注解解析成真实类型,再交给转换器。少了这一步,生成器往往不会报错,只会悄悄把类型映射错,这反而更难排查。

Python 函数签名里已经藏着协议

使用:

inspect.signature(func)

可以拿到:

(path: str, encoding: str = 'utf-8') -> str

再遍历:

signature.parameters

就能拿到每个参数。

先建立类型映射

最基础:

PYTHON_TO_JSON_TYPE = {
    str: "string",
    int: "integer",
    float: "number",
    bool: "boolean",
}

然后:

annotation = parameter.annotation

如果是:

str

转:

string

如何判断 required?

Python 参数:

inspect.Parameter.empty

表示没有默认值。

所以:

if parameter.default is inspect.Parameter.empty:
    required.append(name)

这就自动生成必填字段。

自动生成最小 Schema

下面代码完整可运行:

输出会接近:

{
  "name": "read_file",
  "description": "读取指定文本文件。",
  "parameters": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string"
      },
      "encoding": {
        "type": "string"
      }
    },
    "required": [
      "path"
    ]
  }
}

现在 Python 函数一改:

def read_file(
    path: str,
    encoding: str = "utf-8",
    max_chars: int = 10000,
):
    ...

Schema 会自动跟着变化。

这就是自动生成的价值。

list 怎么处理?

例如:

def read_files(
    paths: list[str],
):
    ...

Schema 应该更像:

{
  "type": "array",
  "items": {
    "type": "string"
  }
}

所以类型映射不能永远只是一个 dict。

我们需要读取:

typing.get_origin()
typing.get_args()

Optional 怎么处理?

例如:

from typing import Optional

def search_files(
    keyword: str,
    suffix: Optional[str] = None,
):
    ...

这里 suffix 可以是 string,也可以为空。

Python 3.9 中使用:

Optional[str]

本质上是联合类型。

我们可以生成:

{
  "type": [
    "string",
    "null"
  ]
}

或者采用适合目标 Provider 的兼容形式。

这里还要分清两件经常被混在一起的事:可为空 和可省略 不是同一个意思。

suffix: Optional[str]          必须传,但值可以是 null
suffix: str = ".py"           可以不传;传入时应是字符串
suffix: Optional[str] = None   可以不传,也可以显式传 null

类型标注决定一个值能不能是 null,有没有默认值决定参数是否进入 required。只看见 Optional 就从 required 中删除,是很常见的 Schema 生成错误。

课程这里先理解原理。

不同模型 Provider 最终如何包装 Schema,会留在 Provider Adapter 层。

一个增强版类型转换器

下面代码支持:

str
int
float
bool
list[T]
dict[str, T]
Optional

关于可变默认值的一个额外提醒

上面为了展示 Schema,我们写了:

roots: list[str] = ["."]

真实代码里不建议把可变 list 直接作为默认值。

更稳的是:

roots: Optional[list[str]] = None

然后函数内部:

if roots is None:
    roots = ["."]

这不是 Tool Schema 特有问题,而是 Python 本身的重要工程习惯。

自动生成并不意味着“完全不用人工设计”

这是本节非常重要的一个观点。

自动生成最适合处理:

name
参数名
参数类型
required

但 Tool Description、参数语义、业务限制,很多时候仍然需要人工设计。

例如:

def query_metric(
    metric: str,
):
    """查询指标"""

虽然 Schema 能自动生成,但模型仍然不知道:

metric 能填什么?
收入叫 revenue 还是 income?
什么时候用这个 Tool?

所以后面我们还要给参数增加描述、枚举、范围等能力。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容