LangGraph入门到入土(1):从 LangChain 到 LangGraph 的思维跃迁

0 阅读10分钟

一、为什么要学 LangGraph?

1.1 LangChain 的局限:线性流程不够用了

如果你用过 LangChain,你一定写过这样的代码:

chain = prompt_template | model | output_parser
result = chain.invoke({"topic": "人工智能"})

这种写法非常优雅,但它隐含了一个前提:流程是固定的、单向的、无环的。数据从起点流到终点,不会回头,也不会中途分叉。

但现实中的 AI 应用往往更复杂:

  • 智能客服:需要先判断用户意图,然后走不同的流程分支
  • 代码生成 Agent:写完代码 → 跑测试 → 发现 bug → 修改代码 → 再跑测试(循环)
  • 金融审批系统:需要在关键节点暂停,等人工审核员确认后再继续

这些场景需要的不是一个线性管道,而是一个有状态、可循环、可中断的执行引擎——这就是 LangGraph 诞生的原因。

1.2 LangChain vs LangGraph:互补关系

LangChainLangGraph
核心模型线性管道(Pipeline)有向图(Graph)
流程方向单向、固定可循环、可分叉
状态管理无状态,数据流过即消失有状态,节点共享数据
控制流固定顺序条件分支、循环、中断
适用场景简单的 LLM 调用链复杂的 Agent、多轮对话、工作流

一句话总结:LangChain 让你快速跑起来,LangGraph 让你跑得复杂、跑得可控、跑得持久。两者是互补关系——你用 LangChain 的组件(模型、工具、检索器)作为积木,用 LangGraph 作为把这些积木粘合在一起的蓝图。

1.3 什么时候该用 LangGraph?

如果你遇到以下任何一个问题,就该考虑从 LangChain 升级到 LangGraph:

  1. 你的 Agent 需要"反思" :写完代码后自己检查,发现 bug 后重新修改
  2. 你的流程需要"分叉" :根据用户的输入,走完全不同的处理路径
  3. 你的任务需要"暂停等人" :在某个节点停下来,等领导审批通过后再继续
  4. 你的系统需要"断点续跑" :服务器重启后,Agent 能从上次中断的地方继续
  5. 你有多个 Agent 需要协作:一个负责规划,一个负责执行,一个负责质检

二、LangGraph 三大核心要素

LangGraph 的所有能力都建立在三个核心概念之上:State(状态)Node(节点)Edge(边) 。理解这三者,你就掌握了 LangGraph 的 80%。

2.1 State(状态):图流转的"公共黑板"

什么是 State?

State 是 LangGraph 的灵魂。它是一份全局共享的数据结构,图中的每一个节点都可以读取它、修改它。

你可以把 State 想象成一块挂在墙上的公共黑板

  • 所有人(节点)都能看到黑板上写了什么
  • 每个人都可以在上面添加新内容
  • 后一个人能看到前一个人写下的所有内容

如何定义 State?

在 LangGraph 中,State 通常用 Python 的 TypedDict来定义:

from typing import TypedDict, List, Annotated
from langgraph.graph.message import add_messages
import operator

class AgentState(TypedDict):
    # 消息列表:使用 add_messages Reducer,实现追加而非覆盖
    messages: Annotated[List, add_messages]
    
    # 步骤计数器:使用 operator.add,每次累加
    step: Annotated[int, operator.add]
    
    # 用户意图:普通字段,会被直接覆盖
    intent: str
    
    # 是否已完成:布尔标志
    is_complete: bool

什么是 Annotated?

Annotated是 Python 标准库 typing模块里的一个工具,它的作用是给类型提示附加额外的元信息。

对于 Python 本身来说,Annotated几乎不做任何事情——它只是给类型标注"贴了个标签"。但 LangGraph 会读取这个标签,从中提取出第二个参数(比如 add_messages),把它当作这个字段的更新策略。

from typing import Annotated

# 语法:Annotated[实际类型, 额外信息1, 额外信息2, ...]
x: Annotated[int, "这是一个年龄字段"]

Reducer(归约器)

Reducer 是一个函数,它定义了"当多个节点都想修改同一个 State 字段时,如何把这些修改合并成一个最终结果"。

默认情况下,节点返回的状态会覆盖旧状态。但在很多场景中,我们需要的是追加累加

# 不使用 Reducer:覆盖行为
state["messages"] = ["你好"]  # 第一次设置
state["messages"] = ["世界"]  # 第二次设置 → 覆盖了"你好"

# 使用 add_messages Reducer:追加行为
state["messages"] = ["你好"]  # 第一次设置
state["messages"] = ["世界"]  # 第二次设置 → 结果是 ["你好", "世界"]

LangGraph 内置了几种常用的 Reducer:

  • add_messages:消息列表追加(最常用)
  • operator.add:数值累加或列表拼接
  • operator.set:集合合并

你也可以自定义 Reducer 函数,实现任何你想要的合并逻辑。


2.2 Node(节点):图里的"执行工人"

什么是 Node?

Node 就是图中的一个个执行单元。每个节点都是一个普通的 Python 函数,它接收当前的 State,执行一些业务逻辑,然后返回一个状态更新字典

def my_node(state: AgentState) -> dict:
    """
    参数 state:当前的全局状态
    返回值 dict:要更新的状态字段
    """
    # 读取当前状态
    last_message = state["messages"][-1] if state["messages"] else ""
    
    # 执行业务逻辑(比如调用 LLM)
    # response = llm.invoke(last_message)
    
    # 返回要更新的状态
    return {
        "messages": ["这是节点处理后的回复"],
        "step": 1,  # 由于 Reducer 是 operator.add,实际效果是 step += 1
    }

节点的设计原则

  1. 无副作用:理想的节点应该只通过返回值修改 State,不要直接操作文件、数据库或全局变量
  2. 单一职责:一个节点只做一件事。比如"调用 LLM"是一个节点,"格式化输出"是另一个节点
  3. 可观测:节点的输入(State)和输出(State 更新)都应该清晰可辨

将节点添加到图中

from langgraph.graph import StateGraph

workflow = StateGraph(AgentState)

workflow.add_node("llm_call", my_node)       # 节点名称 + 节点函数
workflow.add_node("tool_execute", tool_node)
workflow.add_node("format_output", format_node)

2.3 Edge(边):控制流的"神经脉络"

如果说 Node 是图中的"名词",那么 Edge 就是图中的"动词"——它决定了节点之间的连接关系和执行顺序

普通边(Normal Edge)

普通边定义的是确定性的先后顺序:A 执行完之后,一定执行 B。

workflow.add_edge(START, "llm_call")       # 入口 → 调用模型
workflow.add_edge("llm_call", "tool_execute")  # 顺序执行
workflow.add_edge("tool_execute", END)     # 结束

这段代码的含义是:START → llm_call → tool_execute → END,一个标准的线性流程。

条件边(Conditional Edge)

条件边是 LangGraph 真正的杀手锏。它允许你根据当前 State的内容,动态决定下一步去哪一个节点

def router_function(state: AgentState) -> str:
    """返回值是目标节点的名称"""
    last_message = state["messages"][-1].content if state["messages"] else ""
    
    if "需要查资料" in last_message:
        return "search_tool"       # 去搜索节点
    elif "需要计算" in last_message:
        return "calculator_tool"   # 去计算节点
    elif "已完成" in last_message:
        return END                 # 结束
    else:
        return "llm_call"          # 继续对话(循环回 LLM 节点)

workflow.add_conditional_edges(
    "llm_call",                    # 源节点
    router_function,               # 路由函数
    {
        "search_tool": "search_tool",
        "calculator_tool": "calculator_tool",
        END: END,
        "llm_call": "llm_call",
    }
)

条件边的威力:它让图具备了动态决策能力。同样的输入,可能走完全不同的路径;同一个节点,可以被反复访问(形成循环)。

START 与 END

LangGraph 预定义了两个特殊节点:

  • START:图的入口点。所有从外部 invoke()传入的数据,都会首先到达 START 节点
  • END:图的终止点。当执行流到达 END 时,图执行完毕,返回最终 State

注意:你必须至少有一条路径从 START 出发,也必须至少有一条路径到达 END,否则编译会报错。


三、完整示例:带反思机制的 Agent

下面展示从零构建一张可运行图的完整流程。

from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
import operator

# ===== Step 1: 定义 State =====
class AgentState(TypedDict):
    messages: Annotated[List, add_messages]        # 消息历史(追加)
    reflection_count: Annotated[int, operator.add] # 反思次数(累加)
    max_reflections: int                            # 最大反思次数(覆盖)

# ===== Step 2: 定义 Node 函数 =====
llm = ChatOpenAI(model="gpt-4o-mini")

def call_model(state: AgentState) -> dict:
    """调用 LLM 生成回复"""
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state: AgentState) -> str:
    """条件路由:判断是否需要继续反思"""
    if state["reflection_count"] >= state.get("max_reflections", 3):
        return END
    last_message = state["messages"][-1]
    if isinstance(last_message, AIMessage):
        if "让我再想想" in last_message.content:
            return "reflect"
    return END

def reflect_node(state: AgentState) -> dict:
    """反思节点:对之前的回答进行自我审查"""
    reflection_prompt = f"""
    请对你之前的回答进行反思,找出可能的不足或遗漏。
    如果需要改进,请给出更好的版本。
    原始回答:{state['messages'][-1].content}
    """
    response = llm.invoke([HumanMessage(content=reflection_prompt)])
    return {"messages": [response], "reflection_count": 1}

# ===== Step 3: 构建图 =====
workflow = StateGraph(AgentState)

workflow.add_node("call_model", call_model)
workflow.add_node("reflect", reflect_node)

workflow.add_edge(START, "call_model")
workflow.add_conditional_edges(
    "call_model",
    should_continue,
    {"reflect": "reflect", END: END}
)
workflow.add_edge("reflect", "call_model")  # 反思后回到模型调用(形成循环)

# ===== Step 4: 编译图 =====
app = workflow.compile()

# ===== Step 5: 运行图 =====
initial_state = {
    "messages": [HumanMessage(content="帮我写一首关于秋天的诗,要有意境")],
    "reflection_count": 0,
    "max_reflections": 2,
}

result = app.invoke(initial_state)
print(result["messages"][-1].content)

执行流程:

START → call_model → should_continue
                          │
                    ┌─────┴─────┐
                    │           │
                  reflect     END
                    │
                    └──→ call_model(循环)

四、总结

核心概念说明类比
State全局共享的数据结构公共黑板
Node执行单元,读取并更新 State工人
Edge控制节点间的执行顺序传送带
Reducer定义 State 字段的更新策略合并规则

LangGraph 的核心思想:把复杂的 AI 流程拆解成一个个节点,用边把它们连接起来,通过 State 在节点间传递数据,最终形成一个可执行的计算图。


五、相关面试题

面试题1:LangChain 和 LangGraph 的本质区别是什么?

参考答案:

LangChain 的核心模型是线性管道,数据沿着固定的方向单向流动,不支持循环和分支。LangGraph 的核心模型是有向图,支持条件分支、循环、中断等复杂控制流。

打个比方:LangChain 是一条传送带,工件从一头进去从另一头出来;LangGraph 是一个车间,工件可以在不同工位之间来回移动,甚至可以在某个工位前停下来等人。

面试题2:什么是 Reducer?为什么需要它?

参考答案:

Reducer 是定义 State 字段更新策略的函数。当多个节点都想修改同一个字段时,Reducer 决定如何合并这些修改。

需要 Reducer 的原因是:默认情况下,后执行的节点会覆盖先执行节点的修改。但在很多场景中(比如消息列表),我们需要的是追加而不是覆盖。Reducer 让我们可以自定义这种行为。

面试题3:Annotated在 LangGraph 中的作用是什么?

参考答案:

Annotated是 Python 的类型注解工具,用于给类型提示附加额外信息。在 LangGraph 中,它用来给 State 字段绑定 Reducer。

例如 messages: Annotated[List, add_messages]表示:messages字段的类型是 List,更新策略是 add_messages(追加模式)。

面试题4:条件边和普通边有什么区别?

参考答案:

  • 普通边:确定性执行,A 完成后一定执行 B
  • 条件边:根据当前 State 的内容,动态决定下一步去哪个节点

条件边让图具备了动态决策能力,是实现循环、分支、路由等功能的基础。

面试题5:START 和 END 节点有什么作用?

参考答案:

  • START:图的入口点,所有外部输入首先到达 START
  • END:图的终止点,执行流到达 END 时图执行完毕

必须至少有一条路径从 START 出发,也必须至少有一条路径到达 END,否则编译会报错。