在使用大模型的工具调用时,我们需要编写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, } }
运行效果如下图所示:
图片
最后给大家出个思考题:如果函数的参数包含嵌套参数,应该怎么处理?