在上一篇文章中,我们用LangGraph搭建了一个最简单的聊天机器人,这个机器人可以通过接收用户输入并使用 LLM 生成响应来进行基本对话。
但是,这个机器人的知识仅限于其训练数据中的内容。在这篇博客中,我们将添加一个网络搜索工具,以扩展机器人的知识,使其更强大。
基本的聊天机器人
基本的聊天机器人代码如下所示,详细教程请看[LangGraph教程]LangGraph01——用LangGraph构建基本的聊天机器人:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph
class State(TypedDict):
messages:Annotated[list, add_messages]
graph_builder = StateGraph(State)
from langchain_community.chat_models import ChatOllama
local_llm = "qwen2.5:latest"
llm = ChatOllama(model=local_llm, format="json", temperature=0)
def chatbot(state: State):
return {"messages": [llm.invoke(state["messages"])]}
# 第一个参数是节点名称,第二个是要调用的函数
graph_builder.add_node("chatbot", chatbot)
from langgraph.graph import START, END
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
graph.invoke({"messages":"你好"})
使用工具增强聊天机器人
定义状态类并构建图
首先定义状态类并构建图,这部分和之前讲的一样,因此不过多赘述
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph
# 1. 定义状态类
class State(TypedDict):
messages:Annotated[list, add_messages]
# 2. 构建图
graph_builder = StateGraph(State)
定义工具节点
这里 ,TavilySearchResults
是一个利用 Tavily 定义的搜索 API工具类,用于执行自定义的网络搜索并返回搜索结果。Tavily是一款专为 AI 代理 (LLM) 构建的搜索引擎,能够快速提供实时、准确和基于事实的结果。
max_results=2
表示每次搜索最多返回 2 条结果。
然后创建了一个列表 tools,并将之前创建的 tool 实例添加到列表中。 tools列表包含了所有可以被语言模型调用的工具,这里我们只有一个工具。
tool = TavilySearchResults(max_results=2)
tools = [tool]
之前调用大模型用的是ChatOllama
#from langchain_community.chat_models import ChatOllama
#llm = ChatOllama(model="qwen2.5:latest", format="json", temperature=0)
但是ChatOllama
没有bind_tools
函数,所以这里我们使用ChatOpenAI
来调用Ollama模型,并把工具绑定到大模型上:
# 3.定义工具节点
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_openai import ChatOpenAI
local_llm = ["deepseek-r1:8b","qwen2.5:latest"]
llm = ChatOpenAI(model=local_llm[1], temperature=0.0, api_key="ollama", base_url="http://localhost:11434/v1")
llm_with_tools = llm.bind_tools(tools)
然后用 llm_with_tools
构建工具节点
def chatbot(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
把节点和边加入图
ToolNode
是 LangGraph 提供的一个预构建节点类,专门用于表示和处理工具调用的节点。它封装了工具的调用逻辑,使得在状态图中可以方便地集成和调用工具。
# 4. 把节点加入图
# 第一个参数是节点名称,第二个是要调用的函数
from langgraph.prebuilt import ToolNode
tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)
graph_builder.add_node("chatbot", chatbot)
tools_condition
是一个预构建的条件函数,用于判断是否需要调用工具。如果要用tools_condition
,则节点必须叫tools
,因为tools_condition
里写死了名称。源码如下:
def tools_condition(
state: Union[list[AnyMessage], dict[str, Any], BaseModel],
messages_key: str = "messages",
) -> Literal["tools", "__end__"]:
if isinstance(state, list):
ai_message = state[-1]
elif isinstance(state, dict) and (messages := state.get(messages_key, [])):
ai_message = messages[-1]
elif messages := getattr(state, messages_key, []):
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__"
如果节点换成别的名字,会报错
ValueError: At ‘chatbot’ node, ‘tools_condition’ branch found unknown target ‘tools’
# 5. 把边加入图
from langgraph.prebuilt import tools_condition
graph_builder.add_conditional_edges(
"chatbot",
tools_condition,
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.set_entry_point("chatbot")
构建完节点和边后,可以用代码绘制流程图进一步理解整个流程:
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
绘制的流程图如下所示:
最后编译图并用invoke
调用:
graph = graph_builder.compile()
graph.invoke({"messages":"今天武汉天气怎么样"})
结果如下:
{'messages': [HumanMessage(content='今天武汉天气怎么样', additional_kwargs={}, response_metadata={}, id='69f7b557-4129-4b15-82b5-22b578cf00a5'),
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_fos1y52p', 'function': {'arguments': '{"query":"今天 武汉 天气"}', 'name': 'tavily_search_results_json'}, 'type': 'function', 'index': 0}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 191, 'total_tokens': 240, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen2.5:latest', 'system_fingerprint': 'fp_ollama', 'id': 'chatcmpl-977', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-7c002089-7dad-40ed-ab84-2a4f862da9ef-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': '今天 武汉 天气'}, 'id': 'call_fos1y52p', 'type': 'tool_call'}], usage_metadata={'input_tokens': 191, 'output_tokens': 49, 'total_tokens': 240, 'input_token_details': {}, 'output_token_details': {}}),
ToolMessage(content='[{"title": "武汉市天气预报_天气查询- 墨迹天气", "url": "https://tianqi.moji.com/weather/china/hubei/wuhan", "content": "武汉市今天实况:16度雾,湿度:95%,北风:0级。白天:27度,雾。 夜间:多云,16度,天气较热,墨迹天气建议您选择短袖上衣加七分裤的搭配,针织衫是进出空调房的必备单品。", "score": 0.8464638}, {"title": "武汉市, 湖北省, 中华人民共和国天气预报和情况 - The Weather Channel", "url": "https://weather.com/zh-SG/weather/today/l/2637660151899903e8cbdd23636051470b6731863286ec74b3033421cb87e1e8", "content": "武汉市, 湖北省, 中华人民共和国今日天气 ; 高/ 低. --/8° ; 大风. 6 公里/小时 ; 湿度. 91% ; 露点. 8° ; 气压. 1019.0 毫巴.", "score": 0.82795376}]', name='tavily_search_results_json', id='2d2359f9-19bd-463b-9593-d0a7d18bd589', tool_call_id='call_fos1y52p', artifact={'query': '今天 武汉 天气', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'url': 'https://tianqi.moji.com/weather/china/hubei/wuhan', 'title': '武汉市天气预报_天气查询- 墨迹天气', 'content': '武汉市今天实况:16度雾,湿度:95%,北风:0级。白天:27度,雾。 夜间:多云,16度,天气较热,墨迹天气建议您选择短袖上衣加七分裤的搭配,针织衫是进出空调房的必备单品。', 'score': 0.8464638, 'raw_content': None}, {'url': 'https://weather.com/zh-SG/weather/today/l/2637660151899903e8cbdd23636051470b6731863286ec74b3033421cb87e1e8', 'title': '武汉市, 湖北省, 中华人民共和国天气预报和情况 - The Weather Channel', 'content': '武汉市, 湖北省, 中华人民共和国今日天气 ; 高/ 低. --/8° ; 大风. 6 公里/小时 ; 湿度. 91% ; 露点. 8° ; 气压. 1019.0 毫巴.', 'score': 0.82795376, 'raw_content': None}], 'response_time': 1.26}),
AIMessage(content='根据查询结果,今天武汉的天气情况如下:\n\n- 实况:温度为16度并伴有雾,湿度达到95%,北风微弱(0级)。\n- 白天最高气温预计在27度左右,并且会有雾;夜间最低气温约为16度,天气以多云为主。\n\n墨迹天气建议您选择短袖上衣加七分裤的搭配,同时针织衫也是进出空调房时的好选择。请注意保暖和防风哦!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 105, 'prompt_tokens': 562, 'total_tokens': 667, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'qwen2.5:latest', 'system_fingerprint': 'fp_ollama', 'id': 'chatcmpl-631', 'finish_reason': 'stop', 'logprobs': None}, id='run-e64359cd-9122-40f3-86c5-719df507bc63-0', usage_metadata={'input_tokens': 562, 'output_tokens': 105, 'total_tokens': 667, 'input_token_details': {}, 'output_token_details': {}})]}
完整代码
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph
# 1. 定义状态类
class State(TypedDict):
messages:Annotated[list, add_messages]
# 2. 构建图
graph_builder = StateGraph(State)
# 3.定义工具节点
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_openai import ChatOpenAI
local_llm = ["deepseek-r1:8b","qwen2.5:latest"]
llm = ChatOpenAI(model=local_llm[1], temperature=0.0, api_key="ollama", base_url="http://localhost:11434/v1")
tool = TavilySearchResults(max_results=2)
tools = [tool]
llm_with_tools = llm.bind_tools(tools)
def chatbot(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# 4. 把节点加入图
# 第一个参数是节点名称,第二个是要调用的函数
from langgraph.prebuilt import ToolNode
graph_builder.add_node("chatbot", chatbot)
tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)
# 5. 把边加入图
from langgraph.prebuilt import tools_condition
graph_builder.add_conditional_edges(
"chatbot",
tools_condition,
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile()
graph.invoke({"messages":"今天武汉天气怎么样"})
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))