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

机器人架构搭建核心准则:先论文论证,后工程落地

机器人架构搭建核心准则:先论文论证,后工程落地

原创声明:本文为原创技术干货,基于真实工程实践总结,未经授权严禁转载与篡改。 本文写给那些正在或将要主导机器人架构的技术决策者与一线工程师——无论你是CTO、架构师,还是嵌入式开发、算法工程师,只要你关心如何让机器人项目不再烂尾,这篇文章值得你读完。 注意:文中反复出现的“论文”,特指“工程论文”(区别于学术论文),是一份写给团队自己的工程蓝图。请务必读完第二部分的定义,再决定是否认同。 核心观点 在机器人架构设计与实施过程中,先完成系统性论文论证,再开展工程化架构落地,是保障项目可行、流程闭环、资源高效利用的核心前提,也是区分专业机器人架构师与无序开发的关键标准。 金句:先论文后落地,本质上是用确定性的逻辑推导,去对抗不确定性的物理世界。 一、行业普遍认知误区 当前机器人领域从业者普遍存在开发误区:直接跳过前期规划与逻辑论证,盲目开展硬件采购、框架搭建、代码开发与接口调试,将功能拼接等同于架构设计。这种模式缺乏顶层逻辑支撑与可行性验证,本质是无方向的盲目实施,也是多数机器人项目停滞、返工、烂尾的核心诱因。 这种开发就像农村自建房,凭感觉垒砖,从不考虑地质勘测和结构力学

无人机避障新思路:手把手教你用APF-RRT*算法实现高效轨迹规划(附Python代码)

无人机避障新思路:手把手教你用APF-RRT*算法实现高效轨迹规划(附Python代码) 去年夏天,我在一个无人机巡检项目里遇到了一个棘手的问题:传统的RRT算法在复杂林地环境中规划路径时,经常“卡”在密集的树木之间,要么采样效率低下导致规划时间过长,要么生成的路径曲折得让无人机像喝醉了一样左右摇摆。团队尝试了各种参数调整,效果都不理想。直到我们把人工势场法的引导机制引入到双向RRT*算法中,情况才发生了根本性转变——不仅规划速度提升了近70%,生成的路径也平滑了许多。 这种结合了APF(人工势场法)和双向RRT的混合算法,如今已经成为许多无人机开发者解决复杂环境路径规划的秘密武器。它巧妙地将APF的方向引导优势与RRT的渐进最优特性结合起来,同时利用双向搜索大幅提升收敛速度。今天,我就从工程实践的角度,带你一步步实现这个算法,分享我在实际项目中积累的参数调优经验,并提供可直接运行的Python代码。 1. 理解APF-RRT*算法的核心思想 在开始写代码之前,我们需要先弄清楚这个混合算法到底解决了什么问题。传统的RRT算法虽然概率完备,但在复杂环境中存在明显的局限性:随机采

项目介绍 MATLAB实现基于LSTM-ACO 长短期记忆网络(LSTM)结合蚁群算法(ACO)进行无人机三维路径规划的详细项目实例(含模型描述及部分示例代码) 还请多多点一下关注 加油 谢谢 你的鼓

项目介绍 MATLAB实现基于LSTM-ACO 长短期记忆网络(LSTM)结合蚁群算法(ACO)进行无人机三维路径规划的详细项目实例(含模型描述及部分示例代码) 还请多多点一下关注 加油 谢谢 你的鼓

MATLAB实现基于LSTM-ACO 长短期记忆网络(LSTM)结合蚁群算法(ACO)进行无人机三维路径规划的详细项目实例 更多详细内容可直接联系博主本人   或者访问对应标题的完整博客或者文档下载页面(含完整的程序,GUI设计和代码详解) 随着人工智能和自主导航技术的飞速发展,无人机(UAV)在军事侦察、环境监测、物流配送和灾害救援等领域展现出巨大的应用前景。三维空间中的路径规划作为无人机自主飞行的核心技术之一,直接决定着无人机的安全性、效率和智能化水平。在复杂多变的三维环境下,障碍物分布复杂且动态变化,传统的二维路径规划方法无法满足无人机实际作业对灵活性和安全性的高要求。三维路径规划要求不仅能高效地避开多种类型的障碍物,还要在有限的能量和时间约束下完成任务,这对算法的全局搜索能力、收敛速度和路径平滑性提出了更高的挑战。 近年来,深度学习技术与群体智能算法的结合成为智能路径规划的重要研究方向。长短期记忆网络(LSTM)因其优异的时序信息学习能力,在处理复杂轨迹数据、预测无人机运动趋势等任务中表现突出。与此同时,蚁群算法(ACO)以其自适应全局优化能力,能够高效地搜索到最优

OpenClaw中飞书机器人配置指南:如何让群消息免 @ 也能自动回复

用 OpenClaw 做飞书机器人时,默认配置下,群里的消息必须 @ 机器人 才能触发回复。这在很多场景下很不方便——如果希望机器人在群里"隐身"工作,不用 @ 就能自动监听和回复,需要额外配置。 本文记录我解决这个问题的完整过程,供同样踩坑的同学参考。 问题描述 现象: * 飞书群里 @ 机器人 → 正常回复 ✅ * 飞书群里不 @ 机器人 → 没有任何反应 ❌ 环境: * OpenClaw 框架 * 飞书自建应用(机器人) * WebSocket 长连接模式 解决过程 第一步:修改 OpenClaw 配置 在 openclaw.json 中找到飞书渠道配置: "channels":{"feishu":{"requireMention&