LangGraph
不同的Agent架构赋予LLM不同程度的控制权。在一个极端情况下,路由器允许LLMLLM充当路由器从指定的一组选项中选择一个步骤,而在另一个极端情况下,完全自主的长期运行代理可以只有选择任何它希望为给定问题选择的步骤序列。
许多Agnet框架都使用了以下一些概念:
- 工具调用:这通常是LLM做出决策的方式
- 采取行动:通过,LLM的输出用作操作的输入
- 内存:可靠的系统需要了解发生的事情
- 计划:计划步骤(显式或隐式)有助于确保LLM在做出决策时以最高保真度做出决策。
图
LangGraph 的核心是将代理工作流建模为图。您可以使用三个关键组件定义代理的行为
- 状态:一个共享数据结构,表示应用程序的当前快照。它可以是任何 Python 类型,但通常是
TypedDict
或 PydanticBaseModel
。 - 节点:Python 函数,用于编码代理的逻辑。它们以当前
状态
作为输入,执行一些计算或副作用,并返回更新后的状态
。 - 边:Python 函数,根据当前
状态
确定要执行的下一个节点
。它们可以是条件分支或固定转换。
通过组合 节点
和 边
,您可以创建随着时间推移而演变 状态
的复杂循环工作流。但是,真正的力量来自于 LangGraph 如何管理 状态
。强调一点:节点
和 边
仅仅是 Python 函数 - 它们可以包含 LLM 或普通的 Python 代码。
简而言之:节点完成工作,边指示下一步要做什么。
消息传递
在LangGraph中,图由多个节点(代表操作或任务)和连接这些节点的边组成。消息传递是图中节点之间进行通信的方式。当一个节点完成操作时,他会将结果作为一条消息,沿着连接的边发送给其他节点。接收到这些消息的节点则会使用该信息执行自己的操作,然后继续讲处理后的消息传递给下一个节点。这个过程类似于分布式系统中的通信方式。
状态图(StateGraph)
StateGraph
类是使用的主要图类。它由用户定义的State
对象参数化。
消息图(MessageGraph)
MessageGraph
类是一种特殊图类型。MessageGraph
的State
仅为消息列表。除了聊天机器人外,很少使用此类,因为大多数应用程序都需要State
比消息列表更复杂。
编译图
要构建图,首先定义状态,然后添加节点和边,最后编译它。
编译是一个非常简单的步骤。它对图的结构进行一些基本检查(没有孤立节点等)。它也是您可以在其中指定运行时参数的地方,例如检查点
和断点
。您可以通过调用 .compile
方法来编译您的图
graph = graph_builder.compile(...)
必须在使用前编译它。
State(状态)
定义图时,首先要做的就是定义图的State
。State
包含图的模式以及化简器函数,这些函数指定如何将更新应用于状态。State
的模式将成为图中所有节点
和边
的输入模式,并且可以是TypedDict
或Pydantic
模型。所有节点
都将 发出对State
的更新,这些更新随后使用指定的化简器函数进行应用。
状态的模式将成为图中所有 节点 和 边 的输入模式,这意味着节点和边的所有操作都是基于这个模式的状态进行的。换句话说,图的所有组件在处理数据时都会遵循这个模式的定义。
模式
在LangGraph中,定义状态的主要方式是使用TypedDict
。TypedDict
是Python中的一种工具,允许为字典中的每个键指定明确类型。例如
class MyState(TypedDict):
key1: int
key2: str
除了TypedDict外,LangGraph也支持使用Pydantic的BaseModel来定义状态。
使用Pydantic,可以为状态中的每个字段设置默认值,并且Pydantic会自动进行类型检查和验证。
from pydantic import BaseModel
class MyState(BaseModel):
key1: int = 0 # 默认值
key2: str
默认的输入和输出模式
默认情况下爱,LangGraph的图会使用相同的模式来处理输入和输出,也就是说,图的输入状态结构和输出状态结构是一样的。这适用于大多数场景,因为输入和输出通常是一致的。
显式指定输入和输出模式
在某些情况下,希望图的输入模式和输出模式有所不同(例如,当图的输入和输出有不同的键,或者有些键旨在输入时有意义,而输出时不需要),则可以显式地指定不同的输入和输出模式。
多个模式的使用
-
**内部节点传递信息:**有些信息可能不需要再输入或输出时暴露给图外部,这是我们可以使用内部节点来传递这些数据,形成一种“私有状态”。
-
不同的输入/输出模式:有时,输入模式和输出模式不完全相同。例如,图的输入可能包含很多字段,但输出可能只需要一个重要字段。通过定义不同的输入/输出模式,我们可以控制输入和输出的数据格式。
代码讲解
1.定义状态模式
首先,代码定义了四个不同的状态模式 (
InputState
、OutputState
、OverallState
和PrivateState
),这些模式用于描述图中不同部分的状态结构。InputState
:包含一个键user_input,用于表示图的输入,即用户提供的输入数据。OutputState
:包含一个键graph_output,表示图的输出结果,图处理后的最终输出。OverallState
:是图的整体状态,包含三个字段foo
:用来存储中间的计算结果。user_input
:用于记录用户输入。graph_output
:记录图的最终输出结果。
PrivateState
:包含一个字段bar
,是图内部使用的私有状态,用来在节点间传递数据,不会直接暴露给外部。
3.构建图
StateGraph(OverallState, input=InputState, output=OutputState)
:- 这里初始化了一个图,定义了该图的整体状态为
OverallState
,输入模式为InputState
,输出模式为OutputState
。也就是说,图将从InputState
获取输入,并最终输出OutputState
。
- 这里初始化了一个图,定义了该图的整体状态为
builder.add_node(...)
:这些行代码将节点函数添加到图中,节点按顺序执行:node_1
处理用户输入并返回OverallState
。node_2
处理OverallState
并返回PrivateState
。node_3
处理PrivateState
并返回最终的OutputState
。
builder.add_edge(...)
:这些行代码定义了图的执行顺序,规定了节点之间的执行顺序:- 图从
START
开始,先执行node_1
。 node_1
执行完后,结果传递给node_2
。- 然后执行
node_3
,并在END
节点结束。
- 图从
class InputState(TypedDict): user_input: str class OutputState(TypedDict): graph_output: str class OverallState(TypedDict): foo: str user_input: str graph_output: str class PrivateState(TypedDict): bar: str def node_1(state: InputState) -> OverallState: # Write to OverallState return {"foo": state["user_input"] + " name"} def node_2(state: OverallState) -> PrivateState: # Read from OverallState, write to PrivateState return {"bar": state["foo"] + " is"} def node_3(state: PrivateState) -> OutputState: # Read from PrivateState, write to OutputState return {"graph_output": state["bar"] + " Lance"} builder = StateGraph(OverallState,input=InputState,output=OutputState) builder.add_node("node_1", node_1) builder.add_node("node_2", node_2) builder.add_node("node_3", node_3) builder.add_edge(START, "node_1") builder.add_edge("node_1", "node_2") builder.add_edge("node_2", "node_3") builder.add_edge("node_3", END) graph = builder.compile() graph.invoke({"user_input":"My"}) {'graph_output': 'My name is Lance'}
化简器(Reducer)
化简器概念
化简器(Reducer)是用来管理状态更新的函数。它的作用是在节点返回部分状态更新时,决定如何将这些更新应用到图的整体状态。
- 每个状态键可以有自己的化简器函数。
- 如果没有明确指定化简器,默认行为是**覆盖(replace)**旧的值,即节点返回的更新会直接替换现有的状态值。
示例A:默认化简器
在这个例子中,没有为状态键显式指定reducer,所以会使用默认reducer,即覆盖当前状态。
from typing_extensions import TypedDict
class State(TypedDict):
foo: int
bar: list[str]
State
包含两个字段:foo
: 整数类型。bar
: 字符串列表类型。
假设输入状态为:
{"foo": 1, "bar": ["hi"]}
假设第一个节点返回:
{"foo": 2}
-
根据默认reducer的规则,只更新
foo
字段. -
结果状态变为:
{"foo": 2, "bar": ["hi"]}
假设第二个节点返回:
{"bar": ["bye"]}
最终状态变为:
{"foo": 2, "bar": ["bye"]}
示例B:指定化简器函数
在这个示例中,为 bar
字段指定了一个化简器函数 operator.add
,这意味着更新时将执行 累加操作 而不是覆盖操作。
from typing import Annotated
from typing_extensions import TypedDict
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
- 在这个
State
中,foo
没有化简器,因此仍然使用默认的覆盖行为。 bar
使用operator.add
作为化简器,这意味着每次更新bar
时,它会将新的列表与现有的列表 合并(通过add
)。
假设输入状态:
{"foo": 1, "bar": ["hi"]}
假设第一个节点返回:
{"foo": 2}
-
结果状态变为
{"foo": 2, "bar": ["hi"]}
假设第二个节点返回:
{"bar": ["bye"]}
-
由于
bar
使用了operator.add
作为化简器,["bye"]
将与["hi"]
合并。 -
最终状态变为:
{"foo": 2, "bar": ["hi", "bye"]}
在状态图中使用消息
1.为什么要使用消息?
LLM模型,尤其对话模型,通常接受一个消息列表作为输入。每个消息对象可能有不同的类型,比如:
- **HumanMessage:**代表用户输入的消息。
- **AIMessage:**代表LLM生成的响应。
- **SystemMessage:**代表系统给出的说明或指令。
这些消息对象能够帮助模型理解对话的上下文。因此,使用消息对象列表在图中存储和管理对话历史非常有用。
2.在图状态中使用消息
在LangGraph中,可以将消息历史存储在图状态中,这样可以跟踪与LLM的交互历史。
- **消息存储:**可以通过向图状态添加一个键(例如
message
),将消息对象的列表存储在其中。 - 使用 reducer 管理消息更新:当状态更新时,reducer 函数会告诉图如何处理这些更新。如果你不指定 reducer,每次状态更新都会覆盖之前的消息列表。
- 如果想要将新的消息追加到现有的消息列表中,可以使用
operator.add
作为 reducer,这样每次状态更新时,新消息都会追加到现有消息中。
- 如果想要将新的消息追加到现有的消息列表中,可以使用
示例代码说明
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict
class GraphState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
- GraphState:这里定义了一个
GraphState
,它包含一个messages
键,该键使用add_messages
作为其 reducer 函数。 add_messages
函数:这个函数不仅可以处理消息的追加,还可以跟踪消息的唯一 ID,确保更新已经存在的消息而不是重复添加。
3.手动更新消息列表
如果你想手动更新图中的消息(例如人工干预),使用 operator.add
时会将你的更新追加到现有消息列表中,而不是覆盖。为了避免这种情况,你可以使用 add_messages
函数,它会检查消息的 ID:
- 如果是全新的消息,它会将消息追加到列表中。
- 如果消息已存在,它会更新该消息,而不是简单地追加。
4.序列化与反序列化
-
add_messages 函数的序列化功能:该函数还具备将消息反序列化为 LangChain 消息对象的功能,这意味着你可以以不同的格式更新消息。例如,以下两种格式都可以被图状态接受:
{"messages": [HumanMessage(content="message")]}
或
{"messages": [{"type": "human", "content": "message"}]}
使用
add_messages
时,LangChain 会自动将 JSON 格式反序列化为对应的消息对象,你可以通过state["messages"][-1].content
访问最新的消息内容。
5.预构建的MessagsState
因为消息列表的使用非常常见,LangGraph 提供了一个预构建的状态类 MessagesState,其中包含 messages
键,并且已经内置了 add_messages
作为其 reducer 函数。
from langgraph.graph import MessagesState
class State(MessagesState):
documents: list[str]
- MessagesState:它是一个已经预定义好消息处理逻辑的状态类,你可以直接使用或对其进行子类化。
- State:在此例中,
State
继承了MessagesState
,并添加了一个documents
字段。这表明你不仅可以跟踪消息,还可以在状态中同时跟踪其他信息(如文档列表)。
6.如何使用 MessagesState 和 add_messages
通过使用 MessagesState 和 add_messages,你可以:
- 在图状态中轻松地管理消息历史。
- 使用
add_messages
来处理消息的追加、更新和序列化。 - 通过点表示法访问最新的消息内容,例如
state["messages"][-1].content
。
Annotated用法
Annotated
是 Python 的类型提示系统中的一种工具,来自 typing
模块,用于在类型注解中为类型添加额外的元数据。Annotated
的主要作用是为类型提示提供附加信息,这些信息可以在运行时或编译时被特定的框架、工具或开发者自己使用。
基本语法
from typing import Annotated
# Annotated语法:Annotated[原始类型, 附加信息1, 附加信息2, ...]
x: Annotated[int, "Positive"] = 5
-
Annotated
:接受两个或多个参数。- 第一个参数是原始类型(例如
int
、str
、list
等)。 - 后续参数是附加的元数据,这些元数据可以是任何类型,可以是字符串、类、函数等,用来为类型添加额外信息。
在上面的例子中,
Annotated[int, "Positive"]
表示x
是一个整数类型,并附加了一个元数据"Positive"
,表示该整数应该是正数(但 Python 语言本身不会对其进行强制检查,元数据的解释需要你自己或使用工具来处理)。 - 第一个参数是原始类型(例如
Nodes(节点)
节点函数以当前的State
作为输入,并返回一个包含更新后的message
列表(位于键“messages”下)的字典。这是所有 LangGraph 节点函数的基本模式。
1.节点的定义
在 LangGraph 中,节点是图的基本单元,通常是 Python 函数,它们负责执行图中的实际操作。节点可以是同步或异步(async)函数,函数的第一个参数是图的状态,而第二个参数是可选的配置,用于传递额外的可配置参数。
示例代码:
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
builder = StateGraph(dict)
def my_node(state: dict, config: RunnableConfig):
print("In node: ", config["configurable"]["user_id"])
return {"results": f"Hello, {state['input']}!"}
# 第二个参数是可选的
def my_other_node(state: dict):
return state
builder.add_node("my_node", my_node)
builder.add_node("other_node", my_other_node)
my_node
:这是一个包含两个参数的节点函数。第一个参数是state
,即图的当前状态。第二个参数是config
,这是一个可选的 RunnableConfig 对象,用于传递额外的配置信息(例如user_id
)。节点函数执行后,它返回一个更新后的状态,例如"results": "Hello, <input>!"
。my_other_node
:这是一个只有状态输入的节点函数,它直接返回状态,什么也不改变。
2.如何将节点添加到图中
使用 add_node
方法可以将节点添加到图中。add_node
的第一个参数是节点名称,第二个参数是节点的函数。如果你不为节点指定名称,它将自动使用函数名作为节点的默认名称。
示例代码:
builder.add_node("my_node", my_node) # 添加名为“my_node”的节点
builder.add_node(my_other_node) # 没有指定名称,使用函数名“my_other_node”作为节点名称
3.节点是可以批处理和异步支持的
在 LangGraph 的内部,节点函数会被转换为 RunnableLambda
,这提供了额外的功能,例如:
- 批处理支持:如果需要,LangGraph 可以批量处理节点的输入,增加处理效率。
- 异步支持:函数也可以是异步函数,用于处理并发任务。
- 跟踪和调试:LangGraph 提供了对节点执行的本地跟踪和调试功能,方便排查问题。
4.START
和END
节点
LangGraph 提供了两个特殊节点:START
和 END
,它们分别表示图的开始和结束。
START
节点:
START
节点 是一个特殊节点,表示用户输入进入图的入口。通过定义从START
节点到其他节点的边,你可以指定图中哪些节点应该首先执行。
from langgraph.graph import START
graph.add_edge(START, "node_a") # 将用户输入传递到 "node_a" 节点
- 在这段代码中,用户输入会首先传递给
"node_a"
节点进行处理。
END
节点:
END
节点 是图的终点,表示图中的任务已完成。你可以通过END
来表示哪些节点是图的结束节点。
from langgraph.graph import END
graph.add_edge("node_a", END) # 指定 "node_a" 节点是图的结束节点
- 这里
"node_a"
处理完成后,图会结束,表示没有进一步的操作。
5.边(Edge)和节点连接
在图中,节点之间通过**边(edge)**连接。通过 add_edge
方法可以定义节点的执行顺序。
示例代码:
graph.add_edge(START, "my_node") # 输入从 START 节点传递到 my_node
graph.add_edge("my_node", "other_node") # 执行顺序:my_node -> other_node
graph.add_edge("other_node", END) # other_node 执行后结束图
bind_tools
Edges(边)
边是图结构中至关重要的组成部分,决定了各个节点如何相互通信,以及图在某些情况下如何结束
1.边的作用
在 LangGraph 中,边 决定了图中节点的路由逻辑和执行顺序。它们定义了从一个节点到下一个节点的路径。边的类型和逻辑决定了图如何从一个节点过渡到下一个节点,包括以下关键类型:
- **普通边:**简单的从一个节点到另一个节点。
- **条件边:**基于某个条件决定下一个节点。
- **入口点:**从图开始执行时,第一个被调用的节点。
- **条件入口点:**基于条件来决定图启动时的第一个节点。
2.普通边
普通边 是最基本的边类型,它简单地将一个节点连接到下一个节点。如果你始终希望在节点 A 执行之后转到节点 B,可以直接使用 add_edge
方法。
示例代码:
graph.add_edge("node_a", "node_b")
解释:
- 这里,
node_a
执行后,图会直接转到node_b
,继续执行node_b
的逻辑。 - 这是最直接的节点连接方式,适用于固定的逻辑路径。
3.条件边
条件边用于根据某个条件来决定接下来执行哪个节点。可以使用add_conditional_edges
方法,它接受一个路由函数routiong_function
。这个函数会基于图的当前状态(state
)返回一个字,该值决定下一步路由的目标节点。
示例代码:
graph.add_conditional_edges("node_a", routing_function)
解释:
routing_function
是一个函数,它会在node_a
执行完后被调用,并返回下一个节点的名称。这个函数的输出决定路由逻辑。- 如果你有多个路由选项,可以通过返回不同的值来决定下一步的节点。
使用映射表(字典):
可以使用一个字典,将 routing_function
的输出映射到具体的节点名称。这样你可以更精确地控制路由逻辑。
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
解释:
- 在这里,
routing_function
会返回True
或False
,图会根据返回值选择不同的节点。True
时图转到node_b
,False
时转到node_c
。
4.入口点
入口点 是图启动时运行的第一个节点。图的执行从入口点开始。你可以使用虚拟的 START
节点来定义入口点,通过 add_edge
将 START
节点连接到图中的第一个实际节点。
示例代码:
from langgraph.graph import START
graph.add_edge(START, "node_a")
解释
- 在这里,
START
表示图的起点,当图被调用时,首先执行node_a
。这个方法适合静态定义图的第一个节点。
5.条件入口点
条件入口点 是一种特殊的入口点,它允许根据条件选择不同的起点。通过使用 add_conditional_edges
来定义根据状态或其他逻辑来决定哪个节点作为图的启动节点。
示例代码:
from langgraph.graph import START
graph.add_conditional_edges(START, routing_function)
解释
- 这里,
routing_function
用于决定图的第一个执行节点。你可以根据不同的条件从不同的节点开始。
使用映射表(字典):
你可以为 routing_function
的返回值定义映射,决定不同条件下的入口节点。
graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"})
解释:
routing_function
返回True
时图从node_b
开始,返回False
时从node_c
开始。
6.并行执行
在 LangGraph 中,一个节点可以有多个输出边,当节点有多个输出时,这些目标节点会作为下一步的超级步骤并行执行。即,多个目标节点会在同一超级步骤中同时运行。
示例代码:
graph.add_edge("node_a", "node_b")
graph.add_edge("node_a", "node_c")
解释:
- 这里,
node_a
执行完后,node_b
和node_c
会并行执行。多个输出边的节点可以在同一时刻运行,提升图的并行处理能力。
Send(发送)
1.背景:为什么需要Send
在传统的图结构中,节点和边是预先定义的,图中的所有节点运行在共享的状态上。也就是说,通常情况下,节点的数量和它们之间的连接路径在图构建时是固定的,状态也是单一的。
然而,在某些情况下,图的结构和状态在运行时可能需要动态生成。例如:
- Map-Reduce 模式:第一个节点生成一个对象列表,每个对象都需要被单独处理。对象的数量可能不确定(也就是说,我们不知道需要多少条边),而且下游的节点需要对这些对象单独操作,这意味着每个对象应该有其自己的状态版本。
2.Send机制的作用
为了支持这种动态场景,LangGraph提供了Send对象。通过Send,可以动态地为不同的对象生成新的边,并为每个边传递不同的状态。这允许在运行时根据对象的数量动态地生成图结构,而不是事先固定。
Send 的两个参数:
- 第一个参数:目标节点的名称。
- 第二个参数:要传递到该节点的状态,这个状态可以是每个对象独有的。
3.Send的使用示例
示例代码:
def continue_to_jokes(state: OverallState):
# 为 state['subjects'] 列表中的每个 subject 动态生成一条边,传递不同的状态
return [Send("generate_joke", {"subject": s}) for s in state['subjects']]
graph.add_conditional_edges("node_a", continue_to_jokes)
解释:
continue_to_jokes
函数:- 假设
state['subjects']
是一个列表,包含若干个主题(例如"cat"
,"dog"
)。 continue_to_jokes
函数会为subjects
列表中的每个元素生成一个Send
对象,每个Send
对象都指向同一个下游节点generate_joke
,但每个对象会传递不同的状态(例如,{"subject": "cat"}
和{"subject": "dog"}
)。- 这样,
generate_joke
节点会并行处理每个subject
,并且每次处理时都会有自己独立的状态。
- 假设
graph.add_conditional_edges("node_a", continue_to_jokes)
:- 这行代码将
continue_to_jokes
函数添加为从node_a
到generate_joke
节点的条件边。图执行到node_a
时,会调用continue_to_jokes
函数,根据subjects
列表的内容动态生成边和状态。 - 每个
Send
对象代表一个从node_a
到generate_joke
的边,以及要传递的状态。
- 这行代码将
4.Send的用途:动态边和状态
使用 Send 允许在运行时根据数据动态生成边,而不是预定义图的结构。这在以下场景中非常有用:
- 未知的对象数量:当对象的数量在图构建时不确定时,你可以在运行时动态生成所需的边。例如,处理一个列表中的多个对象,每个对象都需要被单独处理,且状态可能不同。
- 并行执行:Send 允许多个对象被并行处理,每个对象的处理流程互不影响,状态也彼此独立。
5. Send 的实际场景
Send 机制常用于类似 Map-Reduce 的工作流设计:
- Map 阶段:将一个输入分解为多个部分,每个部分都需要独立处理。例如,处理一个包含多个主题的列表,每个主题生成一个新的处理任务。
- Reduce 阶段:将多个独立的处理结果整合起来。Send 机制通常用于 Map 阶段,生成多个子任务。
6. Send 在条件边中的使用
Send
对象通常与 条件边(conditional edges)结合使用,因为条件边允许根据状态动态地决定下一个节点及其状态。在 Send 的场景下,routing_function
不仅决定了下一个节点的名称,还生成了每个对象的状态,并将这些状态传递给相应的节点。
checkpoint(检查点)
持久性 是通过 检查点器 实现的,允许在图的每个 超步(Superstep) 后保存图的状态,形成一个快照。这带来了以下几个关键功能:
- 人工干预:在执行过程中可以随时查看和修改图的状态,允许人工参与。
- 内存管理:可以记住执行过程中发生的事情,方便在下一次执行时从特定位置继续。
- 容错:如果执行中断或失败,检查点器允许从保存的快照恢复,避免从头开始。
图迁移
LangGraph 支持 图迁移,即在图的拓扑结构(节点、边、状态)发生变化时,图仍然能够继续工作。这在需要更新或调整图的功能时非常有用,特别是在长期运行的系统中。
- 未中断的线程:对于当前正在执行的线程,如果它们没有中断,你可以更改图的拓扑结构,包括添加、删除、重命名节点和边。
- 中断的线程:对于已经中断的线程,支持大多数拓扑结构的更改,但不能重命名或删除当前即将执行的节点,因为这样会导致无法继续执行。
- 状态修改:可以添加或删除状态键,具有向前和向后兼容性。不过,重命名状态键或修改其类型可能会导致问题。
配置(Configuration)
LangGraph 支持 可配置图,允许在运行时根据配置动态调整图的行为。例如,你可以在同一个图结构中切换不同的 LLM(大型语言模型),而不需要重新构建图。
配置示例:
class ConfigSchema(TypedDict):
llm: str
graph = StateGraph(State, config_schema=ConfigSchema)
config = {"configurable": {"llm": "anthropic"}}
graph.invoke(inputs, config=config)
- config_schema:定义了图的配置结构(例如 LLM 类型)。
- configurable 字段:允许在调用图时传递配置,并在图的节点中访问这些配置。例如,动态选择不同的 LLM。
递归限制
递归限制 用于防止图在单次执行过程中无限循环。默认情况下,LangGraph 限制图执行的最大超步数量为 25 步。如果超过这个限制,系统会引发 GraphRecursionError
。
修改递归限制示例:
graph.invoke(inputs, config={"recursion_limit": 5, "configurable": {"llm": "anthropic"}})
- recursion_limit:设置递归限制,可以通过配置字典来指定递归限制的最大步数。
- 配置独立:递归限制是独立的 config 键,不能与其他用户定义的配置混合。
断点
断点 允许在图的执行中暂停,等待人工干预或批准,特别适合在重要节点执行前后进行手动检查。LangGraph 提供了两种类型的断点:
interrupt_before
:在节点执行之前设置断点。interrupt_after
:在节点执行之后设置断点。
断点功能依赖于 检查点器,因为一旦执行暂停,需要能够从中断点恢复执行。
断点的使用示例:
# 首次执行图
graph.invoke(inputs, config=config)
# 图在某处命中断点后,传入 None 以恢复执行
graph.invoke(None, config=config)
动态断点
有时候,你可能希望根据动态条件在图内部中断执行。LangGraph 提供了 NodeInterrupt
异常,允许在节点内部根据特定条件动态中断图的执行。
动态断点示例:
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
- NodeInterrupt:这是一个特殊的异常,用于在节点执行时动态中断图。你可以根据输入的某个条件(例如输入长度)决定是否中断图的执行。
LangGrpah官方入门
LangGraph 和 Agent 系统的基础总结
在之前的讲解中,我们详细探讨了如何使用 LangGraph 库和 Agent 系统构建一个智能助手系统,主要用于处理用户查询,并通过工具执行相应操作。以下是使用 LangGraph 和 Agent 的重点部分的归纳:
1. 状态图(StateGraph)构建与工作流程控制
状态图是整个系统的核心,它定义了对话和工具调用的工作流程。
关键点:
- 状态图定义:
StateGraph(State)
用于初始化状态图,其中State
是对话状态的定义,包含消息列表等信息。状态图控制了对话的流向和工具的调用。 - 节点(Nodes):节点是图中的工作单元,每个节点处理特定的任务。在这个项目中,两个主要的节点是:
assistant
节点:负责与用户交互,通过调用 LLM 生成响应。tools
节点:负责调用工具来处理具体任务,如航班查询、预订、取消等操作。
- 边(Edges):边决定了流程的走向,控制从一个节点流向另一个节点。简单边定义了直接的控制流,比如从
START
到assistant
节点;条件边则基于特定条件,控制是否调用工具节点。 - 持久化状态:通过
MemorySaver()
将图的状态持久化,这样即使对话过程较长,系统也能记住之前的上下文,确保连续性。
使用示例:
builder = StateGraph(State)
builder.add_node("assistant", Assistant(part_1_assistant_runnable))
builder.add_node("tools", create_tool_node_with_fallback(part_1_tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
memory = MemorySaver()
part_1_graph = builder.compile(checkpointer=memory)
2. 代理(Agent)与 LLM 的集成
代理是 LangGraph 中的关键组件,用于结合 LLM(大型语言模型)来生成对话响应,并利用工具来执行任务。
关键点:
-
代理功能:代理作为一个助手,通过对话状态和工具配置来生成响应。它的主要工作流程包括:
- 获取当前的对话状态和用户信息。
- 通过调用 LLM 生成响应(如航班查询结果)。
- 调用工具进行更具体的任务执行(如票务修改、取消等)。
- 如果 LLM 没有返回有效响应,代理会重新请求一个有效的输出。
-
LLM(Claude 或 GPT 系列):通过
ChatAnthropic
或其他 LLM(如 OpenAI 的 GPT 系列)生成自然语言响应。这些模型基于定义好的提示(prompts)生成对话输出。 -
工具调用:代理通过工具的绑定,可以在生成的响应中触发具体的操作任务。例如,用户询问航班状态时,代理会通过工具节点去查询航班信息,返回给用户。
使用示例:
class Assistant:
def __init__(self, runnable: Runnable):
self.runnable = runnable
def __call__(self, state: State, config: RunnableConfig):
while True:
result = self.runnable.invoke(state)
if not result.tool_calls and not result.content:
state["messages"] += [("user", "Respond with a real output.")]
else:
break
return {"messages": result}
llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
3. 提示模板与工具集成
**提示模板(Prompts)**定义了 LLM 生成响应的上下文。通过设计良好的提示,可以指导 LLM 生成有意义的答案。
关键点:
- 提示模板的设计:使用
ChatPromptTemplate
定义提示,明确指示 LLM 如何处理用户的查询,并提醒其使用提供的工具。例如,当查询结果为空时,提示 LLM 扩大搜索范围。 - 工具集(Tools):工具集包括多种功能模块,负责执行具体任务,如航班查询、预订、租车、酒店预订等。通过绑定工具,代理可以在对话中动态调用这些工具来完成用户请求。
使用示例:
primary_assistant_prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are a helpful customer support assistant for Swiss Airlines. "
" Use the provided tools to search for flights, company policies, and other information..."
),
("placeholder", "{messages}"),
]).partial(time=datetime.now())
part_1_tools = [
TavilySearchResults(max_results=1),
fetch_user_flight_information,
search_flights,
lookup_policy,
update_ticket_to_new_flight,
# 其他工具如租车和酒店预订等
]
part_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)
4. 错误处理与回退机制
在复杂的系统中,代理可能会遇到错误,无法执行某些任务。LangGraph 提供了回退机制,以便在出现错误时执行适当的处理。
关键点:
- 错误处理:通过定义错误处理函数,系统可以捕获并处理代理执行过程中出现的错误。例如,当工具调用失败时,系统会生成一条包含错误信息的响应,让用户了解问题并进行下一步操作。
- 回退机制:当工具调用失败时,系统会通过回退机制将流程返回到一个安全状态(如重新调用工具或重新生成响应),保证对话的连续性。
使用示例:
def handle_tool_error(state) -> dict:
error = state.get("error")
tool_calls = state["messages"][-1].tool_calls
return {
"messages": [
ToolMessage(content=f"Error: {repr(error)}\n please fix your mistakes.", tool_call_id=tc["id"])
for tc in tool_calls
]
}
def create_tool_node_with_fallback(tools: list) -> dict:
return ToolNode(tools).with_fallbacks([RunnableLambda(handle_tool_error)], exception_key="error")
5. 图形化与可视化
为了更直观地展示状态图和工作流程,可以通过生成图表来查看整个对话的结构。LangGraph 支持将图结构生成图像,使开发者能清晰地看到节点之间的关系及其工作流。
关键点:
- 可视化状态图:使用
get_graph(xray=True)
可以生成状态图的图形表示,结合 Mermaid 图表语言生成 PNG 格式的可视化图像。 - 调试与分析:通过图像展示,可以更好地调试对话流程,分析每个节点的工作情况及其交互逻辑。
使用示例:
display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))
总结
使用 LangGraph 和 Agent 系统的核心在于:
- 通过状态图管理对话流程:节点和边的灵活定义确保了对话的可控性与工具的调用。
- 结合 LLM 生成响应:代理通过 LLM 与用户进行交互,并在需要时调用工具完成具体任务。
- 提示设计与工具集成:合理的提示设计和强大的工具集,使得助手可以处理复杂的查询,并动态响应用户需求。
- 错误处理与回退机制:通过回退和错误处理,确保即使在复杂或错误场景下,系统仍能平稳运行。
- 可视化与调试:通过图形化工具,开发者可以更清晰地看到对话流程和工具调用的执行情况,便于调试和优化系统。
这些部分构成了 LangGraph 与 Agent 系统的基础,实现了智能对话和任务自动化的集成。
图重点讲解
状态图(StateGraph)构建与工作流程控制的详细拓展讲解
在智能对话系统中,状态图(StateGraph) 是用来定义对话流程、工具调用和状态管理的核心框架。通过状态图,可以控制整个对话系统的逻辑流向、节点之间的转换,以及如何根据用户输入选择合适的操作。因此,理解状态图的构建和控制流程是设计对话系统的关键。接下来我将深入讲解状态图的构建步骤、各组件的功能以及如何使用状态图进行工作流程控制。
1. StateGraph 的核心概念
状态图(StateGraph) 是对对话流程的抽象模型,用于控制系统从一个状态流向另一个状态。它通过 节点(Nodes) 来表示操作步骤,通过 边(Edges) 来表示状态间的转换。状态图的好处是可以清晰地描述复杂的对话逻辑,并且能够结合各种条件来触发不同的操作(如调用工具或生成响应)。
核心组件:
- 节点(Nodes):节点是图中的主要操作单元,代表着某个状态或操作。每个节点执行某项具体任务,比如调用一个工具,生成 LLM 响应,或等待用户输入。
- 边(Edges):边是图中的控制流,决定了流程如何从一个节点转向另一个节点。边可以是简单的,表示固定流程,也可以是条件边,表示基于特定条件触发的跳转。
- 状态(State):状态保存对话中的上下文信息,如用户的输入、当前的消息记录、已调用的工具等。通过状态的维护,可以确保对话过程中的连续性。
- 开始节点(START)和结束节点(END):每个状态图都有明确的开始和结束节点,表示图的启动和终止。
2. 状态图的构建步骤
通过以下几个步骤来构建和定义状态图。
2.1. 定义状态(State)结构
首先,我们定义图的状态结构。状态是图的核心,因为它保存了用户的输入和系统的上下文。可以使用 TypedDict
来定义状态,其中的键可以包括对话的消息历史、工具调用的结果等。
例如,定义包含消息列表的状态:
class State(TypedDict):
messages: list[AnyMessage]
2.2. 初始化状态图
使用 StateGraph
初始化图,并传入定义好的状态结构:
builder = StateGraph(State)
这一步建立了一个空的状态图框架,后续我们会为这个图添加节点和边。
2.3. 添加节点(Nodes)
每个节点都负责执行特定的任务。在 LangGraph 中,节点可以执行调用工具、生成 LLM 响应,或者执行一些自定义的任务。
# 创建助手节点
builder.add_node("assistant", Assistant(part_1_assistant_runnable))
# 创建工具节点
builder.add_node("tools", create_tool_node_with_fallback(part_1_tools))
在这个例子中,assistant
节点负责调用助手逻辑(LLM 和工具结合),而 tools
节点负责实际调用工具,比如查询航班、预订车或酒店等。
assistant
节点:这是系统的核心,它处理用户的输入,并通过 LLM 生成响应。它还会调用合适的工具来完成具体任务。tools
节点:这个节点管理工具的调用。不同的工具用于完成不同的功能任务,例如航班查询、票务修改、租车预订等。
2.4. 添加边(Edges)
节点之间通过边进行连接,控制系统如何从一个节点转移到另一个节点。边可以是简单的固定边,表示流程总是从一个节点流向另一个节点;也可以是条件边,表示基于条件的跳转。
# 定义简单边:从 START 节点到 assistant 节点
builder.add_edge(START, "assistant")
# 定义条件边:如果满足工具调用条件,则从 assistant 节点跳转到 tools 节点
builder.add_conditional_edges("assistant", tools_condition)
# 定义简单边:从 tools 节点返回 assistant 节点
builder.add_edge("tools", "assistant")
- 简单边:例如,从
START
节点到assistant
节点的边表示,当图启动时,流程总是先进入assistant
节点。 - 条件边:条件边通常使用特定的逻辑(如
tools_condition
),表示如果条件满足,就从一个节点流向另一个节点。在这里,tools_condition
表示在助手节点处理完后,如果需要调用工具,则跳转到tools
节点。
2.5. 状态持久化
在对话过程中,系统需要记住之前的状态(例如用户已经询问过的问题或已完成的工具调用)。通过 MemorySaver
来持久化状态,可以确保对话上下文不会丢失。
memory = MemorySaver()
part_1_graph = builder.compile(checkpointer=memory)
MemorySaver
会将状态保存在内存中,确保系统能够跟踪每次对话的进展。这样即使对话较长或复杂,系统也能记住之前的上下文,进行合理的响应和操作。
3. 工作流程控制
状态图的另一个重要功能是控制对话的工作流程。通过状态图的节点和边,系统可以根据对话的上下文和用户的输入,动态决定接下来的操作步骤。通过以下几个关键步骤,系统能够实现工作流程的控制:
3.1. 调用工具的条件判断
状态图中的条件边可以根据当前状态的内容(如用户输入、历史消息等)决定是否调用工具。例如,用户提出查询航班的请求时,assistant
节点会判断是否需要调用工具来完成任务。如果满足条件,就会通过 tools_condition
跳转到 tools
节点。
builder.add_conditional_edges("assistant", tools_condition)
tools_condition
是一个函数或逻辑条件,可能会检查状态中的某些字段(如用户的查询意图),以决定是否需要调用工具。
3.2. 递归调用与回退机制
在某些场景下,如果助手节点无法提供有效的响应,可能会需要多次调用 LLM 生成新的响应,直到得到合适的结果。此外,如果在工具调用中出现错误(如工具调用失败或结果为空),系统可以通过回退机制返回到安全状态。
def create_tool_node_with_fallback(tools: list) -> dict:
return ToolNode(tools).with_fallbacks([RunnableLambda(handle_tool_error)], exception_key="error")
通过回退机制(fallbacks
),系统可以处理工具调用失败的情况,并返回到助手节点重新生成响应。
3.3. 状态的动态更新
系统在每次对话中都会动态更新状态。通过状态图中的节点执行逻辑,状态会在每一步进行修改。例如,新的用户消息会被添加到状态的消息列表中,工具调用的结果也会更新到状态中。
state = {**state, "user_info": passenger_id}
这一步确保状态图能够跟踪每次对话的进展,保证对话上下文的一致性。
4. 状态图的可视化与调试
构建状态图的一个好处是可以将整个对话流程可视化,帮助开发者更好地理解系统的工作流程,并进行调试。通过可视化工具生成状态图,可以清晰地看到节点之间的关系及其工作流。
display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))
在调试过程中,状态图的可视化可以帮助发现问题所在,比如某个节点没有正确触发,或者某个条件边没有被执行。
总结
状态图(StateGraph)是 LangGraph 系统中控制工作流程的核心工具。通过定义节点、边、条件判断和状态更新,系统能够灵活地处理复杂的对话场景,结合 LLM 和工具集,动态响应用户需求。持久化状态的引入进一步增强了系统的健壮性,确保在长对话中上下文不会丢失。
代码
很好的问题!我们将根据你的反馈,逐步解决并修改代码。
修改 1: 全部使用 .py
格式文件
你希望将代码从 Jupyter Notebook (ipynb
) 格式转换为标准的 Python 文件。为了实现这一点,我们将原先的所有逻辑放入独立的 .py
文件中。
修改 2: 使用 @tool
装饰器
在 tools.py
文件中之前没有使用 @tool
装饰器,导致工具的注册不够显式。我们将对每个工具函数加上 @tool
装饰器,使它们符合 langchain
的标准工具注册机制。
修改 3: State
定义时使用 TypedDict
之前定义状态时没有继承 TypedDict
,但你希望将状态定义为一个类型化字典(TypedDict
),这样更符合静态类型检查规范。我们将调整定义,使其符合这一需求。
修改后的项目结构:
math-assistant/
├── assistant.py # 主逻辑文件
├── tools.py # 工具文件
├── state.py # 状态定义文件
├── requirements.txt # 项目依赖
└── main.py # 启动入口
1. tools.py
文件
我们在 tools.py
中使用 @tool
装饰器来注册工具。这些工具将执行数学计算任务,例如求和、解方程、矩阵乘法等。
import numpy as np
import sympy as sp
from langchain_core.tools import tool
@tool
def calculate_sum(numbers: list[float]) -> float:
"""计算数字列表的和"""
return sum(numbers)
@tool
def solve_equation(equation: str):
"""解单个方程"""
x = sp.symbols('x')
eq = sp.sympify(equation)
return sp.solve(eq, x)
@tool
def solve_system_of_equations(equations: list[str]):
"""解方程组"""
symbols = sp.symbols('x y z')
eqs = [sp.sympify(eq) for eq in equations]
return sp.solve(eqs, symbols)
@tool
def determinant(matrix: list[list[float]]) -> float:
"""计算矩阵的行列式"""
mat = np.array(matrix)
return np.linalg.det(mat)
@tool
def matrix_multiplication(matrix_a: list[list[float]], matrix_b: list[list[float]]) -> list[list[float]]:
"""计算两个矩阵的乘积"""
mat_a = np.array(matrix_a)
mat_b = np.array(matrix_b)
return np.dot(mat_a, mat_b).tolist()
2. state.py
文件
我们在 state.py
中使用 TypedDict
来定义 State
类,它将保存对话的上下文信息。
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import AnyMessage, add_messages
class State(TypedDict):
"""状态保存对话中的消息记录"""
messages: Annotated[list[AnyMessage], add_messages]
3. assistant.py
文件
这个文件包含了助手的主要逻辑,包括如何调用 LLM 和工具,以及如何在不同状态之间流转。
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition
from tools import calculate_sum, solve_equation, solve_system_of_equations, determinant, matrix_multiplication
from datetime import datetime
from state import State
# 定义 Assistant 类
class Assistant:
def __init__(self, runnable):
self.runnable = runnable
def __call__(self, state: State, config: RunnableConfig):
while True:
configuration = config.get("configurable", {})
user_id = configuration.get("user_id", None)
state["user_info"] = {"user_id": user_id}
result = self.runnable.invoke(state)
if not result.tool_calls and (not result.content or isinstance(result.content, list) and not result.content[0].get("text")):
state["messages"].append(("user", "请给出一个实际的回应。"))
else:
break
return {"messages": result}
# 初始化 LLM
llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
# 定义提示模板
primary_assistant_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个数学助手,帮助用户进行数学计算。你可以处理求和、解方程、矩阵乘法等任务。"
" 使用工具来完成这些任务,并确保每次都提供准确的答案。\n\n当前用户:<User>\n{user_info}\n</User>\n当前时间:{time}。"
),
("placeholder", "{messages}"),
]).partial(time=datetime.now())
# 工具集
part_1_tools = [
calculate_sum,
solve_equation,
solve_system_of_equations,
determinant,
matrix_multiplication,
]
# 结合 LLM 和工具的助手
part_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)
# 构建状态图
builder = StateGraph(State)
# 添加节点
builder.add_node("assistant", Assistant(part_1_assistant_runnable))
builder.add_node("tools", part_1_assistant_runnable)
# 添加边
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
# 编译状态图
part_1_graph = builder.compile()
4. main.py
文件
这是项目的入口文件,你可以在此启动助手并测试不同的数学计算任务。
from assistant import part_1_graph
from state import State
from langchain_core.runnables import RunnableConfig
# 启动助手并测试示例对话
if __name__ == "__main__":
state = State()
config = RunnableConfig(configurable={"user_id": "math_user"})
# 测试对话轮次
response = part_1_graph.invoke(state, config)
print(response)
5. requirements.txt
文件
依赖项保持不变:
langchain-core
langchain-anthropic
numpy
sympy
ipython
6. 总结
通过以上修改,我们实现了以下目标:
- 使用
.py
文件:所有代码均已转换为 Python 脚本文件,适合用于标准 Python 项目中。 - 工具函数使用
@tool
装饰器:所有工具函数现在都使用@tool
装饰器来注册工具,使它们在langchain
框架中得到正式注册和调用。 - 状态继承
TypedDict
:在state.py
中,状态State
使用TypedDict
定义,使状态更加符合 Python 的类型化规范,并便于静态类型检查。
你可以通过 main.py
文件运行整个助手系统,并测试数学相关任务,例如矩阵运算、方程求解等。如果有其他修改需求,欢迎提出!