LangGraph 权威指南:从原理到生产

33 阅读12分钟

前言

随着大语言模型 (LLM) 的爆发,应用架构正在从简单的“提示词工程 (Prompt Engineering)”向复杂的“智能体工程 (Agent Engineering)”演变。LangChain 团队推出的 LangGraph 正是为了应对这一挑战而生。

我们将从最底层的图计算理论讲起,深入源码剖析其运行机制,探讨各种复杂的设计模式,最后落地到生产环境的部署与监控。无论你是初学者还是资深架构师,都能从中获得启发。

第一章:LangGraph 哲学与架构深度剖析

1.1 为什么是 Graph?从 Chain 到 Graph 的演变

在 LangChain 早期,核心抽象是 Chain (链)。SequentialChain 让我们可以将步骤串联起来:A -> B -> C。这种有向无环图 (DAG) 结构非常适合确定性的工作流,比如: 检索 (RAG) -> 格式化 -> 生成

然而,真实的智能体 (Agent) 往往是非线性的:

  • 循环 (Cycles): "思考 -> 行动 -> 观察 -> 思考..." 是一个无限循环,直到任务完成。
  • 分支 (Branching): 根据 LLM 的输出,可能跳转到工具 A,也可能跳转到工具 B,或者直接结束。
  • 状态保持 (Statefulness): 在多轮对话中,需要持续维护上下文。

LangGraph 引入了 图 (Graph) 的概念来解决这些问题。图由 节点 (Nodes)边 (Edges) 组成,原生支持循环和条件跳转。

1.2 Pregel 模型详解:LangGraph 的计算引擎

LangGraph 的运行机制深受 Google Pregel 图计算模型的启发。这是一种 BSP (Bulk Synchronous Parallel) 模型。理解这一点对于掌握 LangGraph 至关重要。

运行流程图解

  1. Super-step (超步): 图的执行过程被划分为一系列离散的步骤,称为 Super-step。
  2. 并行执行: 在同一个 Super-step 中,所有 活跃 (Active) 的节点是 并行运行 的。这意味着如果节点 A 和节点 B 同时收到消息,它们会同时启动(在异步模式下)。
  3. 消息传递: 节点运行结束后,并不直接调用下一个函数,而是返回一个 状态更新 (State Update)。这个更新被视为一条“消息”。
  4. Barrier (同步点): 系统等待当前 Super-step 的所有节点运行完毕。然后应用所有的状态更新(执行 Reducer)。
  5. 控制流决策: 系统根据更新后的状态,评估所有的 条件边 (Conditional Edges),决定下一轮 Super-step 哪些节点将变为“活跃”。

核心源码逻辑 (伪代码):

while not should_terminate:
    # 1. 找出当前活跃的节点 (根据上一轮的边)
    active_nodes = get_active_nodes()
    
    # 2. 并行执行这些节点
    results = parallel_execute(active_nodes)
    
    # 3. 应用状态更新 (Reducer)
    current_state = apply_reducers(current_state, results)
    
    # 4. 持久化检查点 (Checkpointer)
    save_checkpoint(current_state)
    
    # 5. 决定下一轮的路由
    next_nodes = evaluate_edges(current_state)

1.3 核心对象生命周期

  • StateGraph: 这是图的定义阶段。你在这里定义 Schema、注册节点、添加边。此时图还未运行,只是一个蓝图。
  • CompiledGraph (App): 调用 graph.compile() 后生成的对象。它是一个 Runnable,具备了运行时的能力(如检查点管理、中断处理)。
  • Runtime: 当你调用 app.invoke()app.stream() 时,LangGraph 会实例化一个运行时环境来管理具体的执行。

第二章:状态管理 (State Management) 的艺术

2.1 Schema 定义:TypedDict vs Pydantic

状态 (State) 是 LangGraph 中最重要的概念。它定义了图的“内存”。

方案 A: TypedDict (推荐)

最轻量,性能最好。适合大多数场景。

from typing import TypedDict, Annotated, List
import operator

class AgentState(TypedDict):
    # 必须使用 Annotated 来指定 Reducer,否则默认是覆盖行为
    messages: Annotated[List[str], operator.add]
    user_id: str  # 默认覆盖
    count: int    # 默认覆盖

方案 B: Pydantic BaseModel

提供运行时数据验证。如果你需要确保 LLM 输出的数据格式严格符合要求,可以使用 Pydantic。但会有微小的性能开销。

from pydantic import BaseModel, Field

class AgentState(BaseModel):
    messages: List[str] = Field(default_factory=list)
    # Pydantic 模式下,Reducer 的定义方式略有不同,通常在 add_node 时处理

2.2 Reducer 详解与源码分析

Reducer 决定了新产生的状态如何合并到旧状态中。

operator.add 原理

# 实际上就是 list 的 extend 或 + 操作
def add(a, b):
    return a + b

当节点返回 {"messages": [new_msg]} 时,LangGraph 执行 old_state["messages"] + [new_msg]

自定义 Reducer 实战:去重消息列表

有时 LLM 可能会重复生成消息,或者我们需要保持消息窗口大小(只保留最近 10 条)。

def deduplicate_reducer(current: List[str], new: List[str]) -> List[str]:
    # 合并并去重
    combined = current + new
    return list(set(combined))

class State(TypedDict):
    tags: Annotated[List[str], deduplicate_reducer]

自定义 Reducer 实战:保留最近 N 条 (Sliding Window)

def sliding_window_reducer(current: list, new: list) -> list:
    combined = current + new
    return combined[-10:] # 只保留最后10条

class State(TypedDict):
    messages: Annotated[List[BaseMessage], sliding_window_reducer]

2.3 状态隔离设计模式 (Input/Output/Internal Schema)

在复杂的图中,你可能不希望向外部暴露所有的内部状态(如中间推理步骤、临时变量)。

class InputState(TypedDict):
    question: str

class OutputState(TypedDict):
    answer: str

class InternalState(TypedDict):
    question: str
    answer: str
    scratchpad: List[str] # 内部草稿,不对外暴露
    retry_count: int

# 定义图时指定
workflow = StateGraph(input=InputState, output=OutputState, state=InternalState)

def node_a(state: InternalState):
    # 可以访问所有字段
    return {"scratchpad": ["step 1..."]}

这样,当用户调用 app.invoke({"question": "..."}) 时,最终返回的字典只会包含 answer 字段,scratchpad 会被过滤掉。


第三章:节点 (Nodes) 与 边 (Edges) 编程指南

3.1 Node 的多种形态

节点本质上是 Callable[[State], Update]

1. 同步函数

def my_node(state: State):
    return {"key": "value"}

2. 异步函数 (Async) LangGraph 原生支持异步。如果你的节点涉及 IO (如 LLM 调用、数据库查询),务必使用 async def

async def my_node(state: State):
    response = await llm.ainvoke(...)
    return {"messages": [response]}

3. 返回 Command 对象 (v0.2+) 这是最新的控制流方式,允许节点同时更新状态决定下一步跳转,跳过 Edge 的计算。

from langgraph.types import Command

def router_node(state: State):
    if "error" in state:
        # 直接跳转到 'handle_error' 节点,并更新状态
        return Command(goto="handle_error", update={"status": "failed"})
    return Command(goto="next_step")

3.2 Edge 的高级用法与动态路由

add_conditional_edges 是实现 Agent 逻辑的核心。

参数详解:

  • source: 出发节点。
  • path: 路由函数,接收 State,返回一个字符串 (目标节点名)。
  • path_map (可选): 字典,将路由函数的返回值映射到实际的节点名。

实战:基于 LLM 的动态路由

def route_logic(state: State) -> str:
    last_msg = state["messages"][-1]
    if not last_msg.tool_calls:
        return "end" # 映射到 END
    return "continue" # 映射到 tools

workflow.add_conditional_edges(
    "agent",
    route_logic,
    {
        "end": END,
        "continue": "tools"
    }
)

3.3 Map-Reduce 模式与 Send API

当我们需要对列表中的每个元素并行执行某个操作时(例如:在这个主题列表中的每个主题下生成一篇博客),普通的 Edge 无法做到。这需要 Map-Reduce 模式。

LangGraph 提供了 Send API 来实现动态分支。

from langgraph.types import Send

def continue_to_jokes(state: State):
    # Map 步骤:为每个 subject 生成一个 Send 对象
    return [
        Send("generate_joke", {"subject": s}) 
        for s in state["subjects"]
    ]

# 注册条件边
workflow.add_conditional_edges("map_node", continue_to_jokes)

注意: 这里的 generate_joke 节点会并行运行多次,每次接收不同的 state。


第四章:人机协同 (Human-in-the-Loop) 终极指南

4.1 中断机制 (Interrupts) 的底层原理

当你调用 workflow.compile(interrupt_before=["node_a"]) 时,LangGraph 会在进入 node_a 之前抛出一个特殊的异常 GraphInterrupt

  1. Runtime 捕获这个异常。
  2. 保存当前的 Checkpoint。
  3. 停止执行。

这就解释了为什么必须配置 Checkpointer 才能使用中断功能。没有 Checkpointer,程序停止后状态就丢失了,无法恢复。

4.2 Command 模式详解

在 v0.2 版本中,interrupt 函数结合 Command 对象提供了更细粒度的控制。

from langgraph.types import interrupt

def human_node(state: State):
    # 程序在此处挂起
    # 这里的 value 将是用户 resume 时提供的值
    user_feedback = interrupt("请审批...")
    
    return {"feedback": user_feedback}

恢复执行 (Resume):

# 获取当前暂停的线程配置
config = {"configurable": {"thread_id": "123"}}

# 这里的 value 会被传递给上面的 user_feedback 变量
app.invoke(
    Command(resume="同意"), 
    config=config
)

4.3 实战:构建企业级审批流 Agent

场景:用户要求转账 -> Agent 生成工具调用 -> 系统暂停 -> 管理员审批 -> 系统恢复 -> 执行转账。

代码实现片段:

def human_approval(state: State):
    last_msg = state["messages"][-1]
    # 只有当有工具调用时才中断
    if not last_msg.tool_calls:
        return
        
    # 抛出中断,把工具调用详情返回给前端
    approval = interrupt({
        "type": "tool_approval",
        "tool_call": last_msg.tool_calls[0]
    })
    
    # 如果管理员拒绝
    if approval["status"] == "rejected":
        # 我们可以修改消息历史,假装工具调用报错了,让 LLM 重试
        return {
            "messages": [
                ToolMessage(
                    tool_call_id=last_msg.tool_calls[0]["id"],
                    content=f"Error: User rejected operation. Reason: {approval['reason']}"
                )
            ]
        }
    
    # 如果同意,什么都不做,继续执行(进入 ToolNode)
    return

第五章:持久化 (Persistence) 与 记忆 (Memory)

5.1 Checkpointer 深度对比

Checkpointer存储介质适用场景优点缺点
MemorySaverPython 内存 (Dict)测试、Demo、本地脚本速度极快,无依赖重启即丢失,无法横向扩展
SqliteSaverSQLite 文件单机生产、桌面应用持久化,轻量并发写入性能差,单点故障
PostgresSaverPostgreSQL 数据库企业级生产环境高并发,可靠,支持 JSONB需要维护数据库实例

PostgresSaver 配置示例:

from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool

pool = ConnectionPool("postgresql://user:pass@localhost:5432/db")
checkpointer = PostgresSaver(pool)

# 首次使用需初始化表结构
checkpointer.setup()

app = workflow.compile(checkpointer=checkpointer)

5.2 时间旅行 (Time Travel) 调试法

Checkpointer 保存了每一步的状态快照。我们可以像 git 一样回滚。

场景:Agent 陷入死循环,一直在重复同样的错误工具调用。

  1. 查看历史:

    history = list(app.get_state_history(config))
    # history[0] 是最新状态, history[1] 是上一步...
    
  2. 修改状态 (Forking): 我们不仅要回滚,还要“篡改”记忆,告诉 Agent 它之前做错了,引导它走正确的路。

    # 假设我们回滚到倒数第二步,并插入一条 SystemMessage 提示
    config_to_fork = history[2].config
    
    app.update_state(
        config_to_fork,
        {"messages": [SystemMessage(content="注意:不要使用 Search 工具,直接回答。")]},
    )
    # 此时,LangGraph 会基于这个旧的 Checkpoint 创建一个新的分支
    

5.3 Long-term Memory (Store) 实战

Checkpointer 只管当前会话 (Session)。如果用户第二天再来,换了一个 thread_id,之前的记忆就没了。 Store 接口用于解决跨会话记忆。

from langgraph.store.memory import InMemoryStore

store = InMemoryStore()

# 在节点中使用 Store
def memory_node(state: State, store: InMemoryStore):
    user_id = state["user_id"]
    # 读取用户偏好
    prefs = store.get("user_prefs", user_id)
    
    # 写入新的记忆
    store.put("user_prefs", user_id, {"theme": "dark", "language": "zh"})

第六章:多智能体 (Multi-Agent) 架构模式

6.1 Supervisor (主管模式)

核心思想: 星型拓扑。一个中心节点 (Supervisor) 连接所有 Worker 节点。

适用场景: 任务复杂,需要统筹规划。例如:写一个游戏,需要策划、程序、美术协同。

实现技巧:

  • Supervisor 应该是一个专门微调过的 LLM,或者使用强大的 Prompt,明确它的职责是“路由”而非“执行”。
  • 使用 structured_output 强制 Supervisor 输出结构化的路由指令 ({"next_worker": "Coder", "instructions": "..."})。

6.2 Network (协作模式)

核心思想: 网状拓扑。没有领导,每个 Agent 知道它可以把任务转交给谁。

适用场景: 流程相对固定,或者 Agent 之间职责边界非常清晰。例如:客服系统。

  • GeneralBot 遇到技术问题 -> 转交 TechSupportBot
  • TechSupportBot 遇到退款问题 -> 转交 BillingBot

实现: 使用 Command(goto="TechSupportBot") 直接跳转。

6.3 Hierarchical (分层模式)

核心思想: 树状拓扑。父图调用子图。

代码实现:

# 1. 定义子图
research_graph = StateGraph(ResearchState)
# ... 添加节点 ...
research_app = research_graph.compile()

# 2. 在父图中作为节点调用
def call_researcher(state: MainState):
    # 转换状态
    input_state = {"query": state["messages"][-1].content}
    # 调用子图
    result = research_app.invoke(input_state)
    # 返回结果
    return {"messages": [result["summary"]]}

main_graph.add_node("researcher", call_researcher)

这种模式的好处是封装性。父图不需要知道子图内部有多少个节点,也不需要关心子图的中间状态。


第七章:流式输出 (Streaming) 与 异步并发

7.1 三种流式模式详解

构建 Chatbot 时,流式输出是必须的。LangGraph 提供了精细的控制。

  1. stream_mode="values":

    • 每次图中的任何节点完成并更新状态后,通过流发送完整的最新状态
    • 适合:前端需要全量刷新 UI 的场景。
  2. stream_mode="updates":

    • 只发送增量。即某个节点返回的那个字典。
    • 适合:前端进行增量渲染,节省带宽。
  3. stream_mode="messages" (V2):

    • 自动聚合 LLM 的流式 Token。即使你在 Node 内部调用 LLM,LangGraph 也会帮你把 Token 像水流一样通过管道传出来。
    • 这是最适合 Chatbot 的模式。

7.2 Token 级流式最佳实践

在 Node 内部,你应该只返回最终结果。但通过配置 .astream_events,你可以捕获中间过程。

# 客户端代码
async for event in app.astream_events(inputs, version="v2"):
    kind = event["event"]
    
    # 捕获 LLM 的 token 生成
    if kind == "on_chat_model_stream":
        content = event["data"]["chunk"].content
        if content:
            print(content, end="")
            
    # 捕获工具调用的开始
    elif kind == "on_tool_start":
        print(f"\n正在调用工具: {event['name']}...")

第八章:生产环境部署与运维

8.1 部署方案

  1. LangGraph Cloud (推荐):

    • LangChain 官方托管服务。
    • 自动将你的 Graph 变成高并发 API。
    • 内置持久化、Task Queue (异步任务)、Cron Jobs (定时任务)。
    • 只需要一个 langgraph.json 配置文件。
  2. 自托管 (FastAPI + Docker):

    • 你需要自己编写 API 包装器。
    • 需要自己维护 Postgres 数据库作为 Checkpointer。
    • 难点: 处理并发冲突(Optimistic Locking)、WebSocket 连接管理。

8.2 监控与评估 (LangSmith)

LangGraph 与 LangSmith 是天作之合。

  • Trace: 在 LangSmith 后台,你可以看到完整的图执行可视化路径。
    • 哪个节点耗时最长?
    • 哪个节点报错了?
    • Token 消耗统计。
  • Evaluation:
    • 可以把历史上的 Trace 转换为测试集。
    • 运行回归测试,确保修改 Graph 结构后没有破坏原有逻辑。

结语

LangGraph 不仅仅是一个库,它是一种构建复杂系统的思维方式。通过将系统分解为 Node 和 Edge,我们将不可控的 LLM 关进了可控的流程笼子里。

掌握了 LangGraph,你就掌握了构建 Agentic Application 的钥匙。希望这份指南能成为你开发路上的得力助手。