Chapter 02

核心概念:State / Node / Edge

LangGraph 只有三件套:State 是共享数据,Node 是函数,Edge 是跳转规则。从一个"只会数数的图"开始,彻底吃透这三者。

第一个 Graph:数数

别急着上 LLM,先看最小例子——一个把数字加 1 的图:

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    n: int

def add_one(state: State) -> dict:
    return {"n": state["n"] + 1}

g = StateGraph(State)
g.add_node("add", add_one)
g.add_edge(START, "add")
g.add_edge("add", END)

app = g.compile()
print(app.invoke({"n": 0}))  # {'n': 1}

这段 20 行代码里包含了所有要素:

State:数据流过图的载体

定义方式:3 种

# 方式 1: TypedDict(推荐,最常见)
class State(TypedDict):
    messages: list
    user_id: str
    step: int

# 方式 2: Pydantic(需要验证)
from pydantic import BaseModel
class State(BaseModel):
    messages: list
    user_id: str = ""

# 方式 3: Dataclass
from dataclasses import dataclass, field
@dataclass
class State:
    messages: list = field(default_factory=list)
用 TypedDict 就够了
绝大多数场景 TypedDict 足够。Pydantic 带来的验证开销对高频节点是性能负担。除非你确定需要运行时校验,否则先 TypedDict。

Reducer:合并策略的关键

默认行为:节点返回 {"n": 5} 就把 state 里的 n 覆盖为 5。但如果要"追加"而不是"覆盖"怎么办?

from typing import Annotated
from operator import add

class State(TypedDict):
    # 不加 Annotated: 返回 {"log": ["x"]} 会覆盖整个 list
    # 加 Annotated[..., add]: 返回 {"log": ["x"]} 会 append
    log: Annotated[list, add]
    count: int          # 无 reducer → 覆盖

def step_a(s): return {"log": ["a done"], "count": 1}
def step_b(s): return {"log": ["b done"], "count": 2}

# 跑完: state == {"log": ["a done", "b done"], "count": 2}
# log 被累加,count 被覆盖

消息专用:add_messages

对话场景最常用的 reducer 是 add_messages。它比普通 add 聪明——支持按 id 更新消息、删除消息:

from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage

class State(TypedDict):
    messages: Annotated[list, add_messages]

# 节点 A 返回一条新消息 → append
def node_a(s): return {"messages": [AIMessage(content="hi", id="m1")]}

# 节点 B 返回同 id 消息 → 替换(而不是追加)
def node_b(s): return {"messages": [AIMessage(content="fixed", id="m1")]}

Node:干活的函数

函数签名

# 节点本质是: (State) -> dict(要更新的字段)
def my_node(state: State) -> dict:
    # 1. 读 state
    q = state["messages"][-1].content

    # 2. 干事(调 LLM、查库、调 API...)
    reply = llm.invoke(q)

    # 3. 返回"增量更新"
    return {"messages": [reply]}
    # 不要返回完整 state,返回你想改的字段就行

节点也可以是对象 / 子图

# Runnable 对象(LangChain 的 Chain 也是 Runnable)
g.add_node("rewrite", rewrite_prompt | llm)

# Tool 节点(见 Ch4)
from langgraph.prebuilt import ToolNode
g.add_node("tools", ToolNode([search, calc]))

# 子图(见 Ch8)
g.add_node("sub", subgraph.compile())

异步节点

节点可以是 async def。跑的时候用 app.ainvoke()app.astream():

async def fetch(state):
    data = await http.get(state["url"])
    return {"data": data}

result = await app.ainvoke({"url": "..."})

Edge:谁连谁

静态边

g.add_edge("a", "b")    # A 跑完必去 B
g.add_edge(START, "a")   # 图入口进 A
g.add_edge("z", END)     # Z 跑完结束

条件边(重点,见 Ch3 详解)

def router(state) -> str:
    if state["intent"] == "refund":
        return "refund_flow"
    elif state["intent"] == "query":
        return "query_flow"
    return END

g.add_conditional_edges("classify", router)

并行边(fan-out / fan-in)

# 从一个节点分裂到多个
g.add_edge("start", "search_web")
g.add_edge("start", "search_db")
# 两个都跑完 → 一起进 merge
g.add_edge("search_web", "merge")
g.add_edge("search_db", "merge")

LangGraph 自动并行执行同级节点,然后在汇合节点等齐。

Compile 与调用方式

app = g.compile(
    checkpointer=None,   # Ch5 讲
    interrupt_before=[],  # Ch6 讲
)

# 1. 同步一把梭
result = app.invoke({"n": 0})

# 2. 流式,逐节点看
for chunk in app.stream({"n": 0}):
    print(chunk)   # {"add": {"n": 1}}

# 3. 异步
result = await app.ainvoke({"n": 0})

可视化:一行画图

# 生成 Mermaid 代码
print(app.get_graph().draw_mermaid())

# 生成 PNG (要装 pygraphviz)
app.get_graph().draw_png("graph.png")

一个带 LLM 的最小例子

from langchain_openai import ChatOpenAI
from langgraph.graph.message import add_messages

llm = ChatOpenAI(model="gpt-4o-mini")

class State(TypedDict):
    messages: Annotated[list, add_messages]

def chatbot(state: State):
    return {"messages": [llm.invoke(state["messages"])]}

g = StateGraph(State)
g.add_node("chat", chatbot)
g.add_edge(START, "chat")
g.add_edge("chat", END)
app = g.compile()

result = app.invoke({"messages": [{"role": "user", "content": "你是谁?"}]})
print(result["messages"][-1].content)

常见坑

症状解法
返回整个 state覆盖了别的节点的更新只返回要改的字段,让 reducer 合并
列表字段忘加 reducer每个节点都覆盖 listAnnotated[list, add]add_messages
节点返回 None报错 "Invalid update"不想更新就 return {}
节点名重复报错 Node exists节点名全局唯一
忘连 START图跑不起来至少要 add_edge(START, "xx")

本章小结