
LangGraph 的其他特性
在把前面三个基础案例给大家介绍完成之后,我们再来讲解 LangGraph 里一些可以单独拎出来重点学习的拓展知识点。
使用 Overwrite 绕过 reducer
我们之前讲解案例的时候,给大家提到过状态追加更新 的机制,大家应该还有印象。我们自定义的消息状态,默认可以通过 add 这种聚合方式,把每一次更新的内容追加拼接 到列表当中。那大家有没有思考过一个问题:我们能不能打破这种默认的追加规则?
举个实际场景,我们和 AI 持续对话,message 列表里会慢慢累积十几条历史消息。如果这时我们想清空所有历史对话 ,不再沿用追加逻辑,有没有办法无视 add 的合并规则,直接用一条新消息覆盖替换 掉整个历史消息列表?
答案是有的。想要绕过默认的追加更新机制,我们可以使用 LangGraph 提供的专属关键字 Overwrite 来实现状态覆盖,这也是我们要讲的第一个核心知识点。
在 LangGraph 中,reducers 用于控制状态更新的处理方式。默认情况下,每个状态键都有其独立的 reducer 函数,用于决定如何合并节点返回的更新。但有时我们需要完全覆盖状态值而不是合并,这时就需要使用 Overwrite。
为什么需要 Overwrite?想象一下开发一个聊天应用时:
- 正常情况下,新消息会追加到消息列表中。
- 但有时需要清空聊天记录并重新开始,这时候就需要绕过追加逻辑,直接覆盖整个消息列表。
理解原理之后,我们直接通过代码案例实操演示,逻辑非常简单。首先自定义状态 ,状态中定义 messages 字段,类型为字符串列表,搭配 operator.add 实现默认追加规则。
接着编写两个节点: 第一个节点专门用来追加消息 ,按照默认规则往列表里新增内容;第二个节点我们不使用追加逻辑,而是引入 Overwrite 关键字,用全新列表直接覆盖原有历史列表。
随后构建 StateGraph 流程图 ,依次添加两个节点、配置节点执行边,最后调用 invoke 方法执行流程图并打印最终结果。
我们可以分两次测试:不使用 Overwrite 时,新消息会正常追加在历史消息后方;使用 Overwrite 后,会直接清空旧的历史消息,只保留最新覆盖的内容,完美实现状态覆盖效果。
而 Overwrite 在实际开发中有非常多实用场景:比如对话系统中历史消息过多时,一键清空对话记录;程序出现异常报错后,重置整体应用状态;清理系统中过时、损坏的中间数据等。核心作用就是绕过 LangGraph 默认的状态追加更新机制,直接整体覆盖状态值。
python
from langgraph.graph import StateGraph, START, END
from langgraph.types import Overwrite
from typing_extensions import Annotated, TypedDict
import operator
class State(TypedDict):
messages: Annotated[list, operator.add]
def add_message(state: State):
return {"messages": ["first message"]}
def replace_messages(state: State):
# 绕过reducer并替换整个消息列表
return {"messages": Overwrite(["replacement message"])}
builder = StateGraph(State)
builder.add_node("add_message", add_message)
builder.add_node("replace_messages", replace_messages)
builder.add_edge(START, "add_message")
builder.add_edge("add_message", "replace_messages")
builder.add_edge("replace_messages", END)
graph = builder.compile()
result = graph.invoke({"messages": ["initial"]})
print(result["messages"])
输出: ['replacement message'],而不是 ['initial message', 'first message', 'replacement message']。
使用 Overwrite 的应用场景如下:
- 重置对话:清空聊天历史,开始新对话
- 状态重置:在错误恢复后重置应用状态
- 数据清理:替换损坏或过时的数据
讲完 Overwrite,我们接着学习第二个知识点:定义独立的输入输出模式。
定义输入输出模式
回顾我们之前写的所有案例,LangGraph 默认只使用单一全局状态 ,流程图的输入、输出、节点间通信,全部共用同一个状态结构。但在实际企业开发中,我们可以把输入状态、输出状态、内部流转状态拆分开独立定义,这就是输入输出模式的核心意义。
我们可以单独定义 InputState 作为工作流专属输入结构、OutputState 作为专属输出结构,再额外定义一个完整的内部状态,用来承载节点流转过程中需要用到的上下文、中间数据等不对外暴露的信息。
在构建 StateGraph 的时候,通过 input_schema 和 output_schema 两个参数,分别指定图的输入规范和输出规范。schema 本身具备数据验证作用,input_schema 会校验传入的输入数据结构,output_schema 则会过滤最终输出结果。即便节点内部流转时产生了多个状态字段,最终也只会返回我们指定的输出字段,自动隐藏内部冗余字段。
这种独立输入输出模式,落地场景十分广泛:做 API 接口开发时,可以定义清晰的请求、响应数据格式,隐藏后端内部处理逻辑;微服务架构中,能统一服务之间的数据交互契约;数据管道开发里,也能明确整个流程的输入输出标准,让接口规范更清晰、更安全。
简单来说:
默认情况下,LangGraph 使用单一的状态模式。但我们可以定义独立的输入和输出模式,解释如下:
- 独立的输入模式:验证输入数据的结构
- 独立的输出模式:过滤输出数据,只返回需要的信息
- 内部模式:节点间通信使用的完整状态
为什么需要独立的内部模式?考虑一个问答系统:
- 输入:用户的问题(字符串)
- 输出:AI 的答案(字符串)
- 内部:可能需要存储中间结果、上下文等信息
我们不想把内部状态都暴露给用户!
在 LangGraph 中,如果想要定义独立的输入和输出模式,可以使用 StateGraph 的初始化参数:
| 参数名 | 类型 | 描述 |
|---|---|---|
input_schema |
type[InputT] 或 None |
定义 StateGraph 输入的 State 类 |
output_schema |
type[OutputT] 或 None |
定义 StateGraph 输出的 State 类 |
state_schema |
type[StateT] |
定义 StateGraph 的 State 类 |
python
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
# 1. 定义输入模式 - 只包含用户问题
class InputState(TypedDict):
question: str
# 2. 定义输出模式 - 只包含AI答案
class OutputState(TypedDict):
answer: str
# 3. 定义完整状态模式(内部使用)
class OverallState(InputState, OutputState):
pass
def answer_node(state: InputState):
"""处理输入并生成答案"""
# 这里可以访问 question, 生成 answer
return {
"answer": f"Answer to: {state['question']}",
"question": state["question"]
}
# 构建图时指定输入输出模式
builder = StateGraph(
OverallState,
input_schema=InputState, # 输入验证
output_schema=OutputState # 输出过滤
)
builder.add_node("answer_node", answer_node)
builder.add_edge(START, "answer_node")
builder.add_edge("answer_node", END)
graph = builder.compile()
# 测试
result = graph.invoke({"question": "What is LangGraph?"})
print(result) # 输出: {'answer': 'Answer to: What is LangGraph?'}
# 注意: question 字段被过滤掉了, 不在输出中
独立的输入和输出实际应用场景如下:
- API 开发:定义清晰的请求 / 响应格式
- 微服务:服务间明确的数据契约
- 数据管道:明确的输入输出规范
接下来讲解第三个核心特性:节点间传递私有状态。
在节点间传递私有状态
我们都知道,LangGraph 节点的核心逻辑是:接收状态、处理逻辑、返回状态更新 ,节点之间依靠状态完成通信。基于这个特性,我们可以实现私有状态传递:有些敏感数据、临时中间数据,只需要在指定节点之间共享,不需要暴露给后续节点,也不用对外输出。
举个典型场景: 我们设计三个执行节点,节点 1 负责从数据源获取敏感隐私数据,节点 2 需要接收这份隐私数据做业务处理,而节点 3 只需要接收处理后的公开结果,完全不需要感知原始敏感数据。
具体实现方式很简单:定义一个全局公共状态 ,用来存放最终对外展示的结果;再单独自定义节点 1 的输出状态、节点 2 的输入状态,专门用来承载私有敏感字段。节点 1 执行后,不返回公共状态,而是返回自定义的私有状态;节点 2 接收这份私有状态完成数据处理,处理完成后只把脱敏后的公开结果更新到全局公共状态;节点 3 只依赖公共状态执行业务逻辑,完全无法访问前面的私有敏感数据。
同时如果多个节点是固定链式执行顺序,还可以使用 add_sequence 方法,一次性批量添加多个节点,自动帮我们配置好顺序执行的边,简化流程图配置代码。
现在我们需要根据数据库中的信息,生成相关数据报告,因此可以设置三个节点:
- 节点 1:从数据库获取原始数据(包含敏感信息)
- 节点 2:处理数据,过滤掉敏感信息
- 节点 3:生成最终报告
此时,节点 1 和 节点 2 需要共享原始数据,但 节点 3 不应该看到敏感信息。代码如下:
python
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
# 公共状态(最终输出中可见)
class OverallState(TypedDict):
final_result: str
# 节点1的私有输出
class Node1Output(TypedDict):
sensitive_data: str # 这个字段不会出现在最终状态中
# 节点2需要的输入(包含私有数据)
class Node2Input(TypedDict):
sensitive_data: str
def node_1(state: OverallState) -> Node1Output:
"""第一步:获取包含敏感信息的原始数据"""
private_data = "这是敏感信息"
print(f"Node1: 获取到敏感数据,但不会暴露给最终输出")
return {"sensitive_data": private_data}
def node_2(state: Node2Input) -> OverallState:
"""第二步:处理数据,移除敏感信息"""
print(f"Node2: 处理敏感数据: {state['sensitive_data']}")
# 处理数据,返回清理后的结果
return {"final_result": "清理后的处理结果"}
def node_3(state: OverallState) -> OverallState:
"""第三步:只看到清理后的数据"""
print(f"Node3: 只能看到最终结果: {state['final_result']}")
return {"final_result": state["final_result"] + " - 完成"}
# 构建图
builder = StateGraph(OverallState)
# add_sequence: 支持添加一系列节点,按所给的顺序执行。
# 注意:我们使用 add_sequence 但类型系统会处理私有状态
builder.add_sequence([node_1, node_2, node_3])
builder.add_edge(START, "node_1")
graph = builder.compile()
# 测试
response = graph.invoke({"final_result": "initial"})
print(f"\n最终输出: {response}")
运行上述代码,会看到:
bash
Node1: 获取到敏感数据,但不会暴露给最终输出
Node2: 处理敏感数据: 这是敏感信息
Node3: 只能看到最终结果: 清理后的处理结果
最终输出: {'final_result': '清理后的处理结果 - 完成'}
节点间传递私有状态实际应用场景如下:
- 数据处理:中间处理步骤的临时数据
- 认证流程:令牌等敏感信息的传递
- 复杂计算:中间计算结果
- 错误处理:错误详情在内部传递,但对外提供友好消息
以上三个特性让 LangGraph 能够处理复杂的企业级应用场景,同时保持代码的清晰和安全性。
总的来说,Overwrite 状态覆盖、独立输入输出模式、节点私有状态传递,这三大拓展特性,底层都是围绕 节点可自定义接收和返回不同状态 这个核心原理延伸出来的。掌握这三个特性,我们就可以开发更规范、更安全、更适配企业级复杂场景的 LangGraph 应用。
到这里,LangGraph 快速入门以及三大拓展核心知识点就全部讲解完毕了。入门阶段我们已经掌握了固定开发流程:定义状态 → 定义业务节点 → 构建流程图 → 配置节点边 → 编译并运行图。后续更高级的功能,也都是在这套基础流程上,新增参数、拓展字段、叠加能力而已。后续我们再继续学习 LangGraph 工作流常见模式和更多高阶独有能力,帮大家全面吃透 LangGraph 框架。