- LangGraph 入门基础全解析
- LangGraph中的Reducer是什么
- LangGraph 核心概念详解:从编译到可视化
- LangGraph实战教程:一文搞懂图的状态(State)管理
- LangGraph实战教程:状态管理与
graph.invoke入参深度解析
在构建复杂的LangGraph应用时,单一的状态Schema往往难以满足需求。你可能希望:
- 限制外部调用方只能传入特定字段
- 隐藏内部实现细节,只返回对外有用的数据
- 在部分节点之间传递临时数据,而不污染全局状态
LangGraph的 Multi Schema 机制正是为了解决这些问题而设计的。它支持在一个图中使用多个状态Schema,分别管理不同类型的数据。
一、四类状态概述
LangGraph支持以下四类状态:
| 状态类型 | 英文名称 | 用途 | 创建方式 |
|---|---|---|---|
| 全局状态 | Global State | 图内部主要使用的状态 | StateGraph(state_schema=...) |
| 输入状态 | Input State | 约束图的外部输入 | StateGraph(input_schema=...) |
| 输出状态 | Output State | 约束图的最终输出 | StateGraph(output_schema=...) |
| 私有状态 | Private State | 节点间传递的临时数据 | 节点入参类型注解声明 |
1.1 全局状态(Global State)
全局状态是创建 StateGraph时通过 state_schema参数传递的状态Schema。它包含图运行过程中需要读写的大部分字段,是所有节点默认可以访问的状态。
python
class OverAllState(TypedDict):
username: str
nickname: str
graph_output: str
1.2 输入状态(Input State)
输入状态通过 input_schema参数定义,用于约束调用图时允许传入哪些字段。它相当于图的对外接口,外部调用方只能传入输入状态中定义的字段。
kotlin
class InputState(TypedDict):
username: str
1.3 输出状态(Output State)
输出状态通过 output_schema参数定义,用于约束图最终返回哪些字段。它相当于图的对外返回值声明,即使图内部有很多字段,最终也只返回输出状态中定义的字段。
kotlin
class OutputState(TypedDict):
graph_output: str
1.4 私有状态(Private State)
私有状态不通过 StateGraph构造函数声明,而是通过节点函数的入参类型注解来定义。它用于图内部某些节点之间传递临时数据,不作为图的输入,也不作为图的最终输出。
kotlin
class PrivateState(TypedDict):
greeting: str
二、状态之间的关系与设计规范
2.1 设计规范
规范一:输入状态和输出状态通常是全局状态的子集
输入状态描述图对外需要接收的数据,输出状态描述图最终需要返回的数据。通常情况下,它们都应该是全局状态的一部分。
python
class InputState(TypedDict):
username: str
class OutputState(TypedDict):
graph_output: str
class OverAllState(TypedDict):
username: str
nickname: str
graph_output: str
规范二:私有状态和全局状态应尽量避免字段重名
私有状态的定位是图内部某些节点之间传递的临时字段。如果私有状态字段和全局状态字段重名,容易让人误以为该字段是全局共享字段,造成理解混乱。
规范三:节点函数应明确声明入参状态类型和返回状态类型
perl
def node_1(state: InputState) -> OverAllState:
return {"nickname": "Dear " + state["username"]}
规范四:节点函数中不应该访问入参状态类型中不存在的字段
节点实际接收到的状态会按照其入参类型进行裁剪。如果访问了不存在的字段,可能抛出 KeyError。
kotlin
def node_1(state: InputState) -> OverAllState:
# ✅ 正确:InputState中有username字段
return {"nickname": state["username"]}
# ❌ 错误:InputState中没有nickname字段
# return {"nickname": state["nickname"]}
规范五:节点返回的字典应尽量和返回类型注解保持一致
虽然从LangGraph的运行机制看,节点返回的是对状态的部分更新,只要字段已被图记录即可生效,但从工程规范上讲,保持返回字段与返回类型注解一致更利于阅读和维护。
2.2 底层机制
状态的记录
LangGraph的状态在底层会被拆分成多个可读写的状态字段,每个字段通常对应一个 Channel。
- 全局状态、输入状态、输出状态 :在创建
StateGraph时被记录 - 私有状态 :在调用
add_node()添加节点时,根据节点入参类型注解被记录
ini
builder = StateGraph(
OverAllState,
input_schema=InputState,
output_schema=OutputState
)
状态的访问
- 外部输入 :按照
input_schema进行裁剪,只保留输入状态中定义的字段 - 节点接收:按照节点入参类型进行裁剪,节点只能读取其入参类型中定义的字段
- 节点返回:节点返回的是部分更新,不是完整状态
- 最终输出 :按照
output_schema进行裁剪,只返回输出状态中定义的字段
三、完整案例
下面通过一个案例演示四类状态的定义和使用。
py
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. 定义四类状态
class InputState(TypedDict):
username: str
class OutputState(TypedDict):
graph_output: str
class OverAllState(TypedDict):
nickname: str
username: str
graph_output: str
class PrivateState(TypedDict):
greeting: str
# 2. 定义节点函数
def node_1(state: InputState) -> OverAllState:
"""读取输入状态,写入全局状态"""
return {"nickname": "Dear " + state["username"]}
def node_2(state: OverAllState) -> PrivateState:
"""读取全局状态,写入私有状态"""
return {"greeting": state["nickname"] + ", 早上好~"}
def node_3(state: PrivateState) -> OutputState:
"""读取私有状态,写入输出状态"""
return {"graph_output": state["greeting"] + " 很高兴认识你!"}
# 3. 构建图
builder = StateGraph(
OverAllState,
input_schema=InputState,
output_schema=OutputState
)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", "node_3")
builder.add_edge("node_3", END)
graph = builder.compile()
# 4. 调用图
result = graph.invoke({"username": "小黄"})
print(result)
输出:
arduino
{'graph_output': 'Dear 小黄, 早上好~ 很高兴认识你!'}
四、案例执行过程分析
第1步:输入状态约束
调用 graph.invoke({"username": "小黄"})时,输入按照 InputState裁剪,只保留 username字段。
第2步:node_1 执行
node_1的入参类型是 InputState,它只能读取 username字段。它向全局状态写入 nickname。
此时图中的状态:
vbnet
username: "小黄"
nickname: "Dear 小黄"
第3步:node_2 执行
node_2的入参类型是 OverAllState,它可以读取所有全局字段。它读取 nickname,并向私有状态写入 greeting。
此时图中的状态新增:
vbnet
greeting: "Dear 小黄, 早上好~"
第4步:node_3 执行
node_3的入参类型是 PrivateState,它只能读取 greeting字段。它读取 greeting,并向输出状态写入 graph_output。
此时图中的状态新增:
vbnet
graph_output: "Dear 小黄, 早上好~ 很高兴认识你!"
第5步:输出状态裁剪
最终输出按照 OutputState裁剪,只保留 graph_output字段。username、nickname、greeting等内部字段全部被过滤掉。
五、总结
| 概念 | 要点 |
|---|---|
| 全局状态 | 图内部主要使用的状态,通过 state_schema定义 |
| 输入状态 | 约束外部输入,通过 input_schema定义,通常是全局状态的子集 |
| 输出状态 | 约束最终输出,通过 output_schema定义,通常是全局状态的子集 |
| 私有状态 | 节点间传递的临时数据,通过节点入参类型注解声明 |
| 状态裁剪 | 输入按 input_schema裁剪,节点按入参类型裁剪,输出按 output_schema裁剪 |
| 节点返回值 | 部分更新,只需返回要修改的字段 |
一句话总结:全局状态是图的"主干道",输入状态和输出状态是图的"大门",私有状态是节点间的"内部通道"。合理使用四类状态,可以让你的LangGraph应用更加健壮、安全且易于维护。
六、相关面试题
面试题1:为什么要使用Multi Schema?单一Schema不能满足需求吗?
参考答案:
单一Schema在简单场景下够用,但在复杂应用中存在以下问题:
- 安全性:使用单一Schema时,外部调用方可以看到并传入所有字段,可能导致意外覆盖内部数据。输入Schema可以限制外部只能传入特定字段。
- 封装性:使用单一Schema时,最终输出会暴露所有内部字段。输出Schema可以隐藏内部实现细节,只返回对外有用的数据。
- 清晰性:私有状态可以明确表达"这个字段只在某些节点之间传递,不是全局共享的",提高代码可读性。
- 可维护性:随着图规模增长,单一Schema会变得越来越臃肿。拆分后每个Schema职责清晰,易于维护。
面试题2:私有状态(PrivateState)和全局状态(OverAllState)有什么区别?什么时候用私有状态?
参考答案:
| 维度 | 全局状态 | 私有状态 |
|---|---|---|
| 生命周期 | 整个图运行期间都存在 | 只在相关节点之间传递 |
| 可见范围 | 所有节点都可读写 | 只有声明了该类型的节点可读写 |
| 输入约束 | 可作为图的外部输入 | 不能作为外部输入 |
| 输出约束 | 可作为图的最终输出 | 不能作为最终输出 |
| 典型用途 | 共享的业务数据 | 临时的中间计算结果 |
使用私有状态的场景:
- 两个或多个节点之间需要传递临时数据,但这些数据不需要被其他节点访问
- 中间计算结果,后续节点用完即弃
- 不希望暴露给图外部调用方的内部数据
面试题3:如果节点返回的字段既不在全局状态中,也不在任何私有状态中,会发生什么?
参考答案:
该字段会被静默忽略。LangGraph只会将返回值中那些已被图记录的字段作为有效的状态更新进行处理。未被记录的字段会被丢弃,不会引发错误。
这可能导致难以调试的问题------开发者可能认为某个更新已经生效,但实际上它被忽略了。建议在开发阶段仔细检查节点返回的字段名是否与已注册的状态字段一致。
面试题4:输入状态和输出状态必须是全局状态的子集吗?如果不是会怎样?
参考答案:
这是官方推荐的最佳实践,但不是强制要求。从技术角度看:
- 输入状态包含全局状态中没有的字段:该字段会被记录到图中,但其他节点如果不通过合适的入参类型声明,可能无法访问它。这会导致字段定义分散,增加理解难度。
- 输出状态包含全局状态中没有的字段:如果没有任何节点写入该字段,最终输出中该字段的值会是默认值(如果有)或缺失。
结论:虽然技术上可行,但不推荐这样做。保持输入状态和输出状态作为全局状态的子集,可以使状态关系更清晰、更易维护。
面试题5:节点函数的入参类型决定了它能读取哪些字段,那它写入的字段范围呢?
参考答案:
节点写入的字段范围不受入参类型限制,而是由返回值中的字段名 和图中已记录的状态字段共同决定。
具体规则:
- 节点返回值是一个字典,其中的key就是要更新的字段名
- LangGraph会检查这些字段名是否已被图记录
- 如果字段已被记录,则按照该字段的Reducer规则进行合并
- 如果字段未被记录,则静默忽略
例如,即使节点的入参类型是 InputState(只有 username字段),它仍然可以返回 {"nickname": "..."}来更新全局状态中的 nickname字段,只要 nickname已被图记录。
面试题6:如果两个节点都声明了相同的私有状态类型,它们能共享这个私有状态的数据吗?
参考答案:
可以。私有状态一旦被某个节点通过入参类型注解声明并被图记录后,它就成为了图中一个可读写的状态字段。后续任何声明了相同私有状态类型的节点,都可以读写这些字段。
例如:
python
class SharedPrivateState(TypedDict):
temp_data: str
def node_a(state: SomeState) -> SharedPrivateState:
return {"temp_data": "来自node_a的数据"}
def node_b(state: SharedPrivateState) -> SomeState:
print(state["temp_data"]) # 可以读取到"来自node_a的数据"
return {...}
这使得私有状态成为一种灵活的"半全局"状态------它不会被外部看到,也不会出现在最终输出中,但可以在声明了该类型的节点之间自由传递。
面试题7:如何调试Multi Schema下的状态传递问题?
参考答案:
以下是一些实用的调试技巧:
- 打印中间状态:在每个节点中添加打印语句,查看实际接收到的状态
python
def node(state: SomeState) -> SomeState:
print(f"[node] received: {state}")
result = do_something(state)
print(f"[node] returning: {result}")
return result
- 逐步简化:先去掉私有状态和输出状态,用单一Schema验证逻辑正确性,再逐步引入Multi Schema
- 检查字段命名:确保所有字段名拼写正确,避免因拼写错误导致更新被忽略
- 使用类型检查工具:利用mypy或Pyright等静态类型检查工具,可以在编码阶段发现类型不匹配的问题
希望这篇教程能帮助你掌握LangGraph的Multi Schema机制,让你的图应用更加健壮和安全!