跳到主要内容

内容阅读

旧教程全过期了:LangGraph 0.x → v1 迁移实录

Agent阿新聊ai

TL;DR: 2025 年 10 月的 LangGraph 1.0 是分水岭:create_react_agent 废弃、预置类型大清洗、Python 3.9 出局,而中文互联网的主流教程和三个翻译站还停在 0.x。这篇给一条五步迁移路径,加上社区实测出来的坑:最疼的不是 API 改名,是 pip install -U 之后 import 直接炸。

为什么这轮迁移和以前不一样

0.x 时代 LangGraph 的 API 也频繁变过,很多人对「升级」已经脱敏:反正改个 import 就完事,半天搞定。1.0 不一样的地方在于,它动的不是包内 API,而是包的边界。最标志性的一件事:预置能力(prebuilt)从 LangGraph 挪去了 LangChain,create_react_agent 被废弃,替代者 create_agent 住在 langchain.agents 里。这意味着迁移不是「同包内改名」,而是跨包搬家,牵连面完全不同:同包改名,编译器和 grep 就能兜底;跨包搬家,连带的是依赖树、锁文件、发布流程,出错的位置往往不在你改的那一行。

第二个特殊性是中文生态的滞后。主流中文教程和三个常用翻译站的内容仍停在 0.x,而 0.x 的写法里有一部分在 1.x 是直接报错的。第 01 篇讲过「教程时效风险」:报错信息不会告诉你是教程老了,新人照着旧教程写,得到的第一反应是「这个库真难用」,而不是「这篇教程过期了」。这一篇把「怎么判、怎么迁、坑在哪」一次讲完,也给你一套识别过期教程的方法,让团队不再重复交这笔学费。

还有一层容易被忽略:这轮迁移的痛苦分布很不均匀。标准用法半天搞定,人审重度项目一两周起步,多 agent 项目卡在半路的帖子在社区里不止一帖。同样是「从 0.x 到 1.x」,不同项目的实际工作量能差一个数量级。所以迁移前的评估比迁移本身更重要,先判断你自己属于哪一档,再决定投入多少。

先判断:迁、不迁、还是绕

三种情况,答案不同,判断的依据是「项目的生命力和迁移成本的走势」。

存量项目、还要继续演进:迁。 理由很直接:0.x 已进维护尾声,新特性和新修复的重心都在 1.x,每在旧 API 上多写一个功能,都是在给迁移面添砖。拖的代价不是线性的:今天迁是改几十处 import,半年后迁可能是把新写的三个子图一起返工。还有一个隐性走势:社区的新内容(文章、issue 回复、示例代码)会越来越以 1.x 为默认,停在 0.x 意味着你能抄的作业越来越少,排查问题时能对上的答案也越来越少。第 01 篇的判断线管「要不要用」,这一节管「已经在用的怎么办」,结论是:只要项目还活着,迁移成本只会涨不会跌。

存量项目、功能冻结:不迁。 锁死全家族版本继续跑,把精力省下来。这里「冻结」的定义要诚实:是未来两个季度内没有明确需求变更,而不是「暂时没人提需求」。锁版本不是免检通行证,有一个例外情况要想清楚:如果 0.x 版本爆出安全问题需要打补丁,你被锁在旧版本上,升级和迁移就得一起做,反而更被动。所以冻结策略要配一条触发条件写进文档:什么事件发生就启动迁移,比如安全通告,或者冻结期结束。没有触发条件的「先不迁」,实际上是没有决策,只有遗忘。

新项目:没有选择,直接 1.x。 别抄旧教程起手。这是三种情况里最简单的一种,也是唯一一种没有任何权衡余地的。唯一要做的事是把这个要求传达到位:团队里所有能搜到旧教程的人,都得知道为什么不能照抄,否则总有人「参考一下网上现成的写法」把 0.x 代码带进来。

介于三者之间还有一种常见想法:新旧双轨,新代码用 1.x、旧代码保持 0.x,慢慢换血。这条路的问题在于同一个 Python 环境里装不下两个大版本的 langgraph,双轨要么靠拆服务实现(运维成本翻倍,就为了一个过渡期),要么靠长期分支隔离(分支漂移比版本漂移更难管)。拆服务做过渡在个别团队成立,但作为默认方案不值得,它的复杂度比直接迁移还高。想「绕」之前先算这笔账。

1.0 官方承诺了 1.x 内无破坏性变更,这是迁移窗口的保证:迁到 1.x 之后,至少到 2.0 之前不会再有这一轮规模的折腾。0.x 从来没有过这种承诺,这也是「继续演进就早迁」的根本依据:你不是在两个稳定态之间选,而是在「有承诺的稳定」和「没有承诺的漂移」之间选。

迁移前先做一次选型复核

动手迁移之前,有一个必做的动作:把第 01 篇的四个问题重新过一遍。原因听起来有点反直觉:迁移是一次纯投入、没有新功能的工程,如果项目本身已经不在 LangGraph 的适用区,迁移就是在给一个该退场的方案追加投资。

第 19 篇的 Grid Dynamics 案例就是现成的参照:他们的深度研究 agent 要的核心其实是 durable execution,最终整体迁去了 Temporal。假如他们先做了迁移评估,结论很可能不是「迁到 1.x」而是「借这个机会换引擎」:反正都要动,与其付一次迁移税继续用错工具,不如把这笔预算花在真正对口的平台上。所以迁移评估的正确顺序是:先确认「还要不要」,再确认「怎么迁」。四个问题里有硬性「是」的,放心迁;四个全「否」、只是历史原因用着的,认真考虑下线或换轻量方案,那条路通常更便宜。

复核通过的,再进入下面的五步法。

五步迁移法

路径不复杂,但顺序重要。每一步存在的理由先讲清楚,照着做不容易漏。

第一步:冻结现状,让测试全绿。 迁移前把当前版本全家族锁死:langgraphlanggraph-checkpointlanggraph-checkpoint-postgreslangchain 各自 pin 住,不是只锁主包。原因是这个家族是多个包联动发布,只锁 langgraph 的话,-U 一下其余兄弟包照样新版本进来,你以为的「迁移前基线」其实是个漂移靶。锁的粒度建议精确到 patch 版本号,不要用 ~=>= 这类范围声明。做这一步时顺便盘点一下:你实际依赖了家族里的哪几个包,锁版本清单就列哪几个,没有用到的不用进清单,但要用 pip 的依赖树确认一遍「没用过」是事实而不是印象。

然后补齐核心路径的测试。标准不是覆盖率数字,是每张图至少有一条 invoke 冒烟加关键断言:给定输入,最终 state 里哪些字段应该是什么。有持久化的图,再加两条:中断后恢复的路径跑得通(人审恢复是迁移的高危区,后面专门讲),checkpoint 的写和读在目标数据库上真的工作。没有测试的迁移等于盲迁,出问题连回归基线都没有:你不知道一个行为差异是迁移改坏的,还是本来就这样。测试环境里可以用 InMemorySaver 跑,但注意内存版和生产 checkpointer 的行为差异(第 13 篇专门讲过),涉及持久化语义的断言要在真数据库上过一遍。

第二步:换掉 create_react_agent。 高频改动就这两行:

# 0.x(已废弃)
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(model, tools, prompt="你是客服")

# 1.x
from langchain.agents import create_agent
agent = create_agent(model, tools, system_prompt="你是客服")

两处不同:包名从 langgraph.prebuilt 换到 langchain.agents,参数名 prompt= 换成 system_prompt=。名字迁移是体力活,grep 一遍逐个改就是了。

真正要想清楚的是后半句:原来靠 prompt 参数注入的复杂提示逻辑,1.x 的做法是写 middleware,而不是继续堆字符串。这个设计变化的逻辑值得理解:prompt 参数本质上是一个字符串插槽,业务长起来之后,这个插槽里塞满了「系统角色 + 输出格式要求 + 工具使用规范 + 各种 if else 拼接」,改一处怕碰坏另一处。middleware 把这些横切逻辑拆成独立单元,每一片可以单独测试、单独开关。第 11 篇专门拆这层。

迁移时的实操建议:简单 prompt 直接换参数名,当天完成;超过三行拼接逻辑的 prompt,先标记出来单独排期改写成 middleware,不要在迁移当天顺手重写。一次变更只做一类事,这是迁移项目能保持「随时可回滚」的关键。当天顺手重写的后果是:迁移出问题时,你分不清是 API 换错了还是重写逻辑写错了。

第三步:人审相关类型逐个对。 这是改动最碎的部分,因为类型在两个来源里,命运不同。langgraph.types 里的 interrupt()Command 还在,这部分代码不用动。但 0.x 预置的 HumanInterruptHumanInterruptConfigActionRequest 被移除,相关能力并入 langchain.agents 的 middleware 体系(如 HumanInTheLoopMiddleware)。旧代码里凡是 import 了这组名字的,都要二选一重写。

选哪条路看你的现状,判断标准是「你当初用了预置类型的多少语义」:如果原本的审批流就是自己接的前端、自己的审批后台,预置类型只是帮你传了个话,直接重写成裸 interrupt() 加自定义 payload 最省事,语义几乎不变;如果当初就是想用预置的人审约定(审批动作的枚举、允许的交互类型那套结构),才值得评估 HumanInTheLoopMiddleware

重写成 interrupt() 的形状大致是这样:原来的中断请求结构变成你自己的 dict,原来依赖预置类型传递的审批动作,变成 payload 里显式的字段:

from langgraph.types import interrupt, Command

def review_node(state) -> dict:
    # 0.x 里用 HumanInterrupt/ActionRequest 拼的请求,
    # 1.x 直接用可序列化 dict,字段自己定,带上业务标识
    decision = interrupt({
        "type": "refund_approval",        # 业务唯一标识,恢复时对账用
        "order_id": state["order_id"],
        "amount": state["amount"],
    })
    if decision["action"] == "approve":
        return {"status": "approved"}
    return {"status": "rejected"}

恢复端用 Command(resume=...) 把人的决定传回来,恢复值按发生顺序对齐到各个 interrupt()。第 06 篇的六个坑(恢复值按索引匹配、一个节点每轮只能 interrupt 一次、payload 必须可序列化、前置副作用要幂等、别用 try/except 包 interrupt、并行分支按 id 映射恢复)在这一步全部适用,迁移不是绕过这些坑,是换个姿势踩到它们。人审子图迁移完,优先把「中断后恢复」的测试补跑一遍,这是整条迁移路径上最容易静默出错的地方。

第四步:清扫移除清单。 AgentState(含 Pydantic 版)、ValidationNodeMessageGraph 全部出局。逐个 grep 旧 import,命中即改:

grep -rn "AgentState\|MessageGraph\|ValidationNode\|HumanInterrupt\|create_react_agent" src/

三个移除项都有明确的对应物,改法不难,但值得理解它们为什么被移除,因为方向就是 1.x 的设计哲学:从「预置的全能默认」转向「显式的自定义」。AgentState 是一个预置的全能 state 结构,试图覆盖常见场景,代价是里面总有你用不到也控不住的字段;1.x 的口径是不再推荐 Pydantic 做 state,统一走 TypedDict 风格的显式声明,state 里有什么、每个 key 的合并规则是什么,全部自己写清楚(这套合并规则的机制在第 02 篇)。MessageGraph 是专为纯消息流场景准备的简化图,被 StateGraphMessagesState 取代,功能等价,只是不再需要一个专门的图类。ValidationNode 的校验逻辑挪进节点函数自己写,校验失败返回什么、要不要重试,本来就是业务语义,预置节点替你决定不了。

这一步没有技术难度,只有遗漏风险,所以 grep 要在迁移收尾时再跑一遍作为出厂检查:命中数为零才算完,靠记忆保证「应该都改完了」不算数。

第五步:Python 版本。 3.9 支持已移除,运行时和 CI 的矩阵一起升。这一步最容易被忘的不是主程序,是外围:Dockerfile 的基础镜像标签、CI 配置里的版本矩阵、内部文档里写的「需要 Python 3.9+」、内部工具库的 python_requires 声明。主程序升了、CI 里还留着个 3.9 的 job,报错会以一种和迁移毫无关系的形式出现在别人面前,排查的人不会把它和迁移联系起来。找齐这些角落没有捷径,把「python 3.9」作为关键词在仓库里全局搜一遍,命中处逐个确认。

三件评估阶段就该做的事

这三件事不在五步里,因为它们不属于「改代码」,但漏掉任何一件,返工成本都比五步加起来高。

第一件:验证存量 checkpoint 数据。 生产库里存着 0.x 写入的 checkpoint,迁移后新版本代码要能读它,恢复中的流程要能接着跑。这件事没有现成答案可抄:state schema 演进本来就没有官方版本化迁移工具(第 13 篇讲过),跨大版本的数据兼容更要自己验证。验证方法不复杂:预发环境用一份生产快照数据,起一个迁移后的服务,挑几个「中断中」的 thread 走一遍恢复路径,看状态字段是否都对。特别留意人审中断中的 thread:它们在数据库里躺了几天甚至几周,是「旧数据被新代码消费」的最纯粹案例。验证不过,就得在迁移方案里加一环:存量 thread 的处置策略(跑完再升、或显式作废重跑),这个决定越早做越从容。

第二件:盘点外围依赖。 除了 langgraph 家族,把仓库里所有 import 这些包的内部库、所有把它们写进 requirements 的部署单元列出来。多服务架构下经常出现「主服务迁了,旁边一个不起眼的工具服务还在用旧 API」的组合,这种组合在运行时才暴露,暴露形式是那个服务突然开始报 import 错误。盘点产出一张清单:服务名、用的包、迁移责任人。五分钟的事,省掉的是上线后一周的散装排障。

第三件:想好回滚方案。 默认的分支模型就够用:迁移在独立分支进行,生产一直跑旧版本,直到验收四条全过才合并发布。要想清楚的是合并后的回滚触发条件:发布后 N 小时内出现哪类错误就回滚。因为 checkpoint 数据已经被新版本写过,回滚不只是「把代码退回去」,还要回答「新版本写的数据,旧版本能不能读」。这个问题和第一件是同一个验证的镜像,一起验证掉。最稳的发布顺序是:先发一个只读过渡版本(新代码、但存量 thread 继续用旧 worker 处理完),存量清零后再全量切换。要不要做这一层,取决于你手里有多少跨天中断的长流程,流程越贵,这一层越值。

组织侧:排期与纪律

迁移是工程,工程有组织成本,三件事提前定好。

人。 迁移团队里必须有熟悉人审代码的人,而且最好是最初写它的人。人审是改动最碎、坑最深的部分,靠读代码上手的人来迁,踩坑概率成倍涨。如果原作者已经不在,先花半天让一个人把第 06 篇的六个坑和现网人审代码对上号,再开工。

排期。 迁移期间冻结相关功能开发。这不是教条:迁移中主干同时在加新功能,等于迁移的回归基线一直在漂,测试全绿永远等不到。真要并行,就把功能开发放进独立分支,迁移合并后再 rebase,代价算清楚再选。标准用法半天档的项目不需要这么正式,两周档的项目必须。

评审。 每个子图迁移单独一个 PR,评审重点是「语义等价」而不是「代码好看」:恢复值结构变了没有、payload 字段名变了没有、幂等键还成立吗。名字迁移类的改动可以批量过,人审和 state 定义的改动必须逐行看。评审清单就三条:中断恢复路径测过、state 字段无增删改(有则列出兼容方案)、幂等键未变。

pip install -U 即刻炸 import。 这是最疼的一个,值得把因果链讲透。你升级的是 langgraph,但它的依赖声明会把 LangChain 1.0 一并拉进来;项目里如果还有旧的 langchain 链式 import(prompts、chains 那一套),这些 import 立刻断。疼在报错的位置和原因完全对不上:炸的是 langchain 的 import 路径,你第一反应不会想到是「迁移 LangGraph」引发的,排查时间全耗在这层错位上。社区里「升级完跑不起来」的帖子密度很高,解法只有一条:全家族精确锁版本,升级走变更流程(先预发、再生产),别用 -U 裸升。

第一次升级前还有个便宜的自保动作:在一个干净环境里按目标版本组合装一遍、跑一遍测试套件,确认依赖解析出来的全家桶版本就是你想要的组合。这个动作五分钟,能把「升完发现版本组合不对」的返工挡在开发机阶段。这条纪律和第 13 篇讲的 patch 版本事故(langgraph-checkpoint-postgres 2.0.22 打挂 metadata 序列化)是同一件事的两面:这个家族里,任何形式的「顺手升级」都有前科。

DeprecationWarning 不是安全期。 0.x 后期的弃用警告意味着 1.0 移除,看到警告就该排期改,而不是压掉警告继续跑。很多团队的默认做法是过滤警告让日志干净,这等于把迁移预告全部签收为「不处理」,等到 1.0 落地,每一张被压掉的警告都变成一次线上报错。建议的姿势:CI 里用 -W error::DeprecationWarning 把警告提升为报错,做不到这么严的,至少每周统计一次警告数量,数量开始涨就是迁移排期的信号。警告是官方给你的迁移排期表,扔掉它等于扔掉唯一的预告。

多 agent 项目是重灾区。 单 agent 改两行完事,多 agent 项目里 prebuilt 的 supervisor/swarm 写法、状态传递、人审类型缠在一起,社区有迁移到一半卡住求援的长帖。缠在一起的根因是这三个东西共享同一批 0.x 预置类型,动一个牵三个。这类项目的正确姿势是逐个子图迁移、每个子图过完测试再动下一个。前面说过同环境装不下两个大版本,所以「新旧共存」指的是代码层面:迁移分支上,未迁移的子图先按 1.x 语义做最小等价改写,已迁移的子图完整优化,每合入一批就全量回归一次,保持主干随时可发布。排序上建议先迁没有人审的子图,把纯结构性的改动清完,最后攻人审子图:人审是类型改动最碎的部分,单独隔离处理,不和结构迁移混在同一个变更里。

旧教程识别指南

拿到一篇 2025 年之前的教程或一份翻译站内容,看三个信号判断是否已过期:

  1. import 里有 from langgraph.prebuilt import create_react_agent,0.x 无疑。
  2. 文档域名是 langchain-ai.github.io/langgraph,旧站已停止更新,正身在 docs.langchain.com
  3. 人审示例用 interrupt_before 而不是 interrupt(),旧范式,官方已明确不推荐用于 HITL。

这三个信号的可靠性来自它们分别踩在三个层面上:包边界(import 路径)、基础设施(文档域名)、范式(人审写法)。三个层面同时过期的内容,在概念讲解上可能仍然正确(状态机的思想没变过),但代码一定不能抄。命中任意一条,只当概念参考。

拿不准的时候有一个机械的验证办法:把教程里的第一段代码放进 1.x 环境跑一遍。跑得过,至少 API 层面是新的,再往下看;跑不过,直接归入旧教程 pile,不值得逐段甄别。这个办法比看发布日期可靠,因为好文章会更新,更新后的旧文日期是新的,API 还是旧的。

翻译站还有一层额外的问题:原文更新了翻译不一定跟,页面上也看不出翻译时间与原文版本的对应关系,所以对翻译站的信任级别要再降一档。识别旧内容这件事值得变成团队习惯:新人入职要看的第一个内部文档,就应该有这三条信号,成本一段话,省掉的是每个新人一次「为什么跑不起来」的排查。

迁移后高频报错对照

迁移收尾阶段出现的问题,大多能按「报错形态」直接定位到迁移步骤。对照关系如下,报错文本以实际为准,这里给的是排查入口:

  • import 阶段就炸,报的是包内不存在的模块或名字。 九成是旧 import 路径没清干净,回到 grep 清单补漏。如果报错的是 langchain 系的 import,先查依赖树里 langchain 的实际版本,确认是不是被联动升上来的(对应 pip install -U 那个坑)。
  • 调用阶段报不认识的关键字参数。 典型是 prompt 相关参数:旧参数名还在传,参数名对照第二步逐个核。
  • 人审流程「看起来能跑,恢复后行为不对」。 恢复值对不上预期,按第 06 篇的坑二排查:多个 interrupt 时恢复值按发生顺序匹配,迁移重构时如果调整过节点内 interrupt 的先后位置,所有恢复端都要跟着对。
  • 恢复旧 thread 时状态字段缺失。 存量 checkpoint 和新 state 定义对不上了,对应「三件事」里的第一件,补存量数据处置策略,别让线上自己发现。
  • 运行时和 CI 报 Python 版本不兼容。 外围角落没清完,按第五步的全局搜索再过一遍。

这张表值得在迁移期贴在团队频道里:一半的迁移排障,其实是在重复确认这五种已知的错位。

权衡

迁移的真实成本分布很不均匀:标准用法半天,人审重度项目一两周。评估时按「人审类型引用量 × 多 agent 复杂度」估,不要按代码行数。这个公式的原因在上面都出现过:行数多但都是简单改名的项目最便宜;行数少但 HumanInterrupt 遍布各处、加上多子图耦合的项目最贵。动手前先跑一遍上面那条 grep,数一数命中分布:create_react_agent 的命中是便宜的工作量,HumanInterrupt 的命中是贵的工作量,两类命中的比例基本决定了你是半天档还是两周档。

成本还要分「一次性」和「持续性」两类来看。改 import、换参数名、迁人审类型,全是一次性的,做完就结束。持续性成本只有一项但永久存在:预置能力挪进 LangChain 之后,你要同时追两个仓库的变更。这个成本没法消除,只能管理,管理方式就是把 langchain 的 release note 固定进升级 checklist。忽略它的代价不是抽象的:下一次家族联动升级时,破坏性变更从你没看的那个仓库里来,你又一次体验「报错位置和原因对不上」的排查。

另一个值得记录的观察:这一轮迁移后官方把「预置能力」从 LangGraph 挪去了 LangChain,意味着以后追 changelog 要看两个仓库,升级 checklist 里应该把 langchain 的 release note 加进去,这是 0.x 时代没有的心智负担。它也是第 01 篇说的「分层倒转」在迁移侧的投影:引擎和壳分家之后,壳的变更也要进你的视野。把这一条写进团队的升级流程文档,比记在个人脑子里可靠,人会在下一次大升级前离职,流程文档不会。

最后是验收标准,迁移算完成的定义建议写成四条:grep 清单命中清零;核心路径测试全绿(含人审恢复路径);预发环境按目标版本组合连续跑一周无新增报错;DeprecationWarning 统计归零。四条都过再动生产,跳过任何一条省下的时间,都会以排障的形式还回来。

延伸阅读


原文出处:91ai / LangGraph 生产实战系列

评论

0
登录后可以参与评论和讨论。

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。