LangChain v1.0迁移实战:从LCEL链到LangGraph状态机

发布时间:2026/10/21 9:03:13
LangChain v1.0迁移实战:从LCEL链到LangGraph状态机 最近在把一个基于 LangChain v0.x 的老应用往 v1.0 迁版本从 v0.3 一路推到 v1.0中间恰好踩在 LangGraph v1.0-11 这个节点上。这一轮迁移最核心的目标不是“把 import 改一改跑通”而是系统性梳理升级策略和 Breaking Changes真正把旧链路切到新版推荐的编排方式上。这篇东西就是这次迁移的完整记录适合手里有 v0.x 代码、准备升 v1.0 但对变化范围心里没底的团队也适合那些刚接触 v1.0、想知道新旧版本边界在哪的开发者。先说一个结论v1.0 不是 v0.x 的“修修补补”它把 LangGraph 放到了框架中心把原来 LCEL 链的很多职责收编进图里。如果从一开始就按“v0.x 怎么写v1.0 就换个 import 继续写”的思路去迁百分之百会中途卡住。我这次迁移前后花了一周中间反复回滚了三次最后才整理出一套相对平滑的升级路径。下面按升级顺序展开。1. 升级前先搞清楚 v1.0 到底动了哪些筋骨1.1 用一张对照表看懂变化范围v0.x 到 v1.0 的破坏性变更并不集中在某一个 API 上而是分散在包结构、模型调用、工具注册、流式事件、持久化这几个层面。如果只盯着某一个报错去改会陷入“改一个崩一个”的循环。我先列一张我自己的对照表建议你迁移前也建一张类似的变化领域v0.x 写法v1.0 推荐写法影响程度核心抽象导入langchain.llms/langchain.chat_models统一走langchain_core与集成子包致命编排方式LCEL链prompt | llm | parserStateGraph/LangGraph图高消息输入llm.invoke(一句话)llm.invoke([HumanMessage(content...)])中高结构化输出output_parsers.PydanticOutputParserllm.with_structured_output(Schema)中工具调用toolbind_toolstoolToolNode 图节点高流式事件astream_events()老版本默认参数astream_events(..., versionv2)中高会话记忆ConversationBufferMemorycheckpointerMessagesState高这张表不是我拍脑袋列的而是迁移过程中真实遇到的报错和重写点。看完这张表你会发现v1.0 的设计逻辑很明确核心抽象全部下沉到langchain_core业务编排统一交给 LangGraph模型和向量库等外部依赖用独立集成包安装。理解了这个逻辑后面改代码就有方向了。1.2 升级前必须完成的三件准备工作第一件事锁定旧版本快照。迁移不是一蹴而就的你需要一个能随时回滚的基线。我当时先把requirements.txt里所有langchain/langgraph/langchain-openai等版本号固定下来用pip freeze导出当前环境保存成requirements-lock-v0.txt。后面每次尝试迁移都可以回到这个状态重来。第二件事打开 deprecation 警告。v0.x 的多数废弃 API 在真正删除前都保留了过渡期代码运行时会有DeprecationWarning。不要忽略这些警告它们是官方给你的免费迁移清单。运行应用时加上-W default::DeprecationWarning或者直接在脚本顶部写warnings.filterwarnings(default, categoryDeprecationWarning)把所有废弃调用收集到日志里。我靠这个方式一次性抓出了十几个隐藏的旧接口引用。第三件事全局扫描旧的导入路径。在项目根目录执行grep -rn from langchain.llms\|from langchain.chat_models\|from langchain.chains\|from langchain.memory --include*.py .把所有命中的文件列出来按“纯内部模块”和“对外服务入口”分类。这里要特别注意不要用正则一键全局替换因为同一个函数名在新旧包里的语义可能有细微差别需要逐文件确认。我建议把扫描结果先放一边等理解了核心 API 变化后再动手改。2. 依赖与 import 路径的系统性迁移2.1 顶层入口收敛到 langchain_corev1.0 最直观的变化是顶层包结构。老的langchain.chains、langchain.agents、langchain.llms这些模块基本都被拆掉了核心的数据结构、端口、抽象类统一集中在langchain-core这个包里。听起来变化很大但实际操作起来反而比 v0.x 清晰。我举几个最常见的旧写法以及对应的新写法# 旧写法v0.x from langchain.llms import OpenAI from langchain.chat_models import ChatAnthropic from langchain.chains import LLMChain from langchain.prompts import ChatPromptTemplate from langchain.output_parsers import PydanticOutputParser# 新写法v1.0 from langchain_openai import ChatOpenAI from langchain_anthropic import ChatAnthropic from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import PydanticOutputParser from langchain_core.messages import HumanMessage, SystemMessage, AIMessage注意这里langchain_core并非要你直接依赖一个叫core的包名而是一个独立的 PyPI 包权限模型、消息对象、提示词模板、回调基类全在这里。langchain本身保留的是少数上层工具但它不再是“什么都能 import 的瑞士军刀”。我建议迁移时先把langchain_core的版本也固定住最好和langchain主版本保持同一大版本避免小版本之间的隐性不兼容。2.2 模型集成包统一命名与依赖声明第二件事是模型供应商的包拆分。v0.x 时代OpenAI、Anthropic、各种本地模型都可以从langchain或langchain_community里导入。v1.0 则明确要求模型接入使用独立的langchain-*集成包。我这次项目的依赖从这样langchain0.3.x langchain-community openai变成这样langchain1.0,2 langchain-core1.0,2 langgraph1.0.11,2 langchain-openai langchain-anthropic langgraph-checkpoint这里有个关键点langchain-openai和直接装openaiSDK 是两回事。langchain-openai是 LangChain 官方封装层它内部会依赖 OpenAI 的 Python SDK但你在业务代码里只跟ChatOpenAI打交道不需要自己管理OpenAIclient。升级后ChatOpenAI的初始化参数和 v0.x 基本一致from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o-mini, temperature0, )实测下来这种拆分让重装有依赖问题的概率大幅下降。以前装一个langchain-community会把一堆可能用不到的驱动全拉进来现在每个集成包都是轻量的装错、冲突的概率小很多。2.3 需要手动处理的“幽灵 import”最隐蔽的一类问题是代码里并没有直接写“被删除的模块”但通过其他方式间接依赖老路径。比如某个工具函数里写了from langchain.agents import Tool这个在 v1.0 里已经不再推荐Tool应该来自langchain_core.tools。我当时用grep扫出来 30 多个文件里有这类引用逐个确认后统一改掉。另一个容易漏的是动态导入或者字符串形式的类引用例如某些配置系统用importlib.import_module(langchain.llms)。对于这种情况建议直接全局搜索关键词langchain.开头的 import逐条对照新的路径规则。如果项目规模大、担心漏改我建议先把pytest基线的用例跑熟再动 import。没有测试保护直接改 import后期报错会非常痛苦。至少要有冒烟用例覆盖“模型能调用”“一条消息能走通”“一个简单工具能被绑定”这三条主线。3. 把 LCEL 链改成 LangGraph 图的编排迁移3.1 为什么要从链过渡到图v0.x 时代LCEL 是官方推荐的编排方式prompt | llm | parser这种管道式写法简洁优雅我也很喜欢。但 v1.0 实际上把 LangGraph 作为默认的编排框架了原因是 LCEL 倾向于表达“固定管线”一写死就是单向流很难处理循环、条件分支、人工介入、多 Agent 协作这些真实业务场景。为了讲清楚这个变化我用一个常见的“客服问答”逻辑来对比。老的 LCEL 写法大概是# v0.x LCEL prompt ChatPromptTemplate.from_messages([ (system, 你是客服助手), (human, {input}), ]) chain prompt | llm | StrOutputParser() response chain.invoke({input: 请问退货流程是什么})这段代码没什么问题但如果要加上“先判断用户是否在问退货再决定要不要调用售后系统工具”LCEL 就只能在一边旁路写RunnableBranch代码很快就变拧巴了。LangGraph 的方式是把每条处理逻辑定义成节点节点之间的连线就是状态转换复杂逻辑只是加节点和边而已。3.2 一条典型链的图式改写我用刚才那个客服场景展示 v1.0 的最小 LangGraph 写法from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langgraph.checkpoint.memory import MemorySaver class State(TypedDict): messages: Annotated[list, add_messages] def chat_node(state: State): response llm.invoke(state[messages]) return {messages: [response]} builder StateGraph(State) builder.add_node(chat, chat_node) builder.add_edge(START, chat) builder.add_edge(chat, END) graph builder.compile(checkpointerMemorySaver())这段代码里最需要理解的是State和add_messages。State是整个图执行过程的“共享黑板”所有节点读它、写它。add_messages是一个 reducer定义了“当多个节点往messages字段里写值时应如何合并”——它不是覆盖而是追加。这一下就解决了老代码里“手动维护对话历史列表”的麻烦LangGraph 会自动把上一轮的状态传给下一轮执行。调用方式也简单config {configurable: {thread_id: user-123}} result graph.invoke({ messages: [HumanMessage(content请问退货流程是什么)] }, configconfig)thread_id就是会话标识同一个会话的多次调用会共享历史状态。这一点对老代码迁移特别有价值因为它直接替代了以前ConversationBufferMemory的职责而且状态存储在 Checkpointer 里天然支持持久化和并发隔离。3.3 状态定义与 reducer 的入门避坑第一次从链迁移到图的人最容易在Annotated和 reducer 上栽跟头。state[messages]本身是一个普通的列表add_messages做的事是把新消息与已有消息合并而不是简单append。如果你在自定义节点里直接返回{messages: state[messages] [new_msg]}虽然也能跑但会丢失 LangGraph 针对重复消息去重的内置行为。另一个容易踩的坑是节点返回值里不需要回传整个 state。LangGraph 只更新你返回的那些字段其他字段保持不动。所以节点函数写得短小干净就好不要产生“手动搬运全局状态”的冲动。我在迁移一个老 Agent 时就是因为每个节点都把整个 state 传了一遍导致图运行速度明显下降后来砍掉了大量冗余字段才恢复。4. 模型调用、结构化输出与工具调用的破坏性变更4.1 字符串消息不再是无脑可用的输入v0.x 后期虽然也推荐用消息对象但很多代码里仍然习惯直接llm.invoke(你好)。v1.0 对这个行为收紧了规范化直接传字符串的做法被抛弃推荐显式传消息列表。原因是字符串输入缺少对角色、工具调用记录这些上下文的表达能力容易出现歧义。改成from langchain_core.messages import HumanMessage response llm.invoke([HumanMessage(content你好)])别小看这个变化。它会牵连出一批业务代码比如以前在 Agent 里拼 prompt 字符串的写法现在都要改成构造消息序列。不过习惯之后会发现消息列表的方式反而更符合“对话系统”的直觉system、user、assistant、tool 四种角色各有各的载体上下文的层次清晰很多。4.2 结构化输出统一走 with_structured_outputv0.x 时代要做 JSON 输出不少人用的是PydanticOutputParser加format_instructions或者自己写提示词让模型输出 JSON。这种方案的问题在于格式提示是一条“软约束”模型偶尔会不遵守解析就崩。v1.0 推荐的with_structured_output把 schema 直接交给模型 API由模型端原生保证结构解析成功率提升非常明显。最小示例from pydantic import BaseModel, Field from langchain_openai import ChatOpenAI class MovieReview(BaseModel): title: str Field(description电影名) score: int Field(description评分1到10) llm ChatOpenAI(modelgpt-4o-mini) structured_llm llm.with_structured_output(MovieReview) review structured_llm.invoke([HumanMessage(content评价一下某部电影)]) print(review.title, review.score)从PydanticOutputParser迁移到with_structured_output不只是换个 API还涉及业务判断如果模型输出失败异常类型和捕获逻辑会不同。with_structured_output在模型端不返回合法 JSON 时通常会直接抛错需要做好try/except和 fallback。如果你的下游要求高可用我建议加一段兜底逻辑结构化输出失败时降级成普通文本输出再用传统 parser 二次解析。4.3 工具注册从 bind_tools 到 ToolNodev0.x 时代工具调用的常见路径是tool定义工具然后用llm.bind_tools(tools)让模型看到工具。到了 v1.0虽然bind_tools本身没有消失但更完整的方案变成了ToolNode。我这次踩了一个很真实的坑直接把 v0.x 的agent_executor AgentExecutor(agentagent, toolstools)代码往上搬结果发现AgentExecutor在 v1.0 里已经边缘化了。官方推荐的是create_react_agent或者自己用StateGraph搭建带ToolNode的图。最简单的迁移方式是直接用langgraph.prebuiltfrom langchain_core.tools import tool from langgraph.prebuilt import create_react_agent tool def get_weather(city: str) - str: 查询指定城市的天气。 return f{city} 今天多云25度 tools [get_weather] agent create_react_agent(llm, tools) result agent.invoke({ messages: [HumanMessage(content北京天气怎么样)] })create_react_agent的好处是把“模型决策→调用工具→返回结果→再决策”这条循环封装好了不需要你自己手动写循环节点。如果你的场景需要严格控制工具调用时机再演进到自定义StateGraphToolNodefrom langgraph.prebuilt import ToolNode builder StateGraph(State) builder.add_node(agent, agent_node) builder.add_node(tools, ToolNode(tools)) builder.add_edge(START, agent) builder.add_conditional_edges(agent, should_continue, [tools, END]) builder.add_edge(tools, agent)这里should_continue是一个判断函数如果模型给出的最后一条消息带tool_calls就去tools节点否则结束。这样一套下来对工具循环的控制力比老的AgentExecutor强很多。5. 流式事件、回调与持久化改造5.1 astream_events 在 v1.0 里的行为变化如果你负责的模块里有前端打字机效果那必然绕不开流式输出。v0.x 里的astream_events有个很烦的问题是默认会输出大量嵌套事件需要层层过滤才能拿到真正的 token。v1.0 把默认行为改成了“只显示引擎层的关键事件”减少噪音。代价是老代码如果没指定version参数事件结构会变前端解析逻辑就得跟着改。新版推荐写法async for event in graph.astream_events( {messages: [HumanMessage(content讲个故事)]}, versionv2, ): if event[event] on_chat_model_stream: chunk event[data].get(chunk) if chunk is not None: print(chunk.content, end)注意这里有两个细节。第一versionv2必须显式传不传它会按新的默认规则走但为了兼容很多老代码其实还挂着旧事件结构。第二事件的event名称仍以on_chat_model_stream这类为准但chunk的取法略有差异。我建议先写一个小脚本把完整事件打出来看一遍再改前端对事件的过滤条件不要一眼都不看就按老代码改。5.2 Checkpointer 与持久化带来的状态管理革命v1.0 中会话记忆从“内存里的变量”变成了“可插拔的持久化存储”。MemorySaver只适合本地调试重启后状态会丢。线上场景一般用SqliteSaver或PostgresSaver。一个 Sqlite 持久化示例from langgraph.checkpoint.sqlite import SqliteSaver with SqliteSaver.from_conn_string(checkpoints.sqlite) as saver: graph builder.compile(checkpointersaver) config {configurable: {thread_id: session-a}} result graph.invoke( {messages: [HumanMessage(content你还记得我名字吗)]}, configconfig, )从ConversationBufferMemory迁移到这个模式有一个思维转换以前你在一个链对象里塞一个全局 memory每次调用自动记住现在你必须在每次调用时都带上config里的thread_id否则每次调用都是独立会话模型“失忆”。我当时踩的坑就是前端每次请求都生成一个新的thread_id结果用户对话永远接不上。修复方式很简单——把thread_id和用户 ID 绑定存到前端 cookie 或后端 session 里。5.3 回调与可观测性迁移v1.0 里回调体系和 v0.x 相比变化不大但导入路径清了。像from langchain_core.callbacks import BaseCallbackHandler这类写法在 v0.x 后期已经逐渐统一到 core升级到 v1.0 反而更顺。真正需要留意的是当图执行时回调事件会嵌套在节点执行过程中你要在on_chat_model_start、on_tool_start这些事件里区分当前处于哪个节点。我在实践里的做法是自定义一个 Handler在事件里打上run_id和节点名。然后配合 LangSmith 之类的可观测平台排查问题。不要小看这一步图和链的调试方式完全不同链是单向的出错看栈就能定位图有循环、有分支不加 trace 几乎没法调。6. 高概率踩坑清单与迁移节奏建议6.1 高频报错对照速查表迁移过程中我记录了一批出现频率最高的报错这会是你升级时最实用的速查表报错或现象大概率原因处理动作ModuleNotFoundError: No module named langchain.llms旧顶层模块已移除改为langchain_openai/langchain_anthropic等子包TypeError: invoke() got an unexpected keyword argument input旧链的入参方式和图/模型接口不兼容改用{messages: [...]}或messages入参ValueError: Must provideversionwhen using astream_events流式事件版本参数缺失显式传versionv2对话总是丢失上文每次调用没有复用thread_id将thread_id与会话绑定工具调用报“Tool not found“工具未注册到ToolNode或模型不可见检查bind_tools或tools[...]列表AttributeError: AgentExecutor object has no attribute ... 仍在使用旧版编排器迁移到create_react_agent或自定义 StateGraphPydantic 校验报错结构化输出 schema 使用了旧版Field语法统一升级到 Pydantic v2 字段写法这些报错有个共同特点它们不会一次性出现而是修一个冒一个。所以不要指望一个晚上改完最好留出充足的时间做回归。6.2 迁移节奏怎么安排才算“平滑”我这次按三个阶段推进效果不错。第一阶段保守迁移。只替换 import 路径和包名不重写业务逻辑。目标是让原有链在 v1.0 依赖下能跑通哪怕还在用 LCEL 也没关系。这个阶段能快速暴露存量问题风险最低。第二阶段编排重构。把核心业务链逐步改成 LangGraph 图优先迁“有状态”的 Agent 和“有工具调用”的链路。像那种纯 Prompt 模型输出的简单链可以留着用 LCEL不强制一步到位。第三阶段能力升级。全部链路切到图后再统一上持久化、流式事件 v2、结构化输出这些新能力。这一步才是真正吃透 v1.0 的时刻前面两步不过关直接上这一步会把问题搅在一起。6.3 几个我后来觉得“幸好提前做了”的决定回看这次迁移有几个决定帮我省了很多时间。第一我保留了完整的requirements-lock-v0.txt每次改一半想放弃时都能快速还原这让我试错成本极低。第二我新写了一套最小回归脚本专门覆盖“模型调用、简单对话续接、工具调用、结构化输出”四条路径任何一次改动后跑一遍脚本五分钟就能确认有没有引入新的破坏。第三我没有删除任何旧工具函数只在接口层做了适配等新链路验证稳定后再逐步清掉旧代码。这次迁移让我最大的体会是升级框架真正的成本从来不在“换包名”而在“换心智模型”。LCEL 链的思维是“数据流”LangGraph 的思维是“状态机”两者看着都能实现同一个 Demo但面对真实业务场景时状态机的表达力和可维护性高太多了。如果你正在计划升级我建议第一周只做一件事把官方文档里关于 State、节点、边的三个概念吃透再用最简单的两节点图跑通一次对话。这一步走顺了剩下的迁移就是体力活。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询