轻松掌握 LangGraph 的状态与节点

32 阅读4分钟

LangGraph 可以看成一张会运行的流程图:State 保存数据,Node 处理数据,Edge 决定下一步执行谁。节点只返回自己修改的部分,框架负责把更新合并回状态。

1️⃣ 状态规约:先约定数据

普通状态常用 TypedDict 定义:

from typing import TypedDict

class CommonState(TypedDict, total=False):
    username: str
    messages: list[str]

total=False 表示字段可以逐步产生。普通字段没有特殊合并规则,同一字段被再次更新时,新值会覆盖旧值。

调用图时只需提供入口字段,节点返回局部更新,未更新的旧字段会继续保留。

2️⃣ 普通状态:消息会被覆盖

下面两个节点都更新 messages

def common_a(state: CommonState) -> dict:
    text = "你好,我是 " + state["username"]
    return {"messages": [text]}

def common_b(state: CommonState) -> dict:
    text = "AI:你好," + state["username"]
    return {"messages": [text]}

把它们按顺序连接并打印结果:

from langgraph.graph import StateGraph, START, END

builder = StateGraph(CommonState)
builder.add_edge(START, "common_a")
builder.add_sequence([common_a, common_b])
builder.add_edge("common_b", END)

graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print(res["messages"])

输出只有第二条消息:

['AI:你好,小黄']

原因是普通 messages 只是一个列表字段,common_b 返回的新列表覆盖了 common_a 的列表。

被覆盖的只是 messagesusername 仍会保留;普通列表不会自动判断应该追加还是替换。

3️⃣ MessagesState:消息会被保留

先把上面的例子原样换成 MessagesState。两个节点依旧返回字符串列表:

from langgraph.graph.message import MessagesState

class MessageState(MessagesState):
    username: str

def message_a(state: MessageState) -> dict:
    return {"messages": ["你好,我是 " + state["username"]]}

def message_b(state: MessageState) -> dict:
    return {"messages": ["AI:你好," + state["username"]]}
builder = StateGraph(MessageState)
builder.add_node("message_a", message_a)
builder.add_node("message_b", message_b)
builder.add_edge(START, "message_a")
builder.add_edge("message_a", "message_b")
builder.add_edge("message_b", END)

graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print([message.content for message in res["messages"]])

输出变成了两条:

['你好,我是 小黄', 'AI:你好,小黄']

同样的更新内容,普通状态只剩最后一条,MessagesState 却保留了两条。原因是它自带 messages 字段和 reducer(归并器),字符串也会被转换成消息对象后追加到历史中。

延伸:加入 HumanMessage、AIMessage 和真实模型

真实对话最好明确消息角色:用户输入用 HumanMessage,模型返回的是 AIMessage

from dotenv import load_dotenv
from langchain.messages import HumanMessage, AIMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState

load_dotenv(override=True)
model = ChatDeepSeek(model="deepseek-v4-flash")

# MessagesState 已经自带 messages 字段及消息合并规则
class OverAllState(MessagesState):
    username: str
    output: str

def node_a(state: OverAllState) -> dict:
    text = "你好,我是 " + state["username"]
    return {"messages": [HumanMessage(text)]}

def node_b(state: OverAllState) -> dict:
    response: AIMessage = model.invoke(state["messages"])
    return {
        "messages": [response],
        "output": response.content,
    }

builder = StateGraph(state_schema=OverAllState)

# 写法一:逐个注册节点,再用 add_edge 串行连接
# builder.add_node("node_a", node_a)
# builder.add_node("node_b", node_b)
# builder.add_edge(START, "node_a")
# builder.add_edge("node_a", "node_b")
# builder.add_edge("node_b", END)

# 写法二:add_sequence 自动注册节点并连接 node_a -> node_b
builder.add_edge(START, "node_a")
builder.add_sequence([node_a, node_b])
builder.add_edge("node_b", END)

graph = builder.compile()
res = graph.invoke({"username": "小黄"})

print([message.type for message in res["messages"]])
print(res["output"])

MessagesState 已自带 messages,子类只需补充业务字段。模型读入 HumanMessage,返回的 AIMessage 会加入历史。第一行输出 ['human', 'ai'],第二行输出回复正文。

4️⃣ 串行:后一步依赖前一步

flowchart LR
    S([START]) --> A[node_a<br/>生成用户消息]
    A --> B[node_b<br/>生成回复]
    B --> E([END])

因为 node_b 依赖 node_a 产生的消息,所以两个节点不能同时开始。

上面的两种构图写法等价。add_sequence([node_a, node_b]) 只是串行链的简写,只能用来按顺序连接节点,不能创建并行分支。

5️⃣ 并行:两个节点同时执行

并行节点应互不依赖,并尽量写入不同字段:

class ParallelState(TypedDict, total=False):
    username: str
    greeting: str
    farewell: str

def make_greeting(state: ParallelState) -> dict:
    return {"greeting": "你好,我是 " + state["username"]}

def make_farewell(state: ParallelState) -> dict:
    return {"farewell": state["username"] + ",下次见!"}
flowchart LR
    S([START]) --> A[make_greeting]
    S --> B[make_farewell]
    A --> E([END])
    B --> E
builder = StateGraph(ParallelState)
builder.add_node("make_greeting", make_greeting)
builder.add_node("make_farewell", make_farewell)
builder.add_edge(START, "make_greeting")
builder.add_edge(START, "make_farewell")
builder.add_edge("make_greeting", END)
builder.add_edge("make_farewell", END)

graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print(res)

输出中的三个业务字段会同时存在:

{
  'username': '小黄',
  'greeting': '你好,我是 小黄',
  'farewell': '小黄,下次见!'
}

START 发出的两条边会在同一轮调度两个节点,二者可以同时执行,不会等前一个结束再启动后一个。它们分别写入不同字段,完成后才统一合并结果。

并行写入: 多个节点最好写入不同字段;必须写同一字段时,需要配置 reducer。

串行依赖: 后一个节点需要前一个节点的结果时,就要按顺序连接,不能同时执行。

6️⃣ 总结

  • 保存用户名、结果等普通数据,可以使用 TypedDict
  • 保存对话历史,使用自带消息合并规则的 MessagesState
  • 后一步需要前一步的结果,就用边或 add_sequence() 串行连接。
  • 多个任务互不依赖,就从同一起点连接多条边,让它们同时执行。

简单来说:普通状态保存数据,MessagesState 保存对话;有先后依赖就串行,没有依赖就可以并行。