1.LangGraph

🌟 零步:LangGraph 开发的黄金法则(最佳实践)

在我们深入学习节点和边之前,必须牢记一个 LangGraph 开发的最高纲领永远不要一上来就写代码!先画图,后写码,再核对!

完整的开发工作流应该是这样的:

  1. 纸笔草图(设计):在写任何一行代码之前,先在纸上或白板上画出业务流程图。明确有几个节点?哪一步需要条件判断?循环怎么绕回来?
  2. 编写代码(实现):看着草图,把状态、节点、条件边一一用代码写出来。
  3. 打印验证(核对) :代码写完后,必须使用 app.get_graph().draw_mermaid_png() 将代码实际生成的图打印出来(正如我们在前面的例子中所做的那样)。
  4. 对比纠错:将代码生成的图,与你最初在纸上画的草图进行对比。如果两者结构完美重合,说明逻辑正确;如果有出入,立刻排查代码。

第一部分: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 里定义的所有自定义变量(重试次数、文档进度等)在每一个节点执行完毕后,通通拍成快照存入数据库!

这种"游戏存档式"的记忆,带来了两个极其强大的能力:

  1. 零成本多线程隔离 :只要给不同用户分配不同的 thread_id,底层会自动隔离他们的所有状态,互不干扰。
  2. 时光穿梭 (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 提供了 Send API 。在路由函数中,我们不再只返回下一个节点的字符串名称,而是返回一个装满 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 的终极武库:

  1. 条件边判断循环(自动纠错、多步骤执行)
  2. MemorySaver 持久化(多用户隔离、时光穿梭读档)
  3. Interrupt 人工介入(状态机断点暂停、安全审批)
  4. Subgraphs 子图嵌套(黑盒抽象、复杂团队架构解耦)
  5. Stream 流式输出(实时推送节点进度、非阻塞式体验)
  6. Send 动态并发(Map-Reduce 高效裂变、大幅缩减耗时)

并且牢记**"先画图,再写码,后核对"**的黄金法则,你现在已经拥有了用 LangGraph 驾驭最复杂业务流的顶级视野!

相关推荐
青山是哪个青山12 小时前
LangChain 学习笔记(四):Message 与提示词模板
笔记·学习·langchain
JaydenAI16 小时前
[基于OpenEvals的自动化评估-10]针对Agent对话的评估[上篇]
ai·langchain·agent·evaluation·openevals
FlyWIHTSKY1 天前
在智能体系统中,什么是多模态,举例详细说明
人工智能·python·langchain
JaydenAI1 天前
[基于OpenEvals的自动化评估-16]自动模拟多轮对话实施评估
ai·langchain·agent·evaluation·openevals
北斗落凡尘1 天前
LangGraph 入门实战(6)
python·langchain
小田学Python2 天前
100行Python代码,搭一个能干活的AI Agent
python·langchain·大模型·ai agent
FlyWIHTSKY2 天前
使用langchain框架开发智能问答系统,需要有mcp服务和工具调用
langchain
Joy T2 天前
Agent 开源项目全景解析(下):LlamaIndex、Dify、FastGPT 与真实工程选型
langchain·开源·框架·agent·springai·langgraph·mcp
迷路爸爸1802 天前
RAG 优化方案汇总介绍
python·langchain·agent·rag