LangGraph官方文档笔记——2.使用工具增强聊天机器人


为了处理我们的聊天机器人无法“凭借记忆”回答的查询,我们将集成一个网络搜索工具。我们的机器人可以使用此工具查找相关信息并提供更好的回复。

这里的案例使用的是Tavily搜索引擎工具,也就是说,给我们的聊天机器人配置好这个工具后,我们的机器人就可以按需求去在线搜索相关的信息来辅助回答。

项目前的准备工作

获取TAVILY_API_KEY

首先前往tavily官网获取你的TAVILY_API_KEY。

安装环境依赖

pip install -U langchain-tavily

尝试调用搜索工具

我们可以先通过以下方式看看是否能够成功使用tavily工具:
TavilySearch API资料

from langchain_tavily import TavilySearch

os.environ['TAVILY_API_KEY'] = 'YOUR_TAVILY_API_KEY'

tool = TavilySearch(max_results=2)#返回两条结果
tools = [tool]
tool.invoke("What's a 'edge' in LangGraph?")

返回结果:

{'query': "What's a 'edge' in LangGraph?",
 'follow_up_questions': None,
 'answer': None,
 'images': [],
 'results': [{'title': 'LangGraph Basics: Understanding State, Schema, Nodes, and Edges',
   'url': 'https://medium.com/@vivekvjnk/langgraph-basics-understanding-state-schema-nodes-and-edges-77f2fd17cae5',
   'content': 'LangGraph Basics: Understanding State, Schema, Nodes, and Edges | by Story_Teller | Medium LangGraph Basics: Understanding State, Schema, Nodes, and Edges LangGraph Basics: Understanding State, Schema, Nodes, and Edges These predefined structures in the messaging app are synonymous with the schema of the state in LangGraph. Just as a messaging app ensures all interactions (messages) follow a consistent format, the schema in LangGraph ensures the state passed along edges is structured and interpretable. This static schema allows nodes to rely on a consistent state format, ensuring seamless communication along edges throughout the graph. In this article, we explored the foundational concepts of graph-based systems, drawing parallels to familiar messaging applications to illustrate how edges, nodes, and state transitions function seamlessly in dynamic workflows.',
   'score': 0.72042775,
   'raw_content': None},
  {'title': 'LangGraph for Beginners, Part 3: Conditional Edges - Medium',
   'url': 'https://medium.com/ai-agents/langgraph-for-beginners-part-3-conditional-edges-16a3aaad9f31',
   'content': 'In this article, we will create a simple graph that explains conditional edges in LangGraph. In the previous articles we discussed, we learnt how to create a simple graph and call LLM. We will create a simple graph, that forecasts weather as sunny or rainy based on a random number. Its dummy weather forecast function that predicts sunny weather 50% of the time and rainy weather 50% of the time. We define two more nodes, namely rainy\\_weather and sunny\\_weather. We will define another function forecast\\_weather() that will decide whether the weather is sunny or rainy. ## Create the graph instance ## Add the nodes to the graph ## Test the graph In this article we learnt how to create a conditional edge.',
   'score': 0.48250288,
   'raw_content': None}],
 'response_time': 2.16}

可以看到工具返回的是JSON格式的页面摘要,这意味着我们的聊天机器人可以使用它们来回答问题。

看官方的示例

#==============================第一部分===============================#

from langchain_tavily import TavilySearch

tool = TavilySearch(max_results=2)
tools = [tool]

#==============================第二部分===============================#
from typing import Annotated

from langchain.chat_models import init_chat_model
from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
# Modification: tell the LLM which tools it can call
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)

#==============================第三部分===============================#

import json

from langchain_core.messages import ToolMessage


class BasicToolNode:
    """A node that runs the tools requested in the last AIMessage."""

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

    def __call__(self, inputs: dict):
        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
                ToolMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        return {"messages": outputs}# 返回工具调用结果消息列表


tool_node = BasicToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)

#==============================第四部分===============================#

def route_tools(
    state: State,
):
    """
    Use in the conditional_edge to route to the ToolNode if the last message
    has tool calls. Otherwise, route to the 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# 无工具调用,路由至结束节点


# The `tools_condition` function returns "tools" if the chatbot asks to use a tool, and "END" if
# it is fine directly responding. This conditional routing defines the main agent loop.
graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node
    # It defaults to the identity function, but if you
    # want to use a node named something else apart from "tools",
    # You can update the value of the dictionary to something else
    # e.g., "tools": "my_tools"
    {"tools": "tools", END: END},# 路由映射表:将route_tools的返回值映射到实际节点名称:"tools"返回值 → "tools"节点,END返回值 → END节点
)
# Any time a tool is called, we return to the chatbot to decide the next step
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()

#==============================第五部分===============================#

while True:
    try:
        user_input = input("User: ")
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Goodbye!")
            break

        stream_graph_updates(user_input)
    except:
        # fallback if input() is not available
        user_input = "What do you know about LangGraph?"
        print("User: " + user_input)
        stream_graph_updates(user_input)
        break

这里我把官方的代码流程分为了五大部分:

第一部分:工具初始化

初始化了一个Tavily搜索引擎工具,用于获取外部信息。该工具被配置为最多返回2个结果,并被添加到工具列表中。

第二部分:聊天模型与状态图初始化

  1. 定义了一个状态类型State,包含消息列表。
  2. 初始化了Claude-3-5-Sonnet大型语言模型,并将其与之前定义的工具绑定。
  3. 创建了一个名为chatbot的节点函数,负责生成AI回复,并将其添加到状态图构建器中。

第三部分:工具调用节点实现

实现了一个BasicToolNode类,用于处理AI消息中包含的工具调用请求。该类会:

  1. 根据工具名称查找对应的工具。
  2. 执行工具调用并获取结果。
  3. 将结果封装为工具消息返回。
    这个工具节点随后被添加到状态图构建器中。

第四部分:状态图路由逻辑

  1. 定义了一个路由函数route_tools,根据AI消息中是否包含工具调用来决定下一步流向。
  2. 设置了状态图的边,定义了节点间的流转逻辑:
    • chatbot节点出发,根据工具调用情况决定流向工具节点或结束。
    • 工具节点执行后返回chatbot节点。
    • 初始状态流向chatbot节点。
  3. 编译状态图,完成代理的逻辑构建。

第五部分:用户交互循环

实现了一个简单的命令行交互界面,允许用户输入问题:

  1. 用户输入问题后,系统会处理并流式输出回答。
  2. 支持通过特定命令(如"quit")退出对话。
  3. 包含异常处理逻辑,确保在无法获取用户输入时能提供默认问题并退出。

实践使用工具增强聊天机器人

import getpass
import os


os.environ['TAVILY_API_KEY'] = 'YOUR_TAVILY_API_KEY'

def _set_env(var: str):
    print(f"Checking if {var} is set...")
    if not os.environ.get(var):
        print(f"{var} is not set, prompting user for input.")
        os.environ[var] = getpass.getpass(f"{var}: ")
        print(f"{var} has been set.{os.environ.get(var)}")
    else:
        print(f"{var} is already set to: {os.environ[var]}")

_set_env("TAVILY_API_KEY")

from langchain_community.tools.tavily_search import TavilySearchResults

tool = TavilySearchResults(max_results=2)
tools = [tool]
# tool.invoke("What's a 'node' in LangGraph?")

# 接下来就跟第一部分一样,创建LLM,创建图、节点、起终点
# 把上面的tavily变成一个节点塞到graph里面
from typing import Annotated

from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

from IPython.display import Image, display
from langchain.schema import HumanMessage, AIMessage
from langgraph.prebuilt import ToolNode, tools_condition


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)

# tavily作为工具被deepseek LLM用起来,想象一下,DS大帅哥抄起一个tavily扳手
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    max_retries=2,
    base_url="https://api.deepseek.com/v1",
    api_key="YOUR_API_KEY",  # 替换为你的API密钥
    model="deepseek-chat"  # 根据实际模型名称修改
)
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    messages = llm_with_tools.invoke(state["messages"])
    print("模型原始响应:", messages)  # 检查是否包含正确的tool_calls
    return {"messages": messages}


graph_builder.add_node("chatbot", chatbot)

import json
tool_node = ToolNode(tools=tools)
graph_builder.add_node("tools", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
)

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

graph = graph_builder.compile()

try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
    # This requires some extra dependencies and is optional
    pass


def stream_graph_updates(user_input: str):
    for event in graph.stream({"messages": [HumanMessage(content=user_input)]}):
        for node, data in event.items():
            if isinstance(data, dict) and "messages" in data:  # 添加类型检查
                msg_list = data["messages"]
                if msg_list and isinstance(msg_list, list):
                    msg = msg_list[-1]
                    print(f"{node.upper()}:", msg.content)


while True:
    try:
        user_input = input("User: ")
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Goodbye!")
            break

        stream_graph_updates(user_input)
    except:
        # fallback if input() is not available
        user_input = "What do you know about LangGraph?"
        print("User: " + user_input)
        stream_graph_updates(user_input)
        break

测试一下,返回结果:

Checking if TAVILY_API_KEY is set...
TAVILY_API_KEY is already set to: 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
User: 一句话介绍你自己
模型原始响应: content='我是一个智能助手,随时为你提供帮助、解答问题和完成任务!' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 147, 'total_tokens': 160, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 147}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_8802369eaa_prod0425fp8', 'id': '7847c72d-55dc-4cfc-9214-e161fcbad52b', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None} id='run--0f510a2a-d21d-4893-b594-c87bdee9a307-0' usage_metadata={'input_tokens': 147, 'output_tokens': 13, 'total_tokens': 160, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
User: 介绍LangGraph
模型原始响应: content='' additional_kwargs={'tool_calls': [{'id': 'call_0_11596b59-6946-4d50-9807-aae8fe62a2a9', 'function': {'arguments': '{"query":"LangGraph 介绍"}', 'name': 'tavily_search_results_json'}, 'type': 'function', 'index': 0}], 'refusal': None} response_metadata={'token_usage': {'completion_tokens': 25, 'prompt_tokens': 147, 'total_tokens': 172, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 128}, 'prompt_cache_hit_tokens': 128, 'prompt_cache_miss_tokens': 19}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_8802369eaa_prod0425fp8', 'id': '29625e2a-ae3d-4e28-b24d-73e2e50b7c69', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None} id='run--7d93a7d3-3069-427f-862b-63fdc64285d4-0' tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph 介绍'}, 'id': 'call_0_11596b59-6946-4d50-9807-aae8fe62a2a9', 'type': 'tool_call'}] usage_metadata={'input_tokens': 147, 'output_tokens': 25, 'total_tokens': 172, 'input_token_details': {'cache_read': 128}, 'output_token_details': {}}
TOOLS: [{"title": "LangGraph:基于图结构的大模型智能体开发框架 - 博客园", "url": "https://www.cnblogs.com/deeplearningmachine/p/18631512", "content": "[LangGraph:基于图结构的大模型智能体开发框架](https://www.cnblogs.com/deeplearningmachine/p/18631512 \"发布于 2024-12-25 21:56\")\n===========================================================================================================\n\nLangGraph 是LangChainAI开发的一个工具库,用于创建代理和多代理智能体工作流。它提供了以下核心优势:周期、可控性和持久性,对于Agent智能体开发者来说无疑减少了许多工作量。以下篇幅仅从本人角度阐述LangGraph在开发过程中的亮点以及使用方法。\n\n基本介绍\n====\n\nLangGraph的StateGraph是一种状态机,包含了节点和边,节点一般是定义好的函数,边用于连接不同的节点,用于表示图的执行顺序。简单来说,使用LangGraph构建工作流的步骤如下:", "score": 0.9055316}, {"title": "LangGraph实战- 哥不是小萝莉- 博客园", "url": "https://www.cnblogs.com/smartloli/p/18276355", "content": "LangGraph是一个功能强大的库,用于构建基于大型语言模型(LLM)的有状态、多参与者应用程序。它旨在创建代理和多代理工作流,以实现复杂的任务和交互。 2.1", "score": 0.8889231}]
模型原始响应: content='LangGraph 是一个由 LangChainAI 开发的工具库,专门用于创建代理(Agent)和多代理智能体工作流。它基于图结构设计,旨在帮助开发者构建有状态、多参与者的应用程序,尤其适用于基于大型语言模型(LLM)的复杂任务和交互。\n\n### 核心特点\n1. **基于图结构的工作流**:\n   - LangGraph 使用状态机(StateGraph)来定义工作流,其中节点是预定义的函数,边用于连接节点并控制执行顺序。\n   - 这种设计使得工作流更加灵活和可控。\n\n2. **周期性和持久性**:\n   - 支持周期性任务和状态持久化,适合需要长期运行或多步骤交互的智能体应用。\n\n3. **多代理支持**:\n   - 可以轻松构建多代理系统,实现复杂的协作任务。\n\n### 使用场景\n- 开发基于 LLM 的智能体(Agent)。\n- 构建多代理协作的工作流。\n- 实现复杂的任务自动化或交互式应用。\n\n### 示例资源\n1. [LangGraph:基于图结构的大模型智能体开发框架](https://www.cnblogs.com/deeplearningmachine/p/18631512) - 详细介绍 LangGraph 的设计理念和使用方法。\n2. [LangGraph实战](https://www.cnblogs.com/smartloli/p/18276355) - 提供实际应用案例和代码示例。\n\n如果你对 LangGraph 的具体实现或代码示例感兴趣,可以参考上述链接中的内容。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 314, 'prompt_tokens': 504, 'total_tokens': 818, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 128}, 'prompt_cache_hit_tokens': 128, 'prompt_cache_miss_tokens': 376}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_8802369eaa_prod0425fp8', 'id': 'e14c811a-69db-4b54-97de-df2bbfca94af', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None} id='run--a882e43d-7092-4e2d-ade4-e5d1ac2cb0ba-0' usage_metadata={'input_tokens': 504, 'output_tokens': 314, 'total_tokens': 818, 'input_token_details': {'cache_read': 128}, 'output_token_details': {}}
User: q
Goodbye!

可以看到当我提出“介绍LangGraph”时,聊天机器人调用了tavily搜索工具,并结合工具的返回消息给出了回答。
恭喜! 您已经在langgraph中创建了一个会话代理,该代理可以在需要时使用搜索引擎检索更新的信息。现在它可以处理更广泛的用户查询。

posted @ 2025-06-11 16:08  Filament  阅读(484)  评论(0)    收藏  举报
返回顶端