Add example demonstrating parallel nodes and reducers in LangGraph, including a valid graph with a custom reducer and an invalid graph triggering an update conflict. The script showcases the execution flow and error handling for concurrent updates.

This commit is contained in:
Eric Wang
2026-07-25 22:59:04 +08:00
parent 907393fbbc
commit df11b2b526

236
10_parallel_and_reducers.py Normal file
View File

@@ -0,0 +1,236 @@
"""LangGraph 并行节点与 Reducer 示例。
本例包含两个图:
1. good_graph:两个搜索节点并行执行,并用 reducer 合并结果。
2. bad_graph:两个并行节点同时覆盖同一字段,触发 InvalidUpdateError。
主图结构:
┌─> search_web ─────┐
START -> prepare ──┤ ├─> summarize -> END
└─> search_database ┘
运行:
python 10_parallel_and_reducers.py
"""
import operator
import time
from typing import Annotated, TypedDict
from langgraph.errors import InvalidUpdateError
from langgraph.graph import END, START, StateGraph
# -----------------------------------------------------------------------------
# 1. Reducer
# -----------------------------------------------------------------------------
def merge_unique(left: list[str], right: list[str]) -> list[str]:
"""合并两个列表并去重,同时保留第一次出现的顺序。"""
return list(dict.fromkeys(left + right))
class ResearchState(TypedDict):
topic: str
# operator.add:把各节点返回的列表连接起来。
results: Annotated[list[str], operator.add]
# 自定义 reducer:合并日志并去除重复内容。
logs: Annotated[list[str], merge_unique]
# 没有 reducer:使用默认覆盖策略。
# 这里只有 summarize 节点更新它,因此不会发生并行冲突。
summary: str
# -----------------------------------------------------------------------------
# 2. 主图节点
# -----------------------------------------------------------------------------
def prepare(state: ResearchState) -> dict:
"""准备搜索;完成后两个下游节点会在同一 super-step 并行执行。"""
print(f"[prepare] 准备研究:{state['topic']}")
return {"logs": ["准备完成"]}
def search_web(state: ResearchState) -> dict:
"""模拟耗时 1 秒的网页搜索。"""
print("[search_web] 开始")
time.sleep(1)
print("[search_web] 完成")
return {
"results": [
f"网页结果:{state['topic']} 支持有状态工作流",
"共同结果:LangGraph 使用 StateGraph 编排节点",
],
"logs": ["网页搜索完成", "搜索阶段完成"],
}
def search_database(state: ResearchState) -> dict:
"""模拟耗时 1 秒的数据库搜索。"""
print("[search_database] 开始")
time.sleep(1)
print("[search_database] 完成")
return {
"results": [
f"数据库结果:{state['topic']} 支持持久化和恢复",
"共同结果:LangGraph 使用 StateGraph 编排节点",
],
"logs": ["数据库搜索完成", "搜索阶段完成"],
}
def summarize(state: ResearchState) -> dict:
"""汇合节点:两个搜索分支都结束后才会执行。"""
print(f"[summarize] 收到 {len(state['results'])} 条结果")
lines = "\n".join(
f"{index}. {result}"
for index, result in enumerate(state["results"], start=1)
)
return {
"summary": f"主题:{state['topic']}\n{lines}",
"logs": ["摘要生成完成"],
}
# -----------------------------------------------------------------------------
# 3. 构建正确的并行图
# -----------------------------------------------------------------------------
good_builder = StateGraph(ResearchState)
good_builder.add_node("prepare", prepare)
good_builder.add_node("search_web", search_web)
good_builder.add_node("search_database", search_database)
good_builder.add_node("summarize", summarize)
good_builder.add_edge(START, "prepare")
# 一个节点拥有两条普通出边:下一 super-step 中两个目标节点并行执行。
good_builder.add_edge("prepare", "search_web")
good_builder.add_edge("prepare", "search_database")
# 两个分支都指向 summarize。LangGraph 会等待同一轮的两个分支完成后汇合。
good_builder.add_edge("search_web", "summarize")
good_builder.add_edge("search_database", "summarize")
good_builder.add_edge("summarize", END)
good_graph = good_builder.compile()
# -----------------------------------------------------------------------------
# 4. 错误示例:并行节点覆盖同一个无 reducer 字段
# -----------------------------------------------------------------------------
class BadState(TypedDict):
value: str # 没有 reducer,默认策略是覆盖
def left_branch(state: BadState) -> dict:
return {"value": "来自左分支"}
def right_branch(state: BadState) -> dict:
return {"value": "来自右分支"}
bad_builder = StateGraph(BadState)
bad_builder.add_node("left", left_branch)
bad_builder.add_node("right", right_branch)
bad_builder.add_edge(START, "left")
bad_builder.add_edge(START, "right")
bad_builder.add_edge("left", END)
bad_builder.add_edge("right", END)
bad_graph = bad_builder.compile()
# -----------------------------------------------------------------------------
# 5. 演示函数
# -----------------------------------------------------------------------------
def demo_good_graph() -> None:
print("=== 正确示例:并行 + Reducer + 汇合 ===")
initial_state: ResearchState = {
"topic": "LangGraph",
"results": [],
"logs": [],
"summary": "",
}
started = time.perf_counter()
result = good_graph.invoke(initial_state)
elapsed = time.perf_counter() - started
print(f"\n耗时:{elapsed:.2f} 秒")
print("两个搜索节点各等待 1 秒;总耗时约 1 秒,说明它们并行执行。")
print("\nresults(operator.add,保留重复项):")
for item in result["results"]:
print(f"- {item}")
print("\nlogs(merge_unique,重复日志只保留一次):")
for item in result["logs"]:
print(f"- {item}")
print("\nsummary(默认覆盖,由 summarize 单独写入):")
print(result["summary"])
def demo_stream_updates() -> None:
print("\n=== 使用 updates 观察并行 super-step ===")
initial_state: ResearchState = {
"topic": "LangGraph",
"results": [],
"logs": [],
"summary": "",
}
for part in good_graph.stream(
initial_state,
stream_mode="updates",
version="v2",
):
if part["type"] != "updates":
continue
node_names = ", ".join(part["data"].keys())
print(f"节点更新:{node_names}")
def demo_conflict() -> None:
print("\n=== 错误示例:并行更新冲突 ===")
try:
bad_graph.invoke({"value": "初始值"})
except InvalidUpdateError as exc:
print(f"捕获到:{type(exc).__name__}")
print("原因:left 和 right 在同一个 super-step 中都写入 value,")
print("而 value 没有 reducer,LangGraph 不知道应该保留哪一个值。")
print(f"原始错误:{exc}")
def main() -> None:
demo_good_graph()
demo_stream_updates()
demo_conflict()
print("\n=== 学习结论 ===")
print("1. 多条普通出边会让目标节点在同一个 super-step 中并行运行。")
print("2. 并行分支汇合时,下游节点等待本轮所有上游分支完成。")
print("3. 无 reducer 的字段采用覆盖策略,同一步只能接收一个更新。")
print("4. operator.add 可以连接列表,但不会去重。")
print("5. 自定义 reducer 可以实现去重、排序或其他合并规则。")
if __name__ == "__main__":
main()