When all nodes in a graph share the same Schema, the information boundaries are blurred and the interface is bloated.
LangGraph's multiple Schema mechanism allows you to precisely control which data is visible to the outside world and which data is only circulated internally.
In the previous lessons, we learned the basics of LangGraph:All nodes share the same State (OverallState), the input and output of the graph are also the same State. This works well in simple scenarios, but as the complexity of the graph increases, several obvious problems are exposed.
Imagine a question and answer system: the user only needs to provide a "question" and get an "answer". However, during the calculation process inside the graph, a lot of temporary data will be generated - search keywords, drafts of intermediate reasoning, scoring values, etc. If using a single Schema, all of this data is exposed to the caller:
The caller only wantsanswer, but got all the internal fields, the interface is bloated and easy to misuse.
Sometimes,node_1 needs to pass an intermediate value to node_2, but this intermediate value has nothing to do with the final input/output of the graph. If you put it into OverallState, you have to expose this implementation detail in the "public interface" of the entire graph.
LangGraph provides two mechanisms to solve these two problems:Private StateUsed to transfer internal data between nodes,Input/Output SchemaUsed to filter data at the boundaries of the graph. This is the core content of this lesson.
Note: Private State is a concept from another dimension. It exists on the "channel" between nodes and does not even appear in OverallState. We will explain them one by one next.
Private State refers to:Some data only circulates between specific nodes and is neither the input nor the output of the graph, nor does it belong to the global OverallState. It is a note for "private chat" between nodes and cannot be seen by other nodes or external callers.
LangGraph allows node functions to beType HintDeclare the Schema you use in . Nodes can be: usedOverallStateas input type but returnsPrivateState;Also acceptablePrivateStateas input, returnsOverallState. LangGraph's routing engine will automatically match these types to ensure correct data flow.
| Dimensions | OverallState (global state) | PrivateState (private state) |
|---|---|---|
| Visible range | All nodes of the graph + external callers | Only the specific node where this Schema is declared |
| life cycle | Exists from START to END | Only exists when passing between specific nodes |
| appears in the output | yes | No (not in OverallState) |
| Defined way | As the main Schema of StateGraph | Define a TypedDict separately, referenced by the node type annotation |
| Typical uses | Core data such as questions, answers, conversation history, etc. | Intermediate calculated values, temporary cache, inter-node protocol data |
Below is the complete example code for private state in the notebook. Let’s break it down piece by piece:
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
foo. This is the interface exposed by the entire picture: the caller passes in{"foo": 1}, return after the graph is executed{"foo": 3}。baz field does not exist in OverallState at all, so it will never appear in the final result.baz. It is the "temporary intermediate value" produced by node_1 and consumed by node_2. You can understand it as: node_1 wrote on the notebaz=2Give it to node_2, and node_2 will throw it away after use and will not be archived anywhere.# 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) -> PrivateStateThis signature tells LangGraph: when executing node_1, pass it the current OverallState; the dictionary returned by node_1 will be interpreted as PrivateState instead of directly merged into OverallState. LangGraph's internal routing engine determines the data flow direction based on this type annotation.# 构建图——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 从未出现
bazFieldNever appears in the final invoke() return value. Because it only belongs to PrivateState, and StateGraph ultimately returns OverallState. This is exactly the purpose of private state design: temporary data between nodes does not pollute the public interface.
Before introducing Input/Output Schema, we first use the example in the notebook to intuitively feel the limitations of the single Schema approach.
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()
graph.invoke({"question": "hi"})
# 实际输出(包含用户不需要的字段):
# {
# 'question': 'hi', ← 用户自己知道,不需要再返回
# 'answer': 'bye Lance', ← 用户真正需要的
# 'notes': '... his name is Lance' ← 内部中间数据,泄露给外部!
# }
Pain point one (input side): The caller cannot pass onlyquestion——If you don’t pass it onanswerandnotes, they are None in State, and the node function may error or require additional processing.
Pain point two (output side):notesIt is an internal implementation detail. The caller will be confused when seeing it, and may even rely on this field that should not be exposed, causing interface coupling.
These two pain points may seem insignificant in a simple example, but in a real production scenario - where the graph has dozens of fields and external systems rely on its output - they become serious design issues.Input/Output Schema is the solution to these two pain points.
StateGraphThree construction parameters are supported to define multiple Schemas:
graph = StateGraph(
OverallState, # 第一个参数:内部全局状态(节点间通信用)
input_schema=InputState, # 关键字参数:定义 invoke() 接受的输入结构
output_schema=OutputState # 关键字参数:定义 invoke() 返回的输出结构
)
The functions of these two parameters:
Input/Output Schema Does not change the internal operating logic of the graph——Nodes still use complete OverallState communication. Schema is only inAt the boundary of the graph (when entering the graph/when leaving the graph)Do data clipping. Just like an API's request/response serialization layer, it does not affect the back-end business logic.
When the graph uses multiple Schemas, the type annotation of the node function can clearly declare which Schema it uses, increasing code readability:
# 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"}
Type annotations on node functions (-> OutputState) mainly plays the role ofDocumentation notes and IDE tipsrole. What really determines which fields invoke() ultimately returns isStateGraph(output_schema=OutputState)this parameter. When used together, the intent of the code is clearest.
The following is the complete implementation code of Input/Output Schema in the notebook. Let’s analyze it line by line:
# ① 输入 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 中
question. when callinggraph.invoke({"question": "hi"})When , LangGraph will verify that the incoming dictionary conforms to the structure of InputState. You don't need to pass itanswerornotes, they will be initialized in OverallState with a default value (None or does not exist).answer. When the graph is executed, LangGraph extracts onlyanswerfield returns.questionandnotesEven if it exists in OverallState, it will not appear in the return value of invoke().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"}
# 关键!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 都不见了!
# 图内部确实有这两个字段,只是输出时被过滤掉了。
When outputting results from a single Schema{'question': 'hi', 'answer': 'bye Lance', 'notes': '... his name is Lance'}became concise{'answer': 'bye Lance'}. This is the effect of output_schema filtering.
| stage | visible data | illustrate |
|---|---|---|
| invoke() passes in | {"question": "hi"} |
Constrained by InputState, just pass question |
| after thinking_node execution | OverallState: {question, answer:"bye", notes:"..."} |
All fields can be read and written inside the node |
| After answer_node is executed | OverallState: {question, answer:"bye Lance", notes:"..."} |
answer is updated, other fields remain unchanged |
| invoke() returns | {"answer": "bye Lance"} |
Filtered by OutputState, only answers are returned |
| Contrast Dimensions | Private State (PrivateState) | Input/Output Schema |
|---|---|---|
| Problem solved | Temporary intermediate data between nodes, do not want to enter OverallState | The external interface of the graph is bloated and has too many input/output fields. |
| Scope | graphicinternal(between nodes) | graphicborder(caller ↔ picture) |
| Configuration method | Declared in the type annotation of the node function | Declared in the StateGraph() constructor parameter |
| Impact on OverallState | private fielddoes not belong OverallState | OverallState remains unchanged, justfilterInput and output |
| Typical usage scenarios | Intermediate calculation results, protocol data between nodes | API interface design, hiding internal implementation details |
In a complex graph, you can use private state and Input/Output Schema at the same time:
# 同时使用两种机制的设计示例
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 ...
Think of the picture as aWell packaged module:
· input_schemaDefine "module parameters" - what the caller needs to provide
· output_schemaDefine the "return value of the module" - what the caller gets
· OverallStateIt is "module internal state" - implementation details, not perceived by the outside world
· PrivateStateIt is a "local variable" - it is only passed between specific functions and does not even write the internal state.
In the search graph, the retrieval node outputs "document list + relevance score", and the sorting node outputs "sorted document list" after consumption.Relevance score is an intermediate calculated value, which means nothing to the end user. Use private state to carry ratings to avoid contaminating OverallState.
class ScoredDocsState(TypedDict):
# 私有:带评分的文档,只在检索和排序节点之间流通
scored_docs: list[tuple[str, float]]
Chain-of-Thought scenario: The LLM internal reasoning node produces detailed reasoning steps, while the final answer node only requires a concise conclusion.inference step stringCan be used as private state and does not enter OverallState.
class ReasoningState(TypedDict):
# 私有:详细推理过程,最终用户不需要看到
chain_of_thought: str
confidence: float
When exposing a LangGraph graph as a backend service to the frontend or other microservices,Interface fields should be minimized. The front end only cares about "questions" and "answers" and does not need to know how many rounds of searches the graph has gone through and what intermediate notes have been generated.
# 前端友好的接口
class ChatInput(TypedDict):
message: str # 用户发送的消息
session_id: str # 会话 ID
class ChatOutput(TypedDict):
reply: str # AI 回复
tokens_used: int # 消耗的 token 数(计费用)
When the graph is deployed to LangGraph Cloud,input_schemaandoutput_schemaAPI documentation and request validation are automatically generated. This isBest practices for production deployment——Clear Schema makes the API self-documenting, reducing the possibility of misuse by callers.
In multi-agent systems or parallel graphs, subgraphs may have their own OverallState that is different from the parent graph. Through a well-defined input/output Schema, the child graph exposes a clear interface to the parent graph.The internal states of the two do not interfere with each other。
In the prototype stage, an OverallState is used to cover all fields and quickly verify the logic. Don't optimize prematurely.
The graph needs to be called by external systems (API, front-end, other microservices), or you find that the return value field of invoke() has too many fields, affecting usability.
You find that some fields in OverallState are "only used by two adjacent nodes, and are never read or written elsewhere." This is a candidate for private state.
No matter how many Schemas are used, OverallState is always the complete data structure for communication between nodes within the graph. Make sure it contains all the fields that the node actually needs to read and write.
The essence of multiple Schema isinformation encapsulation: Clearly separate "data that is useful to whom" and "data that is useless to whom". This is in line with the idea of object-oriented encapsulation and the principle of minimal exposure of APIs. LangGraph only provides language tools to implement this encapsulation at the graph level.