Graph API
State:
定义:
在 LangGraph 中,State(状态) 是贯穿整张图执行过程的一份共享数据快照。每个节点都会接收当前 State,执行自己的逻辑,然后返回一份“状态更新”;LangGraph 再根据每个字段对应的 Reducer(规约函数),把这份更新合并回全局 State。
最值得先记住的是这句话:
State = Schema(模式) + Reducer(规约函数)
- Schema 负责定义“状态里有哪些字段、每个字段是什么类型”;放到本章语境里,主要指的就是 State Schema。
- Reducer 负责定义“某个节点返回了这个字段的新值时,应该怎么和旧状态合并”。
如果没有统一 State,节点之间就很容易退回到“你手动从 A 函数拿一堆返回值,再自己拼给 B 函数”的写法,流程一复杂就会乱。LangGraph 把 State 放在中心位置,本质上是在帮你建立一个单一事实来源(Single Source of Truth):所有节点都围着同一份状态读写,而不是各自维护一套容易打架的私有数据。
先建立两个基础直觉:
TypedDict不是业务逻辑,而是在声明“这张图的状态长什么样”。invoke()的核心输入是一整个状态字典,不是给每个字段单独传一堆位置参数。
# State 由 Schema(模式)和 Reducer(规约函数)两部分组成
# 本例用 TypedDict(下方 `BasicState`)定义 State Schema(字段名与类型);图中所有节点读写同一份状态结构
# 字段未用 `Annotated[..., reducer]` 指定 Reducer 时,使用 LangGraph 默认 Reducer(常见为节点返回的新值覆盖该字段旧值)
#
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class BasicState(TypedDict):
"""本图的 State Schema:字段名 + 类型共同定义这张图允许流转的状态结构。"""
user_input: str
response: str
count: int
process_data: dict
# 创建状态图:BasicState 是本图的 state_schema;本例不写 Annotated,因此各字段都走默认覆盖规则
basicState = StateGraph(BasicState)
# 无中间节点:直接从 START 到 END,状态会原样透传
basicState.add_edge(START, END)
app = basicState.compile()
# invoke 只接收一个核心参数(状态字典);process_data 为 dict,需传入嵌套字典
initial_state = {
"user_input": "a",
"response": "resp",
"count": 25,
"process_data": {"k1": "v1"},
}
result = app.invoke(initial_state)
print("执行结果:", result)
State Schema怎么选
| State Schema 写法 | 怎么理解 | 优点 | 更适合的场景 |
| TypedDict | 带类型标注的字典结构 | 轻量、写法简单、性能开销低、很适合教学和流程状态 | 大多数 LangGraph 状态建模起步场景 |
| Pydantic BaseModel | 带运行时校验和字段约束的数据模型 | 能做更强的数据校验、默认值、嵌套模型、字段说明 | 对外接口更严格、数据结构更复杂、希望失败尽早暴露 |
| dataclass | Python 数据类 | 适合需要默认值、字段写起来更像对象属性的场景 | 需要默认值,但不想引入 Pydantic 校验成本 |
怎么选更贴近真实项目?
- 如果你还在设计工作流主线,优先用 TypedDict,因为它最轻,改起来也快。
- 如果这张图已经要作为稳定服务对外暴露,且输入输出字段必须强校验,可以考虑把边界层做成 Pydantic。
- 如果只是想给 State 字段加默认值,dataclass 也可以考虑。
- 不要为了“显得更工程化”一上来把所有内部状态都做成复杂嵌套模型。LangGraph 状态最怕的不是“不够高级”,而是“字段太多、边界不清、谁在改什么看不出来”。
Schema分类
| 名称 | 作用 | 直观理解方式 |
|---|---|---|
| state_schema | 定义图内部完整状态空间,节点通常围绕它读写 | “图内部完整工作台上有哪些数据” |
| input_schema | 限制调用方 invoke(...) 进图时允许传哪些字段 |
“外部用户进门时只能带什么材料” |
| output_schema | 限制图执行结束后最终对外返回哪些字段 | “最终只把哪些结果交给调用方,不把内部草稿全暴露出去” |
这里可以顺手把术语关系记成一条线:
- Schema 是总称,表示“数据结构的定义方式”
- State Schema 是本章最核心的 Schema,专门描述图内部状态
state_schema、input_schema、output_schema是代码层面的具体参数名
# “State 不只有一种 Schema”:`OverallState` 是内部完整 State Schema,`InputState` / `OutputState` 是图对外暴露的输入输出契约
# 构建时 `StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)`,第一个位置参数描述内部完整状态,后两个参数负责限制边界输入输出
# 节点内部仍围绕完整状态空间工作;只有「图的边界」受 input/output 约束,这种分层更贴近真实项目接口封装
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
# 仅包含「输入」字段的 Schema:限制调用方进图时能传什么
class InputState(TypedDict):
question: str
# 仅包含「输出」字段的 Schema:限制图最终对外返回什么
class OutputState(TypedDict):
answer: str
# 图内部使用的完整 State Schema(输入 + 输出)
class OverallState(InputState, OutputState):
pass
def answer_node(state: InputState):
"""处理节点:根据 question 生成 answer。"""
print(f"执行 answer_node 节点:")
print(f" 输入: {state}")
answer = "再见" if "bye" in state["question"].lower() else "你好"
result = {"answer": answer, "question": state["question"]}
print(f" 输出: {result}")
return result
def demo_input_output_schema():
"""演示:调用时只传 question,返回时只得到 answer。"""
print("=== 演示输入输出模式 ===")
# 指定 input_schema / output_schema,约束图的对外接口
builder = StateGraph(
OverallState, input_schema=InputState, output_schema=OutputState
)
builder.add_edge(START, "answer_node")
builder.add_node("answer_node", answer_node)
builder.add_edge("answer_node", END)
graph = builder.compile()
# invoke 只传 InputState 的字段;返回结果仅包含 OutputState 的字段
result = graph.invoke({"question": "你好"})
print(f"图调用结果: {result}")
print(graph.get_graph().print_ascii())
print()
def main():
print("=== LangGraph 图输入输出模式===\n")
demo_input_output_schema()
print("=== 演示完成 ===")
if __name__ == "__main__":
main()
学完 State Schema 和 input_schema / output_schema 之后,State 的“字段长什么样”这一半就比较清楚了。接下来要补上的,就是另一半:字段更新时到底怎么合并。 这正是 Reducer 负责的事。
Reducer规约函数
可以把前后关系理解成:
Graph负责描述流程结构State负责描述共享数据Schema负责描述 State 里有哪些字段Reducer负责描述这些字段更新时怎么合并
也可以把它记成一句更顺的话:Schema 决定字段长什么样,Reducer 决定字段更新时怎么合并。
定义:
节点一般只返回“局部状态更新”,那这些更新到底怎么和旧状态合并?答案就是 Reducer(规约函数)。对某个 State 字段来说,Reducer 本质上就是一个“旧值 + 新增更新 → 合并后新值”的函数。不同字段可以有不同 Reducer;如果某个字段没有显式指定 Reducer,LangGraph 默认就按覆盖更新处理,也就是节点返回的新值直接替换这个字段原来的旧值
在 TypedDict 里,指定 Reducer 的常见写法是 Annotated[字段类型, reducer函数],例如:
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class MyState(TypedDict):
messages: Annotated[list, add_messages]
tags: Annotated[list[str], operator.add]
count: Annotated[int, operator.add]
latest_answer: str
这段定义可以这样理解:
messages用add_messages合并,适合聊天历史这种“新增消息追加到列表里”的场景。tags和count用operator.add,分别表示列表拼接和数值累加。latest_answer没写 Reducer,所以默认覆盖,后写入的新答案会替换旧答案
情景1:覆盖更新:
如果一个字段没有写 Annotated[..., reducer],默认就是覆盖更新。这个行为本身没问题,反而非常适合“这个字段永远只关心最新值”的场景
情景2:追加更新:
如果你把对话历史存在 messages 字段里,通常不希望每个节点一返回
新消息就把整段历史覆盖掉,而是希望把新消息追加到旧消息后面。这时就适合用 add_messages
和普通 operator.add 相比,add_messages 更适合聊天消息场景,因为它不仅会追加新消息,还能根据 message id 更新已有消息,并且会把输入里的消息数据反序列化成 LangChain 的 Message 对象
from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# messages 使用 add_messages:节点只返回增量,自动追加
class AddMessagesState(TypedDict):
messages: Annotated[List, add_messages]
def chat_node_1(state: AddMessagesState) -> dict:
return {"messages": [("assistant", "Hello from node 1")]}
def chat_node_2(state: AddMessagesState) -> dict:
return {"messages": [("assistant", "Hello from node 2")]}
def run_demo():
print("2. add_messages Reducer(消息列表专用)演示:")
builder = StateGraph(AddMessagesState)
builder.add_node("chat1", chat_node_1)
builder.add_node("chat2", chat_node_2)
builder.add_edge(START, "chat1")
builder.add_edge(START, "chat2") # 两节点并行,各自追加消息
builder.add_edge("chat1", END)
builder.add_edge("chat2", END)
graph = builder.compile()
result = graph.invoke({"messages": [("user", "Hi there!")]})
print(f"执行结果: {result}\n")
if __name__ == "__main__":
run_demo()
调用 graph.invoke(...) 时,messages 里既可以传 HumanMessage(...) 这种对象,也可以传 {"role": "...", "content": "..."} 这类字典格式;如果 State 上用了 add_messages,运行时会尽量把它们转换成 LangChain Message 对象
情景3:operator.add用在字符串、列表数值上
operator.add 是一个很常见的内置合并策略,但它对不同数据类型的语义不一样:
- 对 列表 来说,是列表拼接,效果类似
current + update。 - 对 字符串 来说,是字符串连接。
- 对 数值 来说,是数值相加。
这意味着 operator.add 很适合下面这些业务场景:
- 多个节点各自产生一批标签、文档片段、候选结果,最后合并成一个列表。
- 多个节点依次生成文案片段,最后拼成完整文本。
- 多个节点分别贡献分数、计数、成本增量,最后累加出总值。
# operator.add 作为 Reducer(列表):对列表字段做「 extend 」式追加,多节点返回的列表会按顺序合并成一个列表
# Annotated[List[int], operator.add] 表示该字段用 operator.add 规约:语义为列表的 extend,即 current + update 拼成新列表。
# 适合多节点各自产生一段数据、最后合并成一条列表的场景(如多路采集再汇总);前提是业务确实允许简单拼接
import operator
from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class ListAddState(TypedDict):
data: Annotated[List[int], operator.add]
def producer_1(state: ListAddState) -> dict:
return {"data": [1, 2]}
def producer_2(state: ListAddState) -> dict:
return {"data": [3, 4]}
def producer_3(state: ListAddState) -> dict:
return {"data": [4, 5]}
def run_demo():
print("3.1 列表追加 Reducer 演示:")
builder = StateGraph(ListAddState)
builder.add_node("producer1", producer_1)
builder.add_node("producer2", producer_2)
builder.add_edge(START, "producer1")
builder.add_edge("producer1", "producer2")
builder.add_edge("producer2", END)
graph = builder.compile()
result = graph.invoke({"data": [0]})
print(f"初始状态: {{'data': [0]}}")
print(f"执行结果: {result}\n")
if __name__ == "__main__":
run_demo()
"""
【输出示例】
3.1 列表追加 Reducer 演示:
初始状态: {'data': [0]}
执行结果: {'data': [0, 1, 2, 3, 4]}
"""
字符串连接:
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class StringConcatState(TypedDict):
text: Annotated[str, operator.add]
def add_text_1(state: StringConcatState) -> dict:
return {"text": "Hello "}
def add_text_2(state: StringConcatState) -> dict:
return {"text": "World!"}
def run_demo():
print("3.2 字符串连接 Reducer 演示:")
builder = StateGraph(StringConcatState)
builder.add_node("add_text_1", add_text_1)
builder.add_node("add_text_2", add_text_2)
builder.add_edge(START, "add_text_1")
builder.add_edge(START, "add_text_2")
builder.add_edge("add_text_1", END)
builder.add_edge("add_text_2", END)
graph = builder.compile()
result = graph.invoke({"text": "Say: "})
print(f"初始状态: {{'text': 'Say: '}}")
print(f"执行结果: {result}\n")
if __name__ == "__main__":
run_demo()
"""
【输出示例】
3.2 字符串连接 Reducer 演示:
初始状态: {'text': 'Say: '}
执行结果: {'text': 'Say: Hello World!'}
"""
情景4:自定义Reducer:
当默认覆盖、add_messages、operator.add 这些现成策略都不够用时,就可以自定义 Reducer。自定义 Reducer 的核心不是“写法有多复杂”,而是把这个签名记住:
def my_reducer(current_value, update_value):
return merged_value
也就是说,Reducer 拿到的是当前旧值和本次节点更新值,返回的是合并后的新值
# 自定义 Reducer 的价值不在“语法复杂”,而在于你可以按业务语义处理首次合并、空值、重复值、顺序稳定性等边界
# 节点仍只返回增量(如 `{\"factor\": 2.0}`),真正决定怎么合并的是 Reducer,而不是节点本身
from typing import Annotated
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
def MyOperatorMul(current: float, update: float) -> float:
"""自定义乘法 Reducer:首次合并时把 current 的边界情况单独处理,再继续乘法累计。"""
# 第一次调用时 current 往往是类型默认值 0.0,若直接 current * update 会得到 0,后续无法恢复
if current == 0.0:
print(f"current:{current}")
print(f"current: {update}")
return 1.0 * update
return current * update
class MutiplyState(TypedDict):
factor: Annotated[float, MyOperatorMul]
def mutipliter(state: MutiplyState) -> dict:
# 节点返回的 update 会与 state["factor"] 经 MyOperatorMul 合并
return {"factor": 2.0}
def run_demo():
print("使用自定义reducer解决乘法问题:")
builder = StateGraph(MutiplyState)
builder.add_state("multiplier", multiplier)
builder.add_edge(START, "multiplier")
builder.add_edge("multiplier", END)
graph = builder.compile()
result = graph.invoke({"factor": 5.0})
print(f"初始状态: {{'factor': 5.0}}")
print(f"执行结果: {result}")
if __name__ == "__main__":
run_demo()
"""
【输出示例】
使用自定义reducer解决乘法问题:
current:0.0
update:5.0
初始状态: {'factor': 5.0}
执行结果: {'factor': 10.0}
解释: 5.0 * 2.0 = 10.0
"""
情景5:同一State给不同字段配置不同合并
真实业务里的状态通常不会只有一个字段。更常见的情况是:聊天消息要追加,标签列表要拼接,评分要累加,最新结论要覆盖。 这就意味着同一个 State 里,不同字段本来就应该有不同 Reducer
messages: Annotated[List, add_messages]:对话消息追加合并。tags: Annotated[List[str], operator.add]:标签列表拼接。score: Annotated[float, operator.add]:数值分数累加。
同时它还演示了一个很有实际价值的结构:从 START 同时连到多个节点,让多个分支分别产出不同状态更新,最后由各字段自己的 Reducer 负责合并。 这正是后面学习并行分支、复杂 Agent 图时很重要的基础。
# 适合建立“State = Schema + Reducer”整体直觉的案例:字段定义是一层,字段怎么合并是另一层。
# add_messages:节点只返回「增量」消息,自动与历史合并为一条对话链;invoke 里也可传 OpenAI 风格的 `{\"role\", \"content\"}` 字典,运行时会转为 Message 对象
# operator.add 作用于列表时相当于拼接,作用于 float 时为普通加法累加;同一个 State 里完全可以给不同字段配置不同 Reducer
# 从同一 START 连到多个节点时,本例重点是观察“不同字段如何被各自的 Reducer 合并”,而不是把并行分支的执行先后当成业务契约
from typing import Annotated, List
import operator
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
class chatState(TypedDict):
# 消息历史:add_messages 规约,新消息追加而非整表覆盖(与 StateReducer_AddMessages 一致可用 List)
messages: Annotated[List, add_messages]
# 标签列表:operator.add 将各节点返回的列表拼到已有列表后
tags: Annotated[List[str], operator.add]
# 累计分数:operator.add 做浮点数相加
score: Annotated[float, operator.add]
def process_user_message(state: ChatState) -> dict:
# 获取最新消息;修复/注意:须用 .content 读正文(dict 入参在运行时已转为 HumanMessage 等对象,勿当普通 str 用)
user_message = state["messages"][-1]
return {
# add_messages 会把本条 assistant 回复与历史合并
"messages": [("assistant", f"Echo: {user_message.content}")],
"tags": ["processed"],
"score": 1.0,
}
def add_sentiment_tag(state: ChatState) -> dict:
# 本节点不写 messages,则 messages 仅由其他节点更新;tags/score 仍参与 operator.add 合并
return {"tags": ["positive"], "score": 0.5}
def run_demo():
builder = StateGraph(ChatState)
builder.add_node("process", process_user_message)
builder.add_node("sentiment", add_sentiment_tag)
# 两节点都从 START 接入:并行分支,各自跑到 END
builder.add_edge(START, "process")
builder.add_edge(START, "sentiment")
builder.add_edge("process", END)
builder.add_edge("sentiment", END)
graph = builder.compile()
# invoke 只接收一个状态字典;messages 可用 dict 列表,与 Chat API 习惯一致
result = graph.invoke(
{
"messages": [{"role": "user", "content": "Hello, how are you?"}],
"tags": ["greeting"],
"score": 0.0,
}
)
print(result)
if __name__ == "__main__":
run_demo()
Reducer怎么选在真实项目中
| 业务字段类型 | 更适合的 Reducer | 典型例子 |
|---|---|---|
| 只关心最新结果 | 默认覆盖 | latest_answer、current_route、final_summary |
| 聊天消息历史 | add_messages |
messages、Agent 轨迹、人工修正后的消息状态 |
| 候选列表 / 标签列表 / 文档片段列表 | operator.add 或自定义去重版 Reducer |
retrieved_docs、tags、candidate_items |
| 计数 / 分数 / token 成本累计 | operator.add |
retry_count、total_score、token_cost |
| 乘积、最大值、去重合并、按时间戳保留最新值等特殊逻辑 | 自定义 Reducer | 风险分数聚合、按 ID 合并对象列表、保留优先级最高结果 |
几个实践建议:
- 不要把所有字段都默认覆盖,也不要把所有列表都无脑
operator.add。 先问清楚这个字段的业务语义:是“最新值”,还是“历史累积”,还是“需要按 ID 合并”。 - 消息历史优先考虑
add_messages,而不是普通列表相加。 因为聊天消息往往涉及消息对象格式转换、按 ID 更新旧消息、人机介入修正等问题。 - 自定义 Reducer 里一定要认真处理首次合并、空值、重复值、顺序稳定性这些边界。 很多状态 bug 不是节点逻辑错了,而是 Reducer 合并规则没设计清楚。
- State 字段不要无限膨胀。 如果一个字段只是临时中间变量,而且后续节点根本不再用,不一定非要长期留在全局 State 里;否则图运行久了,状态会越来越难读。
还有一个经常被忽略、但在真实项目里很重要的点:State 不只是“一次 invoke() 里的临时变量”。 当后面你开始学习 checkpointer、thread_id、状态历史、故障恢复时,就会发现 LangGraph 的 State 其实还承担着“可持久化工作流上下文”的角色。也就是说:
- 同一个
thread_id下,多次调用可以继续沿用同一份状态上下文; - 图执行中途报错时,只要状态已经被持久化,就有机会从上一次检查点恢复;
get_state()/get_state_history()这类能力,本质上也是围绕 State 快照展开的。
这一层内容在本章先建立概念就够了,完整展开会放到后面的高级特性章节;你现在只需要先记住:State 既是图运行时的共享数据结构,也是后续持久化、短期记忆和故障恢复的基础。

浙公网安备 33010602011771号