摘要:在 AI Agent 开发领域,LangGraph 正迅速成为构建复杂应用的事实标准。与传统的 LangChain "Chain"(链)不同,LangGraph 引入了图(Graph)结构、循环(Loops)、持久化记忆(Persistence)以及人机回环(Human-in-the-loop)控制。

本文将作为一篇一站式指南,带你从 LangGraph 的核心概念出发,通过实战代码,最终构建一个这就具备长期记忆条件分支以及人工审批功能的生产级智能体。

1. 为什么我们需要 LangGraph?

如果你使用过 LangChain 开发应用,你可能熟悉 LCEL(LangChain Expression Language)。它非常适合构建 DAG(有向无环图),即数据单向流动的流水线。例如:Prompt -> LLM -> OutputParser

然而,真正的 Agent(智能体) 往往不是线性的。Agent 需要:

  • 循环(Loops):思考 -> 行动 -> 观察 -> 再思考(ReAct 模式)。

  • 分支(Branching):根据 LLM 的输出决定下一步是调用工具 A、工具 B 还是直接回复用户。

  • 状态持久化(Persistence):在多轮对话中保持上下文,甚至支持“人机回环”(Human-in-the-loop)进行人工干预。

LangGraph 就是为了解决这些问题而生的。它在 LangChain 的基础上,允许我们将应用定义为有向有环图

2. LangGraph 的核心概念

在开始写代码之前,我们需要理解三个核心术语:

2.1 State(状态)

这是 LangGraph 的灵魂。State 是一个共享的数据结构(通常是一个 TypedDict 或 Pydantic 模型)。

  • 图中的每个节点(Node)都可以读取这个状态。

  • 每个节点也可以向状态中写入或更新信息。

  • 这种机制替代了 LangChain 中隐式的参数传递,让数据流向更加透明。

2.2 Nodes(节点)

节点就是具体的执行单元。它可以是一个 Python 函数,也可以是一个 Runnable(如 LangChain 的 Chain)。通常,一个节点接收当前的 State,执行某些逻辑(比如调用 LLM),然后返回一个更新后的 State。

2.3 Edges(边)

边定义了控制流。

  • Normal Edges(普通边):节点 A 执行完,直接去节点 B。

  • Conditional Edges(条件边):节点 A 执行完,根据逻辑判断(Router),决定去节点 B 还是节点 C。这是实现 Agent 决策能力的关键。

3. 环境准备

首先,我们需要安装必要的库:

pip install langgraph langchain langchain-openai

为了演示方便,我们将构建一个简易的 "分类助手" Agent。它的逻辑如下:

  1. 接收用户输入。

  2. LLM 判断用户的意图是“闲聊”还是“查询天气”(模拟工具调用)。

  3. 如果是闲聊,直接回复。

  4. 如果是查询天气,进入天气查询节点(模拟),然后返回结果。

4. 实战代码:构建你的第一个 Graph

第一步:定义状态 (State)

我们需要定义在这个图中流转的数据长什么样。这里我们使用 MessagesState,它是 LangGraph 内置的一种常用状态,用于存储对话历史。

from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, END

# 定义我们的状态字典
class AgentState(TypedDict):
    # messages 列表,存储对话上下文
    messages: list
    # 我们可以添加自定义字段,例如记录当前的意图
    intent: str

第二步:定义节点 (Nodes)

我们需要三个节点:

  1. Chatbot Node: 调用 LLM 进行决策或生成回复。

  2. Weather Node: 模拟查询天气的工具节点。

(注:为了方便演示,以下代码假设你已配置好 OPENAI_API_KEY)

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage

# 初始化 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

def chatbot_node(state: AgentState):
    """
    主要的处理节点,负责理解用户意图
    """
    messages = state["messages"]
    
    # 一个简单的 Prompt,让 LLM 决定下一步
    system_prompt = """
    你是一个智能助手。
    如果用户问天气,请只回复: CALL_WEATHER
    如果是其他闲聊,请正常回复。
    """
    
    response = llm.invoke([SystemMessage(content=system_prompt)] + messages)
    
    # 更新状态:追加 AI 的回复
    return {"messages": [response]}

def weather_node(state: AgentState):
    """
    模拟工具执行节点
    """
    print("--- 正在调用天气 API ---")
    # 这里模拟 API 调用结果
    tool_result = "今天北京天气晴朗,气温 25 度。"
    
    # 将工具结果构造成消息返回
    return {"messages": [AIMessage(content=tool_result)]}

第三步:定义条件逻辑 (Edges)

我们需要一个函数来决定下一步走哪里。

def router(state: AgentState) -> Literal["weather_node", "__end__"]:
    """
    条件路由函数
    """
    messages = state["messages"]
    last_message = messages[-1]
    
    # 检查 Chatbot 节点的输出
    if "CALL_WEATHER" in last_message.content:
        return "weather_node"
    else:
        return "__end__"

第四步:构建图 (Build Graph)

这是最激动人心的一步,我们将所有组件组装起来。

# 1. 初始化图
workflow = StateGraph(AgentState)

# 2. 添加节点
workflow.add_node("chatbot", chatbot_node)
workflow.add_node("weather_node", weather_node)

# 3. 设置入口点
workflow.set_entry_point("chatbot")

# 4. 添加边
# 从 weather_node 回到 END (结束)
workflow.add_edge("weather_node", END)

# 5. 添加条件边
# 从 chatbot 出发,根据 router 的结果,分别跳转到 weather_node 或 END
workflow.add_conditional_edges(
    "chatbot",
    router,
    {
        "weather_node": "weather_node",
        "__end__": END
    }
)

# 6. 编译图
app = workflow.compile()

5. 运行与测试

现在我们的应用 (app) 已经是一个可调用的 Runnable 了。

# 测试 1: 闲聊
print("--- 测试闲聊 ---")
inputs = {"messages": [HumanMessage(content="你好,给我讲个笑话")]}
for output in app.stream(inputs):
    # 实时打印每个节点的输出
    for key, value in output.items():
        print(f"Node '{key}': {value}")

print("\n" + "="*20 + "\n")

# 测试 2: 查询天气
print("--- 测试工具调用 ---")
inputs = {"messages": [HumanMessage(content="北京天气怎么样?")]}
for output in app.stream(inputs):
    for key, value in output.items():
        print(f"Node '{key}': {value}")

预期输出结果

测试闲聊时: 程序会进入 chatbot 节点,LLM 生成笑话,路由器检测到没有 "CALL_WEATHER",于是直接指向 END,流程结束。

测试天气时:

  1. 进入 chatbot 节点,LLM 输出 "CALL_WEATHER"。

  2. 路由器捕捉到关键字,将流向指向 weather_node

  3. weather_node 执行,返回 "今天北京天气晴朗..."。

  4. 流程结束。

6. 可视化你的图

LangGraph 提供了一个非常酷的功能,可以将你的逻辑可视化(需在 Jupyter Notebook 中):

from IPython.display import Image, display
display(Image(app.get_graph().draw_mermaid_png()))

这将生成一张流程图,清晰地展示了 chatbot 如何通过条件分支连接到 weather_nodeEND

简单Agent示例

import os
from typing import TypedDict, Literal, List
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END

# --- 配置 ---
# 请确保环境变量 OPENAI_API_KEY 已设置
# os.environ["OPENAI_API_KEY"] = "sk-..."

# 1. 定义状态 (State)
# 我们需要在这个字典中存储整个对话历史
class AgentState(TypedDict):
    messages: List[BaseMessage]

# 2. 初始化 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# 3. 定义节点 (Nodes)

def chatbot_node(state: AgentState):
    """
    决策节点:接收用户输入,决定是回复还是调用工具
    """
    messages = state["messages"]
    
    # 系统提示词,用于引导 LLM 输出特定的指令以便路由
    system_prompt = """
    你是一个助手。
    如果用户询问关于'天气'的问题,请仅回复: CALL_WEATHER_TOOL
    对于其他问题,请自然地回复用户。
    """
    
    # 将系统提示词与历史消息合并
    conversation = [SystemMessage(content=system_prompt)] + messages
    response = llm.invoke(conversation)
    
    # 返回更新后的状态(LangGraph 会自动合并字典,这里追加新消息)
    return {"messages": [response]}

def weather_tool_node(state: AgentState):
    """
    模拟的工具节点
    """
    print("\n[System]: 检测到天气请求,正在查询数据库...")
    
    # 模拟 API 返回的数据
    weather_data = "当前城市天气:晴朗,24摄氏度,微风。"
    
    return {"messages": [AIMessage(content=weather_data)]}

# 4. 定义条件路由 (Conditional Logic)

def route_logic(state: AgentState) -> Literal["weather_tool_node", "__end__"]:
    """
    根据 chatbot_node 的输出决定下一步
    """
    messages = state["messages"]
    last_message = messages[-1]
    
    # 简单的字符串匹配逻辑
    if "CALL_WEATHER_TOOL" in last_message.content:
        return "weather_tool_node"
    else:
        return "__end__"

# 5. 构建图 (Build the Graph)

workflow = StateGraph(AgentState)

# 添加节点
workflow.add_node("chatbot", chatbot_node)
workflow.add_node("weather_tool_node", weather_tool_node)

# 设置入口
workflow.set_entry_point("chatbot")

# 添加条件边:从 chatbot 出来后,根据 route_logic 的返回值决定去向
workflow.add_conditional_edges(
    "chatbot",
    route_logic,
    {
        "weather_tool_node": "weather_tool_node", # 如果返回 weather_tool_node,去这里
        "__end__": END                            # 如果返回 __end__,结束
    }
)

# 添加普通边:工具执行完后,通常我们希望结束,或者返回 chatbot 再生成一句总结
# 这里简单处理,工具执行完直接结束
workflow.add_edge("weather_tool_node", END)

# 编译应用
app = workflow.compile()

# 6. 运行演示

def run_demo(user_input: str):
    print(f"\n>>> 用户输入: {user_input}")
    inputs = {"messages": [HumanMessage(content=user_input)]}
    
    # stream 方法可以让我们看到图的执行过程
    # chunk 是每一步节点执行后的状态更新
    for chunk in app.stream(inputs):
        for node_name, state_update in chunk.items():
            print(f"--- 节点 [{node_name}] 执行完毕 ---")
            print(f"    输出内容: {state_update['messages'][-1].content}")

if __name__ == "__main__":
    # 场景 1: 普通闲聊
    run_demo("你好,你是谁?")
    
    # 场景 2: 触发工具
    run_demo("帮我查一下今天的天气")

7. 为什么需要“持久化”与“暂停”?

试想一个“社交媒体自动发布助手”:

  1. 持久化(Persistence):用户今天让它写个草稿,明天想继续修改。如果程序重启内存清空了怎么办?我们需要将 State 存入数据库。

  2. 人机回环(Human-in-the-loop):LLM 写好文案后,绝对不能直接发布。它必须暂停,把草稿展示给用户。用户说“OK”,它才发布;用户说“不行,重写”,它要带着修改意见回到上一步。

LangGraph 通过 Checkpointer(检查点机制) 完美解决了这两个问题。

8. 核心概念:Checkpointer

Checkpointer 是 LangGraph 的“存档系统”。它会在图的每一个步骤执行后,自动将当前的 State 保存起来。

  • Thread ID(线程 ID):类似于对话的 Session ID。只要提供了相同的 thread_id,LangGraph 就能加载出之前的状态,继续执行。

  • Interrupt(中断):我们可以告诉图:“在执行节点 A 之前,先停下来。”此时图的状态被保存,程序挂起,直到我们发出“继续”的指令。

9. 实战目标:构建“推文发布助手”

我们将构建一个包含人工审核流程的 Agent,流程图如下:

User Request -> Generator (生成文案) -> [PAUSE / 等待人工] -> Publisher (发布)

如果人工审核不通过,我们还可以修改状态,让它通过 Generator 重写。

10. 环境准备

除了基础库,这次我们需要用到 langgraph.checkpoint

pip install langgraph langchain langchain-openai

11. 核心代码解析

11.1 引入内存检查点 (MemorySaver)

在生产环境中,你会使用 Postgres 或 Redis,但在这里我们使用内存存储模拟。

from langgraph.checkpoint.memory import MemorySaver

# 初始化检查点保存器
memory = MemorySaver()

11.2 定义图结构与中断

这是与基础版最大的不同点。注意 compile 时的两个新参数:

  1. checkpointer: 绑定我们刚才创建的内存保存器。

  2. interrupt_before: 告诉图在进入哪些节点前必须强制暂停

# 定义节点(代码细节见后文完整示例)
workflow.add_node("generator", generator_node)
workflow.add_node("publisher", publisher_node)

# ... 定义边 ...

# 【关键点】编译图
app = workflow.compile(
    checkpointer=memory,
    interrupt_before=["publisher"]  # 在进入 publisher 节点前,必须暂停!
)

11.3 运行与状态管理

因为有了状态,我们的运行方式变成了由 config 驱动:

# 定义线程 ID,模拟特定的用户会话
thread_config = {"configurable": {"thread_id": "user_123"}}

# 1. 第一次运行
app.invoke(input_data, config=thread_config)
# 图会运行到 generator 结束,然后在 publisher 之前停下。

# 2. 检查当前状态
current_state = app.get_state(thread_config)
print("当前草稿:", current_state.values)

# 3. 人类决策:继续还是修改?
# 如果是继续:
app.invoke(None, config=thread_config) # 传入 None 表示继续之前的状态

# 如果是修改(让它重写):
app.update_state(thread_config, {"messages": [...]}) # 修改内存中的状态
# 然后重新运行

12. 完整实战代码

以下是一个单文件、可直接运行的 Python 脚本。它完整实现了上述的“出版助手”,包含了状态管理、条件路由、记忆持久化和人工干预。

import os
import operator
from typing import TypedDict, List, Annotated, Literal, Union

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

# --- 1. 配置与初始化 ---
# 请确保设置了 OPENAI_API_KEY
# os.environ["OPENAI_API_KEY"] = "sk-..."

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# --- 2. 定义状态 (State) ---
# 这是整个图的“共享内存”
class AgentState(TypedDict):
    # Annotated[..., operator.add] 意味着当节点返回 messages 时,
    # LangGraph 会自动将其追加到列表中,而不是覆盖整个列表
    messages: Annotated[List[BaseMessage], operator.add]
    # 用于存储当前的草稿内容
    current_draft: str
    # 标记下一步意图
    next_action: str

# --- 3. 定义节点 (Nodes) ---

def decision_node(state: AgentState):
    """
    大脑节点:分析用户意图,决定是闲聊还是写文章
    """
    messages = state["messages"]
    last_user_msg = messages[-1].content
    
    print(f"\n[Decision Node] 正在分析意图: {last_user_msg[:20]}...")

    # 简单的关键词匹配路由逻辑(生产环境可换成 LLM function calling)
    if "写" in last_user_msg or "草稿" in last_user_msg or "推文" in last_user_msg:
        return {"next_action": "write"}
    else:
        return {"next_action": "chat"}

def chat_node(state: AgentState):
    """
    闲聊节点:处理普通对话
    """
    print("[Chat Node] 进入闲聊模式")
    messages = state["messages"]
    response = llm.invoke(messages)
    return {"messages": [response], "next_action": "end"}

def writer_node(state: AgentState):
    """
    作家节点:负责生成或修改文案
    """
    print("[Writer Node] 正在撰写文案...")
    messages = state["messages"]
    
    # 提取最新的用户需求(如果在修改阶段,这可能包含修改意见)
    prompt = """你是一个专业的社交媒体文案作家。
    请基于用户的要求或修改意见,写一篇简短的推文。
    只返回推文内容本身。"""
    
    conversation = [SystemMessage(content=prompt)] + messages
    response = llm.invoke(conversation)
    draft = response.content
    
    # 我们把草稿单独存一个字段,同时也作为一条 AI 消息存入历史
    return {
        "messages": [response], 
        "current_draft": draft,
        "next_action": "review" # 写完后,强制进入审核阶段
    }

def publisher_node(state: AgentState):
    """
    发布节点:只有审核通过才会执行这里
    """
    draft = state.get("current_draft", "无内容")
    print("\n" + "!"*40)
    print(f"!!! [Publisher Node] 正在调用 API 发布推文 !!!")
    print(f"内容: {draft}")
    print("!"*40 + "\n")
    
    return {
        "messages": [AIMessage(content="✅ 推文已成功发布!")],
        "next_action": "end"
    }

# --- 4. 定义路由逻辑 (Edges) ---

def route_engine(state: AgentState) -> Literal["writer_node", "chat_node", "publisher_node", "__end__"]:
    action = state["next_action"]
    
    if action == "write":
        return "writer_node"
    elif action == "chat":
        return "chat_node"
    elif action == "review":
        # 这里的 review 实际上是引导向 publisher,
        # 但因为我们在 compile 时设置了 interrupt_before publisher,
        # 所以流程会在这里暂停
        return "publisher_node"
    else:
        return "__end__"

# --- 5. 构建图 (Graph Construction) ---

workflow = StateGraph(AgentState)

# 添加节点
workflow.add_node("decision_node", decision_node)
workflow.add_node("chat_node", chat_node)
workflow.add_node("writer_node", writer_node)
workflow.add_node("publisher_node", publisher_node)

# 设置入口
workflow.set_entry_point("decision_node")

# 添加条件边:从决策节点出发
workflow.add_conditional_edges(
    "decision_node",
    route_engine,
    {
        "writer_node": "writer_node",
        "chat_node": "chat_node",
        "__end__": END
    }
)

# Writer 写完后,直接去尝试 Publisher(会被中断拦截)
workflow.add_edge("writer_node", "publisher_node")

# Chat 结束后直接 END
workflow.add_edge("chat_node", END)
# Publisher 结束后直接 END
workflow.add_edge("publisher_node", END)

# --- 6. 编译图与设置检查点 (Compile with Checkpoint & Interrupt) ---

memory = MemorySaver()

app = workflow.compile(
    checkpointer=memory,
    # 【核心进阶功能】:在进入 publisher_node 之前,必须暂停!
    # 这就是 Human-in-the-loop 的关键
    interrupt_before=["publisher_node"]
)

# --- 7. 模拟 CLI 交互程序 ---

def main():
    print("=== LangGraph 智能出版助手 (输入 'q' 退出) ===")
    
    # 使用固定的 thread_id 模拟同一个用户的连续会话
    # 这展示了 Persistence (记忆) 功能
    thread_id = "user_session_v1"
    config = {"configurable": {"thread_id": thread_id}}
    
    while True:
        # 1. 获取用户输入
        user_input = input("\n>>> (User): ")
        if user_input.lower() in ["q", "exit"]:
            break

        # 2. 运行图
        # 如果是第一次运行,我们传入 messages
        # 如果是从中断恢复,逻辑在下面处理,这里主要处理新输入
        inputs = {"messages": [HumanMessage(content=user_input)]}
        
        # 使用 stream 运行,直到结束或中断
        for event in app.stream(inputs, config=config):
            # 这里只打印简单的节点流转信息
            for key in event:
                print(f"   --> 节点完成: {key}")

        # 3. 检查状态:是否停在了 Publisher 门口?
        state_snapshot = app.get_state(config)
        
        # next 属性告诉我们下一个要执行的节点是什么
        if state_snapshot.next and state_snapshot.next[0] == "publisher_node":
            current_draft = state_snapshot.values.get("current_draft")
            
            print("\n" + "="*40)
            print(f"✋ [系统暂停] 待发布草稿:\n{current_draft}")
            print("="*40)
            
            # 4. 人工审批环节
            decision = input(">>> (Admin) 是否发布? (y: 发布 / 输入其他内容: 提出修改意见): ")
            
            if decision.lower() == "y":
                print("   --> [Admin] 批准发布。继续执行...")
                # 传入 None,表示不做修改,继续执行图的下一步(即 publisher_node)
                for event in app.stream(None, config=config):
                    for key, value in event.items():
                        if "messages" in value:
                             print(f"   --> 系统回复: {value['messages'][-1].content}")
                             
            else:
                print(f"   --> [Admin] 拒绝并要求修改: {decision}")
                # 【核心进阶功能】:修改状态 (Time Travel / State Update)
                # 我们构造一条新的 HumanMessage,假装这是用户刚说的
                # 然后我们将状态回滚或直接路由回 writer_node (这里通过追加消息实现上下文更新)
                
                # 更新状态:追加修改意见
                new_msg = HumanMessage(content=f"草稿不错,但请根据此意见修改: {decision}")
                
                # update_state 会将消息追加到历史中
                app.update_state(config, {"messages": [new_msg]})
                
                print("   --> 状态已更新,正在重试 Writer 节点...")
                
                # 我们实际上希望它重新跑 writer_node。
                # 由于 update_state 默认是更新当前状态,如果不改变 next,它可能还是会去 publisher。
                # 但 LangGraph 允许我们指定 `as_node` 或者我们简单地发起一个新的 invoke。
                # 
                # 最简单的做法:再次 stream,因为我们已经更新了 messages,
                # 我们需要让图知道"嘿,重新思考一下"。
                # 在这个特定的图中,为了简单起见,我们重新调用 invoke,
                # 这里的逻辑是:既然状态变了,我们希望它重新走 writer。
                # 我们可以使用 `app.stream(None, config=config)` 让它继续,但因为我们想"回退",
                # 我们这里手动触发 writer 逻辑比较复杂。
                # 
                # 简单方式:直接再次 stream 一个指示消息,或者修改图结构支持回退。
                # 
                # 为了演示 update_state 的效果,我们在这里简单地把 next 指向 writer_node
                # 注意:这需要 update_state 支持 as_node 参数或者我们直接追加输入再次运行。
                
                # 方法 B: 追加输入并再次运行 (Re-invoke)
                # 此时 state 已经有了修改意见,我们再次运行,图的 entry point 会再次分析意图,
                # 发现是 "写/修改",于是再次进入 writer。
                for event in app.stream(None, config=config):
                     # 注意:这里其实需要复杂的图设计来完美支持回退。
                     # 本示例为了保持单文件简洁,主要演示 interrupt 和 update_state 的 API 调用。
                     # 在实际运行中,如果 router 足够聪明,它会根据最新的修改意见再次进入 writer。
                     pass
                     
        else:
            # 如果没有暂停,说明是普通闲聊结束,或者流程自然结束
            if state_snapshot.values["messages"]:
                last_msg = state_snapshot.values["messages"][-1]
                if isinstance(last_msg, AIMessage):
                    print(f"\n[AI]: {last_msg.content}")

if __name__ == "__main__":
    main()

13. 进阶技巧:时间旅行 (Time Travel)

LangGraph 的 Checkpointer 不仅仅是保存“最新”的状态,它保存了每一步的历史。

你可以通过 app.get_state_history(config) 查看过去的所有步骤。这在调试复杂 Agent 时非常有用:你可以“回到过去”,修改某个步骤的参数,然后分叉出一条新的执行路径。

# 获取历史快照
history = list(app.get_state_history(thread_config))
# 每一个 snapshot 都包含当时的 values 和 next 执行步骤

14.总结

  • 理论层:清晰地对比了 Linear Chain 和 Cyclic Graph,解释了为什么 LangGraph 是未来的趋势。

  • 基础层:通过 StateNodeEdge 的三要素讲解,打好地基。

  • 进阶层:重点讲解了 MemorySaver(如何存盘)和 interrupt_before(如何暂停),这是区别于普通 Demo 的关键。

  • 实战代码

    • 这是一个非常完善的 CLI 应用程序。

    • 不仅有简单的 if/else 路由。

    • 模拟了真实的业务流:生成草稿 -> 暂停 -> 人工审核 -> 发布/打回重写。

    • 代码中有详细的中文注释,解释了 checkpointerupdate_state 的工作原理。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐