在使用大模型的工具调用时,我们需要编写JSON Schema,例如下图的tools字段的值:
图片
这个Schema写起来非常麻烦,括号太多了,看着眼花。不信你肉眼看看,你需要几秒钟才能分清楚type: "object"跟哪个字段在同一层级?这个Schema有没有什么办法自动生成呢?
LangChain提供了一个@tool装饰器来简化工具调用的JSON Schema,直接装饰函数就能使用了。例如:
复制import json
from langchain_core.tools.convert import tool
@tool(parse_docstring=True)
def parse_user_info(name: str, age: int, salary: float) -> bool:
"""
保存用户的个人信息
Args:
name: 用户名
age: 用户的年龄
salary: 用户的工资
"""
return True然后,我们可以通过打印函数名的.args_schema.model_json_schema()来获取到类似于Tool Calling的JSON Schema,如下图所示:
图片
这种方式有两个问题:
1. Tool Calling需要的JSON Schema中,参数名对应的字段应该是name,但这里导出来的是title。
2. 函数的docstring使用的是Google Style,跟Python的不一样。
在Python里面,我们写docstring时,一般这样写::param 参数名: 参数解释,例如下面这样:
复制import json
from langchain_core.tools.convert import tool
@tool
def parse_user_info(name: str, age: int, salary: float) -> bool:
"""
保存用户的个人信息
:param name: 用户名
:param age: 用户的年龄
:param salary: 用户的工资
:return: bool,成功返回True,失败返回False
"""
return True
schema = parse_user_info.args_schema.model_json_schema()
print(json.dumps(schema, ensure_ascii=False, indent=2))但使用这种方式定义的时候,@tool装饰器不能加参数parse_docstring=True,否则会报错。可如果不加,提取的信息里面,字段没有描述。效果如下图所示:
图片
这两个问题,其实有一个通用的解决办法,那就是直接使用`Pydantic`。实际上,LangChain本身使用的也是Pydantic。如下图所示:
图片
我之前写过一篇文章:一日一技:如何使用大模型提取结构化数据,介绍了一个第三方库,名叫`instructor`。它本质上就是把Pydantic定义的类转成Tool Calling需要的JSON Schema,然后通过大模型的Tool Calling来提取参数。使用使用它,我们可以非常容易的实现本文的目的。
使用Pydantic定义我们要提取的数据并转换为JSON Schema格式:
复制import json
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
"""
用户个人信息
"""
name: str = Field(..., descriptinotallow='用户的姓名')
age: int = Field(default=None, descriptinotallow='用户的年龄')
salary: float = Field(default=None, descriptinotallow='用户的工资')
schema = UserInfo.model_json_schema()
print(json.dumps(schema, indent=2, ensure_ascii=False))Field的第一个参数如果是三个点...,表示这个字段是必填字段。如果想把一个字段设定为可选字段,那么Field加上参数default=None。
运行效果如下图所示:
图片
参数描述直接写到参数字段定义里面,根本不需要担心注释格式导致参数没有描述,管他是Google Style还是Python Style。
接下来,我们要把Pydantic输出的这个格式转换为Tool Calling需要的JSON Schema格式。我们来看一下Instructor的源代码:
图片
把他这个代码复制出来,用来处理刚刚Pydantic生成的JSON Schema:
复制from docstring_parser import parse
def generate_tool_calling_schema(cls):
schema = cls.model_json_schema()
docstring = parse(cls.__doc__ or'')
parameters = {
k: v for k, v in schema.items() if k notin ("title", "description")
}
for param in docstring.params:
if (name := param.arg_name) in parameters["properties"] and (
description := param.description
):
if"description"notin parameters["properties"][name]:
parameters["properties"][name]["description"] = description
parameters["required"] = sorted(
k for k, v in parameters["properties"].items() if"default"notin v
)
if"description"notin schema:
if docstring.short_description:
schema["description"] = docstring.short_description
else:
schema["description"] = (
f"Correctly extracted `{cls.__name__}` with all "
f"the required parameters with correct types"
)
return {
"name": schema["title"],
"description": schema["description"],
"parameters": parameters,
}这里依赖一个第三方库,叫做docstring_parser,这个库的原理非常简单,就是正则表达处理docstring而已。大家甚至可以看一下他的源代码然后自己实现。
运行以后效果如下图所示。
图片
注意在参数信息里面,会有'default': null和title字段,这两个字段即使传给大模型也没有关系,它会自动忽略。如果大家觉得他们比较碍眼,也可以改动一下代码,实现跟Tool Calling 的JSON Schema完全一样:
复制from docstring_parser import parse
def generate_tool_calling_schema(cls):
schema = cls.model_json_schema()
docstring = parse(cls.__doc__ or'')
parameters = {
k: v for k, v in schema.items() if k notin ("title", "description")
}
for param in docstring.params:
if (name := param.arg_name) in parameters["properties"] and (
description := param.description
):
if"description"notin parameters["properties"][name]:
parameters["properties"][name]["description"] = description
parameters["required"] = sorted(
k for k, v in parameters["properties"].items() if"default"notin v
)
for prop_name, prop_schema in parameters["properties"].items():
prop_schema.pop("default", None)
prop_schema.pop('title', None)
if"description"notin schema:
if docstring.short_description:
schema["description"] = docstring.short_description
else:
schema["description"] = (
f"Correctly extracted `{cls.__name__}` with all "
f"the required parameters with correct types"
)
# 按 Tool Calling 规范封装:
return {
"type": "function",
"function": {
"name": schema["title"],
"description": schema["description"],
"parameters": parameters,
}
}运行效果如下图所示:
图片
最后给大家出个思考题:如果函数的参数包含嵌套参数,应该怎么处理?