【零基础入门 LLM 开发 · Day 15】:LangChain 注册工具------告别手写 JSON Schema
场景
Day 8-9 用原生 SDK 手写工具说明书:一个 get_weather 就写了二十来行 JSON Schema。工具一多,手写说明书又臭又长,还容易写错字段名。
今天用 LangChain 把这件事变省力,三种姿势逐级进化:手写字典 → Pydantic 模型 → @tool 装饰器。
起步:bind_tools 一行顶一坨
工具说明书还是 Day 8 那套字典,但绑定和调用交给 LangChain:
python
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
load_dotenv()
model = init_chat_model(
model=os.getenv("YUMC_MODEL_NAME"),
model_provider="openai",
api_key=os.getenv("YUMC_MODEL_API_KEY"),
base_url=os.getenv("YUMC_MODEL_BASE_URL"),
)
# 用字典描述工具(和 Day 8 手写的一样)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如 杭州、北京"}
},
"required": ["city"],
},
},
}
]
# bind_tools() 将工具绑定到模型
model_with_tools = model.bind_tools(tools)
response = model_with_tools.invoke("杭州今天天气怎么样?")
if response.tool_calls:
for tc in response.tool_calls:
print(f"工具名称:{tc['name']}")
print(f"参数:{tc['args']}")
print(f"调用ID:{tc['id']}")
response.tool_calls 是个列表------AI 觉得需要的话会一次请求多个工具调用,别只取第一个。
进阶:Pydantic 模型直接当工具
手写字典的问题:字段名、类型、描述全靠人肉对齐。换成 Pydantic 模型,LangChain 自动帮你生成说明书:
python
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
"""查询指定城市的天气情况"""
city: str = Field(description="城市名称,如 杭州、北京")
unit: str = Field(
default="celsius",
description="温度单位,celsius(摄氏度)或 fahrenheit(华氏度)",
)
class CalculatorInput(BaseModel):
"""执行数学计算"""
expression: str = Field(description="要计算的数学表达式,如'(3+5)*2'")
# 传入 Pydantic 模型,LangChain 自动转换为工具描述
model_with_tools = model.bind_tools([WeatherInput, CalculatorInput])
response = model_with_tools.invoke("北京今天多少度?顺便帮我算一下123*456")
print(f"模型请求了{len(response.tool_calls)}个工具调用:")
for tc in response.tool_calls:
print(f"{tc['name']} ({tc['args']})")
为什么 Pydantic 模型能当工具?
LangChain 拿到对象后自动提取三样东西------和 Day 8 手写的 JSON Schema 是同一个东西,只是框架替你生成:
| Pydantic 里的 | 变成说明书里的 |
|---|---|
| 类名 | function.name |
docstring("""查询指定城市的天气情况""") |
function.description |
字段类型 + Field(description=...) |
parameters 的 JSON Schema |
更简:@tool 装饰器
普通函数也能注册------函数名、docstring、类型注解就是现成的说明书:
python
from langchain_core.tools import tool
@tool
def get_location(location: str) -> str:
"""获取指定位置的经纬度"""
return f"获取{location}的经纬度"
# 和 Pydantic 模型混着用也没问题
model_with_tools = model.bind_tools([WeatherInput, CalculatorInput, get_location])
response = model_with_tools.invoke("北京今天多少度?顺便帮我算一下123*456,经纬度")
三种方式对比
| 方式 | 工具名 | 描述 | 参数 schema | 适用场景 |
|---|---|---|---|---|
bind_tools(字典) |
手写 | 手写 | 手写 | 和 Day 8 等价,没必要再用 |
bind_tools(Pydantic) |
类名 | docstring | 字段定义 | 参数结构复杂、需要校验/复用 |
@tool |
函数名 | docstring | 类型注解 | 最常用,一行搞定 |
@tool 的本质:LangChain 根据函数签名现场生成一个 Pydantic 模型 (get_location.args_schema 可以看),所以后两种方式殊途同归。
和 Day 13 的血缘
with_structured_output(method="function_calling") 的底层就是 bind_tools:Pydantic 模型 → JSON Schema → 发给模型。
- 注册工具:模型主动"申请"调用,你执行后把结果喂回去(Day 8 流程);
- 结构化输出:强制模型把你的 Pydantic 模型当工具调用一次,直接拿到参数当结果。
同一套机制,两个用途。学会一个,另一个就是白送。
运行效果
模型请求了3个工具调用:
WeatherInput ({'city': '北京', 'unit': 'celsius'})
CalculatorInput ({'expression': '123*456'})
get_location ({'location': '北京'})
一个问题触发三个工具调用------AI 自己决定"要不要用、用哪几个、传什么参数"。
今日踩坑点
@tools写成装饰器 :from langchain import tools导入的是模块 ,模块不能调用,报TypeError: 'module' object is not callable。要用from langchain_core.tools import tool然后@tool(或者@tools.tool)。Field(decription=...)拼错 :Pydantic 不认识的关键字被当"额外参数",只给一条 DeprecationWarning 不报错------结果描述丢了,模型只能瞎猜参数含义,工具质量悄悄下降。警告也要看!- f-string 括号位置 :
f"{tc['name'] ({tc['args']})}"会把tc['name']当函数调用(name 后面跟了个括号),报'str' object is not callable。正确写法f"{tc['name']} ({tc['args']})"。 - Pydantic 类名直接变工具名 :
WeatherInput这种名字出现在给模型看的说明书里很怪。@tool用函数名更自然;坚持用 Pydantic 就把类名起得像工具名。
今日小练习
- 必做:用
@tool写一个get_current_time(),跑通"现在几点了?"------对比 Day 8 的必做题,感受少写了多少行。 - 进阶:
print(get_current_time.args_schema),看看 @tool 现场生成的 Pydantic 模型长什么样。 - 思考:把 Day 8 手写说明书的工具全部换成 @tool 版本,模型的工具选择行为有变化吗?
明日预告
Day 15:工具又多又杂时,AI 容易选错、token 也贵。学公司设前台------主 Agent 只管转接,子 Agent 各管一摊。
附录:完整练习代码
langchainTest4-tools.py(字典版 bind_tools)
python
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
load_dotenv()
model = init_chat_model(
model=os.getenv("YUMC_MODEL_NAME"),
model_provider="openai",
api_key=os.getenv("YUMC_MODEL_API_KEY"),
base_url=os.getenv("YUMC_MODEL_BASE_URL"),
)
# 用字典描述工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如 杭州、北京"}
},
"required": ["city"],
},
},
}
]
# bind_tools() 将工具绑定到模型
model_with_tools = model.bind_tools(tools)
response = model_with_tools.invoke("杭州今天天气怎么样?")
if response.tool_calls:
print("模型调用了以下工具")
for tc in response.tool_calls:
print(f"工具名称:{tc['name']}")
print(f"参数:{tc['args']}")
print(f"调用ID:{tc['id']}")
else:
print(f"模型直接回复:{response.content}")
langchainTest4-tools2.py(Pydantic 模型版)
python
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
model = init_chat_model(
model=os.getenv("YUMC_MODEL_NAME"),
model_provider="openai",
temperature=0,
api_key=os.getenv("YUMC_MODEL_API_KEY"),
base_url=os.getenv("YUMC_MODEL_BASE_URL"),
)
class WeatherInput(BaseModel):
"""查询指定城市的天气情况"""
city: str = Field(description="城市名称,如 杭州、北京")
unit: str = Field(
default="celsius",
description="温度单位,celsius(摄氏度)或 fahrenheit(华氏度)",
)
class CalculatorInput(BaseModel):
"""执行数学计算"""
expression: str = Field(description="要计算的数学表达式,如'(3+5)*2'")
# 传入pydantic 模型,LangChain 自动转换为工具描述
model_with_tools = model.bind_tools([WeatherInput, CalculatorInput])
response = model_with_tools.invoke("北京今天多少度?顺便帮我算一下123*456")
print(f"模型请求了{len(response.tool_calls)}个工具调用:")
for tc in response.tool_calls:
print(f"{tc['name']} ({tc['args']})")
langchainTest4-tools3.py(@tool 装饰器版)
python
import os
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
load_dotenv()
model = init_chat_model(
model=os.getenv("YUMC_MODEL_NAME"),
model_provider="openai",
temperature=0,
api_key=os.getenv("YUMC_MODEL_API_KEY"),
base_url=os.getenv("YUMC_MODEL_BASE_URL"),
)
class WeatherInput(BaseModel):
"""查询指定城市的天气情况"""
city: str = Field(description="城市名称,如 杭州、北京")
unit: str = Field(
default="celsius",
description="温度单位,celsius(摄氏度)或 fahrenheit(华氏度)",
)
class CalculatorInput(BaseModel):
"""执行数学计算"""
expression: str = Field(description="要计算的数学表达式,如'(3+5)*2'")
@tool
def get_location(location: str) -> str:
"""获取指定位置的经纬度"""
return f"获取{location}的经纬度"
# 传入pydantic 模型,LangChain 自动转换为工具描述
model_with_tools = model.bind_tools([WeatherInput, CalculatorInput, get_location])
response = model_with_tools.invoke("北京今天多少度?顺便帮我算一下123*456,经纬度")
print(f"模型请求了{len(response.tool_calls)}个工具调用:")
for tc in response.tool_calls:
print(f"{tc['name']} ({tc['args']})")