LangChain v1.0 系列教程——第2章 工具系统

没有工具的 LLM 只是一个"纸上谈兵"的聊天机器人。本章将深入讲解如何为 LLM 赋予调用外部工具的能力,让它不仅能"思考",还能"执行"。

上一篇我们盘点了 LangChain 三大核心组件,把模型、提示词与输出解析串成了基础链路。但链路再顺,LLM 也只会"说"------查不了天气,算不了账,动不了数据库。本篇解决"从会说到会做":先用 @tool 定义工具,用 Pydantic 约束参数,再对比 bind_tools 与 create_agent 两种接入方式,最后拆解工具调用的完整生命周期。读完你就能搭出一个真正能干活的 Agent。


一、@tool 装饰器

先从最基础的做起:定义一个工具。v1.0 里这件事简单到只需要一个装饰器。

1.1 最简单的工具定义

在 LangChain v1.0 中,定义一个工具最简单的方式就是使用 @tool 装饰器(从 langchain.tools 导入):

复制代码
from langchain.tools import tool
​
@tool
def get_current_time() -> str:
    """获取当前的日期和时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

这个装饰器自动做了三件事:

  1. 函数名 → 工具名get_current_time 成为工具的名称

  2. docstring → 工具描述"""获取当前的日期和时间""" 成为工具的说明,LLM 会据此判断何时调用

  3. 类型注解 → JSON Schema :返回值 -> str 生成工具的 JSON Schema

    查看自动生成的工具定义

    print(get_current_time.name) # "get_current_time"
    print(get_current_time.description) # "获取当前的日期和时间"
    print(get_current_time.args) # {} # 无参数

1.2 带参数的工具

无参工具只是热身,真正有用的工具大多需要接收参数:

复制代码
@tool
def calculate(expression: str) -> str:
    """执行数学计算并返回结果"""
    try:
        # 安全地执行数学表达式
        result = eval(expression, {"__builtins__": {}}, {})
        return f"计算结果: {result}"
    except Exception as e:
        return f"计算错误: {str(e)}"
​
# 查看工具参数 schema
print(calculate.args)
# {'expression': {'title': 'Expression', 'description': '数学表达式', 'type': 'string'}}

1.3 类型注解自动生成 JSON Schema

LangChain 会根据 Python 类型注解自动生成 JSON Schema,支持丰富的类型:

复制代码
from typing import List, Optional, Dict, Any
​
@tool
def search_database(
    query: str,
    limit: int = 10,
    tags: Optional[List[str]] = None,
    sort_by: str = "relevance",
) -> List[Dict[str, Any]]:
    """搜索数据库中的记录
    
    Args:
        query: 搜索关键词
        limit: 返回结果数量上限
        tags: 筛选标签列表
        sort_by: 排序方式 (relevance / date / rating)
    """
    # 模拟数据库查询
    results = [
        {"id": 1, "title": f"结果 {i}", "score": 0.95 - i * 0.1}
        for i in range(limit)
    ]
    return results
​
# 自动生成的 args_schema
print(search_database.args)
# {
#   'query': {'title': 'Query', 'type': 'string'},
#   'limit': {'title': 'Limit', 'default': 10, 'type': 'integer'},
#   'tags': {'title': 'Tags', 'default': None, 'type': 'array', 'items': {'type': 'string'}},
#   'sort_by': {'title': 'Sort By', 'default': 'relevance', 'type': 'string'},
# }
类型标注与 JSON Schema 对照表
Python 类型 JSON Schema
str {"type": "string"}
int {"type": "integer"}
float {"type": "number"}
bool {"type": "boolean"}
List[str] {"type": "array", "items": {"type": "string"}}
Optional[str] {"type": "string", "default": None}
Dict[str, Any] {"type": "object"}

1.4 完整的日期工具示例

看一个把可选参数与默认值组合起来的完整示例:

复制代码
from langchain.tools import tool
from datetime import datetime, timedelta
from typing import Optional
​
@tool
def get_date_info(
    date_str: Optional[str] = None,
    format: str = "%Y-%m-%d",
) -> str:
    """获取日期信息,包括星期、是否为工作日等
    
    Args:
        date_str: 日期字符串,格式如 2024-01-15。默认为今天
        format: 日期格式,默认为 %Y-%m-%d
    """
    if date_str:
        date = datetime.strptime(date_str, "%Y-%m-%d")
    else:
        date = datetime.now()
    
    weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
    weekday = weekdays[date.weekday()]
    
    # 判断是否为工作日(周一到周五)
    is_workday = date.weekday() < 5
    
    return (
        f"日期: {date.strftime(format)}\n"
        f"星期: {weekday}\n"
        f"工作日: {'是' if is_workday else '否(周末)'}"
    )
​
# 测试
print(get_date_info.invoke({"date_str": "2024-06-01"}))
# 日期: 2024-06-01
# 星期: 星期六
# 工作日: 否(周末)

1.5 异步工具

对于 I/O 密集型操作(如网络请求、文件读写),应该使用异步工具:

复制代码
import httpx
from langchain.tools import tool
​
@tool
async def fetch_website_title(url: str) -> str:
    """异步获取网页的标题
    
    Args:
        url: 网页 URL
    """
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(url)
        response.raise_for_status()
        
        # 简单提取 title 标签
        content = response.text
        start = content.find("<title>") + len("<title>")
        end = content.find("</title>", start)
        if start > 0 and end > start:
            return content[start:end].strip()
        return "无法找到网页标题"
​
# 调用异步工具
# result = await fetch_website_title.ainvoke({"url": "https://example.com"})

1.6 工具与错误处理

工具在执行过程中可能出错,良好的错误处理能让 Agent 更好地应对异常:

复制代码
@tool
def divide_numbers(a: float, b: float) -> str:
    """计算两个数字的除法
    
    Args:
        a: 被除数
        b: 除数
    """
    try:
        result = a / b
        return f"{a} ÷ {b} = {result}"
    except ZeroDivisionError:
        return "错误:除数不能为零"
    except Exception as e:
        return f"计算错误:{str(e)}"
​
# Agent 会接收到错误信息并判断下一步操作

1.7 实用工具集锦

最后是一组可以直接抄走的实用工具:

复制代码
"""一个实用的工具集合"""
​
@tool
def get_random_joke() -> str:
    """获取一个随机笑话"""
    import random
    jokes = [
        "为什么程序员总是混淆万圣节和圣诞节?因为 Oct 31 == Dec 25!",
        "程序员 A:我有问题。程序员 B:让我 Google 一下。",
        "我不是在写 bug,我是在创建 feature。",
    ]
    return random.choice(jokes)
​
​
@tool
def convert_currency(
    amount: float,
    from_currency: str,
    to_currency: str,
) -> str:
    """模拟货币汇率转换
    
    Args:
        amount: 金额
        from_currency: 源货币代码(如 USD, CNY, EUR)
        to_currency: 目标货币代码
    """
    # 模拟汇率(实际场景应该调用 API)
    rates = {
        "USD": {"CNY": 7.24, "EUR": 0.92, "JPY": 149.50},
        "CNY": {"USD": 0.14, "EUR": 0.13, "JPY": 20.65},
        "EUR": {"USD": 1.09, "CNY": 7.87, "JPY": 162.50},
    }
    
    from_currency = from_currency.upper()
    to_currency = to_currency.upper()
    
    if from_currency in rates and to_currency in rates[from_currency]:
        rate = rates[from_currency][to_currency]
        result = amount * rate
        return f"{amount} {from_currency} = {result:.2f} {to_currency}(汇率:{rate})"
    return f"暂不支持 {from_currency} 到 {to_currency} 的汇率转换"
​
​
@tool
def generate_qr_code(data: str, size: int = 200) -> str:
    """生成 QR 码的 URL(使用在线 API)
    
    Args:
        data: 要编码的数据
        size: 图片尺寸(像素)
    """
    from urllib.parse import quote
    encoded = quote(data)
    return f"![QR Code](https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&data={encoded})"

二、工具参数类型安全

工具能跑还不够,得跑得稳------当 LLM 传来的参数类型不对、取值越界时,谁来兜底?答案是 Pydantic。

2.1 使用 Pydantic 定义复杂参数

当工具参数复杂时,使用 args_schema 结合 Pydantic 模型可以获得更好的类型安全:

复制代码
from pydantic import BaseModel, Field
from typing import List, Optional
from langchain.tools import tool
​
# 定义参数模型
class WeatherQuery(BaseModel):
    """天气查询参数"""
    city: str = Field(description="城市名称(中文)")
    days: int = Field(
        default=1,
        description="预报天数(1-7)",
        ge=1,
        le=7,
    )
    units: str = Field(
        default="metric",
        description="温度单位:metric(摄氏度)或 imperial(华氏度)",
    )
​
@tool(args_schema=WeatherQuery)
def get_weather(city: str, days: int = 1, units: str = "metric") -> str:
    """获取指定城市的天气预报"""
    # 工具实现...
    return f"{city}未来{days}天天气预报:晴天,温度适宜。"
​
# 查看 schema 可以看到明确的约束条件
print(get_weather.args)
# {
#   'city': {'title': 'City', 'description': '城市名称(中文)', 'type': 'string'},
#   'days': {'title': 'Days', 'description': '预报天数(1-7)', 'default': 1, 'minimum': 1, 'maximum': 7, 'type': 'integer'},
#   'units': {'title': 'Units', 'description': '温度单位:metric(摄氏度)或 imperial(华氏度)', 'default': 'metric', 'type': 'string'},
# }

2.2 Field 约束详解

Pydantic 的 Field 提供了丰富的参数约束:

复制代码
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from enum import Enum
​
class SortOrder(str, Enum):
    ASCENDING = "asc"
    DESCENDING = "desc"
​
class SearchParams(BaseModel):
    """搜索参数"""
    # 字符串约束
    query: str = Field(
        ...,
        description="搜索关键词",
        min_length=1,
        max_length=200,
    )
    
    # 数值范围约束
    page: int = Field(
        default=1,
        description="页码",
        ge=1,      # >= 1
        le=1000,   # <= 1000
    )
    
    page_size: int = Field(
        default=20,
        description="每页数量",
        ge=5,
        le=100,
    )
    
    # 固定选项约束
    category: Optional[Literal["tech", "science", "art", "music"]] = Field(
        default=None,
        description="搜索分类",
    )
    
    # 枚举约束
    sort: SortOrder = Field(
        default=SortOrder.DESCENDING,
        description="排序方式",
    )
    
    # 列表约束
    tags: List[str] = Field(
        default=[],
        description="筛选标签",
        max_length=10,
    )

2.3 嵌套对象的参数定义

对于更复杂的场景,Pydantic 支持嵌套模型:

复制代码
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
​
class Location(BaseModel):
    """位置信息"""
    city: str = Field(description="城市")
    district: Optional[str] = Field(description="区/县")
    latitude: Optional[float] = Field(description="纬度", ge=-90, le=90)
    longitude: Optional[float] = Field(description="经度", ge=-180, le=180)
​
class EventSearchParams(BaseModel):
    """活动搜索参数(嵌套结构)"""
    keyword: str = Field(description="搜索关键词")
    location: Location = Field(description="位置信息")
    date_range: Optional[str] = Field(
        description="日期范围,如 '2024-01-01~2024-01-31'"
    )
    max_price: Optional[float] = Field(
        description="最高价格",
        ge=0,
    )
    categories: List[str] = Field(
        default=[],
        description="活动分类",
    )
​
@tool(args_schema=EventSearchParams)
def search_events(
    keyword: str,
    location: dict,
    date_range: Optional[str] = None,
    max_price: Optional[float] = None,
    categories: List[str] = [],
) -> str:
    """搜索本地活动"""
    # location 参数接收一个字典,由 LLM 根据 Location 模型自动生成
    city = location.get("city", "未知")
    return f"在{city}找到以下关于'{keyword}'的活动..."

2.4 类型安全的重要性

不使用类型约束的风险:

复制代码
# ❌ 危险:没有类型约束
@tool
def update_record(record_id, data):
    """更新记录"""
    # 如果 LLM 传入了错误类型,这里可能崩溃
    # 或者更糟:SQL 注入
    pass
​
# ✅ 安全:有类型约束
class UpdateRecordParams(BaseModel):
    record_id: int = Field(..., description="记录ID", gt=0)
    data: str = Field(..., description="更新数据", max_length=1000)
    timestamp: Optional[datetime] = Field(default=None, description="更新时间")
​
@tool(args_schema=UpdateRecordParams)
def update_record_safe(record_id: int, data: str, timestamp: Optional[datetime] = None) -> str:
    """安全地更新记录"""
    # 参数已经过 Pydantic 验证
    return f"记录 {record_id} 已更新"

三、bind_tools 与 create_agent

工具备好了,接下来把它交给 LLM------这一步有两条路:底层的 bind_tools 和高层的 create_agent。

3.1 方式一:手动 bind_tools(底层控制)

如果你需要手动控制工具绑定的细节,可以直接使用 bind_tools

复制代码
from langchain.chat_models import init_chat_model
from langchain.tools import tool
​
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
    """获取城市天气"""
    return f"{city}:晴天,20°C"
​
@tool
def get_current_time() -> str:
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
​
# 2. 创建 LLM 并绑定工具
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
tools = [get_weather, get_current_time]
llm_with_tools = llm.bind_tools(tools)
​
# 3. 调用------LLM 会自主决定是否使用工具
response = llm_with_tools.invoke("北京今天天气怎么样?")
# 如果 LLM 决定调工具,response.tool_calls 会有内容
tool_choice 参数控制

tool_choice 控制 LLM 调用工具的行为模式:

复制代码
# tool_choice = "auto"(默认)
# LLM 自主决定是否调用工具、调用哪个工具
llm_auto = llm.bind_tools(tools, tool_choice="auto")
​
# tool_choice = "any"(或 "required")
# LLM 必须调用工具,不能直接文本回答
llm_force = llm.bind_tools(tools, tool_choice="any")
​
# tool_choice = "none"
# LLM 不能调用任何工具,只做文本生成
llm_none = llm.bind_tools(tools, tool_choice="none")
​
# tool_choice = {"type": "function", "function": {"name": "get_weather"}}
# 强制使用指定工具
llm_specific = llm.bind_tools(tools, tool_choice={
    "type": "function",
    "function": {"name": "get_weather"}
})

三种模式的对比:

复制代码
# auto 模式:灵活,但不一定调用工具
response = llm_auto.invoke("你好!")
print(response.tool_calls)  # []  # 打招呼不调用工具
​
# any 模式:必须调用工具
response = llm_force.invoke("你好!")
print(response.tool_calls)  
# [ToolCall(name='get_current_time', args={}, id='...')]
​
# none 模式:不能调用工具
response = llm_none.invoke("北京天气怎么样?")
print(response.tool_calls)  # []

3.2 方式二:create_agent(v1.0 推荐)

v1.0 推荐直接使用 create_agent,它会自动处理工具绑定和调用循环:

复制代码
from langchain.agents import create_agent
from langchain.tools import tool
​
@tool
def get_weather(city: str) -> str:
    """获取城市天气"""
    return f"{city}:晴天,20°C"
​
@tool
def get_current_time() -> str:
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
​
# create_agent 自动调用 bind_tools
agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_weather, get_current_time],
)
​
# 一次 invoke,内部自动完成:推理→调用工具→观察结果→继续推理→输出
result = agent.invoke({
    "messages": [("human", "北京天气怎么样?现在几点了?")]
})
print(result["messages"][-1].content)

create_agent 相比于手动的优势:

维度 手动 bind_tools create_agent
工具绑定 手动调用 bind_tools 自动完成
调用循环 需要自己实现 内置 LangGraph 循环
记忆持久化 需额外配置 checkpointer 支持 checkpointer 参数
流式输出 需额外配置 内置支持
Middleware 需手动集成 支持 middleware 参数
结构化输出 需额外处理 支持 response_format 参数

3.3 tool_calls 数据结构

当 LLM 决定调用工具时,返回的 AIMessage 会包含 tool_calls 属性:

复制代码
response = llm_with_tools.invoke("北京天气怎么样?")
​
# 查看 tool_calls 结构
for tc in response.tool_calls:
    print(f"工具名称: {tc['name']}")
    print(f"参数: {tc['args']}")
    print(f"调用ID: {tc['id']}")
    print("---")
​
# 实际输出示例:
# 工具名称: get_weather
# 参数: {'city': '北京'}
# 调用ID: call_abc123...

tool_calls 的完整结构:

复制代码
# 标准的 ToolCall 结构
tool_call = {
    "name": "get_weather",           # 工具名称
    "args": {                        # 调用参数
        "city": "北京"
    },
    "id": "call_abc123def456",       # 唯一调用 ID(用于匹配 ToolMessage)
    "type": "tool_call",             # 类型标识
}

3.4 处理并行工具调用

LLM 支持一次返回多个工具调用(并行调用),这在需要获取多种信息时非常高效:

复制代码
from langchain.messages import HumanMessage, ToolMessage
​
# 模拟一个需要多种信息的查询
response = llm_with_tools.invoke(
    "北京和上海今天天气怎么样?现在几点了?"
)
​
# LLM 可能同时调用三个工具
print(f"同时调用 {len(response.tool_calls)} 个工具:")
for tc in response.tool_calls:
    print(f"  → {tc['name']}({tc['args']})")
​
# 并行执行所有工具调用
results = []
for tc in response.tool_calls:
    if tc["name"] == "get_weather":
        result = get_weather.invoke(tc["args"])
    elif tc["name"] == "get_current_time":
        result = get_current_time.invoke(tc["args"])
    results.append(ToolMessage(content=result, tool_call_id=tc["id"]))

四、工具调用生命周期

前几节陆续出现了 tool_calls、ToolMessage 这些名词,这一节把它们串成一条完整的链路。

4.1 完整流程图解

复制代码
用户输入:"北京今天天气怎么样?适合穿什么衣服?"
       │
       ▼
┌─────────────────────────────────────────────┐
│  步骤1: LLM 推理                             │
│  ─────────────────────────────────           │
│  输入: [...] HumanMessage("北京今天天气...")  │
│  思考: "用户想知道北京的天气,                 │
│         我有 get_weather 工具可以获取天气信息"  │
│  输出: AIMessage(tool_calls=[...])            │
└─────────────────────────────────────────────┘
       │
       │  tool_calls 非空
       ▼
┌─────────────────────────────────────────────┐
│  步骤2: 解析 tool_calls                      │
│  ─────────────────────────────────           │
│  for tc in response.tool_calls:              │
│      tool_name = tc["name"]                  │
│      tool_args = tc["args"]                  │
│      tool_id = tc["id"]                      │
└─────────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│  步骤3: 执行工具函数                          │
│  ─────────────────────────────────           │
│  result = get_weather(city="北京")           │
│  → "晴天,15-25°C,微风"                     │
└─────────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│  步骤4: 包装为 ToolMessage                   │
│  ─────────────────────────────────           │
│  ToolMessage(                                │
│    content="晴天,15-25°C,微风",             │
│    tool_call_id="call_xxx"                   │
│  )                                           │
└─────────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────────┐
│  步骤5: 送回 LLM 继续推理                    │
│  ─────────────────────────────────           │
│  输入: [...原始消息...,                       │
│         AIMessage(tool_calls),               │
│         ToolMessage(天气结果)]                │
│  思考: "北京15-25°C,适合穿薄外套"             │
│  输出: AIMessage(content="北京今天...")       │
└─────────────────────────────────────────────┘
       │
       │ tool_calls 为空 → 输出最终答案
       ▼
    最终输出:"北京今天天气晴朗,气温15-25°C,
              建议穿薄外套或长袖衬衫。"

4.2 create_agent 内部的生命周期

当使用 create_agent 时,上述生命周期由 LangGraph 底层自动管理。以下是等价的手动实现,便于理解内部机制:

复制代码
from langchain.agents import create_agent
from langchain.tools import tool
from langchain.messages import HumanMessage, AIMessage, ToolMessage
​
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C"}
    return data.get(city, f"{city}天气数据不可用")
​
@tool
def calculator(expression: str) -> str:
    """执行数学计算"""
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"计算错误: {e}"
​
# 2. 使用 create_agent(一行代码,自动管理生命周期)
agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_weather, calculator],
)
​
# 3. 运行------内部自动完成 推理→调用→观察→循环
result = agent.invoke({
    "messages": [("human", "北京天气怎么样?25乘以4等于多少?")]
})
print(result["messages"][-1].content)

4.3 手动实现工具调用循环

如果想深入理解底层机制,可以手动实现:

复制代码
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, AIMessage, ToolMessage
from langchain.tools import tool
​
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C"}
    return data.get(city, f"{city}天气数据不可用")
​
@tool
def calculator(expression: str) -> str:
    """执行数学计算"""
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"计算错误: {e}"
​
# 2. 工具映射表
tools_map = {
    "get_weather": get_weather,
    "calculator": calculator,
}
tools_list = list(tools_map.values())
​
# 3. 初始化 LLM
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools_list)
​
# 4. 手动实现工具调用循环
def tool_calling_loop(user_input: str, max_iterations: int = 5):
    """工具调用主循环"""
    messages = [HumanMessage(content=user_input)]
    
    for iteration in range(max_iterations):
        print(f"\n{'='*50}")
        print(f"迭代 {iteration + 1}")
        print(f"{'='*50}")
        
        # Step 1: LLM 推理
        response = llm_with_tools.invoke(messages)
        messages.append(response)
        
        # 检查是否有工具调用
        if not response.tool_calls:
            print("→ LLM 决定直接回答")
            break
        
        # Step 2 & 3: 解析并执行工具
        for tc in response.tool_calls:
            tool_name = tc["name"]
            tool_args = tc["args"]
            tool_id = tc["id"]
            
            print(f"→ 调用工具: {tool_name}({tool_args})")
            
            # 执行工具
            tool_fn = tools_map.get(tool_name)
            if tool_fn:
                result = tool_fn.invoke(tool_args)
            else:
                result = f"错误:未知工具 {tool_name}"
            
            print(f"→ 工具结果: {result}")
            
            # Step 4: 包装为 ToolMessage
            tool_message = ToolMessage(
                content=str(result),
                tool_call_id=tool_id,
            )
            messages.append(tool_message)
        
        # Step 5: 继续循环,LLM 会基于工具结果继续推理
    
    return messages[-1].content
​
# 5. 运行
result = tool_calling_loop("北京天气怎么样?25乘以4等于多少?")
print(f"\n最终答案: {result}")

4.4 使用 ToolNode

LangGraph 提供了 ToolNode 来简化工具执行步骤:

复制代码
from langgraph.prebuilt import ToolNode
​
# 创建 ToolNode
tool_node = ToolNode(tools_list)
​
# 执行工具------自动处理 tool_calls 的解析和 ToolMessage 包装
messages = [HumanMessage(content="北京天气怎么样?")]
response = llm_with_tools.invoke(messages)
​
# 将 LLM 回复和工具节点串联
tool_messages = tool_node.invoke({"messages": [response]})
for msg in tool_messages:
    print(f"{msg.type}: {msg.content}")
# tool: 晴天,15-25°C

4.5 完整的 Agent 实现(手写 LangGraph)

如果需要对 Agent 行为进行精细控制,可以手写 LangGraph:

复制代码
from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import InMemorySaver
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from typing import Literal
​
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
    """获取城市天气"""
    data = {"北京": "晴天,15-25°C", "上海": "多云,20-28°C", "广州": "阵雨,25-32°C"}
    return data.get(city, f"暂无{city}的天气数据")
​
@tool
def calculator(expression: str) -> str:
    """执行数学计算"""
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"计算错误: {e}"
​
tools = [get_weather, calculator]
llm = init_chat_model("openai:gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
​
# 2. 构建图
def call_model(state: MessagesState) -> MessagesState:
    """Agent 节点:LLM 推理"""
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}
​
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
    """条件边:判断是否继续调用工具"""
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return "__end__"
​
# 3. 编译图
graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
​
# 添加记忆
memory = InMemorySaver()
app = graph.compile(checkpointer=memory)
​
# 4. 运行
config = {"configurable": {"thread_id": "1"}}
result = app.invoke(
    {"messages": [("human", "北京天气怎么样?")]},
    config,
)
print(result["messages"][-1].content)

五、Middleware 与工具控制

工具能调用只是第一步,生产环境中还需要对调用过程做更精细的控制:

中间件(Middleware)系统提供了更精细的工具控制能力,包括自动重试(ToolRetryMiddleware)、人工审批(HumanInTheLoopMiddleware)、敏感信息过滤(PIIMiddleware)等。详见第4章 Middleware 系统


六、MCP (Model Context Protocol) 简介

另一个值得关注的方向:工具未必都要自己写,也可以通过协议接入现成的工具生态:

MCP 是标准化 LLM 与外部工具通信的开放协议。LangChain 通过 MCP 集成包支持将远程 MCP Server 暴露的接口作为工具使用。详见第15章 上下文工程与MCP


七、本章小结

回顾本章:@tool 装饰器让一个普通函数摇身变成 LLM 可调用的工具,Pydantic 为参数加上类型与约束的双保险,create_agent 则把绑定、执行、循环一股脑托管。三者拼出一条 tool_calls 到 ToolMessage 的完整闭环,这正是 Agent 从"能聊"走向"能干活"的地基。下一篇我们转向 LCEL(LangChain 表达式语言),看如何用管道操作符把这些组件灵活编排起来。

核心公式:

复制代码
v1.0 工具系统 = @tool + Pydantic 类型安全 + create_agent
相关推荐
艾醒(AiXing-w)2 小时前
LangChain 1.0 入门(九):Agent 核心概念与技术架构
人工智能·chatgpt·langchain
多学一分钟3 小时前
讲清 Agent 记忆与上下文管理:长短期记忆、多轮对话、上下文压缩
langchain·agent
聪明蛋子哟4 小时前
从LangChain到LangGraph:Python与Java双栈Agent开发实战对比
java·ai·langchain
Lyra_Infra5 小时前
从一段错误 JSON 说起:Policy、Role 与 IAM
后端·json·aigc
羑悻的小杀马特6 小时前
数据织网者:Jsoncpp库深度解析——从C++原生JSON处理到工程级数据交互实战全攻略
c++·json·交互·jsoncpp·原生库
代码什么用6 小时前
JSON一文速通
java·json
小叶肥辉7 小时前
LangChain链和LangGraph图的学习笔记【四】——(1)调用方法:invoke(2)提示语模板:PromptTemplate
python·langchain·prompt
小小测试开发1 天前
LLM 结构化输出测试:Schema 契约 + 故障注入,让工具调用的 JSON 不再靠重试赌运气
人工智能·json
hasty1 天前
_proto__ 不是普通键:从 JSON 反序列化看原型污染
安全·json