Clipping 微信公众号

LangGraph 底层原理:它是怎么把 LLM 变成一台状态机的

by Jameszyh 原文 ↗
Created: 2026-06-19

公众号名称:James的成长日记

作者名称:Jameszyh

发布时间:2026-04-24 09:26

大家好,我是James。

上一篇我们把 LangChain Memory 模块拆了个底朝天,搞清楚了截断、总结、检索三种记忆方案各自的适用场景。这一篇我们进入 LangGraph 的核心地带——它的底层执行引擎。

你有没有想过:为什么同样是调用 LLM,用了 LangGraph 之后,AI 就能「想一步、停一步、判断一步」?就能在工具调用和 LLM 推理之间来回切换,还不会乱套?

答案就藏在它的状态机设计里。


01 先说说 LangChain 的老问题

LangChain 的早期架构是线性的——Chain。

用户输入 → PromptTemplate → LLM → OutputParser → 输出

这个模型干净利落,但有个致命弱点:控制流是固定的

一旦你想实现「如果 LLM 觉得需要查数据库就查,不需要就跳过」,或者「出错了重试三次」,你会发现原来的 Chain 根本放不下这些逻辑。要么把判断逻辑硬编码在节点里,要么用 Python 的 if/else 在外层手动管流程——乱成一锅粥。

现实中的 Agent 场景根本不是线性的:

  • 调工具 → 看结果 → 决定调下一个还是直接回答

  • 生成草稿 → 自我审查 → 不行就重写

  • 多轮对话 → 根据意图走不同分支

这些都需要带状态的循环控制流。这正是 LangGraph 解决的核心问题。


02 状态机是什么:用最简单的例子解释

状态机(State Machine)不是什么新概念。你手机的蓝牙就是个状态机:

[关闭] --打开--> [扫描中] --发现设备--> [配对中] --成功--> [已连接]
                                           |
                                           --失败--> [扫描中]

三个核心要素:

┌─────────────────────────────────────────────┐
│              状态机三要素                    │
├──────────┬──────────────────────────────────┤
│  State   │  当前的数据快照(系统处于什么情况)│
│  Node    │  执行动作,并更新 State            │
│  Edge    │  决定下一步去哪个 Node             │
└──────────┴──────────────────────────────────┘

LangGraph 就是把这套机制套在了 LLM 上:

  • State = 对话历史 + 工具结果 + 中间变量

  • Node = LLM 调用 / 工具执行 / 业务逻辑

  • Edge = 普通跳转 / 条件分支(Conditional Edge)


03 StateGraph 执行引擎:它的心脏长什么样

打开 LangGraph 的源码(Python 版 langgraph/graph/state.py,JS 版同理),你会看到 StateGraph 类维护了几个核心数据结构:

┌──────────────────────────────────────────────────┐
│                  StateGraph 内部                  │
├─────────────────┬────────────────────────────────┤
│  nodes          │  Map<名称, 函数>                │
│  edges          │  Map                │
│  conditional_   │  Map节点名>     │
│  edges          │                                 │
│  channels/      │  每个状态字段的 Reducer         │
│  schema         │                                 │
└─────────────────┴────────────────────────────────┘

当你调用 graph.compile() 时,它做了什么?

compile() 阶段

     ├─ 验证图结构(有没有孤立节点?有没有到 END 的路径?)
     ├─ 构建邻接表(预计算每个节点的后继)
     ├─ 初始化 Channels(每个状态字段注册 Reducer)
     └─ 返回 CompiledGraph(可以 invoke/stream)

compile() 本质上是把你「描述的图」变成「可执行的调度器」。


04 一次完整执行:从 invoke 到节点运行的全流程

来看最简单的例子,一个单节点 LLM Agent:

import { StateGraph, START, END } from "@langchain/langgraph";
import { Annotation } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";

// 1. 定义状态结构
const AgentState = Annotation.Root({
  messages: Annotation({
    reducer: (prev, next) => [...prev, ...next],
    default: () => [],
  }),
});

// 2. 初始化 LLM
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });

// 3. 定义节点函数
async function callLLM(state: typeof AgentState.State) {
  const response = await llm.invoke(state.messages);
  return { messages: [response] };
}

// 4. 构建图
const graph = new StateGraph(AgentState)
  .addNode("llm", callLLM)
  .addEdge(START, "llm")
  .addEdge("llm", END)
  .compile();

// 5. 运行
const result = await graph.invoke({
  messages: [new HumanMessage("你好,介绍一下自己")],
});

执行流程逐步拆解:

graph.invoke({ messages: [...] })


   1. 初始化 State
      messages = [HumanMessage("你好...")]


   2. 调度器从 START 出发
      → 找到边:START → "llm"


   3. 执行节点 "llm"
      callLLM(state) 被调用
      → llm.invoke(messages)
      → 返回 { messages: [AIMessage("我是...")] }


   4. 合并状态(Reducer)
      新 messages = [...旧, AIMessage("我是...")]


   5. 调度器检查下一步
      → "llm" 的边指向 END
      → 执行结束


   6. 返回最终 State


05 Reducer:状态更新的核心机制

这是很多人没搞清楚的地方。

节点函数返回的不是「新 State」,而是「State 的更新片段」。LangGraph 用 Reducer 把旧状态和更新片段合并。

// 三种常见 Reducer 写法

// 方式1:用内置 messagesStateReducer(追加消息)
const State1 = Annotation.Root({
  messages: Annotation({
    reducer: (prev, next) => [...prev, ...next],
  }),
});

// 方式2:覆盖(最新值覆盖旧值)
const State2 = Annotation.Root({
  step: Annotation({
    reducer: (_, next) => next, // 直接替换
  }),
});

// 方式3:累加计数
const State3 = Annotation.Root({
  callCount: Annotation({
    reducer: (prev, next) => prev + next,
    default: () => 0,
  }),
});

Reducer 的执行时机:

节点返回 { key: value }


  对每个 key,找到对应 Reducer
  newState[key] = reducer(oldState[key], value)


  生成新 State 快照
  (旧 State 不变,不可变数据结构)

关键点:State 是不可变的。 每次节点执行都产生一个新的 State 快照,旧快照被保留(这是 Checkpoint 能实现的基础,后面讲)。


06 图的调度器:它怎么决定下一步去哪

这是 LangGraph 最有意思的部分——调度器(Scheduler)。

调度器本质上是一个事件循环

while 当前节点 != END:
    1. 执行当前节点 node(state) → partial_update
    2. 用 Reducer 合并状态 → new_state
    3. 查询当前节点的出边
       - 普通边:直接去下一个节点
       - 条件边:调用路由函数 router(new_state) → 返回节点名
    4. 把下一个节点加入执行队列
    5. 取队列头 → 重复

条件边(Conditional Edge)长这样:

import { StateGraph, START, END } from "@langchain/langgraph";

// 路由函数:根据 state 返回下一个节点名
function routeAfterLLM(state: typeof AgentState.State): string {
  const lastMessage = state.messages[state.messages.length - 1];

  // 如果 LLM 想调工具
  if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
    return "tools"; // → 去工具节点
  }
  return END; // → 直接结束
}

const graph = new StateGraph(AgentState)
  .addNode("llm", callLLM)
  .addNode("tools", callTools)
  .addEdge(START, "llm")
  .addConditionalEdges("llm", routeAfterLLM, {
    tools: "tools", // 映射:路由函数返回值 → 节点名
    [END]: END,
  })
  .addEdge("tools", "llm") // 工具执行完回到 LLM
  .compile();

执行路径示意:

START → llm → [判断] → 需要工具? 
                  Yes → tools → llm → [判断] → 不需要了? 
                                                  Yes → END
                  No  → END

这就是 ReAct Agent 的本质:一个带条件边的循环图。


07 并行执行:Fan-out / Fan-in 模式

LangGraph 支持一个节点连到多个后继节点,实现并行执行。

// Fan-out:一个节点触发多个并行节点
const parallelGraph = new StateGraph(AgentState)
  .addNode("start", startNode)
  .addNode("search_web", searchWeb)   // 并行
  .addNode("search_db", searchDB)    // 并行
  .addNode("merge", mergeResults)    // 合并
  .addEdge(START, "start")
  .addEdge("start", "search_web")    // 同时出发
  .addEdge("start", "search_db")
  .addEdge("search_web", "merge")    // 都完成后汇聚
  .addEdge("search_db", "merge")
  .addEdge("merge", END)
  .compile();

调度器处理并行的方式:

start 节点执行完毕

         ├──→ search_web(加入执行队列)
         └──→ search_db(加入执行队列)
         
执行队列:[search_web, search_db]
同时调度(异步并发执行)

两者都完成后:
merge 节点的所有前驱都就绪 → merge 入队


        merge 执行 → END

这个 Fan-out/Fan-in 模式,是并行搜索、多 Agent 协作的基础。


08 编译产物:CompiledGraph 里藏了什么

compile() 返回的 CompiledGraph 暴露了四个主要接口:

interface CompiledGraph {
  // 同步执行,返回最终 State
  invoke(input: State, config?: RunnableConfig): Promise;

  // 流式执行,每个节点完成后 yield 一次
  stream(
    input: State,
    config?: RunnableConfig
  ): AsyncGenerator>;

  // 可视化图结构(调试用)
  getGraph(): DrawableGraph;

  // 获取当前状态(需要 Checkpointer)
  getState(config: RunnableConfig): Promise;
}

stream() 的输出格式:

for await (const chunk of graph.stream(input)) {
  // chunk 是一个对象:{ 节点名: 该节点产生的状态更新 }
  // 例如:
  // { llm: { messages: [AIMessage(...)] } }
  // { tools: { messages: [ToolMessage(...)] } }
  console.log(chunk);
}

这就是为什么前端能实现「打字机效果」——每个节点完成,前端就能收到一次更新。


总结

这篇我们从底层拆解了 LangGraph 把 LLM 变成状态机的核心机制:

  • StateGraph 三要素:State 存数据、Node 执行动作、Edge 决定走向,缺一不可

  • Reducer 是关键:节点返回的是「更新片段」而非「完整新状态」,Reducer 负责合并,State 始终不可变

  • 调度器是心脏:本质是一个事件循环,条件边让它能根据运行时状态动态决策

  • compile() 的意义:把声明式的图描述变成可执行的调度引擎,做结构验证和邻接表预计算

  • 并行靠 Fan-out:多条出边同时触发,Fan-in 等待所有前驱完成,调度器自动处理同步

  • stream() 是流式基础:每个节点完成即 yield,这是打字机效果和实时反馈的技术支撑

下一篇我们深入 LangGraph 状态管理的核心:Reducer、Annotation、Channel 是什么关系,以及怎么设计一个合理的 State 结构。


关注我,James 的成长日记,持续分享干货,帮你在 AI 时代少走弯路。


内容效果不满意?点此反馈

输入关键词开始搜索