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_schemainput_schemaoutput_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_messagesoperator.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_answercurrent_routefinal_summary
聊天消息历史 add_messages messages、Agent 轨迹、人工修正后的消息状态
候选列表 / 标签列表 / 文档片段列表 operator.add 或自定义去重版 Reducer retrieved_docstagscandidate_items
计数 / 分数 / token 成本累计 operator.add retry_counttotal_scoretoken_cost
乘积、最大值、去重合并、按时间戳保留最新值等特殊逻辑 自定义 Reducer 风险分数聚合、按 ID 合并对象列表、保留优先级最高结果

几个实践建议:

  • 不要把所有字段都默认覆盖,也不要把所有列表都无脑 operator.add 先问清楚这个字段的业务语义:是“最新值”,还是“历史累积”,还是“需要按 ID 合并”。
  • 消息历史优先考虑 add_messages,而不是普通列表相加。 因为聊天消息往往涉及消息对象格式转换、按 ID 更新旧消息、人机介入修正等问题。
  • 自定义 Reducer 里一定要认真处理首次合并、空值、重复值、顺序稳定性这些边界。 很多状态 bug 不是节点逻辑错了,而是 Reducer 合并规则没设计清楚。
  • State 字段不要无限膨胀。 如果一个字段只是临时中间变量,而且后续节点根本不再用,不一定非要长期留在全局 State 里;否则图运行久了,状态会越来越难读。

还有一个经常被忽略、但在真实项目里很重要的点:State 不只是“一次 invoke() 里的临时变量”。 当后面你开始学习 checkpointerthread_id、状态历史、故障恢复时,就会发现 LangGraph 的 State 其实还承担着“可持久化工作流上下文”的角色。也就是说:

  • 同一个 thread_id 下,多次调用可以继续沿用同一份状态上下文;
  • 图执行中途报错时,只要状态已经被持久化,就有机会从上一次检查点恢复;
  • get_state() / get_state_history() 这类能力,本质上也是围绕 State 快照展开的。

这一层内容在本章先建立概念就够了,完整展开会放到后面的高级特性章节;你现在只需要先记住:State 既是图运行时的共享数据结构,也是后续持久化、短期记忆和故障恢复的基础。

posted @ 2026-05-13 15:00  幻影之舞  阅读(51)  评论(0)    收藏  举报