主题
向后兼容性
在生产环境中更新 LangGraph 图代码,而不破坏进行中的运行。
软件在生产环境中需要不断变化。新的需求、缺陷修复和重构最终都会落到你的图代码中。由于 LangGraph 会针对已为现有线程持久化的状态运行最新部署的图,因此你发布的每一项变更,实际上都是相对于现有检查点的一次向后兼容的 API 变更。
与那些将运行固定在其启动时所用代码版本的工作流引擎不同,LangGraph 会立即将最新的图应用于每一个线程,无论是新线程还是从检查点恢复的线程。这很方便:缺陷修复无需任何仪式就能传播到进行中的对话和智能体。这也意味着你必须考虑每一项变更会如何与在先前代码版本下启动的运行相互作用。
需要留意的兼容性问题有三类,大体按照你会遇到的顺序排列:
- 技术兼容性:最常见;新代码必须仍然能够针对现有的 State 加载并执行。
- 业务兼容性:不太常见;即使代码已经改变,现有运行也应继续遵循旧的业务逻辑。
- 非确定性:仅适用于 Functional API。
TIP
关于运行时默认支持哪些图拓扑和状态变更的简要总结,请参阅图迁移。本页的其余部分将介绍当某项变更超出该受支持范围时可以应用的模式。
技术兼容性
技术兼容性相当于微服务中的 API 破坏性变更。这里的“API”指的是你的图代码与检查点器已为现有线程持久化的数据之间的契约。当线程恢复时,LangGraph 会反序列化已保存的状态,按名称将其分发到一个节点,并期望该节点返回符合状态 schema 的值。
常见的技术性破坏:
- 重命名或移除某个节点,而此时线程正暂停在该节点处或即将进入该节点,例如停在某个
interrupt处,或通过仍路由到旧名称的已检查点化条件边进入。恢复时,LangGraph 无法通过保存的名称找到该节点,运行便会失败。恢复运行的起点是执行停止所在节点的开头,因此缺失的节点没有可恢复的起点。 - 重命名或移除某个 State 键,而较旧的检查点仍包含该键,或下游节点仍读取该键。
- 收紧某个 State 字段,例如将
Optional字段变为必填、缩小类型范围,或添加一个没有默认值的新必填字段。现有的检查点将无法满足新的 schema。
边拓扑本身不会持久化到检查点中。在仍然存在的节点之间添加、移除或重新路由边,对进行中的线程来说是安全的。根据图迁移的总结,唯一可能破坏已中断线程的拓扑变更就是重命名或移除节点。
推荐模式
将新的状态字段添加为
NotRequired(或Optional[...] = None),以便旧检查点仍然可以通过校验:pythonfrom typing import NotRequired from typing_extensions import TypedDict class State(TypedDict): messages: list summary: NotRequired[str]将移除视为弃用。至少在整整一个排空周期内继续在该状态上定义该字段,即使没有节点读取它,也要让现有的检查点继续可加载。
通过先添加后移除的方式重命名。将新字段或新节点与旧的并排添加,在一个弃用窗口期内进行双写或同时路由到两者,然后在你确认没有进行中的线程依赖旧字段或旧节点后,再将其移除。
让节点函数能够容忍未知键。
TypedDict在运行时会忽略多余的键,因此来自较旧代码版本的残留状态不会引发错误,除非某个节点显式读取一个缺失的键。在正式发布之前,使用时间旅行和
graph.get_state在预发布部署中,针对新代码对现有线程进行抽查。
检测进行中的线程
在你移除一个节点、重命名一个 State 键,或以其他方式做出较旧线程无法容忍的变更之前,你会想知道是否有任何线程当前正停留在你即将下线的那个代码版本上。LangGraph 本身并不维护对线程状态的搜索索引,因此答案取决于你的图运行在哪里。
如果你部署到 LangSmith。 使用 Agent Server 的线程搜索按状态进行筛选。status 字段接受 idle、busy、interrupted 和 error,因此你可以批量查询 interrupted 或 busy 线程,并可选地通过元数据筛选器进一步缩小范围。请参阅按线程状态筛选和列出线程。
无论 LangGraph 在哪里运行。 使用 LangSmith 追踪来监控生产环境中正在进入和退出哪些节点。这是表明某个节点或状态字段在任何活动代码路径中都不再可达的最可靠信号。
当你已经拥有 thread_id 时。 直接检查该单个线程:
graph.get_state(config)返回最新的检查点,包括线程暂停在哪个节点,以及任何待处理的中断。graph.get_state_history(config)返回该线程按时间顺序排列的完整检查点列表。
如有疑问,请保留已弃用的节点或字段,直到 Agent Server 线程列表和追踪都显示它不再有任何活动为止。
业务兼容性
有时某项变更在技术上是有效的(每个现有检查点仍能加载,每个节点仍能解析),但新图的含义与旧图不同。新的行为对新线程来说是正确的,而你并不想将它追溯地应用到在旧逻辑下启动的线程上。
例如,假设你的图运行 intake → triage → respond,而你决定在 triage 和 respond 之间插入一个新的 policy_check 步骤:
- 已经通过
triage的线程应继续直接进入respond(旧流程)。 - 新线程应运行完整的新流程。
推荐的模式是在线程开始时将相关的行为版本记录到状态上,然后用条件边对它进行分支:
python
from typing import NotRequired
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
class State(TypedDict):
request: str
flow_version: NotRequired[int]
response: NotRequired[str]
def intake(state: State) -> dict:
# Stamp new threads with the current flow version. Existing threads
# that resume past `intake` keep whatever value was already saved.
return {"flow_version": state.get("flow_version", 2)}
def triage(state: State) -> dict: ...
def policy_check(state: State) -> dict: ...
def respond(state: State) -> dict: ...
def after_triage(state: State) -> str:
if state.get("flow_version", 1) >= 2:
return "policy_check"
return "respond"
builder = StateGraph(State)
builder.add_node("intake", intake)
builder.add_node("triage", triage)
builder.add_node("policy_check", policy_check)
builder.add_node("respond", respond)
builder.add_edge(START, "intake")
builder.add_edge("intake", "triage")
builder.add_conditional_edges("triage", after_triage, ["policy_check", "respond"])
builder.add_edge("policy_check", "respond")
builder.add_edge("respond", END)
graph = builder.compile()在 triage 之后恢复的旧线程会从已保存的状态中读取 flow_version(或回退到 v1 默认值)并跳过 policy_check。新线程从 intake 开始,被打上 flow_version=2 标记,并运行新路径。一旦所有 v1 线程都完成,你就可以移除版本标志和条件边。
该模式仅当你在线程开始时、在任何需要版本化的分支之前设置版本时才有效。如果之后再设置,就意味着现有线程在需要它的时候还没有被设置。
非确定性
这一类仅适用于 Functional API,以及 Graph API 节点内部的 tasks 或 interrupt 调用。普通的 Graph API 节点在恢复时会从节点函数的开头重新运行;请将副作用设计为幂等的,但除非你在该节点中使用了 tasks 或 interrupt,否则无需保留任务调用顺序。
一个 Functional API entrypoint 会编译成单个节点,当运行恢复时,它会从头重放 entrypoint 主体,并使用缓存的 @task 结果来跳过已经完成的工作。有两类变更会破坏这一模型:
- 添加、移除或重新排序位于恢复点之前的
@task调用或interrupt调用。LangGraph 通过调用在重放中的位置来将缓存结果和恢复值与调用相匹配,因此改变该位置可能导致错误的缓存值被重放到另一个调用上。 - 在
@task之外引入非确定性操作,例如time.time()、random.random(),或内联在 entrypoint 主体中的网络调用。在重放时,它们产生的值与首次运行时不同,这可能改变控制流。
如需带示例的深入讲解,请参阅 Functional API 指南中的确定性和常见陷阱。
如果你需要对一个存在进行中运行的 @entrypoint 做出非平凡的代码变更,最安全的做法是:
- 在部署变更之前,让进行中的运行排空。
- 将任何新逻辑包装到一个新的
@task中,使其结果被独立地检查点化。 - 在
langgraph.json中以新的图名称注册一个新的 entrypoint 来实现新行为,并将新线程路由到它。