一日一技:如何快速生成大模型工具調用的JSON Schema
在使用大模型的工具調用時,我們需要編寫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 規(guī)范封裝:
return {
"type": "function",
"function": {
"name": schema["title"],
"description": schema["description"],
"parameters": parameters,
}
}
運行效果如下圖所示:
圖片
最后給大家出個思考題:如果函數的參數包含嵌套參數,應該怎么處理?