> ## 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.

# 11 · 从流水线到会转弯的图：LangGraph 的三件套（状态、节点、边）

> LangGraph 三件套：状态、节点、边，条件边让流程会转弯。

# 11 · 从流水线到会转弯的图：LangGraph 的三件套（状态、节点、边）

## 第零幕：只用 langchain\_core，能做什么？缺什么才要 LangGraph？

小林学到这里，把 langchain\_core 能干的都摸清了。他画了一张"能力边界图"：

```text theme={null}
✅ 只用 langchain_core \+ 集成包，就能做：
   · 对话：model.invoke() / stream()
   · 记忆：手写 history.append \+ InMemoryChatMessageHistory（按 session 隔离）
   · 工具：@tool \+ bind_tools（手动循环执行 tool_calls）
   · 模板：ChatPromptTemplate \+ MessagesPlaceholder
   · 解析：StrOutputParser / with_structured_output
   · RAG：Document \+ TextSplitter \+ Embedding \+ Chroma \+ retriever \+ | 链
   · 结构化流程：LCEL 的 `a | b | c`（固定流水线）
   → 以上全是 langchain_core 范畴，不需要 LangGraph！

❌ 这些做不了 / 做起来很痛苦，才需要 LangGraph：
   · 流程会"转弯"（根据状态走不同分支）——LCEL 只有直线 `|`
   · 流程会"循环"（模型↔工具多轮，手动写 while 又丑又容易错）
   · 中途"暂停"（人审确认后再继续）——interrupt
   · 记忆"持久化、重启不丢"（手写 SQLite 要自己造轮子）——checkpointer
   · 状态"回滚/分支"（图的状态可保存可回退）
   · 可视化成图（节点/边/条件边，create_agent 内部就是一张图）
```

**一句话判断标准：**

```text theme={null}
你的流程是"直线"（A→B→C 固定不变）？  → 用 langchain_core 的 `|` 链（第 09 篇 RAG 那种）
你的流程会"转弯/循环/暂停/记状态"？     → 用 LangGraph（本章）
```

**对照你已学的：**

| 你学过的                  | 属于              | 它能/不能                     |
| --------------------- | --------------- | ------------------------- |
| RAG 链（第 09 篇）         | langchain\_core | 直线流水线，够用                  |
| 手写记忆（第 04/10 篇）       | langchain\_core | 能用，但重启丢、无分支               |
| create\_agent（第 10 篇） | LangGraph       | ⚠️ 你已经在用它了！只是没意识到         |
| Agent 循环（手写 while）    | 手动              | 丑、易错、无状态管理 → LangGraph 内置 |

> 小林在这篇的顿悟起点就是：**他早就开始用 LangGraph 了（create\_agent 返回的就是 CompiledStateGraph），只是从没正式认识它。** 这一章，就是揭开它的面纱。

***

## 开头的现象

小林用 `create_agent` 做出了"会记住、会干活的助手"。但他一直有个心结：**create\_agent 是个黑盒。** 模型下一步要干嘛？循环到哪一步了？老板说"能不能让机器人干危险操作前先问我一句"——小林盯着 create\_agent 的文档，找不到"中途插一手"的开关。

他做了一个让他头皮发麻的实验：`print(type(agent))`——

```python theme={null}
print(type(agent))
# → <class 'langgraph.graph.state.CompiledStateGraph'>
```

小林愣住了。**create\_agent 返回的不是"模型"，不是"链"，是一张 LangGraph 的编译状态图！** 他想起自己一直躲着没学的那个叫 LangGraph 的东西——原来第 10 篇的"鞍具"底下，藏着一整张图。

## 第一幕：小林解剖了 create\_agent——一张 4 节点 4 边的图

小林用 `agent.get_graph()` 把 create\_agent 拆开了。他看到的东西让他倒吸一口凉气：

```text theme={null}
=== 节点 ===
  __start__ → model → tools → __end__

=== 无条件边 ===
  __start__ → model
  tools     → model

=== 条件边 ===
  model 节点：有 tool_calls → 去 tools；没有 → 去 __end__
```

画成图，就是一张"会转弯的图"：

```text theme={null}
                 ┌──────────────────────────┐
                 │                          │
                 ▼                          │
  __start__ → model ──(还有 tool_calls?)──→ tools
                 │                           │
                 │ 没有 tool_calls            └──(执行完工具，绕回 model)
                 ▼
              __end__
```

小林盯着这张图，第 4 篇、第 10 篇的知识全部串起来了：**"原来 create\_agent 内部就是第 10 篇我手写的那个'三步舞'循环——模型节点申请工具、工具节点执行、条件边决定'继续调还是收工'。官方把它画成了图，节点是函数，边是走向，条件边是'转弯的开关'。"**

他还在 `response_metadata` 里看到了证据：模型返回的 `stop_reason` 是 `tool_use`（"我要调工具"）还是 `end_turn`（"我说完了"）——**那张条件边看的，就是这个开关。**

> 想验证"create\_agent 是一张图"吗？跑 `10_agent.py` 后用 `agent.get_graph()` 打印节点——你会看到 `__start__ → model → tools → __end__`，`tools → model` 那个回环就是"自动循环"的物理证据。

## 第二幕：小林亲手搭了一张图——StateGraph 的四个零件

小林想：**"既然 create\_agent 是一张图，那我能不能自己搭一张？"** 他翻到了 LangGraph 的核心类 `StateGraph`，发现搭图只需要四个零件：

```python theme={null}
from langgraph.graph import StateGraph, START, END
from typing import TypedDict

# 零件一：状态（State）——节点间共享的数据结构
class CountState(TypedDict):
    count: int

# 零件二：节点（Node）——处理函数。每个节点只返回"自己改的部分"
def node_a(state):
    return {"count": state["count"] \+ 1}   # count \+1

def node_b(state):
    return {"count": state["count"] * 2}   # count *2

# 零件三：把节点和边装进图
g = StateGraph(CountState)          # 建图，声明状态结构
g.add_node("a", node_a)             # 加节点
g.add_node("b", node_b)
g.add_edge(START, "a")              # 加边：起点 → a → b → 终点
g.add_edge("a", "b")
g.add_edge("b", END)

# 零件四：编译成可调用对象
app = g.compile()

result = app.invoke({"count": 3})
print(result["count"])   # 8   （3 --a(\+1)--> 4 --b(*2)--> 8）
```

小林看着输出 `8`，验证了一个关键机制：**每个节点只返回"自己改的那一小块"（`{"count": ...}`），LangGraph 自动把它合并进状态里。** 这就是 LangGraph 和普通链（`|`）的本质区别：**LCEL 的 `|` 是"单向流水线"，数据流过去就完事；StateGraph 是"共享状态"——每个节点能读能写整个状态，还能根据状态决定走哪条边。**

> 想验证"节点只改自己的"吗？跑 `11_langgraph.py` 第一部分——两个节点各改各的（+1、\*2），LangGraph 自动合并，最后输出 8。你把 node\_b 改成返回 `{"count": 999}` 试试——它会覆盖，说明"后写的节点覆盖前写的"。

## 第三幕：条件边——流程会转弯了

小林搭完直线图，又试了"条件边"——让图根据状态决定走哪条路：

```python theme={null}
def decide(state):
    return "big" if state["count"] > 10 else "small"   # 判断：>10 走 big，否则走 small

g2 = StateGraph(CountState)
g2.add_node("check", lambda s: s)                          # 检查节点（原样过）
g2.add_node("big", lambda s: {"count": s["count"] \+ 100})
g2.add_node("small", lambda s: {"count": s["count"] - 1})
g2.add_edge(START, "check")
g2.add_conditional_edges("check", decide, {"big": "big", "small": "small"})   # ← 条件边
g2.add_edge("big", END)
g2.add_edge("small", END)
app2 = g2.compile()

print(app2.invoke({"count": 5})["count"])    # 4   （走 small：5-1）
print(app2.invoke({"count": 50})["count"])   # 150 （走 big：50\+100）
```

小林盯着 `5 → 4` 和 `50 → 150`，明白了条件边是干嘛的：**`add_conditional_edges("check", decide, {"big": "big", "small": "small"})` 的意思是——从 check 节点出发，调用 decide 函数看结果，结果 `"big"` 就走 big 节点，`"small"` 就走 small 节点。** 这就是流程图里的"菱形判断框"。

他忽然想到 create\_agent 里那张图的"条件边"：**`model` 节点上的条件边不也是看 `tool_calls` 决定走 tools 还是 END 吗？** 他自己搭的图和 create\_agent 是同一套机制。

> 想验证"转弯"吗？跑 `11_langgraph.py` 第二部分——count=5 走 small（期望 4），count=50 走 big（期望 150）。把 decide 的判断条件从 `> 10` 改成 `> 100`，再跑 50——它就走 small 了。

### 🔧 技术细节：这一章涉及的关键类和签名

**① `StateGraph`——搭图的骨架（langgraph.graph）**

```python theme={null}
from langgraph.graph import StateGraph, START, END

StateGraph(
    state_schema: type[StateT],   # 状态结构（TypedDict 或 MessagesState）
    input_schema=None,            # 输入结构（默认用 state_schema）
    output_schema=None,           # 输出结构
)
g.add_node(node_name: str, action: Callable)   # 加节点
g.add_edge(start_key: str, end_key: str)       # 加边（无条件）
g.add_conditional_edges(                        # 加条件边（会转弯）
    source: str,                 # 从哪个节点出发
    path: Callable,              # 判断函数：返回 "big"/"small"/END 等
    path_map: dict,              # {"big": "big_node", "small": "small_node"}
)
g.compile(checkpointer=None, interrupt_before=None, ...) -> CompiledStateGraph
```

**② 节点函数签名——每个节点收到 state，返回"自己改的部分"**

```python theme={null}
def node_a(state: dict) -> dict:
    # 读：state["count"] / state["messages"]
    # 写：只返回自己改的键
    return {"count": state["count"] \+ 1}
# LangGraph 自动合并返回值进状态
```

> 想验证吗？`agent.get_graph()` 打印节点和边——你会看到 `__start__ → model → tools → __end__` 和回环 `tools → model`。

## 结论（小林用一天换来的）

**LangGraph 的基础 = 状态 + 节点 + 边：`StateGraph(状态结构)` 建图，`add_node` 加处理函数，`add_edge`/`add_conditional_edges` 连线（条件边让流程会转弯、会循环），`compile()` 编译成可调用对象。** 每个节点只返回"自己改的部分"，LangGraph 自动合并进状态——这就是"图"和"链"的本质区别：链是单向流水线，图是共享状态 + 会转弯。

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

1. **想让流程"会转弯"（根据状态走不同分支）**——`add_conditional_edges`。
2. **想让流程"循环"（模型↔工具）**——条件边回环（`tools → agent`）。
3. **只是固定问答（RAG）**——别上 LangGraph，第 09 篇的 `|` 链够用。

小林搭好了会转弯的图，但他还不会用它做 Agent。他决定照着 create\_agent 的解剖图，亲手搓一个模型↔工具的循环——[翻到第 12 篇：手搓 create\_agent](/doc/doc/narrative-course/12-手搓create_agent)。
