Skip to main content

15 · LangGraph 是怎么”踩在”langchain_core 肩膀上的

开头的现象

小林学完第 11 篇的 LangGraph,心里冒出一个疑问:“LangGraph 的图、节点、checkpointer,这些跟 langchain_core 到底是什么关系?它们是不是两个独立的东西,只是碰巧都叫’Lang’?” 他随手 pip show langgraph,看到一行让他愣住的话:
LangGraph 的依赖里只有 langchain-core——连 langchain 主包都没依赖! 小林更好奇了:一个”编排框架”居然只依赖”地基包”,它到底从 core 里偷了多少东西? 他决定扒开 langgraph 的源码,看它到底”踩在”core 的哪些肩膀上。

第一幕:依赖声明——LangGraph 只认 core,不认主包

小林先看依赖关系(实测):
而且 langgraph_checkpoint(独立的 checkpointer 包)也只依赖 langchain-core>=0.2.38 最反直觉的发现:小林 grep 了整个 langgraph 包,搜 import langchain(不带 _core)——0 处!反而是 langchain 主包(1.3.14)依赖 langgraphRequires-Dist: langgraph<1.3.0,>=1.2.5)。
小林画了个箭头,明白了:LangGraph 是个”独立门派”,但它拜的祖师爷是 langchain-core——不是 langchain 主包。
想验证吗?pip show langgraph 看 Requires 一栏,只有 langchain-core;再 pip show langchain 看 Requires,会发现它依赖 langgraph。两个方向正好相反。

第二幕:图本身就是一个 Runnable——core 的”统一调用协议”被整碗端走

小林最惊讶的发现在这里。他打开 langgraph/pregel/protocol.py,第 25 行:
LangGraph 的图执行器(Pregel)直接继承 langchain_core 的 Runnable! 这意味着:
这就是为什么 create_agent 返回的图能像模型一样 invoke()——因为它本来就是 Runnable 的后代。LangGraph 没有另起炉灶定义”图的调用方式”,而是直接复用 core 的 Runnable 协议:图 = 一种特殊的 Runnable。 小林想起第 09 篇学的 a | b(LCEL 管道)——他试了一下:
想验证吗?isinstance(app, Runnable) 返回 True——图就是 Runnable 的孩子,invoke/stream/batch/| 全是继承来的。

第三幕:图的状态 = langchain_core 的消息列表

小林继续翻,发现 MessagesState 的定义(langgraph/graph/message.py 第 372 行):
图里流转的”消息状态”,就是 langchain_core 的消息列表! 你第 10 篇学的 AIMessageHumanMessageToolMessageRemoveMessage——全部来自 langchain_core.messages,LangGraph 直接拿它们当状态。 他数了数 langgraph/graph/message.py 的 import:
还有那个 add_messages reducer——它负责把新旧消息合并(按 id 去重)。小林发现它的核心逻辑全是操作 BaseMessage:convert_to_messages 把 dict/元组归一化成消息对象、按 m.id 合并、用 RemoveMessage 做”墓碑删除”。 这就是为什么图里的消息和模型返回的消息是同一套东西——它们本来就是同一个类。
想验证吗?from langgraph.types import AnyMessage; AnyMessage.__module__ 会显示 langchain_core.messages——同一个类,两个名字。

第四幕:节点 = RunnableLambda——core 的”函数变 Runnable”

小林发现 LangGraph 处理节点的方式也复用 core。他打开 langgraph/_internal/_runnable.py 第 529 行:
你写的节点函数 def node_a(state),LangGraph 内部用 core 的 RunnableLambda 包了一下——所以你才能在图节点里用 LCEL 的东西(|、RunnablePassthrough 等)。 小林恍然大悟:“我说节点函数怎么跟 Runnable 那么像——LangGraph 压根就是把它们包成了 Runnable!”
想验证吗?把一个函数传给 coerce_to_runnable,返回的 type()RunnableLambda——core 的类。

第五幕:checkpointer 怎么存消息——借 core 的序列化 + 白名单

小林最后看 checkpointer——它怎么把 BaseMessage 存进 SQLite/内存?他发现是”两级白名单”: 第一级:msgpack 白名单(checkpoint/serde/_msgpack.py 第 21 行)
第二级:运行时动态注册(_internal/_serde.py 第 60 行)
还有 JSON 格式的 checkpointcheckpoint/serde/jsonplus.py 第 31 行 from langchain_core.load.load import Reviver,用 core 的 Reviver 反序列化消息。 小林的顿悟checkpoint 存消息时,是”借用 langchain_core 的序列化机制 + 自己封一层 msgpack”。消息能被安全地存进 SQLite、取出来还是原样的 BaseMessage——全靠 core 提供的序列化协议。
想验证吗?跑第 11 篇的 checkpointer 例子,存入 SQLite 后再读出来,type(msg) 还是 AIMessage——core 的序列化保证了消息”存进去是啥、取出来还是啥”。

第六幕:流式回调、模型接口——core 的最后两处借力

小林又发现两处: ① 流式靠 core 的回调钩子(pregel/_messages.py 第 49 行)
第 11 篇学的 stream_mode="messages"(打字机效果),底层是 LangGraph 挂了个继承 core 的 BaseCallbackHandler 的处理器,监听 on_llm_new_token 等回调。 ② 预置 Agent 用 core 的模型接口(prebuilt/chat_agent_executor.py 第 13 行)
create_agent 的模型参数,类型就是 core 的 BaseChatModel——所以你能把任何继承它的模型(ChatAnthropic/ChatDeepSeek/ChatOpenAI)塞进去。

结论(小林扒完源码换来的)

LangGraph 是”踩在 langchain_core 肩膀上的编排层”——它的依赖只有 langchain-core(连主包都不依赖),并且从 core 借了五样东西:
一句话:LangGraph 只新增了”图编排”(节点/边/条件边/checkpointer 的状态机),其余全部复用 langchain_core——消息、Runnable、回调、序列化、模型接口。这就是为什么 LangGraph 能和 langchain_core 无缝衔接:它们本来就是一家人,LangGraph 是站在巨人肩膀上的那个。

复现信号:什么时候你会想起这一章

  1. 图能 invoke()/|/stream()——因为图是 Runnable 的孩子(core 的协议)
  2. 图里能放 LCEL 组件(RunnablePassthrough 等)——节点被包成 RunnableLambda
  3. checkpointer 存的消息取出来还是原样——core 的序列化白名单在背后工作
  4. 想给图加模型——传任何继承 BaseChatModel 的模型(core 的接口)
  5. 纠结”LangGraph 和 langchain_core 啥关系”——LangGraph 只依赖 core,是”站在巨人肩膀上的编排层”
现在小林搞懂了 LangGraph 的”家底”。他最后一个问题:“那我天天 import 的 langchain_core,整个包里到底装了什么?为什么有些模块我天天用,有些我这辈子都用不上?” 他决定把整个地基翻一遍——翻到第 15 篇:langchain_core 全地图