生产环境中的软件需要不断变更。新需求、Bug 修复和重构最终都会落到你的图代码中。由于 LangGraph 会针对现有线程中持久化的状态运行最新部署的图,因此你每次发布的变更,相对于已有的检查点,实际上都是一次向后兼容的 API 变更。 与那些将一次运行固定到启动时代码版本的工作流引擎不同,LangGraph 会将最新图立即应用到每一个线程上,无论是新线程还是从检查点恢复的线程。这非常方便:Bug 修复会毫无障碍地传播到正在进行的对话和智能体中。但这也意味着,你必须仔细考虑每一次变更会如何与在旧版本代码下启动的运行交互。 需要留意的兼容性问题主要有三类,大致按你遇到它们的顺序排列:
  1. 技术兼容性:最常见;新代码必须仍然能针对现有状态加载和执行。
  2. 业务兼容性:不太常见;即使代码已经改变,现有运行也应继续遵循旧的业务逻辑。
  3. 非确定性:仅适用于函数式 API
关于运行时默认支持哪些图拓扑和状态变更的简短总结,请参阅图迁移。本页其余内容涵盖当变更超出该支持范围时,你可以采用的模式。

技术兼容性

技术兼容性就相当于微服务中的 API 破坏性变更。这里的“API”是指你的图代码与检查点存储器为现有线程所持久化数据之间的约定。当线程恢复时,LangGraph 会反序列化已保存的状态,按名称将其分派到某个节点,并期望该节点返回符合状态模式的值。 常见的技术破坏情况:
  • 重命名或移除节点,而线程正暂停在该节点或即将进入该节点,例如在 interrupt 处,或者通过仍指向旧名称的检查点条件边。恢复时,LangGraph 无法通过已保存的名称找到该节点,运行就会失败。恢复运行的起点是执行停止时所在节点的开头,因此缺失的节点没有可以恢复的位置。
  • 重命名或移除状态键,而旧检查点中仍然包含该键,或者下游节点仍在读取它。
  • 收紧状态字段,例如将一个 Optional 字段改为必填、收窄类型,或者新增一个没有默认值的必填字段。现有检查点将无法满足新模式。
边拓扑本身不会持久化在检查点中。在仍然存在的节点之间添加、移除或重新路由边,对正在进行的线程是安全的。根据图迁移的总结,唯一能破坏已中断线程的拓扑变更就是重命名或移除节点。

推荐的模式

  • 将新状态字段添加为 NotRequired(或 Optional[...] = None),这样旧检查点仍然能通过校验:
    from typing import NotRequired
    from typing_extensions import TypedDict
    
    class State(TypedDict):
        messages: list
        summary: NotRequired[str]
    
  • 将移除视为弃用。至少在一个清空周期内,将字段保留在状态定义中,即使没有节点读取它,以确保现有检查点仍然可以加载。
  • 通过先添加后移除的方式进行重命名。在旧字段或节点旁边添加新的,在弃用窗口期内双写或同时路由到两者,等到确认没有任何进行中的线程依赖旧项时,再将其移除。
  • 保持节点函数对未知键的容忍度。TypedDict 在运行时会忽略多余的键,因此来自旧代码版本的残留状态不会引发错误,除非节点显式读取了缺失的键。
  • 使用时间旅行graph.get_state 在灰度部署时,对现有线程进行抽查,以验证它们在新代码下的表现。

检测进行中的线程

在移除节点、重命名状态键,或进行其他旧线程无法容忍的变更之前,你需要知道是否有任何线程正停留在你即将丢弃的代码版本上。LangGraph 本身不维护线程状态的搜索索引,因此答案取决于你的图在哪里运行。 如果你部署到 LangSmith 可以使用 Agent Server 的线程搜索按状态筛选。status 字段接受 idlebusyinterruptederror,因此你可以批量查询 interruptedbusy 的线程,还可选地使用元数据过滤器缩小范围。请参阅按线程状态筛选列出线程 在任何运行 LangGraph 的地方。 使用 LangSmith 追踪来监控生产环境中哪些节点正在被进入和退出。这是判断某个节点或状态字段在任何活跃代码路径中是否不再可达的最可靠信号。 当你已经拥有 thread_id 时。 直接检查该单个线程: 如有疑问,请保留已弃用的节点或字段,直到 Agent Server 的线程列表和追踪都显示不再有相关活动为止。

业务兼容性

有时,某项变更在技术上是有效的(每个现有检查点仍能加载,每个节点仍能解析),但新图的含义却与旧图不同。新行为对新线程是正确的,而你不希望将其追溯应用到在旧逻辑下启动的线程上。 例如,假设你的图运行 intake → triage → respond,而你决定在 triagerespond 之间插入一个新的 policy_check 步骤:
  • 已经通过 triage 的线程应继续直接进入 respond(旧流程)。
  • 新线程应运行完整的新流程。
推荐的模式是在线程启动时将相关的行为版本记录在状态中,然后通过条件边进行分支:
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:
    # 为新线程打上当前流程版本标记。在 `intake` 之后恢复的现有线程
    # 会保留已保存的任何值。
    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 线程都已完成,你就可以移除版本标志和条件边。 此模式仅在线程启动时、在任何需要版本控制的分支之前设置版本才有效。如果之后才设置,现有线程在需要时就不会拥有该设置。

非确定性

此类别仅适用于函数式 API,以及在 Graph API 节点内部使用任务interrupt 调用的情况。普通的 Graph API 节点在恢复时会从节点函数的开头重新运行;请将副作用设计为幂等的,但除非在节点中使用了任务interrupt,否则无需保留任务调用顺序。 一个函数式 API 入口点会被编译为一个单一的节点,该节点在运行恢复时,会从头开始重放入口点主体,并使用缓存的 @task 结果来跳过已经完成的工作。有两类变更会破坏此模型:
  • 在恢复点之前添加、移除或重新排序 @task 调用或 interrupt 调用。LangGraph 会根据缓存在重放中的位置将缓存结果和恢复值匹配到调用上,因此改变位置可能会导致错误的缓存值被重放到不同的调用中。
  • @task 之外引入非确定性操作,例如 time.time()random.random(),或直接内联在入口点主体中的网络调用。在重放时,这些操作产生的值会与首次运行时不同,这可能会改变控制流。
有关更深入的讲解和示例,请参阅函数式 API 指南中的确定性常见陷阱 如果你需要对存在进行中运行的 @entrypoint 进行非平凡的代码变更,最安全的选择是:
  • 在进行变更之前,让进行中的运行自然结束。
  • 将任何新逻辑包装在一个新的 @task 中,这样其结果会被独立地设置检查点。
  • langgraph.json 中以新的图名称注册一个新的入口点来实现新行为,并将新线程路由到它。