在 LangGraph 智能体中,LLM 节点(通常是 ChatOpenAI
或其他支持函数调用的模型)并不是“魔法般”地知道工具的。它的“知识”和“能力”来源于以下几个关键组成部分和协作模式:
大模型能够使用工具的前提是,这些工具必须以它能理解的格式被“呈现”给它。目前主流的大模型,尤其是 OpenAI 的函数调用(Function Calling)能力,以及 LangChain 的 RunnableTools
,都基于JSON Schema来定义工具。
当你定义一个工具时,你需要提供:
description
也非常重要)。我们定义一个简单的搜索工具来进一步理解
from langchain_core.tools import tool
# 使用 @tool 装饰器可以方便地定义工具
# LangChain 会自动根据函数签名生成 JSON Schema
@tool
def search_web(query: str) -> str:
"""
使用此工具执行互联网搜索以获取最新信息。
输入参数 'query' 是一个字符串,表示要搜索的内容。
"""
print(f"--- Calling Tool: search_web with query: '{query}' ---")
# 实际场景中会调用真实的搜索引擎API
return f"Search result for '{query}': According to the latest data (2025-06-15), quantum computing is progressing rapidly."
@tool
def get_current_weather(location: str) -> str:
"""
获取指定城市当前的实时天气情况。
输入参数 'location' 是一个字符串,表示要查询天气的城市名称,例如 '北京', 'New York'。
"""
print(f"--- Calling Tool: get_current_weather for location: '{location}' ---")
# 实际场景中会调用天气API
return f"Weather in {location}: Sunny, 25°C."
# 将工具列表传递给 LLM
tools = [search_web, get_current_weather]
LLM 知道工具列表后,还需要通过提示 (Prompt) 来接收这些工具的定义,并被明确指示其角色和使用工具的策略。
在 LangChain 中,LLM 如何“接收”这些工具定义呢?通过 bind_tools()
方法。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
# 初始化支持函数调用的 LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 将工具绑定到 LLM 上。
# 这会将工具的 JSON Schema 定义注入到 LLM 的上下文中,
# 使得 LLM 在生成响应时能识别这些工具并按需生成函数调用。
# tools 是一个list,放了一系列工具
llm_with_tools = llm.bind_tools(tools)
背后机制 (Mermaid 图解):
当 llm_with_tools
被调用时,LangChain 会在内部处理,将 tools
列表中每个工具的 JSON Schema 表示添加到发送给 LLM 的请求中(通常在系统消息或特殊参数中)。LLM 收到这些信息后,就知道有哪些工具可用,以及如何构造它们的调用。
当 LLM 接收到用户输入和工具定义后,它会在内部进行“思考”。这个思考过程在 LangChain 中通常通过 messages
列表来模拟,其中包含了对话历史和系统指令。LLM 的输出可能包含:
LLM 思考与输出流程 (Mermaid 图解):
LLM 输出的工具调用请求只是一个“意图”。我们需要一个机制来解析这个意图,并执行对应的工具。
ToolCalling
机制LangChain 的 RunnableTools
和 ToolExecutor
负责解析 LLM 的输出并实际调用工具。
from langgraph.prebuilt import ToolExecutor
# 创建一个工具执行器,它知道如何根据名称调用这些工具
tool_executor = ToolExecutor(tools)
# 代理的 LLM 调用节点(通常包含 LLM 和输出解析)
def call_llm_and_tools(state):
messages = state['messages']
response = llm_with_tools.invoke(messages) # LLM 生成包含工具调用的响应
# LangChain 会自动检测 response 中是否包含 tool_calls
if not response.tool_calls:
# 如果没有工具调用,直接返回 LLM 的文本回复
return {"messages": [response]}
# 如果有工具调用,保存工具调用请求到状态中,准备下一步执行
# LangGraph 的状态会自动合并,所以这里只需要返回需要更新的部分
return {"messages": [response], "tool_calls": response.tool_calls}
# 代理的工具执行节点
async def execute_tools(state):
tool_calls = state['tool_calls']
tool_outputs = []
# 遍历 LLM 生成的所有工具调用请求
for tool_call in tool_calls:
# 使用 ToolExecutor 来执行工具
# LangChain 的 ToolExecutor 能够根据 tool_call.name 找到对应的工具并传递参数
output = await tool_executor.ainvoke(tool_call) # 或者 .invoke()
tool_outputs.append(output)
# 将工具的输出作为 ToolMessage 添加回消息历史,供 LLM 下一轮参考
state['messages'].append(ToolMessage(content=str(output), tool_call_id=tool_call.id))
return {"tool_outputs": tool_outputs, "messages": state['messages']}
工具执行流程 (Mermaid 图解):
LLM 知道“使用什么工具”,但 LangGraph 的图结构和条件路由才真正决定了“何时使用”以及“接下来怎么办”。LLM 的输出仅仅是一个信号,这个信号通过条件函数被转换成图中的下一步决策。
# 核心:根据 LLM 的输出决定下一步
def should_continue(state):
messages = state['messages']
last_message = messages[-1]
# LLM 响应中是否包含工具调用?
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
return "call_tool" # LLM 决定使用工具,跳转到工具执行节点
else:
return "end" # LLM 认为不需要工具,直接结束流程(或转到回复生成节点)
# LangGraph 的设置
from langgraph.graph import StateGraph, END
workflow = StateGraph(dict) # 简化状态为字典
workflow.add_node("llm_node", llm)
workflow.add_node("tool_node", execute_tools)
workflow.set_entry_point("llm_node")
# 根据 LLM 的输出(是否包含 tool_calls)进行条件路由
workflow.add_conditional_edges(
"llm_node", # 源节点
should_continue, # 条件函数:根据 LLM 响应决定下一步
{
"call_tool": "tool_node", # 如果 should_continue 返回 "call_tool",则去 tool_node
"end": END # 如果 should_continue 返回 "end",则结束
}
)
# 工具执行完成后,通常会返回 LLM 节点进行总结或继续对话
workflow.add_edge("tool_node", "llm_node")
app = workflow.compile()
工作流走向(条件路由)Mermaid 图解:
工作流走向的理解:
llm_node
。llm_node
调用 llm_with_tools.invoke()
。LLM 根据其内部对工具的理解和用户输入,决定是否生成工具调用。should_continue
函数被调用,它检查 llm_node
的输出。
should_continue
返回 "call_tool"
。图流转到 tool_node
。should_continue
返回 "end"
。图直接结束(或进入其他结束流程)。tool_node
接收状态中的 tool_calls
,并通过 tool_executor
实际执行这些工具。工具的输出被封装成 ToolMessage
并添加到 messages
历史中。tool_node
执行完毕后,通过 add_edge("tool_node", "llm_node")
,流程再次回到 llm_node
。这时,llm_node
再次接收包含最新工具输出的 messages
历史。LLM 可以根据工具的反馈进行下一步思考:
在上述过程中,AgentState
扮演了至关重要的角色。它是所有节点共享和更新的唯一的、可变的数据结构。
AgentState
作为输入。AgentState
中。
messages
),通常是追加(append)。tool_calls
, tool_outputs
),通常是更新(update)。messages
列表在整个流程中不断积累,确保 LLM 始终拥有完整的对话历史和工具交互记录,这是它进行“思考”和“决策”的基础。状态变化流 (Mermaid 图解):
工具注册: Python 工具函数通过装饰器或封装,生成 JSON Schema 定义。
LLM 绑定: 这些 JSON Schema 定义被传递给 LLM,让 LLM “知道”有哪些工具,它们的用途和参数。
LLM 决策: 当用户输入到达 LLM 节点时,LLM 根据其接收到的提示(包括工具定义)和历史消息,决定是直接回复还是调用工具。如果调用工具,它会生成一个结构化的工具调用请求。
输出解析与路由:
LangGraph 的条件函数检查 LLM 的输出。
工具执行: 工具执行节点解析 LLM 的工具调用请求,并通过 ToolExecutor
实际运行对应的 Python 函数。
结果反馈与循环: 工具的执行结果被封装成 ToolMessage
添加到共享状态的 messages
历史中。然后,流程通常会循环回到 LLM 节点。LLM 再次接收到包含最新工具输出的完整上下文,进行下一轮的“思考-行动-观察”循环,直到任务完成。
状态持久化: 在整个过程中,LangGraph 的 Checkpointer
可以将 AgentState
持久化,确保代理具有长期记忆,并且可以在中断后恢复。