> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

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

> LangGraph 踩在 langchain_core 肩膀上：Runnable、消息体系、序列化的复用。

# 15 · LangGraph 是怎么"踩在"langchain\_core 肩膀上的

## 开头的现象

小林学完第 11 篇的 LangGraph，心里冒出一个疑问：**"LangGraph 的图、节点、checkpointer，这些跟 langchain\_core 到底是什么关系？它们是不是两个独立的东西，只是碰巧都叫'Lang'？"**

他随手 `pip show langgraph`，看到一行让他愣住的话：

```text theme={null}
Requires-Dist: langchain-core<2,>=1.4.7
```

**LangGraph 的依赖里只有 langchain-core——连 langchain 主包都没依赖！** 小林更好奇了：一个"编排框架"居然只依赖"地基包"，它到底从 core 里偷了多少东西？

他决定扒开 langgraph 的源码，看它到底"踩在"core 的哪些肩膀上。

## 第一幕：依赖声明——LangGraph 只认 core，不认主包

小林先看依赖关系（实测）：

```text theme={null}
langgraph 1.2.10 的依赖（pip show / METADATA 实测）：
  langchain-core<2,>=1.4.7     ← 唯一的 LangChain 系依赖！
  langgraph-checkpoint>=4.1.0   ← 它自己的兄弟包（checkpointer）
  langgraph-prebuilt>=1.1.0    ← 预置 agent
  langgraph-sdk>=0.4.2         ← SDK
  pydantic>=2.7.4              ← 数据模型
  xxhash>=3.5.0                ← 哈希
```

而且 `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`）。

```text theme={null}
依赖方向（实测）：
  langchain（主包）──依赖──> langgraph ──依赖──> langchain-core
                                  ↑
                    LangGraph 只踩在 core 上，连主包都不理
```

小林画了个箭头，明白了：**LangGraph 是个"独立门派"，但它拜的祖师爷是 langchain-core——不是 langchain 主包。**

> 想验证吗？`pip show langgraph` 看 Requires 一栏，只有 langchain-core；再 `pip show langchain` 看 Requires，会发现它依赖 langgraph。两个方向正好相反。

***

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

小林最惊讶的发现在这里。他打开 `langgraph/pregel/protocol.py`，第 25 行：

```python theme={null}
# langgraph/pregel/protocol.py 第 25 行（实测源码）
class PregelProtocol(Runnable[InputT, Any], ...):
    # Runnable 来自 langchain_core.runnables
```

**LangGraph 的图执行器（Pregel）直接继承 langchain\_core 的 Runnable！** 这意味着：

```python theme={null}
# 你学过的 Runnable 方法，图全都有：
app.invoke(input)      # 来自 Runnable
app.stream(input)      # 来自 Runnable
app.batch(inputs)      # 来自 Runnable
app.with_config(...)   # 来自 Runnable
app.get_graph()        # 来自 Runnable
```

**这就是为什么 `create_agent` 返回的图能像模型一样 `invoke()`**——因为它本来就是 Runnable 的后代。LangGraph 没有另起炉灶定义"图的调用方式"，而是**直接复用 core 的 Runnable 协议**：图 = 一种特殊的 Runnable。

小林想起第 09 篇学的 `a | b`（LCEL 管道）——他试了一下：

```python theme={null}
# 图能跟普通 Runnable 用 | 串联！
chain = app | StrOutputParser()
# 因为 app 是 Runnable，天然支持 | 组合
```

> 想验证吗？`isinstance(app, Runnable)` 返回 True——图就是 Runnable 的孩子，`invoke/stream/batch/|` 全是继承来的。

***

## 第三幕：图的状态 = langchain\_core 的消息列表

小林继续翻，发现 `MessagesState` 的定义（`langgraph/graph/message.py` 第 372 行）：

```python theme={null}
# langgraph/graph/message.py 第 372 行（实测源码）
class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    #               ↑ langchain_core.messages.AnyMessage
```

**图里流转的"消息状态"，就是 langchain\_core 的消息列表！** 你第 10 篇学的 `AIMessage`、`HumanMessage`、`ToolMessage`、`RemoveMessage`——全部来自 `langchain_core.messages`，LangGraph 直接拿它们当状态。

他数了数 `langgraph/graph/message.py` 的 import：

```python theme={null}
from langchain_core.messages import (
    AnyMessage, BaseMessage, BaseMessageChunk,
    MessageLikeRepresentation, RemoveMessage,
    convert_to_messages, message_chunk_to_message,
)
```

**还有那个 `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 行：

```python theme={null}
# langgraph/_internal/_runnable.py（实测源码）
def coerce_to_runnable(thing):
    if isinstance(thing, Runnable):
        return thing          # 已经是 Runnable → 直接用
    if callable(thing):
        return RunnableLambda(thing)   # 普通函数 → 包成 RunnableLambda
    if isinstance(thing, dict):
        return RunnableParallel(thing) # dict → 并行分支
    ...
```

**你写的节点函数 `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 行）**

```python theme={null}
SAFE_MSGPACK_TYPES = {
    ("langchain_core.messages.base", "BaseMessage"),
    ("langchain_core.messages.base", "BaseMessageChunk"),
    ("langchain_core.messages.human", "HumanMessage"),
    ("langchain_core.messages.ai", "AIMessage"),
    ("langchain_core.messages.system", "SystemMessage"),
    ("langchain_core.messages.tool", "ToolMessage"),
    ...  # 全套消息类 + Document
}
```

**第二级：运行时动态注册（`_internal/_serde.py` 第 60 行）**

```python theme={null}
from langchain_core import messages as lc_messages
def curated_core_allowlist():
    # 把 15 个消息类动态加入反序列化白名单
    return {... (module, name) ...}
```

**还有 JSON 格式的 checkpoint**：`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 行）**

```python theme={null}
class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
    # BaseCallbackHandler 来自 langchain_core.callbacks
    # stream_mode="messages" 就是靠它接住模型的 token 流
```

第 11 篇学的 `stream_mode="messages"`（打字机效果），底层是 LangGraph 挂了个继承 **core 的 BaseCallbackHandler** 的处理器，监听 `on_llm_new_token` 等回调。

**② 预置 Agent 用 core 的模型接口（`prebuilt/chat_agent_executor.py` 第 13 行）**

```python theme={null}
from langchain_core.language_models import (
    BaseChatModel, LanguageModelInput, LanguageModelLike,
)
```

`create_agent` 的模型参数，类型就是 core 的 `BaseChatModel`——所以你能把任何继承它的模型（ChatAnthropic/ChatDeepSeek/ChatOpenAI）塞进去。

***

## 结论（小林扒完源码换来的）

**LangGraph 是"踩在 langchain\_core 肩膀上的编排层"**——它的依赖只有 `langchain-core`（连主包都不依赖），并且从 core 借了五样东西：

```text theme={null}
LangGraph 从 langchain_core 借了什么（源码实证）：
  ① Runnable 协议    → 图本身是 Runnable（PregelProtocol 继承它）→ 能用 invoke/stream/|
  ② 消息体系          → MessagesState 的 messages 就是 AnyMessage 列表
  ③ RunnableLambda    → 图节点函数被 coerce_to_runnable 包成 RunnableLambda
  ④ 序列化机制        → checkpoint 用 core 的 Reviver + 消息类白名单存消息
  ⑤ 回调 + 模型接口   → 流式靠 BaseCallbackHandler，agent 用 BaseChatModel
```

**一句话**：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 全地图](/doc/doc/narrative-course/16-langchain_core地图)。
