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个结果,并被添加到工具列表中。
第二部分:聊天模型与状态图初始化
- 定义了一个状态类型
State
,包含消息列表。 - 初始化了Claude-3-5-Sonnet大型语言模型,并将其与之前定义的工具绑定。
- 创建了一个名为
chatbot
的节点函数,负责生成AI回复,并将其添加到状态图构建器中。
第三部分:工具调用节点实现
实现了一个BasicToolNode
类,用于处理AI消息中包含的工具调用请求。该类会:
- 根据工具名称查找对应的工具。
- 执行工具调用并获取结果。
- 将结果封装为工具消息返回。
这个工具节点随后被添加到状态图构建器中。
第四部分:状态图路由逻辑
- 定义了一个路由函数
route_tools
,根据AI消息中是否包含工具调用来决定下一步流向。 - 设置了状态图的边,定义了节点间的流转逻辑:
- 从
chatbot
节点出发,根据工具调用情况决定流向工具节点或结束。 - 工具节点执行后返回
chatbot
节点。 - 初始状态流向
chatbot
节点。
- 从
- 编译状态图,完成代理的逻辑构建。
第五部分:用户交互循环
实现了一个简单的命令行交互界面,允许用户输入问题:
- 用户输入问题后,系统会处理并流式输出回答。
- 支持通过特定命令(如"quit")退出对话。
- 包含异常处理逻辑,确保在无法获取用户输入时能提供默认问题并退出。
实践使用工具增强聊天机器人
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中创建了一个会话代理,该代理可以在需要时使用搜索引擎检索更新的信息。现在它可以处理更广泛的用户查询。