🌟 零步:LangGraph 开发的黄金法则(最佳实践)
在我们深入学习节点和边之前,必须牢记一个 LangGraph 开发的最高纲领 : 永远不要一上来就写代码!先画图,后写码,再核对!
完整的开发工作流应该是这样的:
- 纸笔草图(设计):在写任何一行代码之前,先在纸上或白板上画出业务流程图。明确有几个节点?哪一步需要条件判断?循环怎么绕回来?
- 编写代码(实现):看着草图,把状态、节点、条件边一一用代码写出来。
- 打印验证(核对) :代码写完后,必须使用
app.get_graph().draw_mermaid_png()将代码实际生成的图打印出来(正如我们在前面的例子中所做的那样)。 - 对比纠错:将代码生成的图,与你最初在纸上画的草图进行对比。如果两者结构完美重合,说明逻辑正确;如果有出入,立刻排查代码。
第一部分:LangGraph 的三大核心概念与底层机制
LangGraph 的核心思想是将大模型(LLM)的复杂工作流抽象为有向图(Graph)。在图中,节点不互相传递参数,它们唯一沟通的桥梁是一个被全图共享的"状态字典"。
1. 状态 (State):Agent 的全局共享内存
State 是整个图的血液。它必须被定义为一个 TypedDict,并在建图时传入。
- 归约器 (Reducer) 的妙用 :当我们定义聊天记录
messages时,必须使用add_messages这个归约器。如果不加,新的消息就会把旧的消息直接"覆盖"掉。加了之后,新消息会自动"追加"到末尾,并且会根据消息 ID 自动去重。 - 普通变量的处理 :如果我们在 State 里定义了其他变量(比如
retry_count: int),那么在节点返回数据时,它的默认行为就是"直接覆盖"。
2. 节点 (Nodes):做事的手脚与大脑
图上的每一个节点本质上就是一个接收 State 并返回新 State 增量的 Python 函数。
- 大脑节点 (Agent):负责读取当前的状态,传给大模型思考,并返回大模型的新指令。
- 手脚节点 (Tools) :当大脑决定调用工具时,
ToolNode就会去执行真实的 Python 函数(比如读写文件、爬取网页),并把结果返回给 State。
3. 起点与终点 (Edges):工作流的马路
边决定了执行的先后顺序。最简单的边是 graph.add_edge("node_a", "node_b"),代表 A 执行完必定执行 B。
第二部分:高阶控制流 ------ 条件边与循环 (Conditional Edges)
!IMPORTANT 概念解析:为什么需要条件边和循环? 真实的智能体绝不能是一条直线走到底的流水线。它需要像交警一样做判断,需要像人一样在遇到问题时反复重试(循环),直到达成目标才停止。
1. 核心套路浓缩:条件边与循环到底怎么写?
无论业务多复杂,条件边和循环的代码其实就三步核心逻辑:
第一步:写一个路由函数(交通警察) 这个函数接收当前的状态(state),并返回一个字符串暗号(告诉你下一步该往哪走)。
python
def router_function(state: AgentState) -> str:
# 你的自定义判断逻辑,比如检查变量、检查最新消息等
if state["retry_count"] > 3:
return "end_process"
else:
return "go_to_node_A"
第二步:在图上挂载条件边 使用 graph.add_conditional_edges,将"出发节点"、"路由函数"和"目标节点映射"绑定起来。
python
graph.add_conditional_edges(
"start_node", # 从哪个节点出发?
router_function, # 谁来做判断?(交通警察)
{
# 根据警察返回的暗号,决定真正的去向
"go_to_node_A": "node_A",
"end_process": END
}
)
第三步:循环 (Loop) 是怎么产生的? 如果在第二步的映射字典里,你把一个分支指向了之前已经执行过的节点 (比如回到起点),循环就自然诞生了!它会不断在几个节点之间绕圈子,直到路由函数返回了一个指向 END 的暗号,循环才会被打破。
2. 【完整实战案例】:自动起草助手 (Drafter.py)
在这个案例中,只要 Agent 调用的是 update(更新草稿),路由函数就会让它循环 回去继续干活;只有当调用了 save(保存文件)并成功后,才会跳出循环走向 END。
python
import os
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from dotenv import load_dotenv
load_dotenv()
# 全局变量:存储文档内容
document_content = ""
# ==========================================
# 1. 定义状态 (State)
# ==========================================
class AgentState(TypedDict):
# 必须使用 add_messages 归约器,确保消息是追加而非覆盖
messages: Annotated[Sequence[BaseMessage], add_messages]
# ==========================================
# 2. 定义工具 (Tools)
# ==========================================
@tool
def update(content: str) -> str:
"""Updates the document with the provided content."""
# 💡 避坑指南:必须声明 global,否则修改的只是局部变量!
global document_content
document_content = content
return f"文档已成功更新!当前内容如下:\n{document_content}"
@tool
def save(filename: str) -> str:
"""Save the current document to a text file and finish the process."""
global document_content
try:
with open(filename, 'w') as file:
file.write(document_content)
# 故意在返回值中包含"成功"和"保存"字眼,作为后续路由判断的"暗号"
return f"文档已成功保存到 '{filename}'。"
except Exception as e:
return f"保存文档出错: {str(e)}"
tools = [update, save]
# ==========================================
# 3. 初始化大模型与大脑节点
# ==========================================
llm = ChatOpenAI(
model="deepseek-v4-flash-260425",
api_key=os.getenv("VOLCENGINE_API_KEY"),
base_url="https://ark.cn-beijing.volces.com/api/v3"
).bind_tools(tools)
def our_agent(state: AgentState) -> AgentState:
"""大脑节点:负责思考和决定调用哪个工具"""
system_prompt = SystemMessage(content="You are a helpful writing assistant...")
messages = [system_prompt] + state["messages"]
response = llm.invoke(messages)
return {"messages": [response]}
# ==========================================
# 4. 路由判断逻辑 (条件边的核心)
# ==========================================
def should_continue(state: AgentState) -> str:
"""
交通警察:决定图是回滚继续循环,还是结束。
"""
messages = state["messages"]
if not messages:
return "continue"
# 💡 深度探讨:为什么要用 reversed(messages) 倒序遍历?
# 因为 LLM 可能会"并行调用工具"(例如同时调用 update 和 save),导致一次性返回多条 ToolMessage。
# 如果只查最后一条 messages[-1],可能会漏掉真正的保存成功信号。倒序遍历能确保不错过任何消息。
for message in reversed(messages):
# 找到了"保存成功"的暗号,直接跳出循环!
if (isinstance(message, ToolMessage) and
"保存" in message.content and
"成功" in message.content):
return "end"
# 没找到保存成功的暗号(说明刚执行完的是 update),那就继续回去让大脑思考
return "continue"
# ==========================================
# 5. 构建与编译图
# ==========================================
graph = StateGraph(AgentState)
graph.add_node("agent", our_agent)
graph.add_node("tools", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_edge("agent", "tools")
# 挂载条件边,形成【黄金循环】
graph.add_conditional_edges(
"tools", # 从哪个节点出发
should_continue, # 执行路由判断逻辑
{
"continue": "agent", # 继续:回到大脑节点,完成闭环!
"end": END, # 结束:跳出闭环,程序结束!
},
)
app = graph.compile()
第三部分:长线记忆与持久化:MemorySaver (核心精髓)
!TIP 概念解析:LangChain 记忆 vs LangGraph 记忆
- 传统手动记忆 :靠外层的
while循环维护一个列表,把历史对话像滚雪球一样发给大模型。这种做法一关机就失忆,且无法区分多用户并发。- LangChain 记忆 :通常是一个
ConversationBufferMemory,它本质上存的只是"聊天文本日志"。- LangGraph 记忆 :它存的是**"全局状态快照(State Snapshot)"**!当你挂载了
MemorySaver后,它不仅会保存聊天记录,还会把你在AgentState里定义的所有自定义变量(重试次数、文档进度等)在每一个节点执行完毕后,通通拍成快照存入数据库!
这种"游戏存档式"的记忆,带来了两个极其强大的能力:
- 零成本多线程隔离 :只要给不同用户分配不同的
thread_id,底层会自动隔离他们的所有状态,互不干扰。 - 时光穿梭 (Time Travel):你可以随时读取历史任意一步的快照,甚至手动修改快照里的变量值,然后让 Agent 从那一步"读档重来"。
【完整实战案例 2】:工业级记忆持久化 (Real_Memory_Agent.py)
以下代码展示了如何用最简练的代码,实现完美的多用户记忆隔离。
python
import os
from typing import TypedDict, Annotated
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver # 🌟 核心:记忆保存器
from dotenv import load_dotenv
load_dotenv()
# ==========================================
# 1. 定义状态
# ==========================================
class AgentState(TypedDict):
# 再次强调:必须使用 add_messages 归约器。
messages: Annotated[list, add_messages]
# 🌟 新增自定义变量:它没有归约器,所以默认行为是"直接覆盖"
retry_count: int
# ==========================================
# 2. 初始化大模型与处理节点
# ==========================================
llm = ChatOpenAI(
model="deepseek-v4-flash-260425",
api_key=os.getenv("VOLCENGINE_API_KEY"),
base_url="https://ark.cn-beijing.volces.com/api/v3"
)
def process(state: AgentState):
# 将现有的所有历史消息发给模型
response = llm.invoke(state["messages"])
# 获取当前存档中的 retry_count(如果是第一次运行,默认给 0)
current_retry = state.get("retry_count", 0)
# 框架会自动把新 response 追加到 messages 后面
# 同时,它会用新的数字直接【覆盖】旧的 retry_count!
return {
"messages": [response],
"retry_count": current_retry + 1
}
# ==========================================
# 3. 构建图与挂载 MemorySaver (核心!)
# ==========================================
builder = StateGraph(AgentState)
builder.add_node("process", process)
builder.add_edge(START, "process")
builder.add_edge("process", END)
# 💡 重点:实例化 MemorySaver。
# 在真正的生产环境中,只要把这里换成 SqliteSaver(conn) 就能无缝对接物理数据库!
memory = MemorySaver()
# 💡 重点:编译图时,将 memory 当作 checkpointer 传进去。
# 此时,Agent 仿佛装上了记忆芯片,每走一步都会自动"存档"。
agent = builder.compile(checkpointer=memory)
# ==========================================
# 4. 多用户线程隔离调用演示
# ==========================================
if __name__ == "__main__":
# 场景 1:给张三分配一个专属的"存档槽位"(thread_id)
config_zhangsan = {"configurable": {"thread_id": "user_zhangsan"}}
# 💡 革命性进步:我们只需传入最新的一句话!
# 不需要再去手动维护和传递那个庞大的 conversation_history 列表了,底层全包了。
res1 = agent.invoke({"messages": [HumanMessage(content="我叫张三,最爱吃火锅。")]}, config=config_zhangsan)
print("🤖 [AI 回复张三]:", res1["messages"][-1].content)
# 场景 2:李四来了,给他分配一个新的 thread_id,实现物理级隔离
config_lisi = {"configurable": {"thread_id": "user_lisi"}}
res2 = agent.invoke({"messages": [HumanMessage(content="我是李四,一点辣都吃不了。")]}, config=config_lisi)
print("🤖 [AI 回复李四]:", res2["messages"][-1].content)
# 场景 3:张三再次提问。只要带上张三的 config,Agent 瞬间就能回忆起他爱吃火锅。
res3 = agent.invoke({"messages": [HumanMessage(content="你还记得我叫啥,喜欢吃啥吗?")]}, config=config_zhangsan)
print("🤖 [AI 再次回复张三]:", res3["messages"][-1].content)
# 场景 4:探索底层,偷看"游戏存档"
snapshot = agent.get_state(config_zhangsan)
print("\n🔍 查看张三在 MemorySaver 里的完美存档快照:")
print(f" - 消息数量: {len(snapshot.values['messages'])} 条")
print(f" - 节点执行次数 (retry_count): {snapshot.values.get('retry_count')} 次")
第四部分:人工介入与审批 (Human-in-the-loop)
!WARNING 概念解析:LangGraph 的中断与 LangChain 的中间件有什么区别?
- LangChain 插队(Middleware/Callbacks):仅仅是在函数前后插一段代码。此时程序是一直在运行等待的(站着等),如果在网页后端里等用户输入,很容易导致服务器请求超时崩溃。
- LangGraph 强行中断(Interrupt) :它是**"断点存档"。走到拦截点前,图会直接停止运行,将状态存入
MemorySaver并释放所有服务器资源**(回家睡大觉)。等几天后用户在网页点击了同意,程序再从数据库读档,从上次卡住的地方精确地继续往下跑!
【完整实战案例 3】:邮件发送审批流程 (Human_Approval_Agent.py)
以下代码展示了如何通过 interrupt_before 在节点执行前紧急刹车,并让人类通过修改状态字典来控制走向。
python
import os
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
# ==========================================
# 1. 定义状态
# ==========================================
class AgentState(TypedDict):
email_content: str
is_approved: bool
# ==========================================
# 2. 定义节点
# ==========================================
def draft_email(state: AgentState):
print("📝 [节点1: 草拟邮件] 正在撰写请假邮件...")
return {"email_content": "老板,我明天想请假去钓鱼。"}
def send_email(state: AgentState):
print("🚀 [节点2: 发送邮件] 开始执行发送动作...")
if state.get("is_approved"):
print(f" 👉 邮件发送成功!内容:'{state['email_content']}'")
else:
print(" ❌ 邮件发送被拒绝,已取消。")
return state
# ==========================================
# 3. 构建图
# ==========================================
builder = StateGraph(AgentState)
builder.add_node("draft_email", draft_email)
builder.add_node("send_email", send_email)
builder.add_edge(START, "draft_email")
builder.add_edge("draft_email", "send_email")
builder.add_edge("send_email", END)
# ==========================================
# 4. 挂载记忆与配置中断拦截 (核心!)
# ==========================================
# 🌟 必须要有 Checkpointer 才能使用中断功能,否则无法存档。
memory = MemorySaver()
# 🌟 在编译时,指定在执行 `send_email` 之前强制暂停!
app = builder.compile(
checkpointer=memory,
interrupt_before=["send_email"] # 👈 拦截点
)
# ==========================================
# 5. 人工审批实战演示
# ==========================================
if __name__ == "__main__":
config = {"configurable": {"thread_id": "demo_thread_1"}}
# 【第一阶段】启动图
print("▶️ [第一阶段] 启动流程...")
# 图跑到 send_email 之前就会自动退出返回!
app.invoke({"email_content": "", "is_approved": False}, config=config)
# 此时流程被强行中断了,我们可以查看当前状态
state_snapshot = app.get_state(config)
print("\n⏸️ 流程已暂停!")
print(f" - 现在的草稿内容是: {state_snapshot.values['email_content']}")
print(f" - 图想要执行的下一个节点是 (next): {state_snapshot.next}")
# 【人工介入】这里你可以把草稿推给前端 UI,让用户点击按钮
user_choice = input("\n👤 [人类主管] 请假去钓鱼?是否同意发送这封邮件?(y/n): ")
# 【修改状态】根据人的输入,手动修改底层状态字典
if user_choice.lower() == 'y':
print("\n🔧 人类点击了同意,正在修改底层状态...")
app.update_state(config, {"is_approved": True})
else:
print("\n🔧 人类点击了拒绝,正在修改底层状态...")
app.update_state(config, {"is_approved": False})
# 【第二阶段】恢复运行
print("\n▶️ [第二阶段] 恢复流程运行...")
# 🌟 重点:给 invoke 传入 None 即可,它会自动从上次断点处(send_email 之前)醒来继续往下走。
app.invoke(None, config=config)
第五部分:子图嵌套 (Subgraphs) - 黑盒抽象与无限套娃
!TIP 设计哲学:图即节点 (Graph as a Node) 一个图有起点(START)和终点(END),而一个节点有输入(入参)和输出(返回值)。 因此,LangGraph 允许你直接把一个编译好的子图,当作一个普通节点,挂载到主图上!主图完全不需要关心这个子图内部是 3 行代码,还是包含了 100 个节点、循环和中断的复杂迷宫。
这种"无限套娃"的能力,完美对应了面向对象编程里的"复杂大类拆分成小类",也对应了现实世界里"大公司拆分成各个独立运作的部门"。这是让 Agent 从单体玩具走向企业级工程架构的关键。
【完整实战案例 4】:研发部门与主干流程 (Subgraph_Agent.py)
在这个例子中,我们把"搜索资料"和"整理数据"封装成了一个独立的黑盒(research_subgraph),然后像普通节点一样挂载到 parent_builder 里。
python
import os
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# ==========================================
# 1. 定义状态 (简单起见,父子图共用同一个状态字典)
# ==========================================
class AgentState(TypedDict):
task_name: str
research_result: str
final_report: str
# ==========================================
# 2. 构建子图 (专门负责研究的"外包团队")
# ==========================================
def search_web(state: AgentState):
print(" 🕵️♂️ [子图节点 1: 搜索网络] 正在网上狂搜资料...")
return {"research_result": f"查到了关于【{state['task_name']}】的 100 篇论文。"}
def summarize_data(state: AgentState):
print(" 📝 [子图节点 2: 整理数据] 资料太多了,正在提取核心摘要...")
return {"research_result": state['research_result'] + " (已提炼为 3 句话)"}
sub_builder = StateGraph(AgentState)
sub_builder.add_node("search_web", search_web)
sub_builder.add_node("summarize_data", summarize_data)
sub_builder.add_edge(START, "search_web")
sub_builder.add_edge("search_web", "summarize_data")
sub_builder.add_edge("summarize_data", END)
# 编译子图,它现在就像一个封装好的黑盒节点!
research_subgraph = sub_builder.compile()
# ==========================================
# 3. 构建主图 (总管大局的"主干流程")
# ==========================================
def assign_task(state: AgentState):
print("👔 [主图节点 1: 分配任务] 收到老板新任务,立刻外包给【研究团队】!")
return state
def write_report(state: AgentState):
print("✍️ [主图节点 3: 撰写报告] 收到研究团队的成果,我来排版写最终报告。")
return {"final_report": f"【最终汇报】:{state['research_result']} 报告撰写完毕!"}
parent_builder = StateGraph(AgentState)
parent_builder.add_node("assign_task", assign_task)
# 🌟 核心精髓:直接把刚才编译好的子图,当成一个普通的节点加进来!
parent_builder.add_node("research_team", research_subgraph)
parent_builder.add_node("write_report", write_report)
parent_builder.add_edge(START, "assign_task")
parent_builder.add_edge("assign_task", "research_team") # 走向子图
parent_builder.add_edge("research_team", "write_report") # 子图执行完毕后,继续主图
parent_builder.add_edge("write_report", END)
app = parent_builder.compile()
if __name__ == "__main__":
app.invoke({"task_name": "如何用 Python 炒股", "research_result": "", "final_report": ""})
第六部分:流式输出 (Streaming) - 从"死等"到"实时进度条"
!WARNING 概念解析:LangGraph 流式 vs LangChain 流式
- LangChain 的
stream:输出的是 Token(字词)。适用于大模型在打字时,前端一个字一个字地往外蹦。- LangGraph 的
stream:默认输出的是 图的节点状态(Node State Updates)。
正如我们在之前的代码里体验到的底层数据结构差异:
app.invoke():它的返回值是一个单一的字典 ,里面装的是图一路走到END后最终沉淀下来的状态。在网页里如果这个图要跑 1 分钟,用户面对的就是 1 分钟的死机画面,没有任何中间数据。app.stream():它返回的是一个生成器(Generator) 。你用for循环去迭代它时,图里只要有一个节点跑完,循环就会立刻吐出一个字典。这个字典包裹着单层的节点更新,格式永远是{"刚刚跑完的节点名": {"你更新的状态变量": "值"}}。这就让前端可以轻松地做一个实时的"步骤进度条"!
【完整实战案例 5】:流式进度条 (Stream_Agent.py)
python
import os
import time
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class AgentState(TypedDict):
content: str
step_count: int
def collect_data(state: AgentState):
time.sleep(1.5) # 模拟耗时搜索
return {"content": "【1. 数据收集完毕】", "step_count": 1}
def analyze_data(state: AgentState):
time.sleep(1.5) # 模拟耗时分析
return {"content": state['content'] + " -> 【2. 数据分析完毕】", "step_count": 2}
def generate_report(state: AgentState):
time.sleep(1.5) # 模拟耗时写报告
return {"content": state['content'] + " -> 【3. 最终报告生成】", "step_count": 3}
builder = StateGraph(AgentState)
builder.add_node("collect_data", collect_data)
builder.add_node("analyze_data", analyze_data)
builder.add_node("generate_report", generate_report)
builder.add_edge(START, "collect_data")
builder.add_edge("collect_data", "analyze_data")
builder.add_edge("analyze_data", "generate_report")
builder.add_edge("generate_report", END)
app = builder.compile()
if __name__ == "__main__":
initial_state = {"content": "", "step_count": 0}
# 🌟 核心突破:我们不再使用 app.invoke() 死等最终结果!
# app.stream() 会返回一个迭代器(Generator)。
# 只要图里有【任何一个节点】运行完毕,它就会立刻吐出一次数据,触发一次 for 循环!
for output in app.stream(initial_state):
# 💡 深度解析:吐出来的 output 长什么样?
# 它是一个字典,Key 是【刚刚执行完的那个节点的名称】,Value 是【该节点返回的状态更新】。
# 举个例子:当 collect_data 节点跑完时,output 打印出来就是:
# {"collect_data": {"content": "【1. 数据收集完毕】", "step_count": 1}}
for node_name, state_update in output.items():
# 这里就是我们"劫持"数据,通过 WebSocket 等方式推给前端 UI 的最佳时机!
print(f"📡 [前端界面收到实时推送] 👇")
print(f" - 哪个节点刚跑完? -> '{node_name}'")
print(f" - 现在的进度条到哪了? -> {state_update['step_count']}/3")
print(f" - 最新产出的内容是? -> {state_update['content']}")
print("-" * 50)
第七部分:动态并发 (Map-Reduce) - 一次分发,千军万马
!TIP 概念解析:什么是动态并发? 当我们需要处理一个列表(比如 100 篇新闻),传统的做法是写一个循环条件边,让节点串行执行,耗时极长。 而 LangGraph 提供了
SendAPI 。在路由函数中,我们不再只返回下一个节点的字符串名称,而是返回一个装满Send对象的列表。框架一看到这个列表,就会像"影分身"一样,瞬间同时拉起无数个节点副本一起干活!
🌟 核心代码套路(固定写法)
你总结得非常对!无论你的业务怎么变,使用 Send 开辟并发的代码套路永远是固定的这几步:
python
from langgraph.types import Send
def distribute_tasks(state: AgentState) -> list[Send]:
sends = []
# 1. 遍历你要并发处理的数据列表(比如 4 家公司)
for sub in state["subjects"]:
# 2. 包装成 Send 对象:Send("你要召唤的节点名称", {"传给该节点的数据"})
sends.append(Send("research_subject", {"subject": sub}))
# 3. 返回这个列表,LangGraph 底层就会立刻、同时拉起 4 个 research_subject 节点!
return sends
【完整实战案例 6】:多线程查财报 (MapReduce_Agent.py)
在这个案例中,每个节点的查询都需要 2 秒。如果串行查 4 家公司需要 8 秒,但使用了并发后,总耗时仅需 2 秒。
python
import os, time, operator
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
# 1. 全局状态
class AgentState(TypedDict):
subjects: list[str]
# ⚠️ 致命避坑:并发节点会同时返回结果!为了防止它们互相把彼此的结果"覆盖"掉,
# 必须加上 operator.add 归约器,让系统把它们返回的数据合并成一个大列表!
reports: Annotated[list[str], operator.add]
# 2. 供并发节点单独使用的局部状态
class ResearchState(TypedDict):
subject: str
def research_subject(state: ResearchState):
print(f" 🕵️♂️ [并发节点启动] 开始全力研究: {state['subject']} ...")
time.sleep(2) # 并发时,大家同时睡 2 秒
return {"reports": [f"关于【{state['subject']}】的深度报告"]}
def aggregate_reports(state: AgentState):
print(f"\n📈 [汇总节点] 所有并发研究已完成,收到 {len(state['reports'])} 份报告。")
return state
# 3. 核心分发函数
def continue_to_research(state: AgentState) -> list[Send]:
sends = []
for sub in state["subjects"]:
sends.append(Send("research_subject", {"subject": sub}))
return sends
# 4. 构建图
builder = StateGraph(AgentState)
builder.add_node("research_subject", research_subject)
builder.add_node("aggregate_reports", aggregate_reports)
# 起点直接走向分发中心
builder.add_conditional_edges(START, continue_to_research)
# 所有并发的 research_subject 跑完后,统一走向汇总节点
builder.add_edge("research_subject", "aggregate_reports")
builder.add_edge("aggregate_reports", END)
app = builder.compile()
if __name__ == "__main__":
initial_state = {
"subjects": ["苹果公司", "微软", "特斯拉", "英伟达"],
"reports": []
}
app.invoke(initial_state)
!WARNING 深度避坑:为什么分发函数没有连出"边"? 你可能会好奇,在构建图的代码里,只有
builder.add_conditional_edges(START, continue_to_research),但是却没有 写builder.add_edge(continue_to_research, "research_subject"),那它是怎么连过去的呢? 答案是:它不是节点,它是"路由器"! 在 LangGraph 中,对于普通的 Node,你必须用add_edge手动把它们的边焊死。但是对于add_conditional_edges挂载的路由函数,你绝对不能 去写它连向谁。它的出口是动态的 ,它返回了什么Send("节点名"),框架就会在运行时"凭空变出"几条连向对应节点的边。
结语
历经千辛万苦,你已经掌握了以下 六大核心模版,它们是构建企业级 AI Agent 的终极武库:
- 条件边判断循环(自动纠错、多步骤执行)
- MemorySaver 持久化(多用户隔离、时光穿梭读档)
- Interrupt 人工介入(状态机断点暂停、安全审批)
- Subgraphs 子图嵌套(黑盒抽象、复杂团队架构解耦)
- Stream 流式输出(实时推送节点进度、非阻塞式体验)
- Send 动态并发(Map-Reduce 高效裂变、大幅缩减耗时)
并且牢记**"先画图,再写码,后核对"**的黄金法则,你现在已经拥有了用 LangGraph 驾驭最复杂业务流的顶级视野!