LangGraph State / Config / Input / Output Schema 深度解析:为什么企业级 Agent 系统必须定义 Schema?
前言
在使用 LangGraph 开发工业级 Agent 时,随着系统复杂度的提升,我们经常会看到以下四个核心概念交织在一起:
- State Schema(状态模式)
- Configurable Schema / Context(运行时配置/上下文)
- Input Schema(输入模式)
- Output Schema(输出模式)
很多初学者会有疑问:"大模型本身就是处理松散 Dictionaries 的专家,为什么 LangGraph 要设计这么多繁琐的 Schema?直接传一个裸 dict 不行吗?"
事实上,这四个 Schema 并不是冗余的代码教条,而是 LangGraph 核心数据流设计的基础设施。它们共同构建了 Agent 在多节点协作、并发处理、状态保持与多租户隔离时的安全边界。本文将为你深度拆解这四种 Schema 的协作奥秘与落地实践。
一、 现代 Agent 的数据流模型
在 LangGraph 中,Graph(图)的本质是一个基于**通道(Channels)**的分布式状态机。数据在图中的流转路径如下:
#mermaid-svg-Sjvd70YakZj2duYR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sjvd70YakZj2duYR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sjvd70YakZj2duYR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sjvd70YakZj2duYR .error-icon{fill:#552222;}#mermaid-svg-Sjvd70YakZj2duYR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sjvd70YakZj2duYR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sjvd70YakZj2duYR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sjvd70YakZj2duYR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sjvd70YakZj2duYR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sjvd70YakZj2duYR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sjvd70YakZj2duYR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sjvd70YakZj2duYR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sjvd70YakZj2duYR .marker.cross{stroke:#333333;}#mermaid-svg-Sjvd70YakZj2duYR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sjvd70YakZj2duYR p{margin:0;}#mermaid-svg-Sjvd70YakZj2duYR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sjvd70YakZj2duYR .cluster-label text{fill:#333;}#mermaid-svg-Sjvd70YakZj2duYR .cluster-label span{color:#333;}#mermaid-svg-Sjvd70YakZj2duYR .cluster-label span p{background-color:transparent;}#mermaid-svg-Sjvd70YakZj2duYR .label text,#mermaid-svg-Sjvd70YakZj2duYR span{fill:#333;color:#333;}#mermaid-svg-Sjvd70YakZj2duYR .node rect,#mermaid-svg-Sjvd70YakZj2duYR .node circle,#mermaid-svg-Sjvd70YakZj2duYR .node ellipse,#mermaid-svg-Sjvd70YakZj2duYR .node polygon,#mermaid-svg-Sjvd70YakZj2duYR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sjvd70YakZj2duYR .rough-node .label text,#mermaid-svg-Sjvd70YakZj2duYR .node .label text,#mermaid-svg-Sjvd70YakZj2duYR .image-shape .label,#mermaid-svg-Sjvd70YakZj2duYR .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sjvd70YakZj2duYR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sjvd70YakZj2duYR .rough-node .label,#mermaid-svg-Sjvd70YakZj2duYR .node .label,#mermaid-svg-Sjvd70YakZj2duYR .image-shape .label,#mermaid-svg-Sjvd70YakZj2duYR .icon-shape .label{text-align:center;}#mermaid-svg-Sjvd70YakZj2duYR .node.clickable{cursor:pointer;}#mermaid-svg-Sjvd70YakZj2duYR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sjvd70YakZj2duYR .arrowheadPath{fill:#333333;}#mermaid-svg-Sjvd70YakZj2duYR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sjvd70YakZj2duYR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sjvd70YakZj2duYR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sjvd70YakZj2duYR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sjvd70YakZj2duYR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sjvd70YakZj2duYR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sjvd70YakZj2duYR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sjvd70YakZj2duYR .cluster text{fill:#333;}#mermaid-svg-Sjvd70YakZj2duYR .cluster span{color:#333;}#mermaid-svg-Sjvd70YakZj2duYR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sjvd70YakZj2duYR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sjvd70YakZj2duYR rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sjvd70YakZj2duYR .icon-shape,#mermaid-svg-Sjvd70YakZj2duYR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sjvd70YakZj2duYR .icon-shape p,#mermaid-svg-Sjvd70YakZj2duYR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sjvd70YakZj2duYR .icon-shape .label rect,#mermaid-svg-Sjvd70YakZj2duYR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sjvd70YakZj2duYR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sjvd70YakZj2duYR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sjvd70YakZj2duYR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 写入/初始化
2. 只读注入
3. 更新部分通道
4. 更新部分通道
5. 裁剪过滤
External Input
Shared State
Configurable Context
Node A
Node B
External Output
简单来说:输入数据初始化全局状态 →\rightarrow→ 各节点读取状态并返回增量补丁 →\rightarrow→ 状态机依据 Schema 规则合并(Reducer)状态 →\rightarrow→ 最终过滤出对外输出。
二、 State Schema:图的全局动态内存
1. 什么是 State Schema
State Schema 定义了整个 Graph 的共享状态空间。它是图中所有 Node(节点)共同维系的"单一事实来源(Single Source of Truth)"。
2. 代码实现与自动合并(Reducer)
python
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
# 企业级实践中,通常结合 Annotated 与 Reducer 函数来控制状态如何合并
class AgentState(TypedDict):
question: str
intent: str
# 使用 add_messages 机制自动追加消息流,而不是覆盖
messages: Annotated[list, add_messages]
internal_logs: list[str]
3. 为什么必须定义 State Schema?
- 规避隐式拼写 Bug :在一个包含数十个节点的复杂 Agent 中,如果没有强类型约束,某个节点误将
state["intent"]写成了state["inteent"],动态类型语言(Python)不会在写入时报错,但下游依赖intent字段的节点将由于拿不到数据而发生灾难性崩溃。 - 定义合并行为(Reducers) :Schema 不仅约束了字段类型,还声明了数据冲突时的合并策略。例如,
messages字段通过add_messages声明了它是Append-only(追加模式),而intent则是Overwrite(覆盖模式)。
三、 Configurable Schema (Context):隔离的运行时上下文
1. 什么是运行时配置/上下文
在开发多租户(Multi-tenant)或需要动态权限校验的企业 Agent 时,有些数据是节点只读 的,且绝对不应该在节点运行过程中被大模型修改。例如:当前请求的用户 ID、商户租户 ID、接口 API Key、当前语言环境(Locale)等。
在 LangGraph 中,这类只读的运行时上下文通过 RunnableConfig 的 configurable 属性 来承载,而不是污染全局 State。
2. 代码实现规范
python
from pydantic import BaseModel, Field
from langchain_core.runnables import RunnableConfig
# 1. 定义可配置的上下文 Schema
class RuntimeContext(BaseModel):
user_id: str = Field(description="当前请求的用户唯一标识")
tenant_id: str = Field(description="多租户隔离的商户ID")
language: str = Field(default="zh-CN")
# 2. 在 Node 中通过标准 RunnableConfig 读取
def generation_node(state: AgentState, config: RunnableConfig):
# 安全地从封装好的 config 中提取上下文,节点无法反向篡改这些值
configurable = RuntimeContext(**config.get("configurable", {}))
user_lang = configurable.language
# 依据外部传入的语言环境,动态调整 Prompt
return {"messages": [("assistant", f"Current language context: {user_lang}")]}
四、 Input 与 Output Schema:对外的 API 契约
在 LangGraph 早期版本中,图的输入、输出、状态使用的是同一个 Schema。但这在生产环境中暴露出巨大的工程隐患。最新版 LangGraph 强化了 Input Schema 与 Output Schema 的分离。
为什么必须把 Input / Output 与 State 分开?
这可以完美对齐经典软件工程中 DTO(数据传输对象) 与 Domain Model(领域模型) 隔离的设计思想:
- Input Schema(外部可见的输入) :对客户端而言,调用 Agent 只需要传入
{"question": "北京天气"}。客户端不需要、也不应该知道系统内部还有intent、retrieved_docs等中间变量。 - Output Schema(外部可见的输出) :系统内部为了让大模型链式思考,会在 State 中记录大量的
internal_logs(如 CoT 思考链、原始 Tool 响应、Prompt 调试信息)。这些数据不仅体积庞大,还可能包含敏感信息。通过 Output Schema,我们可以强制图在最后一步进行字段裁剪 ,只给客户端返回{"answer": "今天晴天"}。
五、 四大 Schema 协同作战:企业级 Agent 标准设计模式
以下是一个标准的工业级多路由 Agent 骨架代码,完整展现了四种 Schema 的分工与联动:
python
from typing import TypedDict, Optional
from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from langchain_core.runnables import RunnableConfig
# ==========================================
# 1. SCHEMA 定义层 (各司其职)
# ==========================================
# 外部输入:极其精简
class GraphInput(TypedDict):
question: str
# 外部输出:隐藏了内部思考细节与中间变量
class GraphOutput(TypedDict):
answer: str
# 全局内部状态:包含中间件、召回文档、意图等
class GraphState(TypedDict):
question: str
intent: Optional[str]
retrieved_docs: list[str]
answer: Optional[str]
# 运行时上下文:环境与多租户权限配置
class GraphContext(BaseModel):
user_id: str
experiment_group: str # A/B 测试分组
# ==========================================
# 2. NODE 节点实现层
# ==========================================
def retrieval_node(state: GraphState, config: RunnableConfig) -> dict:
# 读取只读上下文进行 A/B 测试策略路由
context = GraphContext(**config.get("configurable", {}))
if context.experiment_group == "v2_embedding":
docs = ["Retrieved from Vector DB v2"]
else:
docs = ["Retrieved from Vector DB v1"]
return {"retrieved_docs": docs}
def generator_node(state: GraphState) -> dict:
# 读取 state 中的 question 和 retrieved_docs,生成最终的 answer
raw_question = state["question"]
docs_context = ",".join(state["retrieved_docs"])
return {"answer": f"基于文档【{docs_context}】,回答:{raw_question} 的结果为晴。"}
# ==========================================
# 3. GRAPH 编排层
# ==========================================
# 显式将 input, output, state 进行三权分立
workflow = StateGraph(
state_schema=GraphState,
input_schema=GraphInput,
output_schema=GraphOutput
)
workflow.add_node("retrieval_agent", retrieval_node)
workflow.add_node("generator_agent", generator_node)
workflow.add_edge(START, "retrieval_agent")
workflow.add_edge("retrieval_agent", "generator_agent")
workflow.add_edge("generator_agent", END)
app = workflow.compile()
运行时调用机制
当外部客户端触发该 Agent 时的完整数据剪裁过程如下:
python
# 客户端调用:严格按照 Input Schema 与 Config 传参
runtime_config = {"configurable": {"user_id": "usr_995", "experiment_group": "v2_embedding"}}
initial_input = {"question": "上海明天的天气"}
final_result = app.invoke(initial_input, config=runtime_config)
# 打印最终输出
print(final_result)
# 输出完全符合 GraphOutput 的契约,内部的 `retrieved_docs` 已经被自动裁剪:
# {'answer': '基于文档【Retrieved from Vector DB v2】,回答:上海明天的天气 的结果为晴。'}
六、 总结与核心架构思维导图
在 LangGraph 架构体系中,四种 Schema 构建了清晰的系统边界:
| Schema 类型 | 面向对象 | 读写权限 | 核心白话解释 |
|---|---|---|---|
| Input Schema | 外部调用方 | 客户端只写 / 图初始化只读 | 用户带了什么东西进来 |
| Configurable (Context) | 基础设施/平台层 | 节点内绝对只读 | 系统提供了什么运行环境(安全与隔离) |
| State Schema | 内部 Agent 节点 | 节点内可读、可写、可追加 | Agent 链式运行过程中记住了什么 |
| Output Schema | 外部接收方 | 最终状态裁剪后输出 | 系统最终需要把什么呈现给用户 |
💡 专家建议 :
在写简单 Demo 时,你固然可以只定义一个
State包揽所有字段。但只要项目进入生产环境,涉及到前后端解耦、A/B 测试、多租户安全隔离、灰度发布 等硬核指标时,遵循 Input + Config + State + Output 的四权分立架构设计,是确保 Agent 系统具有高健壮性、高可测试性与高可维护性的唯一解。