一、引言
这是一个系列博客,通过学习Langgraph官方文档自我总结而来、本篇是系列第二篇。
第一篇地址: 一起来学 Langgraph [第一节]
二、Langgraph的核心基础组件
本节以一个实际案例介绍下state、edge、node三个langGraph中的概念
首先我们构建下面的一个图
python
# 这里是自己编写的一个工具函数 非常简单 分别是乘法、除法、加法
tools = [math_tools.multiply, math_tools.add, math_tools.divide]
llm = ChatTongyi(model="qwen-max", api_key=SecretStr(os.getenv("DASHSCOPE_API_KEY")))
# 为大模型绑定工具
llm_with_tools = llm.bind_tools(tools)
# System message
sys_msg = SystemMessage(content="您是一个乐于助人的助手,负责对一组输入执行算术运算.")
# Node
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# Build graph
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant",
tools_condition,
)
builder.add_edge("tools", "assistant")
# Compile graph
graph = builder.compile()
在上面的代码中我们使用了qwen-max作为我们的基准大模型,并给这个大模型绑定了一个简单的运算工具tools,这样大模型在回答运算相关的问题的时候就会使用工具。 同时我们在langGraph-studio中展示了这个图的基本结构,通过add_conditional_edges
方法添加了一个条件边,tools_condition
是langGraph内置的一个条件,表示如果最新的对话消息中存在工具调用,就走向tools节点,否则直接走到_end_结束节点 使用上面的代码,就可以实现一个简单的工具调用。
下面从这个代码开始我们逐步串讲出来Langgraph所有的相关基础知识点
state
我们首先看下上面的定义图的关键代码
python
builder = StateGraph(MessagesState)
定义了图的State类型为MessagesState,相当于定义了图中节点上下文的数据结构,这样后续的每个节点的入参和出参都会是MessagesState类型的数据结构,事实上我们也可以自定义State,继承自TypedDict即可 ( MessagesState本身也是继承的TypedDict) 如下面的代码所示
python
class State(TypedDict):
messages: list[AnyMessage]
extra_field: int
graph_builder = StateGraph(State)
但是有的时候我们希望输入节点和输出节点的入参结构简单点,一个典型的场景是我可能只需要用户给定部分参数,输出的就是一段文本,但是在图的内部节点时我需要用一个复杂的State数据结构来记录过程数据,这时候定义图的时候就可以像下面这样进行定义:
python
# 定义输入Schema
class InputState(TypedDict):
question: str
# 定义图的输出Schema
class OutputState(TypedDict):
answer: str
# 图的State
class OverallState(InputState, OutputState):
pass
def answer_node(state: InputState):
return {"answer": "bye", "question": state["question"]}
# 关键代码定义了图的输入和输出数据结构
builder = StateGraph(OverallState, input=InputState, output=OutputState)
builder.add_node(answer_node)
builder.add_edge(START, "answer_node")
builder.add_edge("answer_node", END)
graph = builder.compile() # Compile the graph
在上面的代码中我们定义的输入的数据结构为InputState,输出的数据结构为OutputState,这样我们的节点接收的数据结构就是InputState类型, 图的总的数据结构为OverallState是继承了InputState和OutputState的,也就是会同时包含question和answer两个属性,一个图必须有一个总的State,也就是说这里的OverallState是必须的。
可能有一种场景就是两个节点之间的数据结构不一致,那么应该怎么处理呢? 可以参考下面的代码
python
class OverallState(TypedDict):
foo: int
class PrivateState(TypedDict):
baz: int
def node_1(state: OverallState) -> PrivateState:
print("---Node 1---")
return {"baz": state['foo'] + 1}
def node_2(state: PrivateState) -> OverallState:
print("---Node 2---")
return {"foo": state['baz'] + 1}
# Build graph
builder = StateGraph(OverallState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
# Logic
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
# Add
graph = builder.compile()
上面的这个图中Node1 到 Node2之间是私有的,node1中设置了baz属性值 node2接收的参数类型是PrivateState 也就是说Node1 和 Node2 之间也可以使用不同的数据结构进行通信而不必强制按照OverallState定义的数据结构进行参数传递
Node
节点其实就是一个 python 函数。第一个位置参数是 state,如上所述。因为每个节点都有 state 作为入参,所以我们就可以通过state 访问到我们定义的State数据结构中的属性,节点中也可以对state进行更新
python
from typing_extensions import TypedDict
class State(TypedDict):
graph_state: str
def node_1(state):
print("---Node 1---")
return {"graph_state": state['graph_state'] + " I am"}
def node_2(state):
print("---Node 2---")
return {"graph_state": state['graph_state'] + " happy!"}
def node_3(state):
print("---Node 3---")
return {"graph_state": state['graph_state'] + " sad!"}
默认情况下,每个节点返回的新值将覆盖之前的 state 值。但是有的时候比如历史对话信息我们往往希望是追加而不是覆盖,具体如何覆盖在第三节会有介绍
Edge
边主要用来连接节点,例如,如果要始终从 node_1 转到 node_2,则使用 Normal Edges
。如果要选择性地在节点之间路由,则使用 Conditional Edge
。条件边被实现为函数,这些函数根据某些逻辑返回下一个要访问的节点。比如我们本节开始的例子中有下面的代码
python
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
# 条件边
builder.add_conditional_edges(
"assistant",
tools_condition,
)
builder.add_edge("tools", "assistant")
在上面的代码中assistant节点可以走到tools节点,也可以直接结束,具体怎么走取决于 tools_condition的条件值,当然我们也可以自定义条件,如下图所示:
python
import random
from typing import Literal
def decide_mood(state) -> Literal["node_2", "node_3"]:
# Often, we will use state to decide on the next node to visit
user_input = state['graph_state']
# Here, let's just do a 50 / 50 split between nodes 2, 3
if random.random() < 0.5:
# 50% of the time, we return Node 2
return "node_2"
# 50% of the time, we return Node 3
return "node_3"
from langgraph.graph import StateGraph, START, END
# Build graph
builder = StateGraph(State)
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_conditional_edges("node_1", decide_mood,["node_2","node_3"])
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
# Add
graph = builder.compile()
在上面的代码中我们定义了条件边 decide_mood,当node_1节点执行完成之后,会调用decide_mood函数,根据decide_mood函数返回的值,决定下一个节点是node_2还是node_3, add_conditional_edges
还可以制定第三个参数,限定下一跳的节点名称集合。比较推荐添加这第三个参数,这样渲染图结构的时候连线会比较清晰,
python
builder.add_conditional_edges("decision", router, ["action", END])
三、 图的编译和执行
图构建完毕之后需要调用compile()方法编译图,编译之后就可以执行图了,那么怎么执行图呢?有两个方法可以执行图一个为invoke一个为stream
invoke
python
graph.invoke({"value": "hi!"})
# graph.invoke({}) 如果没有什么数据需要传递给开始节点也可以直接写个空字典
invoke 方法传递的数据结构需要和我们定义的InputState类型一致,例如我们的State的数据结构为MessagesState,那么invoke方法需要传递的数据结构为MessagesState,如下所示:
python
res = graph.invoke({"messages": [HumanMessage("Hi")]})
print(res)
stream
python
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
initial_input 是输入数据,通常是用户的提问消息,和我们之前讲的invoke方法的入参是一个含义。 thread 是线程ID,用于维护会话记忆和状态隔离。 第三个参数 stream_mode="values" 是用来控制图执行时的输出流模式(streaming mode)。 具体来说,stream_mode 参数决定了 graph.stream() 方法在执行图时,如何逐步返回执行结果的内容和格式。LangGraph 支持多种流模式,常见的包括:
- "values":每一步返回当前整个状态的完整快照(即所有State字段的当前值)。
- "updates":每一步只返回本次执行节点产生的状态更新部分。
- "messages":专门用于流式返回LLM生成的消息(如聊天模型的token流)。
- "debug":返回详细的调试信息,包括任务执行细节。
也可以传入多个模式的列表同时启用多种流模式。
最后stream实际还有一个非阻塞式的 astream_events
方法 :
python
node_to_stream = 'conversation'
config = {"configurable": {"thread_id": "3"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")
# 注意这里使用了astream 不是stream astream是异步的 stream方法是同步的
async for event in graph.astream_events({"messages": [input_message]}, config, version="v2"):
print(f"Node: {event['metadata'].get('langgraph_node','')}. Type: {event['event']}. Name: {event['name']}")
# Get chat model tokens from a particular node
if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == node_to_stream:
print(event["data"])
在上面的方法中我们可以直接获取某个节点生成片段消息,得到类似下面的输出:
{'chunk': AIMessageChunk(content='', id='run-b76ec3b8-9c45-42fe-b321-4ec3a69c185c')}
{'chunk': AIMessageChunk(content='The', id='run-b76ec3b8-9c45-42fe-b321-4ec3a69c185c')}
{'chunk': AIMessageChunk(content=' San', id='run-b76ec3b8-9c45-42fe-b321-4ec3a69c185c')}
{'chunk': AIMessageChunk(content=' Francisco', id='run-b76ec3b8-9c45-42fe-b321-4ec3a69c185c')}
...
invoke和stream都能执行图 那么两者之间的区别是什么呢?
- invoke 是"跑完再给结果",适合批量或非交互场景。
- stream 是"边跑边给结果",适合需要实时反馈的交互式场景。
- stream 支持多种流模式,能细粒度控制输出内容和格式。
- invoke 返回最终状态,stream 返回执行过程中的多个中间结果。
还有个问题就是无论是invoke还是stream,默认情况下都是阻塞式的一下子给出所有结果,如果需要实现打字机的效果怎么来实现呢?可以参考下面的代码进行编写
python
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}
input_str = [HumanMessage(content="请问辣椒炒肉怎么做")]
# 注意这里stream_mode 一定要设置为messages 否则这样写是不行的
for chunk, meta in app.stream({'messages': input_str}, config=config, stream_mode="messages"):
if isinstance(chunk, AIMessage):
print(chunk.content,end="", flush=True)
或者直接利用之前说过的 astream_events
方法, 代码如下所示:
python
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}
input = [HumanMessage(content="请问辣椒炒肉怎么做")]
async def run_async():
node_to_stream = 'conversation'
async for event in app.astream_events({"messages": input}, config=config, version="v2"):
if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node', '') == node_to_stream:
data = event["data"]
print(data["chunk"].content, end="", flush=True)
import asyncio
asyncio.run(run_async())
四、state的reduce功能
默认情况下每个节点返回的新值将覆盖之前的 state 值。那么怎么做到不覆盖呢? 比如我们做大模型对话应用的场景中我们的每次对话信息是存储再state中的messages字段中的,如果每次都覆盖肯定是不对的,我们希望的是把新的对话信息给追加到messages字段中,怎么做呢?可以使用reduce函数来实现 接下来我们通过一个例子来学习reduce函数
python
class State(TypedDict):
value_1: list[str]
def step_1(state: State):
state["value_1"] += [" step_1 "]
print(state["value_1"])
return {"value_1": state["value_1"]}
def step_2(state: State):
state["value_1"] += [" step_2 "]
print(state["value_1"])
return {"value_1": state["value_1"]}
def step_3(state: State):
state["value_1"] += [" step_3 "]
print(state["value_1"])
return {"value_1": state["value_1"]}
graph_builder = StateGraph(State)
# 使用这个方法可以简化代码 节点和边一并加上了
graph_builder.add_sequence([step_1, step_2, step_3])
graph_builder.add_edge(START, "step_1")
graph = graph_builder.compile()
result = graph.invoke({"value_1": []})
print(result)
上面的代码是一个标准的链式调用,图结构如下

最终输出结果为
' step_1 '
' step_1 ', ' step_2 '
' step_1 ', ' step_2 ', ' step_3 '
{'value_1': [' step_1 ', ' step_2 ', ' step_3 ']}
看起来还不错,但是每个节点都需要显式的去往state中更新数据,那么有没有什么方式可以简化代码呢?请看简化之后的实现逻辑
python
from typing import Annotated
from langgraph.graph import START
from langgraph.graph import StateGraph
from typing_extensions import TypedDict
def combine(left: list[str], right: list[str]) -> list[str]:
return left + right
# 第一步定义State 也就是数据结构,默认情况下节点的输入和输出都是这种数据结构
class State(TypedDict):
value_1: Annotated[list[str], combine]
def step_1(state: State):
state["value_1"] = [" step_1 "]
return {"value_1": state["value_1"]}
def step_2(state: State):
state["value_1"] = [" step_2 "]
return {"value_1": state["value_1"]}
def step_3(state: State):
state["value_1"] = [" step_3 "]
return {"value_1": state["value_1"]}
graph_builder = StateGraph(State)
# 使用这个方法可以简化代码 节点和边一并加上了
graph_builder.add_sequence([step_1, step_2, step_3])
graph_builder.add_edge(START, "step_1")
graph = graph_builder.compile()
result = graph.invoke({"value_1": []})
print(result)
在上面的代码中我们通过 Annotated[list[str], combine
] 为最终的输出指定了合并函数,每个节点都会对 state["value_1"] 进行修改,但是等到执行图的时候最终结果会合并,上面的代码执行结果如下
{'value_1': [' step_1 ', ' step_2 ', ' step_3 ']}
五、超步(super-steps)
超级步骤可以被认为是对图节点的一次单次迭代。并行运行的节点属于同一个超级步骤,而顺序运行的节点属于不同的超级步骤。 在图执行开始时,所有节点都处于 inactive 状态。当一个节点在其任何传入边(或"通道")上接收到新的消息(状态)时,它变为 active 。 然后,active节点运行其函数并响应更新。在每个超级步骤结束时,没有传入消息的节点通过将自己标记为 inactive 。当所有节点都是 inactive 且没有传输中的消息时,图执行终止。
对于上面的这段概念我们以一个例子来说明问题:
python
class State(TypedDict):
aggregate: Annotated[list, operator.add]
which: str
def a(state: State):
print(f'Adding "A" to {state["aggregate"]}')
return {"aggregate": ["A"]}
def b(state: State):
print(f'Adding "B" to {state["aggregate"]}')
return {"aggregate": ["B"]}
def c(state: State):
print(f'Adding "C" to {state["aggregate"]}')
return {"aggregate": ["C"]}
def d(state: State):
print(f'Adding "D" to {state["aggregate"]}')
return {"aggregate": ["D"]}
def e(state: State):
print(f'Adding "E" to {state["aggregate"]}')
return {"aggregate": ["E"]}
builder = StateGraph(State)
builder.add_node(a)
builder.add_node(b)
builder.add_node(c)
builder.add_node(d)
builder.add_node(e)
builder.add_edge(START, "a")
def route_bc_or_cd(state: State) -> Sequence[str]:
if state["which"] == "cd":
return ["c", "d"]
return ["b", "c"]
intermediates = ["b", "c", "d"]
builder.add_conditional_edges(
"a",
route_bc_or_cd,
intermediates,
)
for node in intermediates:
builder.add_edge(node, "e")
builder.add_edge("e", END)
graph = builder.compile()
# 指定 which 为 bc 相当于指定了执行条件 会并行执行 b 和 c
res = graph.invoke({"aggregate": [], "which": "bc"})
print(res)
上面的这个图使用了条件边,通过route_bc_or_cd函数来判断是并行执行 b 和 c 还是并行执行 c 和 d,图的结构如下图所示:

上图由于invoke的时候传递的which的值为"bc"
,所以最终b和c会并行执行,b和c并行执行时就是隶属于同一个超级步骤 ,也就是说此时b和c打印的state["aggregate"] 是一样的。 但是当b和c执行完毕之后到了e节点,此时e节点的打印语句会输出 ['A', 'B', 'C']
b和c节点对 state["aggregate"] 进行修改会因为配置了reduce函数(operator.add)而合并
在这个案例里面 b和c并行执行 如果这里在state定义那里时候没有用add函数, b和c对state["aggregate"] 同时进行修改是会报错的 ,因为同一个步骤中对State值进行并行修改,这是不允许的
现在我们修改代码考虑一个更复杂一点的例子,代码如下:
python
class State(TypedDict):
aggregate: Annotated[list, operator.add]
def a(state: State):
print(f'Adding "A" to {state["aggregate"]}')
return {"aggregate": ["A"]}
def b(state: State):
print(f'Adding "B" to {state["aggregate"]}')
return {"aggregate": ["B"]}
def c(state: State):
print(f'Adding "C" to {state["aggregate"]}')
return {"aggregate": ["C"]}
def d(state: State):
print(f'Adding "D" to {state["aggregate"]}')
return {"aggregate": ["D"]}
def b_2(state: State):
print(f'Adding "B_2" to {state["aggregate"]}')
return {"aggregate": ["B_2"]}
builder = StateGraph(State)
builder.add_node(a)
builder.add_node(b)
builder.add_node(b_2)
builder.add_node(c)
builder.add_node(d)
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "b_2")
# 一次性添加多个前驱的边(add_edge(["b_2", "c"], "d"))
# 这是LangGraph提供的语法糖,表示 d 节点需要等待所有前驱节点(b_2 和 c)完成后,才执行一次。
# 也就是说,d 节点的激活是"合并"的,只有当所有指定的前驱都完成时才触发。
builder.add_edge(["b_2", "c"], "d")
# 下面这样写最终结果会有两个D 因为b_2 和 d节点 会并行执行一次(这两个节点的层级一致) 然后 b_2->d 的时候又执行了一次d节点
# builder.add_edge("b_2", "d")
# builder.add_edge("c", "d")
上述代码的图结构如下图所示:

使用下面的代码执行图
python
builder.add_edge("d", END)
graph = builder.compile()
res = graph.invoke({"aggregate": []})
print(res)
最终执行结果如下:
Adding "A" to []
Adding "B" to ['A']
Adding "C" to ['A']
Adding "B_2" to ['A', 'B', 'C']
Adding "D" to ['A', 'B', 'C', 'B_2']
{'aggregate': ['A', 'B', 'C', 'B_2', 'D']}
此时b和c属于同一个超级步骤,所以执行开始前获取到的state["aggregate"] 值只有一个元素A , b_2则属于另一个超级步骤,所以执行开始前获取到的state["aggregate"] 值有三个元素 A、B、C
需要注意的是我们通过builder.add_edge(["b_2", "c"], "d")
添加了多个前驱的边,这样写也规定了d节点需要等待所有前驱节点(b_2 和 c)完成后,才执行一次
这里还有一个问题 因为b节点和c节点是并行的,这时候b先执行还是c先执行 是不确定的,这就导致最终的State的aggregate值是不确定的,怎么解决这个问题呢,我们可以考虑给aggregate增加一个排序逻辑:
python
def sorting_reducer(left, right):
""" Combines and sorts the values in a list"""
if not isinstance(left, list):
left = [left]
if not isinstance(right, list):
right = [right]
return sorted(left + right, reverse=False)
class State(TypedDict):
# sorting_reducer will sort the values in state
aggregate: Annotated[list, sorting_reducer]
此外还有一个办法就是 并行的节点各自往不同的字段写入数据 然后由sink节点合并并行节点的输出,最后写到state的某个变量里面。
六、本章小结
本节详细介绍了 State 、node、edge的相关概念,同时给出了reduce函数的使用以及什么叫做超步,理解超步的概念非常关键,特别是在并行节点的设计中,并行节点无法同时在不定义reduce函数情况下对同一个State的变量进行修改这个限制需要尤其注意。