15 · LangGraph 是怎么”踩在”langchain_core 肩膀上的
开头的现象
小林学完第 11 篇的 LangGraph,心里冒出一个疑问:“LangGraph 的图、节点、checkpointer,这些跟 langchain_core 到底是什么关系?它们是不是两个独立的东西,只是碰巧都叫’Lang’?” 他随手pip show langgraph,看到一行让他愣住的话:
第一幕:依赖声明——LangGraph 只认 core,不认主包
小林先看依赖关系(实测):langgraph_checkpoint(独立的 checkpointer 包)也只依赖 langchain-core>=0.2.38。
最反直觉的发现:小林 grep 了整个 langgraph 包,搜 import langchain(不带 _core)——0 处!反而是 langchain 主包(1.3.14)依赖 langgraph(Requires-Dist: langgraph<1.3.0,>=1.2.5)。
想验证吗?pip show langgraph看 Requires 一栏,只有 langchain-core;再pip show langchain看 Requires,会发现它依赖 langgraph。两个方向正好相反。
第二幕:图本身就是一个 Runnable——core 的”统一调用协议”被整碗端走
小林最惊讶的发现在这里。他打开langgraph/pregel/protocol.py,第 25 行:
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 行):
AIMessage、HumanMessage、ToolMessage、RemoveMessage——全部来自 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 行)
checkpoint/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 行)
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 借了五样东西:
复现信号:什么时候你会想起这一章
- 图能
invoke()/|/stream()——因为图是 Runnable 的孩子(core 的协议) - 图里能放 LCEL 组件(RunnablePassthrough 等)——节点被包成 RunnableLambda
- checkpointer 存的消息取出来还是原样——core 的序列化白名单在背后工作
- 想给图加模型——传任何继承
BaseChatModel的模型(core 的接口) - 纠结”LangGraph 和 langchain_core 啥关系”——LangGraph 只依赖 core,是”站在巨人肩膀上的编排层”
langchain_core,整个包里到底装了什么?为什么有些模块我天天用,有些我这辈子都用不上?” 他决定把整个地基翻一遍——翻到第 15 篇:langchain_core 全地图。