LLM 应用复杂度的演进

理解 LangGraph 的设计,需要先理解它要解决的问题从何而来。

最早一批 LLM 应用极其简单:构造一个 prompt,调用一次 API,拿到回答,结束。这种模式无状态、无副作用、可预期——从工程角度看几乎没有任何复杂性。随着 LLM 能力增强,人们开始把多次调用串联起来:先让模型做摘要,再让它根据摘要生成报告,最后再格式化输出。LangChain 的 Chain 抽象应运而生,把这种线性管道(A → B → C)标准化,解决了组件复用和组合的问题。

但 Agent 场景的出现打破了线性假设。

一个典型的 ReAct Agent 行为模式是这样的:LLM 观察当前状态,决定调用某个工具,工具返回结果,LLM 再次观察,再决定下一步——直到它认为任务完成。这个过程本质上是一个循环。而 LangChain 的 Chain 没有循环语义,它只能描述"做完 A 之后做 B",无法描述"根据 A 的结果决定是做 B 还是再做一次 A"。

强行用 Chain 实现 Agent 的人都碰过类似的困境:要么把循环逻辑藏进某个 Chain 步骤里(变成一个黑盒),要么在 Python 层手写循环来驱动 Chain(框架抽象就基本失效了)。除了循环之外,还有几个痛点同样无法忽视:

这三个痛点并不是 LangChain 设计失误,而是它的设计目标本来就不是 Agent。当问题域变了,工具也必须跟着变。

LangGraph 的核心主张

LangGraph 对上述问题给出了一套系统性的回答,这套回答体现为几个核心设计主张。

图是自然语言

Agent 的行为本质上是一张有向图:节点是操作(调用 LLM、执行工具、处理数据),边是控制流(静态路由或根据状态动态决策)。图天然支持循环,天然支持分支,天然支持并行——这些恰好是 Agent 场景所需要的。

用图来描述 Agent 行为不是一种比喻,而是一种精确建模。当你画出一个 ReAct Agent 的流程图,你画的就是一张有循环的有向图;当你把它翻译成 LangGraph 代码,两者几乎是同构的。这种模型与实现的对应关系,让代码更容易被理解和维护。

状态是一等公民

在 LangChain 里,状态是通过函数参数传递的,是执行的副产品。LangGraph 把这个关系翻转了:状态是图执行的核心对象,节点是对状态的操作。

这个区别在实践中意味着:状态必须有明确的类型定义,必须可序列化,必须可以被持久化和恢复。LangGraph 用 Python 的 TypedDict 或 dataclass 来定义状态结构,这不是偶然的选择——这些类型都可以被直接转换为 JSON,便于存储和传输。

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
import operator
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]  # 追加语义
    current_tool: str                          # 覆盖语义
    retry_count: int                           # 覆盖语义

注意 messages 字段用了 Annotated[list, operator.add]。这里的 operator.add 不只是类型注释,它告诉框架:当多个节点都向 messages 写入值时,应该追加而不是覆盖。这种"合并语义"是通过 Channel 机制实现的,后面会详细讲。