LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 2 · State & Memory
1 2-1 State Schema
2 2-2 Reducers
3 2-3 Multi Schemas
4 2-4 Trim & Filter
5 2-5 Chatbot Summary
6 2-6 Ext Memory
SUM Module Summary
HomeLangGraphModule 2 · State & Memory2-3 Multi Schemas
📓 Notebook 📖 Explained
LANGGRAPH MODULE 2 · LESSON 3

多重 Schema 设计
私有状态 · 输入过滤 · 输出过滤

当一张图的所有节点都共用同一个 Schema 时,信息边界模糊、接口臃肿。
LangGraph 的多重 Schema 机制让你精确控制哪些数据对外可见、哪些只在内部流转。

1 为什么需要多重 Schema?——单 Schema 的局限

在前面的课程中,我们学到了 LangGraph 的基础:所有节点共享同一个 State(OverallState),图的输入和输出也都是这同一个 State。这在简单场景下工作良好,但随着图的复杂度增加,会暴露出几个明显的问题。

问题一:中间计算数据污染了公开接口

想象一个问答系统:用户只需要提供一个"问题",得到一个"答案"。但图内部在计算过程中,会产生很多临时数据——搜索的关键词、中间推理的草稿、评分数值等。如果使用单 Schema,这些数据全部暴露给调用者:

调用者传入
question: "世界最高山峰?"
单 Schema 返回(信息过多)
question: "世界最高山峰?"
notes: "... 分析过程草稿 ..."
search_keywords: ["喜马拉雅", "珠峰"]
answer: "珠穆朗玛峰,8849 米"

调用者只想要 answer,却拿到了所有内部字段,接口臃肿、容易误用。

问题二:节点间的"临时传递"数据不应对外暴露

有时候,node_1 需要传递一个中间值给 node_2,但这个中间值与图的最终输入/输出毫无关系。如果你把它放进 OverallState,就不得不在整个图的"公共接口"里暴露这个实现细节。

LangGraph 的解决方案

LangGraph 提供了两种机制来解决这两个问题:私有状态(Private State)用于节点间传递内部数据,Input/Output Schema用于在图的边界处做数据过滤。这就是本节课的核心内容。

多重 Schema 的全局视角

InputState(输入 Schema)
question: str
调用者传入的字段
图会拒绝其他字段
OverallState(内部 Schema)
question: str
answer: str
notes: str
节点间传递的完整数据
包含所有中间状态
OutputState(输出 Schema)
answer: str
invoke() 返回的字段
只暴露必要信息

注意:私有状态(PrivateState)是另一个维度的概念,它存在于节点与节点之间的"通道"上,甚至不出现在 OverallState 里。我们接下来逐一讲解。

2 私有状态(Private State)——节点间的"内部便签"

核心概念

私有状态(Private State)是指:某些数据只在特定的节点之间流通,既不是图的输入,也不是图的输出,更不属于全局 OverallState。它是节点间"私聊"的便签条,其他节点和外部调用者都看不到。

实现机制

LangGraph 允许节点函数在其类型注解(Type Hint)中声明自己使用的 Schema。节点可以:用 OverallState 作为输入类型,但返回 PrivateState;也可以接受 PrivateState 作为输入,返回 OverallState。LangGraph 的路由引擎会自动匹配这些类型,确保数据正确流转。

图示:私有状态的数据流

私有状态如何在节点间流动

输入: { foo: 1 }
OverallState
node_1
接受 OverallState,返回 PrivateState
私有传递: { baz: 2 }
PrivateState(外部不可见)
node_2
接受 PrivateState,返回 OverallState
输出: { foo: 3 }
OverallState(baz 不出现)

私有状态 vs 全局状态的对比

维度 OverallState(全局状态) PrivateState(私有状态)
可见范围 图的所有节点 + 外部调用者 只有声明了该 Schema 的特定节点
生命周期 从 START 到 END 全程存在 只在特定节点之间传递时存在
出现在输出中 否(不在 OverallState 里)
定义方式 作为 StateGraph 的主 Schema 单独定义一个 TypedDict,由节点类型注解引用
典型用途 问题、答案、对话历史等核心数据 中间计算值、临时缓存、节点间协议数据

3 私有状态完整代码解析

下面是 notebook 中私有状态的完整示例代码。我们逐段拆解分析:

第一步:定义两个 TypedDict Schema

from typing_extensions import TypedDict
from IPython.display import Image, display
from langgraph.graph import StateGraph, START, END

# ① 全局状态:图的"公开面板",输入输出都通过它
class OverallState(TypedDict):
    foo: int

# ② 私有状态:只在 node_1 和 node_2 之间传递
class PrivateState(TypedDict):
    baz: int
OverallState 只有一个字段 foo。这是整张图对外暴露的接口:调用者传入 {"foo": 1},图执行完毕返回 {"foo": 3}baz 字段根本不存在于 OverallState,所以永远不会出现在最终结果里。
PrivateState 只有一个字段 baz。它是 node_1 产出、node_2 消费的"临时中间值"。你可以把它理解成:node_1 在纸条上写了 baz=2 塞给 node_2,node_2 用完就扔掉,不归档到任何地方。

第二步:定义节点函数(关键:类型注解)

# node_1:接受 OverallState,返回 PrivateState
def node_1(state: OverallState) -> PrivateState:
    print("---Node 1---")
    # 从 OverallState 读取 foo,加 1 后写入 PrivateState 的 baz
    return {"baz": state['foo'] + 1}

# node_2:接受 PrivateState,返回 OverallState
def node_2(state: PrivateState) -> OverallState:
    print("---Node 2---")
    # 从 PrivateState 读取 baz,加 1 后写入 OverallState 的 foo
    return {"foo": state['baz'] + 1}
类型注解是关键node_1(state: OverallState) -> PrivateState 这个签名告诉 LangGraph:在执行 node_1 时,把当前 OverallState 传给它;node_1 返回的字典会被解读为 PrivateState,而非直接合并到 OverallState。LangGraph 的内部路由引擎根据这个类型注解来决定数据流向。

第三步:构建并运行图

# 构建图——StateGraph 的主 Schema 是 OverallState
builder = StateGraph(OverallState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)

builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)

graph = builder.compile()

# 运行:传入 OverallState,foo=1
result = graph.invoke({"foo": 1})
# 输出:
# ---Node 1---
# ---Node 2---
# {'foo': 3}   ← 只有 foo,baz 从未出现

完整的数据流追踪

foo: 1
invoke() 传入的初始 OverallState
baz: 2
(foo + 1 = 2)
node_1 返回的 PrivateState
baz: 2
node_2 接收到的 PrivateState
foo: 3
(baz + 1 = 3)
node_2 返回的 OverallState(最终输出)
注意事项

baz 字段从未出现在最终的 invoke() 返回值中。因为它只属于 PrivateState,而 StateGraph 最终返回的是 OverallState。这正是私有状态设计的目的:节点间的临时数据不污染公开接口。

4 单 Schema 的输入输出问题

在引入 Input/Output Schema 之前,我们先用 notebook 里的例子,直观感受单 Schema 方式的局限。

场景:一个简单的问答图

class OverallState(TypedDict):
    question: str   # 用户的问题
    answer: str     # 最终答案
    notes: str      # 内部推理笔记(中间数据)

def thinking_node(state: OverallState):
    # 思考节点:产出中间笔记和初步答案
    return {"answer": "bye", "notes": "... his name is Lance"}

def answer_node(state: OverallState):
    # 答案节点:产出最终答案
    return {"answer": "bye Lance"}

graph = StateGraph(OverallState)
graph.add_node("answer_node", answer_node)
graph.add_node("thinking_node", thinking_node)
graph.add_edge(START, "thinking_node")
graph.add_edge("thinking_node", "answer_node")
graph.add_edge("answer_node", END)

graph = graph.compile()

问题暴露:invoke() 返回了所有字段

graph.invoke({"question": "hi"})

# 实际输出(包含用户不需要的字段):
# {
#   'question': 'hi',        ← 用户自己知道,不需要再返回
#   'answer': 'bye Lance',   ← 用户真正需要的
#   'notes': '... his name is Lance'  ← 内部中间数据,泄露给外部!
# }
单 Schema 的两个痛点

痛点一(输入侧):调用者无法只传 question——如果不传 answernotes,它们在 State 中就是 None,节点函数可能因此出错或需要额外处理。

痛点二(输出侧)notes 是内部实现细节,调用者看到它会感到困惑,甚至可能依赖这个本不该暴露的字段,造成接口耦合。

这两个痛点在简单示例里看似无关紧要,但在真实生产场景中——图有数十个字段、外部系统依赖其输出——就会成为严重的设计问题。Input/Output Schema 就是针对这两个痛点的解决方案。

5 Input / Output Schema——精确的接口过滤

核心机制

StateGraph 支持三个构造参数来定义多重 Schema:

graph = StateGraph(
    OverallState,            # 第一个参数:内部全局状态(节点间通信用)
    input_schema=InputState,  # 关键字参数:定义 invoke() 接受的输入结构
    output_schema=OutputState # 关键字参数:定义 invoke() 返回的输出结构
)

这两个参数的作用:

理解"过滤"二字

Input/Output Schema 不改变图内部的运行逻辑——节点之间仍然用完整的 OverallState 通信。Schema 仅仅是在图的边界处(进入图时 / 离开图时)做数据的裁剪。就像一个 API 的请求/响应序列化层,并不影响后端业务逻辑。

三个 Schema 的层次关系

Schema 的层次与数据流向

InputState
question: str
调用者传入
input_schema 验证(过滤多余字段)
OverallState(图内部)
question
answer
notes(内部专用,不对外)
thinking_node ↔ answer_node 之间完整流转
output_schema 过滤(裁剪只留 answer)
OutputState
answer: str
调用者收到

节点函数的类型注解(可选但推荐)

当图使用多重 Schema 时,节点函数的类型注解可以明确声明它使用的是哪个 Schema,增加代码可读性:

# thinking_node:只需要读 question(InputState 的字段)
# 使用 InputState 类型注解,表明它只关心输入字段
def thinking_node(state: InputState):
    return {"answer": "bye", "notes": "... his name is Lance"}

# answer_node:需要读取所有字段,最终输出 OutputState
# -> OutputState 类型注解表明它的返回值会被过滤为 OutputState
def answer_node(state: OverallState) -> OutputState:
    return {"answer": "bye Lance"}
类型注解是"文档",过滤由 StateGraph 参数控制

节点函数上的类型注解(-> OutputState)主要起到文档说明和 IDE 提示的作用。真正决定 invoke() 最终返回哪些字段的,是 StateGraph(output_schema=OutputState) 这个参数。两者结合使用,代码意图最清晰。

6 Input / Output Schema 完整代码解析

下面是 notebook 中 Input/Output Schema 的完整实现代码,我们逐行深入解析:

第一步:定义三层 Schema

# ① 输入 Schema:定义调用者可以/必须传入的字段
class InputState(TypedDict):
    question: str

# ② 输出 Schema:定义 invoke() 返回给调用者的字段
class OutputState(TypedDict):
    answer: str

# ③ 内部全局状态:图内部节点间通信的完整数据结构
class OverallState(TypedDict):
    question: str   # 来自 InputState
    answer: str     # 最终写入 OutputState
    notes: str      # 纯内部字段,不在 Input/Output Schema 中
InputState 只有 question。当调用 graph.invoke({"question": "hi"}) 时,LangGraph 会验证传入的字典符合 InputState 的结构。你不需要传 answernotes,它们会以默认值(None 或不存在)初始化在 OverallState 中。
OutputState 只有 answer。图执行完毕时,LangGraph 从最终的 OverallState 中只提取 answer 字段返回。questionnotes 即便存在于 OverallState,也不会出现在 invoke() 的返回值里。
OverallState 是图的"内部语言",包含所有字段。节点函数之间通过这个完整的 Schema 通信,不受 Input/Output 约束的限制。

第二步:定义节点函数

def thinking_node(state: InputState):
    # state 只包含 question,因为此节点声明了 InputState 类型
    # 但返回值会被合并到 OverallState 中
    return {"answer": "bye", "notes": "... his is name is Lance"}

def answer_node(state: OverallState) -> OutputState:
    # state 包含完整的 OverallState(question + answer + notes)
    # -> OutputState 声明返回值语义上对应 OutputState
    return {"answer": "bye Lance"}

第三步:构建图,传入 input/output Schema 参数

# 关键!StateGraph 第一个参数是内部 Schema
# input_schema 和 output_schema 是过滤器参数
graph = StateGraph(
    OverallState,
    input_schema=InputState,
    output_schema=OutputState
)

graph.add_node("answer_node", answer_node)
graph.add_node("thinking_node", thinking_node)
graph.add_edge(START, "thinking_node")
graph.add_edge("thinking_node", "answer_node")
graph.add_edge("answer_node", END)

graph = graph.compile()

第四步:运行验证效果

# 调用者只需传 InputState 声明的字段
result = graph.invoke({"question": "hi"})

# 实际输出(经过 OutputState 过滤):
# {'answer': 'bye Lance'}
#
# 注意:question 和 notes 都不见了!
# 图内部确实有这两个字段,只是输出时被过滤掉了。
执行效果验证

输出结果从单 Schema 时的 {'question': 'hi', 'answer': 'bye Lance', 'notes': '... his name is Lance'} 变为了简洁的 {'answer': 'bye Lance'}。这就是 output_schema 过滤的效果。

完整数据流追踪

阶段可见数据说明
invoke() 传入 {"question": "hi"} 由 InputState 约束,只需传 question
thinking_node 执行后 OverallState: {question, answer:"bye", notes:"..."} 节点内部可以读写所有字段
answer_node 执行后 OverallState: {question, answer:"bye Lance", notes:"..."} answer 被更新,其他字段不变
invoke() 返回 {"answer": "bye Lance"} 由 OutputState 过滤,只返回 answer

7 两种机制对比与综合应用

私有状态 vs Input/Output Schema:核心区别

对比维度 私有状态(PrivateState) Input/Output Schema
解决的问题 节点间的临时中间数据,不想进入 OverallState 图的外部接口臃肿,输入/输出字段过多
作用范围 图的内部(节点之间) 图的边界(调用者 ↔ 图)
配置方式 在节点函数的类型注解中声明 在 StateGraph() 构造函数参数中声明
对 OverallState 的影响 私有字段不属于 OverallState OverallState 不变,只是过滤输入输出
典型使用场景 中间计算结果、节点间的协议数据 API 接口设计、隐藏内部实现细节

两种机制可以同时使用

在一个复杂的图中,你完全可以同时使用私有状态和 Input/Output Schema:

# 同时使用两种机制的设计示例
class InputState(TypedDict):
    user_query: str                   # 用户输入

class OutputState(TypedDict):
    final_answer: str                 # 最终答案

class OverallState(TypedDict):
    user_query: str                   # 来自 InputState
    search_results: list              # 搜索结果
    draft_answer: str                 # 草稿答案
    final_answer: str                 # 写入 OutputState

class PrivateState(TypedDict):
    relevance_score: float            # 私有:节点间传递评分
    refined_query: str               # 私有:精炼后的查询词

# 节点 A:接受 InputState,内部处理后传递 PrivateState
def query_refiner(state: InputState) -> PrivateState:
    refined = state["user_query"].strip().lower()
    return {"refined_query": refined, "relevance_score": 0.0}

# 节点 B:接受 PrivateState,写回 OverallState
def searcher(state: PrivateState) -> OverallState:
    results = do_search(state["refined_query"])
    return {"search_results": results, "draft_answer": ""}

# 节点 C:接受完整 OverallState,产出 final_answer
def synthesizer(state: OverallState) -> OutputState:
    answer = synthesize(state["search_results"], state["user_query"])
    return {"final_answer": answer}

# 构建图,同时配置 Input/Output Schema
graph = StateGraph(
    OverallState,
    input_schema=InputState,
    output_schema=OutputState
)
# ... add_node, add_edge, compile ...
设计思路总结

把图想象成一个封装良好的模块
· input_schema 定义"模块的参数"——调用方需要提供什么
· output_schema 定义"模块的返回值"——调用方能得到什么
· OverallState 是"模块内部状态"——实现细节,外部不感知
· PrivateState 是"局部变量"——只在特定函数间传递,连内部状态都不写入

8 真实场景:什么时候用多重 Schema?

适合使用私有状态的场景

场景一:流水线节点间传递评分或令牌

搜索图中,检索节点产出"文档列表 + 相关性评分",排序节点消费后输出"已排序的文档列表"。相关性评分是中间计算值,对最终用户毫无意义。用私有状态承载评分,避免污染 OverallState。

class ScoredDocsState(TypedDict):
    # 私有:带评分的文档,只在检索和排序节点之间流通
    scored_docs: list[tuple[str, float]]

场景二:LLM 推理过程与最终答案分离

思维链(Chain-of-Thought)场景:LLM 内部推理节点产出详细的推理步骤,而最终答案节点只需要简洁的结论。推理步骤字符串可以作为私有状态,不进入 OverallState。

class ReasoningState(TypedDict):
    # 私有:详细推理过程,最终用户不需要看到
    chain_of_thought: str
    confidence: float

适合使用 Input/Output Schema 的场景

场景三:对外提供干净的 API 接口

将 LangGraph 图作为后端服务暴露给前端或其他微服务时,接口字段应当最小化。前端只关心"问题"和"答案",不需要知道图内部经历了几轮搜索、产生了哪些中间笔记。

# 前端友好的接口
class ChatInput(TypedDict):
    message: str     # 用户发送的消息
    session_id: str  # 会话 ID

class ChatOutput(TypedDict):
    reply: str        # AI 回复
    tokens_used: int  # 消耗的 token 数(计费用)

场景四:LangGraph Studio 和 LangGraph Cloud 部署

当图部署到 LangGraph Cloud 时,input_schemaoutput_schema 会自动生成 API 文档和请求验证。这是生产部署的最佳实践——明确的 Schema 让 API 自文档化,减少调用方误用的可能。

场景五:子图(Subgraph)的接口隔离

在多 Agent 系统或并行图中,子图可能有自己的 OverallState,与父图不同。通过定义明确的 input/output Schema,子图对父图暴露清晰的接口,两者的内部状态互不干扰

决策建议

核心设计原则

多重 Schema 的本质是信息封装:把"对谁有用的数据"和"对谁无用的数据"明确分开。这与面向对象的封装思想、API 的最小化暴露原则是一脉相承的。LangGraph 只是提供了在图层面实现这种封装的语言工具。

本节课知识地图

私有状态 (Private State)
  • · 在节点函数类型注解中声明
  • · 不进入 OverallState
  • · 不出现在图的输出中
  • · 节点间"私聊"用的便签
Input / Output Schema
  • · 在 StateGraph() 参数中声明
  • · 过滤 invoke() 的输入输出
  • · 不影响图内部运行逻辑
  • · 图对外的"合同接口"
关键 API 速查
StateGraph(OverallState, input_schema=X, output_schema=Y)
声明图的三层 Schema
def node(state: PrivateState) -> OverallState:
节点的私有状态类型注解
graph.invoke({"question": "hi"})
只需传 InputState 字段
前置知识 TypedDict 定义 State Schema、StateGraph 基本用法
本节掌握 Private State、input_schema、output_schema 三种多重 Schema 技术
后续应用 子图设计、LangGraph Cloud 部署、多 Agent 接口隔离