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

Multiple Schema Design
Private state · Input filtering · Output filtering

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.

1Why multiple schemas are needed? ——Limitations of single Schema

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.

Problem 1: Intermediate calculation data pollutes the public interface

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 passes in
question: "The highest mountain in the world?"
Single Schema returned (too much information)
question: "The highest mountain in the world?"
notes: "...Draft analysis process..."
search_keywords: ["Himalaya", "Everest"]
answer: "Everest, 8849 meters"

The caller only wantsanswer, but got all the internal fields, the interface is bloated and easy to misuse.

Problem 2: "Temporary transfer" data between nodes should not be exposed to the outside world

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's solution

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.

Global perspective of multiple schemas

InputState (input Schema)
question: str
Fields passed in by the caller
Figure will reject other fields
OverallState (internal Schema)
question: str
answer: str
notes: str
Complete data passed between nodes
Contains all intermediate states
OutputState (Output Schema)
answer: str
Fields returned by invoke()
Expose only necessary information

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.

2Private State - "internal notes" between nodes

core concepts

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.

Implementation mechanism

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.

Illustration: Data flow of private state

How private state flows between nodes

Input: { foo: 1 }
OverallState
node_1
Accept OverallState, return PrivateState
Private delivery: { baz: 2 }
PrivateState (not visible to the outside)
node_2
Accept PrivateState, return OverallState
Output: { foo: 3 }
OverallState (baz does not appear)

Comparison of private state vs global state

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

3Complete code analysis of private state

Below is the complete example code for private state in the notebook. Let’s break it down piece by piece:

Step 1: Define two TypedDict Schemas

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
OverallStateonly one fieldfoo. 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.
PrivateStateonly one fieldbaz. 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.

Step 2: Define node functions (key: type annotation)

# 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}
Type annotations are keynode_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.

Step Three: Build and Run the Graph

# 构建图——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 从未出现

Complete data flow tracing

foo: 1
The initial OverallState passed in by invoke()
baz: 2
(foo + 1 = 2)
PrivateState returned by node_1
baz: 2
PrivateState received by node_2
foo: 3
(baz + 1 = 3)
OverallState returned by node_2 (final output)
Things to note

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.

4Single Schema input and output issues

Before introducing Input/Output Schema, we first use the example in the notebook to intuitively feel the limitations of the single Schema approach.

Scenario: A simple question and answer graph

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()

Problem exposed: invoke() returns all fields

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

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

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.

5Input / Output Schema - precise interface filtering

core mechanism

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:

Understand the word "filter"

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.

Hierarchical relationship between three Schemas

Schema levels and data flow

InputState
question: str
The caller passes in
input_schema validation (filter redundant fields)
OverallState (inside the graph)
question
answer
notes (for internal use only, not external)
Complete flow between thinking_node ↔ answer_node
output_schema filtering (clipping only answers)
OutputState
answer: str
The caller receives

Type annotations for node functions (optional but recommended)

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"}
The type annotation is "document" and filtering is controlled by the StateGraph parameter

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.

6Input / Output Schema complete code analysis

The following is the complete implementation code of Input/Output Schema in the notebook. Let’s analyze it line by line:

Step 1: Define the three-layer 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 中
InputStateonlyquestion. 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).
OutputStateonlyanswer. 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().
OverallStateis the "internal language" of the graph and contains all fields. Node functions communicate through this complete Schema and are not limited by Input/Output constraints.

Step 2: Define node functions

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"}

Step 3: Build the graph and pass in the input/output Schema parameters

# 关键!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()

Step 4: Run to verify the effect

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

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

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.

Complete data flow tracking

stagevisible dataillustrate
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

7Comparison and comprehensive application of two mechanisms

Private state vs Input/Output Schema: core differences

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

Both mechanisms can be used simultaneously

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 ...
Summary of design ideas

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.

8Real-life scenarios: When to use multiple schemas?

Scenarios suitable for using private state

Scenario 1: Passing ratings or tokens between pipeline nodes

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]]

Scenario 2: Separation of LLM reasoning process and final answer

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

Scenarios suitable for using Input/Output Schema

Scenario 3: Provide a clean API interface to the outside world

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 数(计费用)

Scenario 4: LangGraph Studio and LangGraph Cloud deployment

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.

Scenario 5: Interface isolation of subgraph

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

Decision suggestions

core design principles

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.

Knowledge map of this lesson

Private State
  • · Declared in the node function type annotation
  • ·Do not enter OverallState
  • · Does not appear in plot output
  • · Notes for "private chat" between nodes
Input / Output Schema
  • · Declared in the StateGraph() parameter
  • · Filter the input and output of invoke()
  • · Does not affect the internal operating logic of the graph
  • · Figure external "contract interface"
Key API Quick Check
StateGraph(OverallState, input_schema=X, output_schema=Y)
Three-layer Schema of declaration diagram
def node(state: PrivateState) -> OverallState:
Node’s private state type annotation
graph.invoke({"question": "hi"})
Just pass the InputState field
prerequisite knowledge TypedDict defines basic usage of State Schema and StateGraph
Master this section Three multiple Schema technologies: Private State, input_schema, and output_schema
Subsequent application Subgraph design, LangGraph Cloud deployment, multi-Agent interface isolation