LangGraph工具调用实战:手把手教你实现ReAct搜索机器人

LangGraph工具调用实战:手把手教你实现ReAct搜索机器人

## 前言

在前两篇文章中,我们分别学习了 LangGraph 的快速入门和 StateGraph 基础。本文将带你进入 LangGraph 的进阶领域——**工具调用(Tool Calling)**。通过为聊天机器人添加 Tavily 搜索引擎,你将掌握 ReAct(Reasoning + Acting)模式的完整实现,让 AI 能够主动调用外部工具获取实时信息。

---

## 一、核心概念

### 1.1 什么是工具调用

工具调用(Tool Calling)是 LLM 的重要能力,它允许 AI:

1. **推理(Reasoning)**:理解用户需求,判断需要什么信息

2. **行动(Acting)**:调用外部工具获取数据

3. **观察(Observation)**:整合工具结果生成回答

这就是经典的 **ReAct 模式**。

### 1.2 为什么需要工具调用

| 场景 | 纯 LLM | 带工具调用的 LLM |

|------|--------|-----------------|

| 实时信息 | ❌ 知识截止,无法回答 | ✅ 调用搜索工具获取 |

| 数学计算 | ❌ 容易出错 | ✅ 调用计算器精确计算 |

| 数据库查询 | ❌ 无法访问 | ✅ 调用 SQL 工具查询 |

| API 调用 | ❌ 无法执行 | ✅ 调用 API 工具操作 |

工具调用让 AI 从"纸上谈兵"变为"实干家"。

### 1.3 核心组件

```

┌─────────────────────────────────────────────────────────┐

│                    工具调用架构                          │

├─────────────────────────────────────────────────────────┤

│  1. 工具定义        │  TavilySearch、Calculator 等      │

│  2. 工具绑定        │  llm.bind_tools(tools)            │

│  3. 工具节点        │  BasicToolNode 执行工具调用       │

│  4. 条件路由        │  route_tools 判断是否需要工具     │

│  5. 循环执行        │  chatbot ↔ tools 形成 ReAct 循环 │

└─────────────────────────────────────────────────────────┘

```

---

## 二、环境准备

### 2.1 安装依赖

```bash

pip install langgraph langchain langchain-openai langchain-community pydantic python-dotenv typing-extensions

```

### 2.2 配置 API 密钥

创建 `.env` 文件:

```env

# 硅基流动平台 API 密钥

SILICONFLOW_API_KEY=your_siliconflow_key

# Tavily 搜索引擎 API 密钥

TAVILY_API_KEY=your_tavily_key

```

**获取 Tavily API Key**:访问 [Tavily](https://tavily.com/) 注册获取免费 API Key。

---

## 三、代码实现

### 3.1 完整代码

```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: 消息列表,使用 add_messages reducer 函数

              确保新消息追加到列表,而不是覆盖

    """

    messages: Annotated[list, add_messages]


 

# ==================== 2. 定义工具节点 ====================

class BasicToolNode:

    """

    工具节点:运行 LLM 请求的工具。

   

    检查状态中的最新消息,如果消息包含 tool_calls,

    则调用相应的工具。

    """

    def __init__(self, tools: list) -> None:

        """

        初始化工具节点。

       

        Args:

            tools: 工具列表

        """

        self.tools_by_name = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):

        """

        执行工具调用。

       

        Args:

            inputs: 包含消息列表的字典

           

        Returns:

            包含工具执行结果的消息字典

        """

        if messages := inputs.get("messages", []):

            message = messages[-1]

        else:

            raise ValueError("No message found in input")

       

        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。

    - 如果有工具调用,路由到 "tools" 节点

    - 如果没有,路由到 END(结束)

   

    Args:

        state: 当前图状态

       

    Returns:

        下一个节点的名称或 END

    """

    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. 搜索工具 ====================

# 使用 Tavily 真实搜索工具

# 需要安装: pip install langchain-community

# Tavily API Key 已在上面的代码中设置


 

# ==================== 5. 创建图 ====================

def create_graph():

    """

    创建并编译 StateGraph。

   

    Returns:

        编译后的图对象

    """

    # 创建图构建器

    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):

        """

        聊天机器人节点。

       

        Args:

            state: 当前状态

           

        Returns:

            包含 LLM 响应的字典

        """

        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")

   

    # 添加条件边:从 chatbot 到 tools 或 END

    graph_builder.add_conditional_edges(

        "chatbot",

        route_tools,

        {"tools": "tools", END: END}

    )

   

    # 添加边:从 tools 回到 chatbot(形成循环)

    graph_builder.add_edge("tools", "chatbot")

   

    # 编译图

    return graph_builder.compile()


 

# ==================== 6. 运行聊天机器人 ====================

def stream_graph_updates(graph, user_input: str):

    """

    流式处理图更新。

   

    Args:

        graph: 编译后的图对象

        user_input: 用户输入的消息

    """

    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]

                # 只打印 AI 消息,不打印工具消息

                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("提示:")

    print("  - 输入 'quit'、'exit' 或 'q' 退出对话")

    print("  - 聊天机器人可以使用搜索工具回答实时问题\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()

```

### 3.2 代码解析

#### 3.2.1 工具绑定

```python

# 创建 Tavily 搜索工具

tool = TavilySearchResults(max_results=2)

tools = [tool]

# 绑定工具到 LLM

llm_with_tools = llm.bind_tools(tools)

```

**关键点**:

- `bind_tools()` 将工具信息注入 LLM 的系统提示

- LLM 根据用户输入判断是否需要调用工具

- 如果需要,LLM 输出包含 `tool_calls` 的特殊消息

#### 3.2.2 工具节点实现

```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}

```

**执行流程**:

1. 解析 LLM 的 `tool_calls` 请求

2. 查找并执行对应工具

3. 将结果封装为 `ToolMessage`

4. 返回更新后的状态

#### 3.2.3 条件路由

```python

def route_tools(state: State):

    """条件边路由函数。"""

    ai_message = state.get("messages", [])[-1]

   

    # 检查是否有工具调用

    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:

        return "tools"  # 有工具调用,路由到 tools 节点

    return END  # 没有工具调用,结束

```

**作用**:根据当前状态动态决定下一步执行哪个节点。

#### 3.2.4 图结构

```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")  # 形成循环

```

**图结构**:

```

START ──▶ chatbot ──┬──▶ END(无工具调用)

                    │

                    └──▶ tools ──▶ chatbot(循环)

                         (有工具调用)

```

---

## 四、运行效果

### 4.1 执行步骤

```bash

python 03添加工具.py

```

### 4.2 输出结果

---

## 五、ReAct 模式详解

### 5.1 执行流程图解

```

用户: "今天重庆天气怎么样?"

    │

    ▼

┌─────────────────────────────────────┐

│  Step 1: chatbot 节点               │

│  LLM 判断需要调用工具               │

│  输出: AIMessage with tool_calls    │

└─────────────────────────────────────┘

    │

    ▼

┌─────────────────────────────────────┐

│  Step 2: route_tools 检查           │

│  发现 tool_calls                    │

│  返回: "tools"                      │

└─────────────────────────────────────┘

    │

    ▼

┌─────────────────────────────────────┐

│  Step 3: tools 节点                 │

│  调用 TavilySearch                  │

│  搜索: "重庆今天天气"               │

│  输出: ToolMessage with results     │

└─────────────────────────────────────┘

    │

    ▼

┌─────────────────────────────────────┐

│  Step 4: chatbot 节点(循环)       │

│  LLM 接收 ToolMessage               │

│  生成最终回答                       │

└─────────────────────────────────────┘

    │

    ▼

┌─────────────────────────────────────┐

│  Step 5: route_tools 检查           │

│  无 tool_calls                      │

│  返回: END                          │

└─────────────────────────────────────┘

    │

    ▼

输出: "根据搜索结果,重庆今天天气晴朗..."

```

### 5.2 消息流转

```

消息历史:

├── UserMessage: "今天重庆天气怎么样?"

├── AIMessage: tool_calls(调用意图)

├── ToolMessage: 搜索结果

└── AIMessage: 最终回答

```

---

## 六、踩坑记录

| 问题 | 原因 | 解决方案 |

|------|------|----------|

| API Key 泄露风险 | 硬编码在代码中 | 使用 `.env` 文件存储 |

| 工具调用不触发 | LLM 未正确绑定工具 | 确认 `bind_tools()` 调用 |

| ToolMessage 格式错误 | content 不是 JSON | 使用 `json.dumps()` 序列化 |

| 无限循环 | 忘记返回 END | 检查 `route_tools` 逻辑 |

| 工具结果丢失 | tool_call_id 不匹配 | 确保 ID 与请求一致 |

---

## 七、总结

通过本文,你学会了:

1. ✅ **工具绑定**:使用 `bind_tools()` 让 LLM 知道可用工具

2. ✅ **工具节点**:实现 `BasicToolNode` 执行工具调用

3. ✅ **条件路由**:使用 `add_conditional_edges()` 动态控制流程

4. ✅ **ReAct 模式**:理解推理-行动-观察的完整循环

5. ✅ **消息类型**:掌握 `AIMessage`、`ToolMessage` 的使用

### 进阶方向

- **多工具支持**:添加计算器、数据库查询等工具

- **工具选择**:让 LLM 智能选择最合适的工具

- **错误处理**:添加工具调用失败的容错机制

- **人机协作**:在关键步骤添加人工确认


 

---

## 参考资料

- [LangGraph 官方教程 - 添加工具](https://langchain-ai.github.io/langgraph/tutorials/introduction/)

- [Tavily 搜索引擎](https://tavily.com/)

- [ReAct 论文](https://arxiv.org/abs/2210.03629)

- [LangChain 工具调用文档](https://python.langchain.com/docs/modules/agents/tools/)

---

> 📌 本文首发于 ZEEKLOG,作者:码上AI_123

> 🔗 转载请注明出处

>

> 💡 如果觉得有帮助,欢迎点赞、收藏、关注!

> 🎯 你的支持是我持续创作的动力!

Read more

前端大数据导出优化:解决Chrome内存崩溃的实战方案

前端大数据导出优化:解决Chrome内存崩溃的实战方案

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[[email protected]] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? * 专栏导航: 码农阿豪系列专栏导航 面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️ Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻 Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡 全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀 目录 * 前端大数据导出优化:解决Chrome内存崩溃的实战方案 * 引言 * 问题分析 * 1. 为什么 Chrome 会崩溃,而 QQ 浏览器正常? * 2. 常见崩溃场景

详细教程:如何从前端查看调用接口、传参及返回结果(附带图片案例)

详细教程:如何从前端查看调用接口、传参及返回结果(附带图片案例)

目录 1. 打开浏览器开发者工具 2. 使用 Network 面板 3. 查看具体的API请求 a. Headers b. Payload c. Response d. Preview e. Timing 4. 实际操作步骤 5. 常见问题及解决方法 a. 无法看到API请求 b. 请求失败 c. 跨域问题(CORS) 作为一名后端工程师,理解前端如何调用接口、传递参数以及接收返回值是非常重要的。下面将详细介绍如何通过浏览器开发者工具(F12)查看和分析这些信息,并附带图片案例帮助你更好地理解。 1. 打开浏览器开发者工具 按下 F12 或右键点击页面选择“检查”可以打开浏览器的开发者工具。常用的浏览器如Chrome、Firefox等都内置了开发者工具。下面是我选择我的一篇文章,打开开发者工具进行演示。 2. 使用

实战干货】打破次元壁:如何实现 Web 端与 AutoCAD 桌面端的双向通信与自动化绘图

前言 在工程建设与制造业数字化转型的浪潮中,我们经常面临一个架构难题:业务流在 Web 端(SaaS 系统、AI 生成内容),而生产流在桌面端(AutoCAD、Revit)。 如何将 Web 端生成的数据(如设计说明、BOM 表、AI 生成的布局方案)无缝传输到 AutoCAD 并自动生成图纸?传统的做法是“导出 Excel/JSON -> 人工打开 CAD -> 导入插件”,效率低下且割裂。 本文将分享我在最近一个项目中采用的**“本地伴随服务(Local Sidecar Server)”**技术方案。通过在 AutoCAD 插件内部嵌入轻量级 Web Server,实现了 Web 页面点击按钮,

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题 在开发 Web 应用时,尤其是集成了 Unity WebGL 内容的页面,遇到一个问题:当 Unity WebGL 渲染内容嵌入到一个 Tab 中时,切换 Tab 后画面会变黑,直到用户点击黑屏区域,才会恢复显示。 这个问题通常是因为 Unity 渲染在 Tab 切换时被暂停或未能获得焦点所致。 在本文中,我们将介绍如何在使用 Layui 框架时,通过监听 Tab 切换事件并强制 Unity WebGL 渲染恢复,来解决这一问题。 1. 问题描述 当 Unity WebGL 内容嵌入到页面中的多个