1.基本概念
Graph:在LangGraph中Graph是一个用来处理数据的有向图,由节点(Node)和连接节点的边(Edge)构成,其处理的数据称为状态(State) 。
State:状态可理解为所有节点共享的数据结构,有一定的模式,并且随着数据的流转而变化。
Node:节点封装了业务逻辑,以当前的状态作为输入,并且在完成业务处理后更新状态
Edge:把节点串联起来。边根据当前的状态判断一个节点执行完成后下一个节点是哪个,在Graph中的边有固定边和条件边两种边,固定边是指不管当前状态如何,都必然流转到一个固定的节点,条件边则根据当前的状态,把执行权流转到不同的节点。
super-step:图中的一次迭代处理称为super-step,多个并行执行的节点视为一个super-step,串行执行的节点分属不同的super-step
在LangGraph中State是一种数据结构,而Node和Edge实际上都是函数。super-step是一个逻辑上的概念。
2.State
2.1简述
State在所有节点和边中共享,也就是说是Graph中所有Node和Edge的输入,当然State是随时间变化的。
State是一种数据结构,由数据和规约函数构成,规约函数用于对State执行更新操作。
每个节点接收最新的State数据后,执行内部业务逻辑处理,然后发出更新数据,在框架中调用规约函数用更新数据对State进行更新。
State中可以使用三种数据类型保存数据,最直接的是使用TypedDict;如果想给每项数据赋予缺省值,可以使用@dataclass;当然也可以使用Pydantic BaseModel。可根据实际需要或个人偏好选择。
2.2多模式
一般情况下,在一个Graph中所有的Node使用同一个State(数据结构),但LangGraph是支持多数据结构并存的。比如,输入和输出状态可以分别使用不同的数据结构,还可以定义在内部节点内流转的私有数据结构。
2.2.1输入和输出使用不同的数据结构
当共用的State中包含很多的数据项,并且输入和输出数据分别关注不同的数据项,为了提高内聚性,输入和输出可以使用不同的数据结构。
有一点需要主要,虽然输入和输出可以采用不同的数据结构,但事实上公共的State仍然是存在的,并且在创建图时需要传入的,所以输出和输出数据结构只能是State的子集。
以下是一个简单示例:
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict# 定义输入数据结构
class InputState(TypedDict):
person: str# 定义输出数据结构
class OutputState(TypedDict):
birthday: str
deathdate: str# 全局数据结构,实际为输出和输出的合集
class OverallState(InputState, OutputState):
pass# 接受输入数据结构作为输入的节点
def answer_node(state: InputState):
#根据输入数据中的人物查询生卒日期
return {"birthday": "1090年1月26日", "deathdate": "1151年9月15日"}# 构建图,传入全局数据接口,并通过关键字指定输入和输出数据结构
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node(answer_node) # Add the answer node
builder.add_edge(START, "answer_node") # Define the starting edge
builder.add_edge("answer_node", END) # Define the ending edge
graph = builder.compile() # Compile the graph# Invoke the graph with an input and print the result
print(graph.invoke({"question": "韩世忠"}))
运行结果如下:
{'birthday': '1000年5月5日', 'deathdate': '1100年5月4日'}
2.2.2使用私有数据结构
有时需要在部分节点之间传递一些与全局数据结构State无关的数据,此时需要使用私有数据结构实现。
以下是一个简单示例:
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDictclass GlobalState(TypedDict):#全局数据结构
global_data: str
class Node1Output(TypedDict): #节点1输出的私有数据
private_data: strdef node_1(state: GlobalState) -> Node1Output:#输入为全局数据,输出为私有数据
output = {"private_data": "1"}
print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}")
return outputclass Node2Input(TypedDict):#节点2输入的私有数据
private_data: strdef node_2(state: Node2Input) -> GlobalState:#输入为私有数据,输出为全局数据
output = {"global_data": "2"}
print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}")
return output# 仅访问全局数据,不能访问私有数据
def node_3(state: GlobalState) -> GlobalState:
output = {"global_data": "3"}
print(f"Entered node `node_3`:\n\tInput: {state}.\n\tReturned: {output}")
return output#把三个节点串接起来。不需要显示的增加节点和边
builder = StateGraph(GlobalState).add_sequence([node_1, node_2, node_3])
builder.add_edge(START, "node_1")
graph = builder.compile()# 调用,看输出结果
response = graph.invoke(
{
"global_data":"0",
}
)print()
print(f"Output of graph invocation: {response}")
输出如下:
Entered node `node_1`:
Input: {'global_data': '0'}.
Returned: {'private_data': '1'}
Entered node `node_2`:
Input: {'private_data': '1'}.
Returned: {'global_data': '2'}
Entered node `node_3`:
Input: {'global_data': '2'}.
Returned: {'global_data': '3'}Output of graph invocation: {'global_data': '3'}
2.3 规约器(Reducer)
Reducer用于更新全局数据结构中的数据,全局数据结构中的每项数据都可以指定自己的规约器。如果未指定,则使用缺省的规约器,缺省规约器用新数据覆盖原有数据。
2.3.1缺省规约器
以下代码中,全局数据中每一项都采取覆盖策略。在每个节点都可以更新全局数据,在return中返回的数据是需要更新的项。也就是说在一个节点内,仅返回需要更新的项即可,而不是返回整个全局数据State。由框架在完成State的更新。
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDictclass State(TypedDict):
data: str
arr: list[str]def node_1(state: State) :#更新全局数据中的data项
output = {"data": "new_data"}
print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}")
return outputdef node_2(state: State) :#更新全局数据中的arr项
output = {"arr": ["new_item"]}
print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}")
return output#两个节点的一个简单图
builder = StateGraph(State).add_sequence([node_1, node_2,])
builder.add_edge(START, "node_1")
graph = builder.compile()# Invoke the graph with the initial state
response = graph.invoke(
{
"data":"old_data",
"arr":["old_item"],
}
)print()
print(f"Output of graph invocation: {response}")
运行结果如下:
Entered node `node_1`:
Input: {'data': 'old_data', 'arr': ['old_item']}.
Returned: {'data': 'new_data'}
Entered node `node_2`:
Input: {'data': 'new_data', 'arr': ['old_item']}.
Returned: {'arr': ['new_item']}Output of graph invocation: {'data': 'new_data', 'arr': ['new_item']}
2.3.2追加规约器
针对全局数据中的列表项,一般使用追加规约器,从而可以保存历史数据。 对以上代码中的的State进行修改,其他代码不变:
class State(TypedDict):
data: str
arr: Annotated[list[str], add]
运行结果如下:
Entered node `node_1`:
Input: {'data': 'old_data', 'arr': ['old_item']}.
Returned: {'data': 'new_data'}
Entered node `node_2`:
Input: {'data': 'new_data', 'arr': ['old_item']}.
Returned: {'arr': ['new_item']}Output of graph invocation: {'data': 'new_data', 'arr': ['old_item', 'new_item']}
注意:在实际的聊天类应用中,规约器使用add_message。
3.Node
3.1普通节点
创建一个Node时,必须传入State参数,可选择性的传入Runtime或RunnableConfig参数,示例代码如下:
def node(state: State, runtime: Runtime[Context], config: RunnableConfig):
print("In node: ", runtime.context.user_id)
print("In node with thread_id: ", config["configurable"]["thread_id"])
return {"results": f"Hello, {state['input']}!"}
其中config保存了配置信息,比如thread,tag等,runtime保存了运行时的上下文或其他信息,比如Store。
把节点增加到图中需要调用add_node方法,比如把上面的节点node增加到图中:
#add_node中的第一个参数为节点名,第二个节点。如果为传入节点名,则生成一个与函数
#名相同的节点
builder.add_node("mynode", node)
3.2特殊节点
在Langgraph中有两类特殊节点:START和END。
START节点的作用是把用户的输入传入图中,其所指向的节点是第一个被执行普通节点。
END节点的作用是表示一个轮次处理的结束,并不执行任何操作。
4.Edge
在LangGraph中边用于控制流传逻辑,定义了agent如何运转以及节点之间如何交互。边有四种类型:普通边、条件边、入口点和条件入口点。
4.1普通边
普通边用户连接两个节点,第一个参数为起点,第二个参数为终点,比如:
graph.add_edge("node1", "node2")
4.2条件边
控制流从一个节点流出后,如果有多个目的地,则需要使用条件边。使用条件边需要先定义路由函数,比如:
#路由函数的输入为全局数据结构state,并根据state中的数据动态决定下一个节点是谁
def make_seal_router(
state: State,
):
message = state.messages[-1].content
prompt = f"""
根据输入信息判断是否表达了将马上为用户制作印章的意图,返回结果如下:
- "yes": 如果表达了将马上为用户制作印章的意图
- "no": 如果未表达将马上为用户制作印章的意图输入信息:{message}
"""
response = llm.predict(prompt)
if response == "yes":
return "make_seal_bot" #如果大模型能够制作印章,则进入make_seal_bot节点
return END
在图中增加条件边:
#从premake_node出发的两条边,分别指向make_seal_bot和END
graph_builder.add_conditional_edges(
"premake_node",
make_seal_router,
{"make_seal_bot": "make_seal_bot", END: END}
)
4.3入口点
入口点用户指定图中第一个执行的节点,直接通过在START和第一个执行节点之间建一个边即可,具体代码如下:
#第一个执行的节点是node1
from langgraph.graph import START
graph.add_edge(START, "node1")
4.4条件入口点
当工作流START节点出发存在多个目的节点时,使用条件入口点。此时仍需要先创建一个路由函数,然后增加条件边。
比如,如下代码中由大模型识别用户的意图,然后根据不同的意图路由至不同的节点:
def entry_router(
state: State,
):
prompt = f"""
根据用户输入分析用户的意图类型,返回以下之一:
- make: 用户想制作电子印章
- manage: 用户想对印章进行管理,比如对印章冻结、解冻、注销、续期和变更
- grant: 用户想把印章授权给其他用户管理管理或使用
— consult: 用户想咨询印章相关的业务知识或技术知识
— chat: 其他
用户输入{state.messages}
"""
response = llm.predict(prompt)
match response:
case "make":
return "premake_node"
case "manage":
return "premanage_node"
case _:
return "chatbot
然后增加条件边,具体代码如下:
graph_builder.add_conditional_edges(
START,
entry_router,
{"premake_node": "premake_node", "premanage_node": "premanage_node","chatbot": "chatbot"}
)
5.Send
在上面的讨论中,图中的节点和边全部是事先确定的。但有些情况下,节点是不能实现确定的。比如大数据处理MapReduce的reduce任务数并不能事先确定,此时需要使用Send来实现。使用Send一方面可以实时指定数量的下游节点,另一方面给不同的下游节点提供不同的输入数据。
具体示例代码如下:
#根据当前的数据列表生成Send列表
def continue_to_asses(state: OverallState):
return [Send("assesments", {"subject": s}) for s in state['persons']]
#增加条件边
graph.add_conditional_edges("node", continue_to_asses)
6.Command
Command是一个非常重要的类,具体应用常见包括:
1)在节点内部同时完成更新的跳转控制
2)子图跳转
3)在工具内修改全局数据
4)在用户回环中从中断恢复
6.1 在节点内部完成更新和跳转控制
前面所述工作流的流转和对全局数据的更新是分开的,其中边(包括条件边)控制流转,在节点内部进行全局数据的更新。如果需要在节点内部同时控制流转和更新全局数据,则需要使用Command。
一个简单的示例如下:
def my_node(state: State) -> Command[Literal["next_node"]]:
return Command(
update={"data": "100"}, #更新全局数据
goto="next_node" )#进入next_node。控制流转
使用Command还可以根据不同条件路由到不同的节点:
#与增加一个条件边一样
def my_node(state: State) -> Command[Literal["next_node"]]:
if state["data"] == "100":
return Command(update={"data": "-100"}, goto="next_node")
else:
return Command(update={"data": "500"}, goto="prev_node")
6.2 子图跳转
在包括子图的复杂图中。如果想从一个子图中的某个节点跳转至另外一个子图(或者是父图的某个节点),则需要使用Command,具体代码如下:
def my_node(state: State) -> Command[Literal["other_subgraph"]]: return Command( update={"foo": "bar"}, goto="other_subgraph", # where `other_subgraph` is a node in the parent graph graph=Command.PARENT )
6.3 在工具内部更新全局数据结构
根据配置中包括的统一社会信用代码,调用外部接口获取企业信息,然后更新全局数据结构和历史对话,具体代码如下:
@tool def lookup_ent_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):
ent_info = get_ent_info(config.get("configurable", {}).get("uniscid"))#调用外部接口
return Command( update={
# 更新全局数据结构
"ent_info": ent_info,
# 更新历史对话
"messages": [ToolMessage("Successfully looked up enterprise information",
tool_call_id=tool_call_id)] }
)
2204

被折叠的 条评论
为什么被折叠?



