
🧬 LangGraph教程(二):掌握Agent核心能力-工具调用(自定义、预构建与MCP)
本文是 《LangGraph入门全解》https://www.wileyzhang.com/posts/264605ee-e889-806e-b847-ce4e7bcba614 系列的第二篇。在这篇文章中,我们为聊天机器人增加搜索工具,分别展示使用langchain的自定义工具和构建MCP Server方式。如果出你是新手,建议先阅读主指南以了解LangGraph的全貌。
在LangGraph中调用工具
LLM 在处理实时性问题、数学计算等方面往往表现不佳。为了弥补这些不足,一个合格的 Agent 应当具备调用外部工具的能力。
本文将从最常见的 Web 搜索工具 入手,带你快速上手 LangGraph 的工具调用。这里我们使用 https://app.tavily.com/(https://app.tavily.com) 作为示例(新用户有 1000 次免费搜索额度)。
在一构建聊天机器人的基础上,只需少量新增代码,即可完成工具调用。LangGraph 已经帮我们封装了大部分复杂逻辑。
示例代码
from typing import Annotated from typing_extensions import TypedDict from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START from langgraph.graph.message import add_messages from pydantic import SecretStr from langchain_tavily import TavilySearch from langgraph.prebuilt import ToolNode, tools_condition # 此处定义你自己的模型 llm = ChatOpenAI(base_url="http://127.0.0.1:8000/v1", api_key=SecretStr("123123"), model="qwen3_32") # 定义工具 tool = TavilySearch(tavily_api_key="你的tavily apikey", max_results=2) tools = [tool] # 工具绑定到模型 llm_with_tools = llm.bind_tools(tools) # 定义图状态 class State(TypedDict): messages: Annotated[list, add_messages] # 此处维护完整的消息历史 graph = StateGraph(State) def chatbot(state: State): return {"messages": [llm_with_tools.invoke(state["messages"])]} # 使用LangGraph提供的工具节点 tool_node = ToolNode(tools=tools) graph.add_node("chatbot", chatbot) # 添加工具节点 graph.add_node("tools", tool_node) # 添加工具 条件分支 graph.add_conditional_edges( "chatbot", tools_condition, ) graph.add_edge("tools", "chatbot") graph.add_edge(START, "chatbot") app = graph.compile() if __name__ == "__main__": messages = [] while True: user_input = input("👨💻: ") if user_input.lower() in ["quit", "exit", "q"]: print("Exiting...") break messages.append({"role": "user", "content": user_input}) response = app.invoke({"messages": messages}) messages = response["messages"] print(f'🤖: {response["messages"][-1].content}')
效果如下
LangGraph自定义工具与工具节点
LangChain为我们预构建了很多工具,常用的有
- 搜索类: Bing, SerpAPI, Tavily
- 代码执行类: Python REPL, Node.js REPL
- 数据库类: SQL, MongoDB, Redis
- Web 数据类: Scraping and browsing
- 其他 API: OpenWeatherMap, NewsAPI 等
不过,在真实的企业开发场景中,更常见的做法是开发 自定义工具,以满足个性化需求。
下面演示一个基于 BaseTool
的自定义 Tavily 搜索工具,以及如何在 LangGraph 中接入。
示例代码
import json from langchain_core.messages import ToolMessage from langgraph.constants import END from typing import Annotated, Type, Optional from langchain_core.callbacks import CallbackManagerForToolRun, AsyncCallbackManagerForToolRun from typing_extensions import TypedDict from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START from langgraph.graph.message import add_messages from pydantic import SecretStr, BaseModel, Field from langchain_core.tools import BaseTool from tavily import TavilyClient, AsyncTavilyClient # 自定义工具部分 class TavilySearchInput(BaseModel): query: str = Field(description=("搜索查询")) class TavilySearchTool(BaseTool): name: str = "tavily_search" description: str = """一个针对全面、准确和可信的结果进行了优化的搜索引擎。 当需要回答有关时事的问题时很有用。 输入应该是搜索查询。""" args_schema: Type[BaseModel] = TavilySearchInput # return_direct: bool = True def _run( self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None ) -> int: client = TavilyClient() search_r = client.search(query=query, max_results=2) return search_r async def _arun( self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, ) -> int: client = AsyncTavilyClient() search_r = await client.search(query=query, max_results=2) return search_r # 自定义的工具执行节点 class ToolNode: 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: print(f'正在执行工具 {tool_call["name"]},参数 {tool_call["args"]}') tool_result = self.tools_by_name[tool_call["name"]].invoke( tool_call["args"] ) print(f'工具{tool_call["name"]}, 执行结果{json.dumps(tool_result, ensure_ascii=False)}') outputs.append( ToolMessage( content=json.dumps(tool_result, ensure_ascii=False), name=tool_call["name"], tool_call_id=tool_call["id"], ) ) return {"messages": outputs} # 此处定义你自己的模型 llm = ChatOpenAI(base_url="http://127.0.0.1:8000/v1", api_key=SecretStr("123123"), model="qwen3_32") # 定义工具 tool = TavilySearchTool() tools = [tool] # 工具绑定到模型 llm_with_tools = llm.bind_tools(tools) # 定义图状态 class State(TypedDict): messages: Annotated[list, add_messages] # 此处维护完整的消息历史 graph = StateGraph(State) def chatbot(state: State): return {"messages": [llm_with_tools.invoke(state["messages"])]} # 工具路由 def route_tools(state: State): 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 # 使用LangGraph提供的工具节点 tool_node = ToolNode(tools=tools) graph.add_node("chatbot", chatbot) # 添加工具节点 graph.add_node("tools", tool_node) # 添加工具条件边 graph.add_conditional_edges( "chatbot", route_tools, {"tools": "tools", END: END}, ) graph.add_edge("tools", "chatbot") graph.add_edge(START, "chatbot") app = graph.compile() if __name__ == "__main__": messages = [] while True: user_input = input("👨💻: ") if user_input.lower() in ["quit", "exit", "q"]: print("Exiting...") break messages.append({"role": "user", "content": user_input}) response = app.invoke({"messages": messages}) messages = response["messages"] print(f'🤖: {response["messages"][-1].content}')
效果如下
我们在工具执行节点,打印工具的执行信息
除了继承 BaseTool
的方式,LangChain 还支持通过 @tool
装饰器 来快速定义工具。相比之下,这种方法更简洁,适合轻量级工具
from typing import Annotated from tavily import TavilyClient from langchain_core.tools import tool @tool("tavily_search") def tavily_search_tool(query: Annotated[str, "搜索查询"]): """一个针对全面、准确和可信的结果进行了优化的搜索引擎。当需要回答有关时事的问题时很有用。输入应该是搜索查询。"""client = TavilyClient() search_r = client.search(query=query) return search_r tavily_search_tool.invoke({"query": "北京2025年8月27日天气怎么样?"})
LangGraph调用MCP
除了自定义工具之外,LangGraph 还支持 MCP(Model Context Protocol),这使得工具的复用和扩展性更强。
要在 LangGraph 中使用 MCP,需要额外安装依赖包:
pip install langchain-mcp-adapters
编写一个简单的MCP Server
关于MCP Server编写,参考之前的文章https://www.wileyzhang.com/posts/222605ee-e889-8098-b91e-f684acac99d2
依旧是以Tavily搜索为例
import json from mcp.server.fastmcp import FastMCP from tavily import TavilyClient mcp = FastMCP("search") @mcp.tool() async def tavily_search(query: str) -> str: """一个针对全面、准确和可信的结果进行了优化的搜索引擎。当需要回答有关时事的问题时很有用。输入应该是搜索查询。"""client = TavilyClient() search_r = client.search(query=query, max_results=2) return json.dumps(search_r, ensure_ascii=False) if __name__ == "__main__": mcp.run(transport="streamable-http")
LangGrpah+MCP完整示例代码
import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.prebuilt import ToolNode, tools_condition from typing import Annotated from typing_extensions import TypedDict from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START from langgraph.graph.message import add_messages from pydantic import SecretStr # 此处定义你自己的模型 llm = ChatOpenAI(base_url="http://127.0.0.1:8000/v1", api_key=SecretStr("123123"), model="qwen3_32") # 配置MCP Server client = MultiServerMCPClient( { "search": { "url": "http://localhost:8000/mcp/", "transport": "streamable_http", } } ) class State(TypedDict): messages: Annotated[list, add_messages] # 此处维护完整的消息历史 graph = StateGraph(State) async def main(): tools = await client.get_tools() # 工具绑定到模型 llm_with_tools = llm.bind_tools(tools) def chatbot(state: State): return {"messages": [llm_with_tools.invoke(state["messages"])]} graph = StateGraph(State) graph.add_node(chatbot) graph.add_node(ToolNode(tools)) graph.add_edge(START, "chatbot") graph.add_conditional_edges( "chatbot", tools_condition, ) graph.add_edge("tools", "chatbot") app = graph.compile() messages = [] while True: user_input = input("👨💻: ") if user_input.lower() in ["quit", "exit", "q"]: print("Exiting...") break messages.append({"role": "user", "content": user_input}) response = await app.ainvoke({"messages": messages}) messages = response["messages"] print(f'🤖: {response["messages"][-1].content}') asyncio.run(main())
效果如下
带工具调用的bot graph图如下