【零基础入门 LLM 开发 · Day 15】:LangChain 注册工具——告别手写 JSON Schema

【零基础入门 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 自己决定"要不要用、用哪几个、传什么参数"。

今日踩坑点

  1. @tools 写成装饰器 :from langchain import tools 导入的是模块 ,模块不能调用,报 TypeError: 'module' object is not callable。要用 from langchain_core.tools import tool 然后 @tool(或者 @tools.tool)。
  2. Field(decription=...) 拼错 :Pydantic 不认识的关键字被当"额外参数",只给一条 DeprecationWarning 不报错------结果描述丢了,模型只能瞎猜参数含义,工具质量悄悄下降。警告也要看!
  3. f-string 括号位置 :f"{tc['name'] ({tc['args']})}" 会把 tc['name'] 当函数调用(name 后面跟了个括号),报 'str' object is not callable。正确写法 f"{tc['name']} ({tc['args']})"。
  4. Pydantic 类名直接变工具名 :WeatherInput 这种名字出现在给模型看的说明书里很怪。@tool 用函数名更自然;坚持用 Pydantic 就把类名起得像工具名。

今日小练习

  1. 必做:用 @tool 写一个 get_current_time(),跑通"现在几点了?"------对比 Day 8 的必做题,感受少写了多少行。
  2. 进阶:print(get_current_time.args_schema),看看 @tool 现场生成的 Pydantic 模型长什么样。
  3. 思考:把 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']})")
相关推荐
Elastic 中国社区官方博客1 小时前
教程:监控 Java 应用
java·开发语言·数据库·elasticsearch·搜索引擎·全文检索
ShineWinsu1 小时前
对于JavaScript:初识JavaScript以及输入输出
java·开发语言·前端·javascript·css·面试·html
小小安于乱1 小时前
linux中java的springboot程序获取IP地址间歇性10秒卡顿问题
java·开发语言
一木 之林2 小时前
LangChain 第8章 记忆系统:短期/长期记忆与 PostgreSQL 持久化
数据库·postgresql·langchain
云絮.2 小时前
mybatis(二)
java·数据库·mybatis
西凉的悲伤2 小时前
Guava-Retrying 重试框架详解
java·guava·重试框架·guava-retrying
SunnyDays10112 小时前
使用 Java 高效管理 Excel 批注:添加、读取、修改和删除
java·开发语言·excel
const_qiu2 小时前
第 2.5 章 2.5.8:质量关 —— 安全审查、UI/UX 审查与踩坑复盘
java·人工智能·安全·ui·ux
chde2Wang2 小时前
hadoop部署错误-namenode address dfs.namenode.servicerpc-address not configured
java·大数据·hadoop