目录
一、导读
环境:OpenEuler、Windows 11、WSL 2、Python 3.12.3 langchain 0.3 langgraph 0.2
背景:前期忙碌的开发阶段结束,需要沉淀自己的应用知识,过一遍LangGraph
时间:20250308
说明:技术梳理,LangGraph 智能体、多智能体概念、原理详解,代码实现
注意:模型不同,代码可能存在bug,执行如果出现异常,可以多测试几次
二、概念原理
1、智能体
智能体在英文文档中一般使用agent表示,所以某些文章中的代理也是表示当前智能体的概念。
智能体(Agent)是指能够感知环境并采取行动以实现特定目标的代理体。它可以是软件、硬件或一个系统,具备自主性、适应性和交互能力。智能体通过感知环境中的变化(如通过传感器或数据输入),根据自身学习到的知识和算法进行判断和决策,进而执行动作以影响环境或达到预定的目标。智能体在人工智能领域广泛应用,常见于自动化系统、机器人、虚拟助手和游戏角色等,其核心在于能够自主学习和持续进化,以更好地完成任务和适应复杂环境。
在LangChain或是LangGraph中,最简单的一个对话系统也可以称之为智能体,所以,不用想的很复杂
2、多智能体
多智能体也可以看作是一个智能体,因为本质上它是一个系统,只是功能多而已。在LangGraph的文档中,使用Multi-agent表示,子图是一种另类的多智能体。
3、智能体弊端
复杂的系统可能会遇到以下问题:
- 智能体拥有的工具太多,无法做出关于调用下一个工具的明智决策
- 上下文变得过于复杂,单个智能体难以跟踪
- 系统中需要多个专业领域(例如计划者、研究者、数学专家等)
为了解决这些问题,可以考虑将应用程序分解为多个较小的独立智能体,并将它们组合成一个多智能体系统。这些独立智能体可以简单到只是一个提示和一次LLM调用,也可以复杂到你想象不到的地步。
4、多智能体优点
模块化:独立的智能体使得开发、测试和维护系统变得更加容易。
专业化:你可以创建专注于特定领域的专家智能体,这有助于提高整个系统的性能。
控制:你可以明确控制智能体之间的通信方式(而不是依赖于函数调用)。
5、多智能体架构
在多智能体系统中,常见的连接方式有以下几种:
网络:每个智能体都可以与其他每个智能体通信。任何智能体都可以决定下一个调用哪个智能体。
监督者:每个智能体都与一个监督者智能体通信。监督者智能体决定下一个调用哪个智能体。
监督者(工具调用):这是监督者架构的一个特例。个体智能体可以表示为工具。在这种情况下,监督者智能体使用工具调用的LLM来决定调用哪个智能体工具,以及传递给这些智能体的参数。
层次化:你可以定义一个具有监督者监督者的多智能体系统。这是监督者架构的泛化,允许更复杂的控制流。
自定义多智能体工作流:每个智能体只与部分智能体通信。部分流程是确定性的,只有某些智能体可以决定调用下一个哪个智能体。
6、交接(Handoffs)
交接指的是该智能体下一步是结束、另一个智能体还是本身。交接需要两个参数:目标智能体、负载。
一般交接均使用Command对象,例如:
在LangGraph中实现交接,智能体节点可以返回Command对象,该对象允许你结合控制流和状态更新:
def agent(state) -> Command[Literal["agent", "another_agent"]]:
# 路由/停止的条件可以是任何东西,例如LLM工具调用/结构化输出等
goto = get_next_agent(...) # 'agent' / 'another_agent'
return Command(
# 指定调用下一个智能体
goto=goto,
# 更新图状态
update={"my_state_key": "my_state_value"}
)
在更复杂的场景中,每个智能体节点本身就是一个图(即,一个子图),一个智能体子图中的节点可能希望导航到另一个智能体。例如,如果你有两个智能体,alice
和bob
(父图中的子图节点),并且alice
需要导航到bob
,你可以在Command
对象中设置graph=Command.PARENT
:
def some_node_inside_alice(state)
return Command(
goto="bob",
update={"my_state_key": "my_state_value"},
# 指定要导航到的图(默认为当前图)
graph=Command.PARENT,
)
最常见的智能体类型之一是ReAct风格的工具调用智能体。对于这种类型的智能体,常见的模式是将交接包装在一个工具调用中
def transfer_to_bob(state):
"""转移到 bob."""
return Command(
goto="bob",
update={"my_state_key": "my_state_value"},
graph=Command.PARENT,
)
7、架构说明
(1)网络
在这种架构中,智能体被定义为图节点。每个智能体可以与每个其他智能体通信(多对多连接),并可以决定调用下一个哪个智能体。这种架构适用于没有明确智能体层次结构或特定智能体调用顺序的问题
(2)监督者
在这种架构中,我们定义智能体为节点,并添加一个监督者节点(LLM),它决定调用哪个智能体节点。我们使用Command根据监督者的决定将执行路由到适当的智能体节点。这种架构也适用于并行运行多个智能体或使用map-reduce模式。
(3)监督者(工具调用)
在这种监督者架构的变体中,我们将个体智能体定义为工具,并在监督者节点中使用工具调用的LLM。这可以实现为一个ReAct-风格的智能体,具有两个节点——一个LLM节点(监督者)和一个执行工具的节点(在这种情况下为智能体)。
(4)层级
当你向系统添加更多智能体时,监督者可能难以管理所有智能体。监督者可能会开始做出关于调用哪个智能体的不良决策,上下文可能变得过于复杂,以至于单个监督者无法跟踪。换句话说,你最终会遇到最初促使多智能体架构的问题。
为了解决这个问题,你可以设计你的系统为层级式。例如,你可以创建由单独监督者管理的专用智能体团队,并由顶层监督者管理这些团队。
(5)自定义
在这种架构中,我们添加单独的智能体作为图节点,并预先定义智能体调用的顺序,形成一个自定义工作流。在LangGraph中,工作流可以通过两种方式定义:
显式控制流(普通边):LangGraph允许你显式定义应用程序的控制流(即智能体通信的顺序),通过普通图边。这是上述架构中最确定的变体——我们总是知道下一个调用哪个智能体。
动态控制流(Command):在LangGraph中,你可以允许LLM决定应用程序控制流的部分。这可以通过使用Command实现。这种架构的一个特例是监督者工具调用架构。在这种情况下,为监督者智能体提供动力的工具调用LLM将决定调用工具(智能体)的顺序。
8、智能体通信
(1)state
上述大多数架构中,智能体是通过state进行通信的
相同state
要通过图状态进行通信,需要将各个智能体定义为图节点。这些节点可以作为函数或整个子图添加。在图执行的每一步中,智能体节点都会接收当前的图状态,执行智能体代码,然后将更新后的状态传递给下一个节点。
不同state
一个智能体可能需要具有与其他智能体不同的状态模式。例如,搜索智能体可能只需要跟踪查询和检索到的文档。在LangGraph中,有两种方法可以实现这一点:
定义具有单独状态模式的子图智能体:如果子图和父图之间没有共享状态键(通道),则需要添加输入/输出转换,以便父图知道如何与子图通信。
定义具有私有输入状态模式的智能体节点函数:该模式与整体图状态模式不同。这允许传递仅用于执行特定智能体的信息。
(2)工具调用
在具有工具调用的监督者的情况下,负载是工具调用的参数。
(3)共享信息
智能体之间最常用的通信方式是通过共享state,通常是消息列表。这假设状态中始终至少有一个通道(键)被智能体共享。
共享完整的历史
智能体可以与所有其他智能体共享完整的思考过程历史(草稿)。这种“草稿”通常看起来像一个消息列表。共享完整思考过程的好处是,它可能帮助其他智能体做出更好的决策,并提高整个系统的推理能力。缺点是随着智能体数量和复杂性的增加,“草稿”会迅速增长,可能需要额外的策略来管理内存。
共享最终结果
智能体可以有自己的私有“草稿”,并仅与其他智能体共享最终结果。这种方法可能更适合具有许多智能体或更复杂智能体的系统。在这种情况下,你需要定义具有不同状态模式的智能体。
对于作为工具调用的智能体,监督者根据工具模式确定输入。此外,LangGraph允许传递state给工具,因此下属智能体可以在需要时访问父图state。
三、Conmand示例
此示例演示使用用Command进行交接
1、功能说明
此处实现两个智能体的系统:加法智能体、乘法智能体,计算功能交给大模型。两个智能体互相可以交接,返回结束或是Command对象,拓扑结构为网络架构。
大模型计算时若是返回带有tool_calls属性,则返回Command对象,否则直接返回消息并结束
2、添加工具
此处需要两个工具,该工具仅具备形式的含义,方便判断大模型返回时是否具有call_tools属性
@tool
def transfer_to_multiplication_expert():
"""向乘法智能体寻求帮助。"""
# 此工具不会返回任何内容:我们只是使用它作为一种方式,让语言模型(LLM)表明需要将任务移交给另一个智能体
return
@tool
def transfer_to_addition_expert():
"""向加法智能体寻求帮助。"""
return
3、添加智能体
智能体有两个,分别时加法、乘法智能体。此时大模型先绑定工具再调用,通过大模型调用返回值来确认是结束还是路由到某个智能体,此逻辑均在智能体中实现,加法智能体如下
def addition_expert(
state: MessagesState,
) -> Command[Literal["multiplication_expert", "__end__"]]:
system_prompt = (
"你是加法专家,当你需要进行乘法运算时可以向乘法专家寻求帮助。在交接前,务必先完成你自己部分的计算。"
)
messages = [{"role": "system", "content": system_prompt}] + state["messages"]
ai_msg = model.bind_tools([transfer_to_multiplication_expert]).invoke(messages)
# 如果存在工具调用,LLM(语言模型)需要将任务移交给另一个智能体(agent)。
if len(ai_msg.tool_calls) > 0:
tool_call_id = ai_msg.tool_calls[-1]["id"]
# 注意:在这里插入一个工具消息是很重要的,因为LLM提供商期望所有的AI消息后面都跟着一个相应的工具结果消息。
tool_msg = {
"role": "tool",
"content": "成功交接",
"tool_call_id": tool_call_id,
}
return Command(
goto="multiplication_expert", update={"messages": [ai_msg, tool_msg]}
)
return {"messages": [ai_msg]}
显然,当 len(ai_msg.tool_calls) > 0时,说明存在工具调用,直接返回Command对象,需要两个参数,目的地(goto)和信息(update)
乘法智能体如下
def multiplication_expert(
state: MessagesState,
) -> Command[Literal["addition_expert", "__end__"]]:
system_prompt = (
"你是一个乘法专家,你可以向加法专家寻求加法方面的帮助。在交接之前,总是先完成你那部分的计算。"
)
messages = [{"role": "system", "content": system_prompt}] + state["messages"]
ai_msg = model.bind_tools([transfer_to_addition_expert]).invoke(messages)
if len(ai_msg.tool_calls) > 0:
tool_call_id = ai_msg.tool_calls[-1]["id"]
tool_msg = {
"role": "tool",
"content": "成功交接",
"tool_call_id": tool_call_id,
}
return Command(goto="addition_expert", update={"messages": [ai_msg, tool_msg]})
return {"messages": [ai_msg]}
由于此处仅有两个智能体,所以目的地时互相指向的,看代码中goto的参数
4、编译graph
由于两个智能体互相指向,所以此处添加两个节点随意指向一个起始节点即可,如下
builder = StateGraph(MessagesState)
builder.add_node("addition_expert", addition_expert)
builder.add_node("multiplication_expert", multiplication_expert)
builder.add_edge(START, "addition_expert")
graph = builder.compile()
5、格式化输出
为了能够更清晰的输出过程信息,此处定义一个输出函数
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# 在打印输出中跳过父图更新。
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图{graph_id}输出:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点{node_name}输出:")
print("\n")
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
5、完整代码
from typing_extensions import Literal
from langchain_core.tools import tool
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.messages import convert_to_messages
# 指定大模型的API Key 等相关信息
model = ChatOpenAI(
base_url="https://lxxxxx.enovo.com/v1/",
api_key="sxxxxxxxwW",
model_name="qwen2.5-instruct"
)
@tool
def transfer_to_multiplication_expert():
"""向乘法智能体寻求帮助。"""
# 此工具不会返回任何内容:我们只是使用它作为一种方式,让语言模型(LLM)表明需要将任务移交给另一个智能体
return
@tool
def transfer_to_addition_expert():
"""向加法智能体寻求帮助。"""
return
def addition_expert(
state: MessagesState,
) -> Command[Literal["multiplication_expert", "__end__"]]:
system_prompt = (
"你是加法专家,当你需要进行乘法运算时可以向乘法专家寻求帮助。在交接前,务必先完成你自己部分的计算。"
)
messages = [{"role": "system", "content": system_prompt}] + state["messages"]
ai_msg = model.bind_tools([transfer_to_multiplication_expert]).invoke(messages)
# 如果存在工具调用,LLM(语言模型)需要将任务移交给另一个智能体(agent)。
if len(ai_msg.tool_calls) > 0:
tool_call_id = ai_msg.tool_calls[-1]["id"]
# 注意:在这里插入一个工具消息是很重要的,因为LLM提供商期望所有的AI消息后面都跟着一个相应的工具结果消息。
tool_msg = {
"role": "tool",
"content": "成功交接",
"tool_call_id": tool_call_id,
}
return Command(
goto="multiplication_expert", update={"messages": [ai_msg, tool_msg]}
)
return {"messages": [ai_msg]}
def multiplication_expert(
state: MessagesState,
) -> Command[Literal["addition_expert", "__end__"]]:
system_prompt = (
"你是一个乘法专家,你可以向加法专家寻求加法方面的帮助。在交接之前,总是先完成你那部分的计算。"
)
messages = [{"role": "system", "content": system_prompt}] + state["messages"]
ai_msg = model.bind_tools([transfer_to_addition_expert]).invoke(messages)
if len(ai_msg.tool_calls) > 0:
tool_call_id = ai_msg.tool_calls[-1]["id"]
tool_msg = {
"role": "tool",
"content": "成功交接",
"tool_call_id": tool_call_id,
}
return Command(goto="addition_expert", update={"messages": [ai_msg, tool_msg]})
return {"messages": [ai_msg]}
builder = StateGraph(MessagesState)
builder.add_node("addition_expert", addition_expert)
builder.add_node("multiplication_expert", multiplication_expert)
builder.add_edge(START, "addition_expert")
graph = builder.compile()
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# 在打印输出中跳过父图更新。
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图{graph_id}输出:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点{node_name}输出:")
print("\n")
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
for chunk in graph.stream({"messages": [("user", "(3 + 5) * 12 等于多少")]}):
pretty_print_messages(chunk)
6、输出结果
从节点addition_expert输出:
================================== Ai Message ==================================
Tool Calls:
transfer_to_multiplication_expert (call_fac397c6-27ac-40ca-9f4c-892a7d750419)
Call ID: call_fac397c6-27ac-40ca-9f4c-892a7d750419
Args:
result_of_addition: 8
multiplier: 12
================================= Tool Message =================================
成功交接
从节点multiplication_expert输出:
================================== Ai Message ==================================
计算 (3 + 5) * 12 的结果时,首先需要计算加法部分,即 3 + 5 = 8,然后我们将这个结果乘以 12,得到 8 * 12 = 96。因此,(3 + 5) * 12 等于 96。
7、分析
显然,先调用了加法智能体,将( 3 + 5 )的结果计算了出来,之后调用了乘法智能体,此时已经计算完毕,所以直接输出了消息
四、工具示例
此示例演示使用工具进行交接
1、功能说明
创建交接工具,工具直接返回Command对象。当智能体调用工具时,工具会路由到指定的智能体。其主体功能与上述示例一样
注意:与前面的例子不同,调用工具的智能体不是一个单独的节点,而是一个可以作为子图节点添加到多智能体图中的另一个图。
由于每个智能体都是另一个图中的一个__子图__节点,并且工具将在智能体子图节点之一中被调用(例如工具执行器),因此需要在Command中指定graph=Command.PARENT,这样LangGraph就知道要在智能体子图之外导航。
我们可以选择指定一个状态更新,该更新将在下一个智能体被调用之前应用于父图的状态。
这些状态更新可用于控制目标智能体可以看到的聊天消息历史记录的多少。例如,你可能选择只共享当前智能体的最后一条AI消息,或者其完整的内部聊天历史记录等。在下面的例子中,我们将共享完整的内部聊天历史记录。
我们可以选择向工具提供以下内容(在工具函数签名中):
图状态(使用InjectedState)
图长期记忆(使用InjectedStore)
当前工具调用ID(使用InjectedToolCallId)
这些不是必需的,但对创建传递给下一个智能体的状态更新很有用。
2、定义数学工具
定义两个工具,加法与乘法
@tool
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两数相乘"""
return a * b
3、定义子图
该函数返回一个graph对象,其内封装了访问工具和访问大模型的功能
def make_agent(model, tools, system_prompt=None):
model_with_tools = model.bind_tools(tools)
tools_by_name = {tool.name: tool for tool in tools}
def call_model(state: MessagesState) -> Command[Literal["call_tools", "__end__"]]:
messages = state["messages"]
if system_prompt:
messages = [{"role": "system", "content": system_prompt}] + messages
response = model_with_tools.invoke(messages)
if len(response.tool_calls) > 0:
return Command(goto="call_tools", update={"messages": [response]})
return {"messages": [response]}
# 注意:这是预构建的ToolNode的一个简化版本
def call_tools(state: MessagesState) -> Command[Literal["call_model"]]:
tool_calls = state["messages"][-1].tool_calls
results = []
for tool_call in tool_calls:
tool_ = tools_by_name[tool_call["name"]]
tool_input_fields = tool_.get_input_schema().model_json_schema()[
"properties"
]
# 这是为了演示目的而简化的,并且与ToolNode的实际实现不同
if "state" in tool_input_fields:
tool_call = {**tool_call, "args": {**tool_call["args"], "state": state}}
tool_response = tool_.invoke(tool_call)
if isinstance(tool_response, ToolMessage):
results.append(Command(update={"messages": [tool_response]}))
# 处理直接返回命令的工具
elif isinstance(tool_response, Command):
results.append(tool_response)
# 在LangGraph中,节点允许您返回更新列表,包括命令对象
return results
graph = StateGraph(MessagesState)
graph.add_node(call_model)
graph.add_node(call_tools)
graph.add_edge(START, "call_model")
graph.add_edge("call_tools", "call_model")
return graph.compile()
4、简单测试
整体功能与Command示例一样,仅仅交接方式不同
from typing import Annotated
from langchain_core.tools.base import InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langchain_openai import ChatOpenAI
from typing_extensions import Literal
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.types import Command
from langchain_core.messages import convert_to_messages
# 指定大模型的API Key 等相关信息
model = ChatOpenAI(
base_url="https://lxxxxx.enovo.com/v1/",
api_key="sxxxxxxxwW",
model_name="qwen2.5-instruct"
)
def make_agent(model, tools, system_prompt=None):
model_with_tools = model.bind_tools(tools)
tools_by_name = {tool.name: tool for tool in tools}
def call_model(state: MessagesState) -> Command[Literal["call_tools", "__end__"]]:
messages = state["messages"]
if system_prompt:
messages = [{"role": "system", "content": system_prompt}] + messages
response = model_with_tools.invoke(messages)
if len(response.tool_calls) > 0:
return Command(goto="call_tools", update={"messages": [response]})
return {"messages": [response]}
# 注意:这是预构建的ToolNode的一个简化版本
def call_tools(state: MessagesState) -> Command[Literal["call_model"]]:
tool_calls = state["messages"][-1].tool_calls
results = []
for tool_call in tool_calls:
tool_ = tools_by_name[tool_call["name"]]
tool_input_fields = tool_.get_input_schema().model_json_schema()[
"properties"
]
# 这是为了演示目的而简化的,并且与ToolNode的实际实现不同
if "state" in tool_input_fields:
tool_call = {**tool_call, "args": {**tool_call["args"], "state": state}}
tool_response = tool_.invoke(tool_call)
if isinstance(tool_response, ToolMessage):
results.append(Command(update={"messages": [tool_response]}))
# 处理直接返回命令的工具
elif isinstance(tool_response, Command):
results.append(tool_response)
# 在LangGraph中,节点允许您返回更新列表,包括命令对象
return results
graph = StateGraph(MessagesState)
graph.add_node(call_model)
graph.add_node(call_tools)
graph.add_edge(START, "call_model")
graph.add_edge("call_tools", "call_model")
return graph.compile()
@tool
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两数相乘"""
return a * b
agent = make_agent(model, [add, multiply])
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# skip parent graph updates in the printouts
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图{graph_id}输出:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点{node_name}输出:")
print("\n")
if isinstance(node_update, list):
node_update = node_update[-1]
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
for chunk in agent.stream({"messages": [("user", "(3 + 5) * 12 等于多少")]}):
pretty_print_messages(chunk)
5、输出内容
从节点call_model输出:
================================== Ai Message ==================================
Tool Calls:
add (call_e5ac406e-310f-4964-8ac1-8fcdd1dbe1d6)
Call ID: call_e5ac406e-310f-4964-8ac1-8fcdd1dbe1d6
Args:
a: 3
b: 5
multiply (call_e5ac406e-310f-4964-8ac1-8fcdd1dbe1d6)
Call ID: call_e5ac406e-310f-4964-8ac1-8fcdd1dbe1d6
Args:
a: 8
b: 12
从节点call_tools输出:
================================= Tool Message =================================
Name: multiply
96
从节点call_model输出:
================================== Ai Message ==================================
计算 (3 + 5) * 12 的结果是 96。
由此可见,可以正常输出
6、定义交接工具
该工具的作用仅仅为路由到指定工具的作用,可以认为是中介或是中间件的作用
def make_handoff_tool(*, agent_name: str):
"""创建一个能够通过命令返回交接信息的工具。"""
tool_name = f"transfer_to_{agent_name}"
@tool(tool_name)
def handoff_to_agent(
# 可选地将当前图的状态传递给工具(该状态将被语言模型忽略)
state: Annotated[dict, InjectedState],
# 可选地传递当前的工具调用ID(该ID将被语言模型忽略)
tool_call_id: Annotated[str, InjectedToolCallId],
):
"""Ask another agent for help."""
tool_message = {
"role": "tool",
"content": f"Successfully transferred to {agent_name}",
"name": tool_name,
"tool_call_id": tool_call_id,
}
return Command(
# # 在父图中导航到另一个代理节点
goto=agent_name,
graph=Command.PARENT,
# 这是智能体 `agent_name` 在被调用时将看到的状态更新。
# 我们传递了代理的全部内部消息历史,并添加了一个工具消息,以确保最终的聊天记录是有效的
update={"messages": state["messages"] + [tool_message]},
)
return handoff_to_agent
7、定义智能体
定义加法、乘法智能体,指定大模型、工具、交接工具以及提示词
addition_expert = make_agent(
model,
[add, make_handoff_tool(agent_name="multiplication_expert")],
system_prompt="您是加法专家,您可以向乘法专家寻求乘法方面的帮助。",
)
multiplication_expert = make_agent(
model,
[multiply, make_handoff_tool(agent_name="addition_expert")],
system_prompt="您是乘法专家,您可以向加法专家寻求加法方面的帮助。",
)
builder = StateGraph(MessagesState)
builder.add_node("addition_expert", addition_expert)
builder.add_node("multiplication_expert", multiplication_expert)
builder.add_edge(START, "addition_expert")
graph = builder.compile()
8、完整代码
此次运行,会调用定义的交接工具,实现演示目的
from typing import Annotated
from langchain_core.tools.base import InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langchain_openai import ChatOpenAI
from typing_extensions import Literal
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.types import Command
from langchain_core.messages import convert_to_messages
# 指定大模型的API Key 等相关信息
model = ChatOpenAI(
base_url="https://lxxxxx.enovo.com/v1/",
api_key="sxxxxxxxwW",
model_name="qwen2.5-instruct"
)
def make_handoff_tool(*, agent_name: str):
"""创建一个能够通过命令返回交接信息的工具。"""
tool_name = f"transfer_to_{agent_name}"
@tool(tool_name)
def handoff_to_agent(
# 可选地将当前图的状态传递给工具(该状态将被语言模型忽略)
state: Annotated[dict, InjectedState],
# 可选地传递当前的工具调用ID(该ID将被语言模型忽略)
tool_call_id: Annotated[str, InjectedToolCallId],
):
"""Ask another agent for help."""
tool_message = {
"role": "tool",
"content": f"成功交接到 {agent_name}",
"name": tool_name,
"tool_call_id": tool_call_id,
}
return Command(
# # 在父图中导航到另一个代理节点
goto=agent_name,
graph=Command.PARENT,
# 这是智能体 `agent_name` 在被调用时将看到的状态更新。
# 我们传递了代理的全部内部消息历史,并添加了一个工具消息,以确保最终的聊天记录是有效的
update={"messages": state["messages"] + [tool_message]},
)
return handoff_to_agent
def make_agent(model, tools, system_prompt=None):
model_with_tools = model.bind_tools(tools)
tools_by_name = {tool.name: tool for tool in tools}
def call_model(state: MessagesState) -> Command[Literal["call_tools", "__end__"]]:
messages = state["messages"]
if system_prompt:
messages = [{"role": "system", "content": system_prompt}] + messages
response = model_with_tools.invoke(messages)
if len(response.tool_calls) > 0:
return Command(goto="call_tools", update={"messages": [response]})
return {"messages": [response]}
# 注意:这是预构建的ToolNode的一个简化版本
def call_tools(state: MessagesState) -> Command[Literal["call_model"]]:
tool_calls = state["messages"][-1].tool_calls
results = []
for tool_call in tool_calls:
tool_ = tools_by_name[tool_call["name"]]
tool_input_fields = tool_.get_input_schema().model_json_schema()[
"properties"
]
# 这是为了演示目的而简化的,并且与ToolNode的实际实现不同
if "state" in tool_input_fields:
tool_call = {**tool_call, "args": {**tool_call["args"], "state": state}}
tool_response = tool_.invoke(tool_call)
if isinstance(tool_response, ToolMessage):
results.append(Command(update={"messages": [tool_response]}))
# 处理直接返回命令的工具
elif isinstance(tool_response, Command):
results.append(tool_response)
# 在LangGraph中,节点允许您返回更新列表,包括命令对象
return results
graph = StateGraph(MessagesState)
graph.add_node(call_model)
graph.add_node(call_tools)
graph.add_edge(START, "call_model")
graph.add_edge("call_tools", "call_model")
return graph.compile()
@tool
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两数相乘"""
return a * b
addition_expert = make_agent(
model,
[add, make_handoff_tool(agent_name="multiplication_expert")],
system_prompt="您是加法专家,您可以向乘法专家寻求乘法方面的帮助。",
)
multiplication_expert = make_agent(
model,
[multiply, make_handoff_tool(agent_name="addition_expert")],
system_prompt="您是乘法专家,您可以向加法专家寻求加法方面的帮助。",
)
builder = StateGraph(MessagesState)
builder.add_node("addition_expert", addition_expert)
builder.add_node("multiplication_expert", multiplication_expert)
builder.add_edge(START, "addition_expert")
graph = builder.compile()
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# 在打印输出中跳过父图更新。
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图{graph_id}输出:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点{node_name}输出:")
print("\n")
if isinstance(node_update, list):
node_update = node_update[-1]
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
for chunk in graph.stream({"messages": [("user", "(3 + 5) * 12 等于多少")]}):
pretty_print_messages(chunk)
9、输出结果
从节点addition_expert输出:
================================ Human Message =================================
(3 + 5) * 12 等于多少
================================== Ai Message ==================================
Tool Calls:
add (call_2436b363-357f-4775-a58a-6adfe6189a3a)
Call ID: call_2436b363-357f-4775-a58a-6adfe6189a3a
Args:
a: 3
b: 5
================================= Tool Message =================================
Name: add
8
================================== Ai Message ==================================
Tool Calls:
transfer_to_multiplication_expert (call_9286e818-9bcf-48ba-9e14-d29ca6218aec)
Call ID: call_9286e818-9bcf-48ba-9e14-d29ca6218aec
Args:
================================= Tool Message =================================
Name: transfer_to_multiplication_expert
成功交接到 multiplication_expert
从节点multiplication_expert输出:
================================ Human Message =================================
(3 + 5) * 12 等于多少
================================== Ai Message ==================================
Tool Calls:
add (call_2436b363-357f-4775-a58a-6adfe6189a3a)
Call ID: call_2436b363-357f-4775-a58a-6adfe6189a3a
Args:
a: 3
b: 5
================================= Tool Message =================================
Name: add
8
================================== Ai Message ==================================
Tool Calls:
transfer_to_multiplication_expert (call_9286e818-9bcf-48ba-9e14-d29ca6218aec)
Call ID: call_9286e818-9bcf-48ba-9e14-d29ca6218aec
Args:
================================= Tool Message =================================
Name: transfer_to_multiplication_expert
成功交接到 multiplication_expert
================================== Ai Message ==================================
Tool Calls:
multiply (call_917e4493-2d45-44d9-be56-603df36a36f4)
Call ID: call_917e4493-2d45-44d9-be56-603df36a36f4
Args:
a: 8
b: 12
================================= Tool Message =================================
Name: multiply
96
================================== Ai Message ==================================
(3 + 5) * 12 等于 96。
9、分析
计算结果是工具计算,而非大模型计算出来的;有专门的工具进行交接,而非由智能体直接路由
五、React Agent
1、方法说明
如果你不需要额外的自定义功能,可以使用预构建的create_react_agent,它通过ToolNode内置了交接工具的支持。也就是说,使用create_react_agent代替了make_agent即可
2、代码变动
使用create_react_agent代替了make_agent
from langgraph.prebuilt import create_react_agent
addition_expert = create_react_agent(
model,
[add, make_handoff_tool(agent_name="multiplication_expert")],
prompt="您是加法专家,您可以向乘法专家寻求乘法方面的帮助。",
)
multiplication_expert = create_react_agent(
model,
[multiply, make_handoff_tool(agent_name="addition_expert")],
prompt="您是乘法专家,您可以向加法专家寻求加法方面的帮助。",
)
3、完整代码
from typing import Annotated
from langchain_core.tools.base import InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.types import Command
from langchain_core.messages import convert_to_messages
# 指定大模型的API Key 等相关信息
model = ChatOpenAI(
base_url="https://lxxxxx.enovo.com/v1/",
api_key="sxxxxxxxwW",
model_name="qwen2.5-instruct"
)
def make_handoff_tool(*, agent_name: str):
"""创建一个能够通过命令返回交接信息的工具。"""
tool_name = f"transfer_to_{agent_name}"
@tool(tool_name)
def handoff_to_agent(
# 可选地将当前图的状态传递给工具(该状态将被语言模型忽略)
state: Annotated[dict, InjectedState],
# 可选地传递当前的工具调用ID(该ID将被语言模型忽略)
tool_call_id: Annotated[str, InjectedToolCallId],
):
"""Ask another agent for help."""
tool_message = {
"role": "tool",
"content": f"成功交接到 {agent_name}",
"name": tool_name,
"tool_call_id": tool_call_id,
}
return Command(
# # 在父图中导航到另一个代理节点
goto=agent_name,
graph=Command.PARENT,
# 这是智能体 `agent_name` 在被调用时将看到的状态更新。
# 我们传递了代理的全部内部消息历史,并添加了一个工具消息,以确保最终的聊天记录是有效的
update={"messages": state["messages"] + [tool_message]},
)
return handoff_to_agent
@tool
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两数相乘"""
return a * b
addition_expert = create_react_agent(
model,
[add, make_handoff_tool(agent_name="multiplication_expert")],
prompt="您是加法专家,您可以向乘法专家寻求乘法方面的帮助。",
)
multiplication_expert = create_react_agent(
model,
[multiply, make_handoff_tool(agent_name="addition_expert")],
prompt="您是乘法专家,您可以向加法专家寻求加法方面的帮助。",
)
builder = StateGraph(MessagesState)
builder.add_node("addition_expert", addition_expert)
builder.add_node("multiplication_expert", multiplication_expert)
builder.add_edge(START, "addition_expert")
graph = builder.compile()
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# 在打印输出中跳过父图更新。
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图{graph_id}输出:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点{node_name}输出:")
print("\n")
if isinstance(node_update, list):
node_update = node_update[-1]
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
for chunk in graph.stream({"messages": [("user", "(3 + 5) * 12 等于多少")]}):
pretty_print_messages(chunk)
4、输出结果
从节点addition_expert输出:
================================ Human Message =================================
(3 + 5) * 12 等于多少
================================== Ai Message ==================================
Tool Calls:
add (call_01a46d37-188b-473d-b5f3-994d9beafe94)
Call ID: call_01a46d37-188b-473d-b5f3-994d9beafe94
Args:
a: 3
b: 5
================================= Tool Message =================================
Name: add
8
================================== Ai Message ==================================
Tool Calls:
transfer_to_multiplication_expert (call_3aeea06b-8b6b-4df0-9d12-0676a2cb3b07)
Call ID: call_3aeea06b-8b6b-4df0-9d12-0676a2cb3b07
Args:
================================= Tool Message =================================
Name: transfer_to_multiplication_expert
成功交接到 multiplication_expert
从节点multiplication_expert输出:
================================ Human Message =================================
(3 + 5) * 12 等于多少
================================== Ai Message ==================================
Tool Calls:
add (call_01a46d37-188b-473d-b5f3-994d9beafe94)
Call ID: call_01a46d37-188b-473d-b5f3-994d9beafe94
Args:
a: 3
b: 5
================================= Tool Message =================================
Name: add
8
================================== Ai Message ==================================
Tool Calls:
transfer_to_multiplication_expert (call_3aeea06b-8b6b-4df0-9d12-0676a2cb3b07)
Call ID: call_3aeea06b-8b6b-4df0-9d12-0676a2cb3b07
Args:
================================= Tool Message =================================
Name: transfer_to_multiplication_expert
成功交接到 multiplication_expert
================================== Ai Message ==================================
Tool Calls:
multiply (call_ebca3e9c-8c7d-43c6-832d-1202117468b8)
Call ID: call_ebca3e9c-8c7d-43c6-832d-1202117468b8
Args:
a: 8
b: 12
================================= Tool Message =================================
Name: multiply
96
================================== Ai Message ==================================
(3 + 5) * 12 等于 96。
六、函数API
本小节使用函数API演示如何实现一个网络架构,其中每个智能体都可以与其他所有智能体通信(多对多连接),并且可以决定下一个调用哪个代理。
1、功能说明
建立一个旅行助手智能体团队,这些智能体能够相互交流。
我们将创建两个智能体:
travel_advisor:可以提供旅行目的地推荐。可以向hotel_advisor寻求帮助。
hotel_advisor:可以提供酒店推荐。可以向travel_advisor寻求帮助。
这是一个完全连接的网络——每个智能体都可以与其他任何智能体通信。
本节未使用常规的节点、边、状态的构建方法,使用了LangGraph的API形式
2、定义工具
添加酒店、城市工具,以及交接工具
@tool
def get_travel_recommendations():
"""获取旅行目的地推荐"""
return random.choice(["哈尔滨", "沈阳"])
@tool
def get_hotel_recommendations(location: Literal["哈尔滨", "沈阳"]):
"""获取给定目的地的酒店推荐。"""
return {
"哈尔滨": ["格林豪泰酒店", "万达酒店"],
"沈阳": ["七天酒店", "白玉兰酒店"]
}[location]
@tool(return_direct=True)
def transfer_to_hotel_advisor():
"""向酒店顾问代理寻求帮助。"""
return "成功交接到hotel advisor"
@tool(return_direct=True)
def transfer_to_travel_advisor():
"""向旅行顾问代理寻求帮助。"""
return "成功交接到travel advisor"
注意:传输工具中使用了@tool(return_direct=True)
。这样做的目的是为了让单个代理(例如,travel_advisor
)在调用这些工具时能够提前退出ReAct循环。这是期望的行为,因为我们希望在代理调用此工具时立即检测到并立即将控制权移交给另一个代理。
3、定义智能体
定义两个智能体,酒店和城市推荐智能体
# 定义城市推荐
travel_advisor_tools = [
get_travel_recommendations,
transfer_to_hotel_advisor
]
travel_advisor = create_react_agent(
model,
travel_advisor_tools,
state_modifier=(
"你是一个通用的旅行专家,可以推荐旅行目的地(如国家、城市等)。"
"如果你需要酒店推荐,请向‘hotel_advisor’寻求帮助。"
"在转接给其他代理之前,你必须提供易于人类阅读的回复。"
),
)
@task
def call_travel_advisor(messages):
# 您还可以添加额外的逻辑,例如更改 agent 的输入 / agent 的输出等。
# 注意:我们正在调用 ReAct 代理,其中包含该状态中消息的完整历史记录
response = travel_advisor.invoke({"messages": messages})
return response["messages"]
# 定义酒店推荐
hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]
hotel_advisor = create_react_agent(
model,
hotel_advisor_tools,
state_modifier=(
"你是一个酒店专家,可以为指定的目的地提供酒店推荐。"
"如果你需要帮助选择旅行目的地,请向‘travel_advisor’寻求帮助。"
"在转接给其他代理之前,你必须提供易于人类阅读的回复。"
),
)
@task
def call_hotel_advisor(messages):
response = hotel_advisor.invoke({"messages": messages})
return response["messages"]
4、定义入口
使用LangGraph的Func,实现功能入口流程
# 程序入口
@entrypoint()
def workflow(messages):
messages = add_messages([], messages)
call_active_agent = call_travel_advisor
while True:
agent_messages = call_active_agent(messages).result()
messages = add_messages(messages, agent_messages)
ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))
if not ai_msg.tool_calls:
break
tool_call = ai_msg.tool_calls[-1]
if tool_call["name"] == "transfer_to_travel_advisor":
call_active_agent = call_travel_advisor
elif tool_call["name"] == "transfer_to_hotel_advisor":
call_active_agent = call_hotel_advisor
else:
raise ValueError(f"期望交接工具, 得到了 '{tool_call['name']}'")
return messages
使用了 entrypoint和task方式,实现了图的流转
5、完整代码
from langchain_openai import ChatOpenAI
from langchain_core.messages import AIMessage
from langgraph.prebuilt import create_react_agent
from langgraph.graph import add_messages
from langgraph.func import entrypoint, task
import random
from typing_extensions import Literal
from langchain_core.tools import tool
from langchain_core.messages import convert_to_messages
# 指定大模型的API Key 等相关信息
model = ChatOpenAI(
base_url="https://lxxxxx.enovo.com/v1/",
api_key="sxxxxxxxwW",
model_name="qwen2.5-instruct"
)
@tool
def get_travel_recommendations():
"""获取旅行目的地推荐"""
return random.choice(["哈尔滨", "沈阳"])
@tool
def get_hotel_recommendations(location: Literal["哈尔滨", "沈阳"]):
"""获取给定目的地的酒店推荐。"""
return {
"哈尔滨": ["格林豪泰酒店", "万达酒店"],
"沈阳": ["七天酒店", "白玉兰酒店"]
}[location]
@tool(return_direct=True)
def transfer_to_hotel_advisor():
"""向酒店顾问代理寻求帮助。"""
return "成功交接到hotel advisor"
@tool(return_direct=True)
def transfer_to_travel_advisor():
"""向旅行顾问代理寻求帮助。"""
return "成功交接到travel advisor"
# 定义城市推荐
travel_advisor_tools = [
get_travel_recommendations,
transfer_to_hotel_advisor
]
travel_advisor = create_react_agent(
model,
travel_advisor_tools,
state_modifier=(
"你是一个通用的旅行专家,可以推荐旅行目的地(如国家、城市等)。"
"如果你需要酒店推荐,请向‘hotel_advisor’寻求帮助。"
"在转接给其他代理之前,你必须提供易于人类阅读的回复。"
),
)
@task
def call_travel_advisor(messages):
# 您还可以添加额外的逻辑,例如更改 agent 的输入 / agent 的输出等。
# 注意:我们正在调用 ReAct 代理,其中包含该状态中消息的完整历史记录
response = travel_advisor.invoke({"messages": messages})
return response["messages"]
# 定义酒店推荐
hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]
hotel_advisor = create_react_agent(
model,
hotel_advisor_tools,
state_modifier=(
"你是一个酒店专家,可以为指定的目的地提供酒店推荐。"
"如果你需要帮助选择旅行目的地,请向‘travel_advisor’寻求帮助。"
"在转接给其他代理之前,你必须提供易于人类阅读的回复。"
),
)
@task
def call_hotel_advisor(messages):
response = hotel_advisor.invoke({"messages": messages})
return response["messages"]
# 程序入口
@entrypoint()
def workflow(messages):
messages = add_messages([], messages)
call_active_agent = call_travel_advisor
while True:
agent_messages = call_active_agent(messages).result()
messages = add_messages(messages, agent_messages)
ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))
if not ai_msg.tool_calls:
break
tool_call = ai_msg.tool_calls[-1]
if tool_call["name"] == "transfer_to_travel_advisor":
call_active_agent = call_travel_advisor
elif tool_call["name"] == "transfer_to_hotel_advisor":
call_active_agent = call_hotel_advisor
else:
raise ValueError(f"期望交接工具, 得到了 '{tool_call['name']}'")
return messages
def pretty_print_messages(update):
if isinstance(update, tuple):
ns, update = update
# 在打印输出中跳过父图更新。
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"从子图更新 {graph_id}:")
print("\n")
for node_name, node_update in update.items():
print(f"从节点更新 {node_name}:")
print("\n")
for m in convert_to_messages(node_update["messages"]):
m.pretty_print()
print("\n")
for chunk in workflow.stream(
[
{
"role": "user",
"content": "我想去中国寒冷的地方。选择一个目的地并给我推荐酒店",
}
],
subgraphs=True,
):
pretty_print_messages(chunk)
6、输出结果
从子图更新 call_travel_advisor:
从节点更新 agent:
================================== Ai Message ==================================
Tool Calls:
get_travel_recommendations (call_cf9d6fba-b6fa-451c-818c-a4a26554eb1b)
Call ID: call_cf9d6fba-b6fa-451c-818c-a4a26554eb1b
Args:
location_preference: cold
country: China
从子图更新 call_travel_advisor:
从节点更新 tools:
================================= Tool Message =================================
Name: get_travel_recommendations
哈尔滨
从子图更新 call_travel_advisor:
从节点更新 agent:
================================== Ai Message ==================================
Tool Calls:
transfer_to_hotel_advisor (call_95329200-b9a4-4fb6-87ab-825c5184853d)
Call ID: call_95329200-b9a4-4fb6-87ab-825c5184853d
Args:
destination: 哈尔滨
从子图更新 call_travel_advisor:
从节点更新 tools:
================================= Tool Message =================================
Name: transfer_to_hotel_advisor
成功交接到hotel advisor
从子图更新 call_hotel_advisor:
从节点更新 agent:
================================== Ai Message ==================================
Tool Calls:
get_hotel_recommendations (call_5a0c9b35-6fc8-490a-87ee-f435cb3ca35e)
Call ID: call_5a0c9b35-6fc8-490a-87ee-f435cb3ca35e
Args:
location: 哈尔滨
从子图更新 call_hotel_advisor:
从节点更新 tools:
================================= Tool Message =================================
Name: get_hotel_recommendations
["格林豪泰酒店", "万达酒店"]
从子图更新 call_hotel_advisor:
从节点更新 agent:
================================== Ai Message ==================================
我为您选择了中国的寒冷目的地:哈尔滨。以下是为您推荐的酒店:格林豪泰酒店 和 万达酒店。希望您会喜欢!
7、分析
整体实现了效果,不过代码并非100%运行成功,偶尔会出现异常,推测和使用的模型相关,一般会在response = hotel_advisor.invoke({"messages": messages})出现问题,经测试,保证100%运行成功可以,有些复杂,此处不再赘述。
本小节实现了网络架构的流程,从选择目的地到推荐酒店。里面使用了之前未曾使用的方法,便于理解多种方式的使用
七、大结局
LangChain系列到此结束,由于写的匆忙,有一些bug可能存在于文章中,欢迎指正