LangGraph State 设计:多节点之间怎么安全地共享数据

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • [LangGraph State 设计:多节点之间怎么安全地共享数据](#LangGraph State 设计:多节点之间怎么安全地共享数据)
    • [一、State 是什么](#一、State 是什么)
    • 二、两种定义方式
      • [2.1 TypedDict(推荐)](#2.1 TypedDict(推荐))
      • [2.2 Pydantic BaseModel](#2.2 Pydantic BaseModel)
      • [2.3 内置的 MessagesState](#2.3 内置的 MessagesState)
    • 三、核心机制:Reducer
      • [3.1 自己写 reducer](#3.1 自己写 reducer)
      • [3.2 add_messages 不只是追加](#3.2 add_messages 不只是追加)
    • 四、四个高频坑
      • [坑 1:字段没在 State 里声明](#坑 1:字段没在 State 里声明)
      • [坑 2:忘了用 Annotated,消息被覆盖](#坑 2:忘了用 Annotated,消息被覆盖)
      • [坑 3:节点返回完整 State 而不是增量](#坑 3:节点返回完整 State 而不是增量)
      • [坑 4:并行节点写同一个字段](#坑 4:并行节点写同一个字段)
    • [五、实战:一个带中间产物的 RAG 状态](#五、实战:一个带中间产物的 RAG 状态)
    • [六、读写 State 的几个技巧](#六、读写 State 的几个技巧)
      • [6.1 只读访问用 state"x",别用 .get 掩盖问题](#6.1 只读访问用 state["x"],别用 .get 掩盖问题)
      • [6.2 在节点里更新嵌套结构](#6.2 在节点里更新嵌套结构)
      • [6.3 私有中间数据不要放 State](#6.3 私有中间数据不要放 State)
    • 七、小结

LangGraph State 设计:多节点之间怎么安全地共享数据

写 LangGraph 时,最容易出问题、也最影响后续扩展性的,

不是节点逻辑,而是 State 怎么定义。

State 定义错了,轻则消息被覆盖,重则并发写入丢数据,

而且这类 bug 很难复现。

这篇讲清楚 State 的两种写法、reducer 机制,以及几个高频踩坑点。

一、State 是什么

State 就是在图的节点之间流动的那份共享数据 。

每个节点接收当前 State,返回"要更新的部分",

LangGraph 把更新合并回 State,再传给下一个节点。

python 复制代码
def node_a(state):
    print(state)          # 读:完整的当前状态
    return {"foo": 1}     # 写:只返回要改的字段

返回值是"增量更新",不是"完整新状态"。

这是新手第一个误解------你不需要把没变的字段原样返回。

二、两种定义方式

2.1 TypedDict(推荐)

python 复制代码
from typing import TypedDict

class State(TypedDict):
    question: str
    docs: list[str]
    answer: str

优点:轻量、可读、IDE 友好。日常首选。

2.2 Pydantic BaseModel

python 复制代码
from pydantic import BaseModel

class State(BaseModel):
    question: str
    docs: list[str] = []
    answer: str = ""

优点:有运行时校验 ,类型错了立刻报错。

代价是每次更新都要做一次校验,有轻微性能开销。

什么时候用 Pydantic? 状态结构复杂、字段有约束(范围、枚举)、

或者你需要序列化/反序列化状态时。其余用 TypedDict。

2.3 内置的 MessagesState

对话类 Agent 有现成的:

python 复制代码
from langgraph.graph import MessagesState

# 等价于:
class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

直接用:

python 复制代码
from langgraph.graph import StateGraph, MessagesState

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

graph = StateGraph(MessagesState)
graph.add_node("chatbot", chatbot)

三、核心机制:Reducer

这是 State 设计里最重要的概念。

默认情况下,节点返回 {"foo": 1} 会覆盖 foo 原来的值。

但很多场景我们需要的是追加,比如消息列表。

Reducer 就是用来改变"合并行为"的:

python 复制代码
from typing import Annotated
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list, add_messages]   # 追加,并支持按 id 更新/删除

Annotated[类型, reducer函数] 的格式:

第一个参数是类型,第二个是"怎么合并"。

3.1 自己写 reducer

python 复制代码
from typing import Annotated, TypedDict
import operator

class State(TypedDict):
    # 列表追加
    logs: Annotated[list[str], operator.add]
    # 取最大值
    max_score: Annotated[float, max]
    # 字典浅合并
    meta: Annotated[dict, lambda a, b: {**a, **b}]
    # 覆盖(默认行为,不写就是覆盖)
    current_step: str

operator.add 对列表是拼接,对数字是相加------同一个 reducer 在不同类型上行为不同 ,

用的时候注意。

3.2 add_messages 不只是追加

add_messages 是专门给消息列表用的,它比简单追加聪明:

python 复制代码
from langchain_core.messages import AIMessage

# 已有消息 id 相同 → 更新而不是追加
return {"messages": [AIMessage(content="修正后的内容", id=existing_id)]}

# 删除:用 RemoveMessage
from langgraph.graph.message import RemoveMessage
return {"messages": [RemoveMessage(id=some_id)]}

消息需要带 id 才能被识别为"同一条"。

LangChain 的消息对象默认有 id,自己构造时要注意。

四、四个高频坑

坑 1:字段没在 State 里声明

python 复制代码
class State(TypedDict):
    messages: Annotated[list, add_messages]

def my_node(state):
    return {"temp": 123}      # ❌ State 里没有 temp

会报错或静默丢弃 (取决于版本)。

节点只能返回 State 里已声明的字段。想加字段就先改 State 定义。

坑 2:忘了用 Annotated,消息被覆盖

python 复制代码
# ❌ 错:每次返回都覆盖掉全部历史,模型永远只看到最后一轮
class State(TypedDict):
    messages: list

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

这个错误的表现是"对话没有记忆",而且很隐蔽------

单轮测试完全正常,多轮才暴露。

坑 3:节点返回完整 State 而不是增量

python 复制代码
# ❌ 冗余且危险:把旧消息又追加了一遍
def node(state):
    return {"messages": state["messages"] + [new_msg]}

# ✅ 只返回增量,reducer 负责合并
def node(state):
    return {"messages": [new_msg]}

第一种写法会让消息翻倍,几轮之后上下文爆炸。

坑 4:并行节点写同一个字段

python 复制代码
graph.add_edge("start", "a")
graph.add_edge("start", "b")   # a 和 b 并行
# a 返回 {"count": 1},b 返回 {"count": 2}
# 谁最后完成?不确定 → 结果不稳定

并行分支写同一个非 reducer 字段,结果是不确定的。

要么让它们写不同字段,要么给该字段配一个可交换的 reducer

(如 operator.add、max)。

五、实战:一个带中间产物的 RAG 状态

python 复制代码
from typing import Annotated, TypedDict, Optional
from langchain_core.messages import AnyMessage
from langchain_core.documents import Document
from langgraph.graph.message import add_messages

class RAGState(TypedDict):
    # 对话历史
    messages: Annotated[list[AnyMessage], add_messages]
    # 改写后的独立问题(每轮覆盖)
    standalone_question: str
    # 检索到的文档(每轮覆盖)
    retrieved_docs: list[Document]
    # 重试次数(累加)
    retry_count: Annotated[int, lambda a, b: a + b]
    # 最终答案
    answer: Optional[str]

def rewrite(state: RAGState):
    q = condense_chain.invoke({
        "history": state["messages"][:-1],
        "question": state["messages"][-1].content,
    })
    return {"standalone_question": q}

def retrieve(state: RAGState):
    docs = retriever.invoke(state["standalone_question"])
    return {"retrieved_docs": docs}

def generate(state: RAGState):
    ctx = format_docs(state["retrieved_docs"])
    ans = (prompt | llm | StrOutputParser()).invoke({
        "context": ctx,
        "question": state["standalone_question"],
    })
    return {"answer": ans}

def grade(state: RAGState):
    """判断答案质量,决定要不要重试"""
    if is_good(state["answer"]):
        return "end"
    if state["retry_count"] >= 2:
        return "end"
    return "retry"

def bump_retry(state: RAGState):
    return {"retry_count": 1}

设计原则:

  • 每轮会被重新计算的字段(问题、检索结果)→ 覆盖(不配 reducer);
  • 需要累积的字段(重试次数、日志)→ 配累加 reducer;
  • 消息历史 → 一律用 add_messages。

六、读写 State 的几个技巧

6.1 只读访问用 state"x",别用 .get 掩盖问题

python 复制代码
# 如果字段必须存在,直接取值让它报错,早发现早修
q = state["standalone_question"]

# 只有真正可选的字段才用 get
ans = state.get("answer")

6.2 在节点里更新嵌套结构

python 复制代码
def add_tag(state: RAGState):
    # ❌ 直接改侵入式修改,LangGraph 可能感知不到
    state["meta"]["tags"].append("new")

    # ✅ 返回新对象
    new_meta = {**state["meta"], "tags": state["meta"]["tags"] + ["new"]}
    return {"meta": new_meta}

不要就地修改(mutate)State。 返回新对象,让 LangGraph 处理合并,

这样 checkpoint 才能正确记录变更。

6.3 私有中间数据不要放 State

State 会被 checkpoint 持久化。

数据库连接、临时大对象这类不该落盘的东西,用闭包或全局变量持有:

python 复制代码
db = get_connection()   # 模块级,不进 State

def query_node(state: RAGState):
    rows = db.execute(...)
    return {"retrieved_docs": rows}

七、小结

  • State 是节点间流动的共享数据,节点返回的是增量更新,不是完整状态;
  • 定义方式:TypedDict(推荐)/ Pydantic(要校验)/ 内置 MessagesState(对话场景);
  • Reducer 是核心 :Annotated[类型, 合并函数] 决定"覆盖还是追加";
    add_messages 支持按 id 更新和删除;
  • 四个坑:字段没声明、忘了 Annotated 导致消息被覆盖、
    返回完整 State 导致消息翻倍、并行节点写同一字段结果不确定
    ;
  • 设计原则:每轮重算的字段覆盖,累积字段配 reducer,消息一律 add_messages;
  • 不要就地修改 State,返回新对象;不落盘的资源别放 State。

下一篇讲 Checkpointer------怎么让图的状态持久化、支持断点续跑。

相关推荐
for_ever_love__2 小时前
LangGraph Checkpointer:持久化、断点续跑与多轮记忆
服务器·数据库·学习·持久化·langgraph·checkpointer·断点续跑
大圣编蚕2 小时前
Redis 为什么快?现在还是单线程吗?
数据库·redis·缓存
Linux实战笔记2 小时前
记一次Redis分布式锁实现线上排查:Redlock算法与 Lua 脚本
redis·高并发·分布式锁·redlock 算法·lua 脚本
轮子大叔3 小时前
Redis深入浅出
redis·缓存
codeejun3 小时前
每日一Go·MySQL-7、MySQL 核心原理全景图谱
开发语言·mysql·golang
小马不起床4 小时前
零基础玩转 FastAPI_12:前端详情增强与角色权限分配
前端·状态模式·fastapi
minebmw74 小时前
第 04 讲 深入浅出索引(上):B+树索引的数据结构与查询过程
mysql
bksczm4 小时前
MySQL进阶篇之备份(下半篇)
数据库·mysql
计算机就业还在扩大4 小时前
Redis中的setnx是原子性的?
数据库·redis·缓存