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 的列表。
被覆盖的只是 messages,username 仍会保留;普通列表不会自动判断应该追加还是替换。
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 保存对话;有先后依赖就串行,没有依赖就可以并行。