LangGraph 条件路由:构建支持工具调用的智能 Agent

背景

在前两篇教程中,我们分别学习了 LangGraph 的快速入门和 StateGraph 基础。本文将深入探讨 LangGraph 的进阶特性------工具调用(Tool Calling)条件路由(Conditional Routing),通过构建一个支持 Tavily 搜索引擎的聊天机器人,掌握 ReAct 模式的完整实现。


实现原理

ReAct 模式

ReAct(Reasoning + Acting)是 LLM Agent 的核心模式:

markdown 复制代码
Reasoning(推理)    →    Acting(行动)    →    Observation(观察)
      │                       │                       │
      ▼                       ▼                       ▼
 理解用户需求            调用工具获取数据        整合结果生成回答
 判断需要什么信息        执行外部操作            形成完整回复

工具调用架构

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                    工具调用架构                          │
├─────────────────────────────────────────────────────────┤
│  1. 工具定义        │  TavilySearchResults              │
│  2. 工具绑定        │  llm.bind_tools(tools)            │
│  3. 工具节点        │  BasicToolNode 执行工具调用       │
│  4. 条件路由        │  route_tools 动态判断             │
│  5. 循环执行        │  chatbot ↔ tools 形成 ReAct 循环 │
└─────────────────────────────────────────────────────────┘

条件边 vs 普通边

特性 普通边 条件边
定义方式 add_edge() add_conditional_edges()
路由逻辑 固定 动态(基于状态)
返回值 节点名称或 END
使用场景 确定流程 分支决策

代码实现

完整代码

python 复制代码
"""
LangGraph 教程 - 为聊天机器人添加工具

本示例演示如何为 StateGraph 聊天机器人添加网页搜索工具。
官方教程地址:https://langchain-ai.github.io/langgraph/tutorials/introduction/
"""

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=UserWarning)

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage
from langchain_community.tools.tavily_search import TavilySearchResults
from dotenv import load_dotenv
import json
import os

load_dotenv()

# 检查 Tavily API Key 是否设置
if not os.getenv("TAVILY_API_KEY"):
    raise ValueError("TAVILY_API_KEY 未设置,请在 .env 文件中配置")


# ==================== 1. 定义状态 ====================
class State(TypedDict):
    """定义图的状态结构。"""
    messages: Annotated[list, add_messages]


# ==================== 2. 定义工具节点 ====================
class BasicToolNode:
    """工具节点:运行 LLM 请求的工具。"""

    def __init__(self, tools: list) -> None:
        self.tools_by_name = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        message = inputs.get("messages", [])[-1]
        outputs = []
        
        for tool_call in message.tool_calls:
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call["args"]
            )
            outputs.append(
                ToolMessage(
                    content=json.dumps(tool_result, ensure_ascii=False),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        return {"messages": outputs}


# ==================== 3. 定义路由函数 ====================
def route_tools(state: State):
    """
    条件边路由函数。
    检查聊天机器人输出中是否包含 tool_calls。
    """
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return END


# ==================== 4. 创建图 ====================
def create_graph():
    """创建并编译 StateGraph。"""
    graph_builder = StateGraph(State)
    
    llm = ChatOpenAI(
        model="Qwen/Qwen3-Next-80B-A3B-Instruct",
        openai_api_key=os.getenv("SILICONFLOW_API_KEY"),
        openai_api_base="https://api.siliconflow.cn/v1",
        temperature=0.7
    )
    
    # 创建 Tavily 搜索工具
    tool = TavilySearchResults(max_results=2)
    tools = [tool]
    
    # 绑定工具到 LLM
    llm_with_tools = llm.bind_tools(tools)
    
    def chatbot(state: State):
        """聊天机器人节点。"""
        return {"messages": [llm_with_tools.invoke(state["messages"])]}
    
    # 添加节点
    graph_builder.add_node("chatbot", chatbot)
    tool_node = BasicToolNode(tools=tools)
    graph_builder.add_node("tools", tool_node)
    
    # 添加边
    graph_builder.add_edge(START, "chatbot")
    
    # 添加条件边
    graph_builder.add_conditional_edges(
        "chatbot",
        route_tools,
        {"tools": "tools", END: END}
    )
    
    # 添加边:从 tools 回到 chatbot(形成循环)
    graph_builder.add_edge("tools", "chatbot")
    
    return graph_builder.compile()


# ==================== 5. 运行 ====================
def stream_graph_updates(graph, user_input: str):
    """流式处理图更新。"""
    for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
        for value in event.values():
            if "messages" in value:
                last_message = value["messages"][-1]
                if hasattr(last_message, "content") and last_message.content:
                    if not isinstance(last_message, ToolMessage):
                        print("助手:", last_message.content)


def main():
    """主函数 - 运行交互式聊天机器人。"""
    print("🤖 LangGraph 工具增强聊天机器人已启动!")
    print("=" * 50)
    print("提示:输入 'quit'、'exit' 或 'q' 退出对话\n")
    
    graph = create_graph()
    
    while True:
        try:
            user_input = input("用户: ")
            if user_input.lower() in ["quit", "exit", "q"]:
                print("\n👋 再见!")
                break
            stream_graph_updates(graph, user_input)
            print()
        except KeyboardInterrupt:
            print("\n\n👋 再见!")
            break
        except Exception as e:
            print(f"发生错误: {e}")
            break


if __name__ == "__main__":
    main()

核心代码解析

工具绑定机制

python 复制代码
# 创建工具
tool = TavilySearchResults(max_results=2)
tools = [tool]

# 绑定工具到 LLM
llm_with_tools = llm.bind_tools(tools)

bind_tools() 将工具描述注入 LLM 系统提示,使 LLM 能够:

  • 理解每个工具的功能和参数
  • 判断何时需要调用工具
  • 生成符合规范的 tool_calls

工具节点实现

python 复制代码
class BasicToolNode:
    def __init__(self, tools: list) -> None:
        self.tools_by_name = {tool.name: tool for tool in tools}
    
    def __call__(self, inputs: dict):
        message = inputs.get("messages", [])[-1]
        outputs = []
        
        for tool_call in message.tool_calls:
            # 执行工具调用
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call["args"]
            )
            # 封装为 ToolMessage
            outputs.append(
                ToolMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        return {"messages": outputs}

条件路由函数

python 复制代码
def route_tools(state: State):
    """
    条件边路由函数。
    返回 "tools" 或 END,决定下一步执行路径。
    """
    ai_message = state.get("messages", [])[-1]
    
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"  # 有工具调用
    return END  # 无工具调用,结束

图结构定义

python 复制代码
# 添加节点
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)

# 普通边
graph_builder.add_edge(START, "chatbot")

# 条件边
graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    {"tools": "tools", END: END}
)

# 循环边
graph_builder.add_edge("tools", "chatbot")

图结构

sql 复制代码
START ──▶ chatbot ──┬──▶ END
                    │
                    └──▶ tools ──▶ chatbot(循环)

运行效果

执行

bash 复制代码
pip install langgraph langchain langchain-openai langchain-community pydantic python-dotenv typing-extensions
python 03添加工具.py

输出


架构图解

状态流转

sql 复制代码
┌─────────┐     ┌─────────────┐     ┌─────────┐
│  START  │────▶│  chatbot    │────▶│   END   │
└─────────┘     └──────┬──────┘     └─────────┘
                       │
              ┌────────┴────────┐
              │ 有 tool_calls?  │
              └────────┬────────┘
                       │
              ┌────────┴────────┐
              ▼                 ▼
        ┌─────────┐       ┌─────────┐
        │  tools  │──────▶│  chatbot│
        └─────────┘       └─────────┘

执行时序

sql 复制代码
User Input
    │
    ▼
┌─────────────────┐
│  chatbot 节点   │
│  LLM 推理       │
│  输出 tool_calls│
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  route_tools    │
│  返回 "tools"   │
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  tools 节点     │
│  执行搜索       │
│  输出 ToolMessage│
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  chatbot 节点   │
│  接收工具结果   │
│  生成最终回答   │
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  route_tools    │
│  返回 END       │
└─────────────────┘
    │
    ▼
Output

总结

本文通过实战代码展示了 LangGraph 工具调用的核心机制:

  1. 工具绑定bind_tools() 让 LLM 感知可用工具
  2. 工具节点BasicToolNode 执行实际的工具调用
  3. 条件路由add_conditional_edges() 实现动态分支
  4. ReAct 循环:chatbot ↔ tools 形成推理-行动闭环
  5. 消息类型ToolMessage 传递工具执行结果

进阶方向

  • 多工具支持:计算器、数据库、API 等
  • 工具选择策略:让 LLM 智能选择工具
  • 错误处理:工具调用失败的容错机制
  • 人机协作:Human-in-the-loop 节点

参考


📌 本文首发于掘金,作者:AI探索者 🔗 转载请注明出处

💡 如果觉得有帮助,欢迎点赞、评论、收藏! 🎯 你的支持是我持续创作的动力!

相关推荐
苍何1 小时前
终于,我把 Openclaw 加 Seed2.0 Skills 做 AI 漫剧搞定了
后端
苍何2 小时前
阿里出手,最强Coding Plan出炉,OpenClaw可以痛快玩了
后端
风象南2 小时前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
神奇小汤圆2 小时前
为什么 Spring 强烈推荐你用 singleton
后端
Java编程爱好者2 小时前
面试必问:Semaphore 凭什么靠 AQS + CAS 实现限流?
后端
Java编程爱好者3 小时前
十万个why:加了 LIMIT 1,为什么查询反而变慢了?
后端
JavaTalks3 小时前
高并发保护实战:限流、熔断、降级如何配合落地
后端·架构·设计
代码丰4 小时前
为什么Java 接口中的存在 Static 和 Default 方法?
后端