如果你已经会用 LangChain 调模型、绑工具,但一碰到「Agent 要循环推理、要分支、要并行」就不知道怎么组织代码——LangGraph 就是为此而生的。

它不是一个 prompt 框架,而是一个编排运行时:把 Agent / LLM 应用建模成一张有向状态图,每个节点读状态、做操作、写回状态,边决定数据往哪流。


三个关键数据结构

回到 LangGraph 的核心,Everything 都可以归结为三个概念:

概念 一句话 本质
Node 数据处理的逻辑 Python 函数
Edge 决定数据流转的结构 Python 函数
State 存储数据的结构 TypedDict

Node 和 Edge 本质上都是函数——Node 接收 State 做操作,Edge 决定下一个 Node 是谁。

State 是三者中设计成本最高的:划分不好,后面加节点、加并行、加汇总都会痛苦。所以第一篇入门,第二篇就专门讲 State,不是巧合。


LangGraph 把 LLM 应用的各部分拆成节点(Node)和边(Edge)

概念 作用
Node 数据处理逻辑(调 LLM、调工具、做判断)
Edge 决定节点之间的流转——固定跳转还是条件分支
State 全局共享的数据结构,所有节点读写它

不管是 Node 还是 Edge,底层本质上都是 Python 函数。

和裸写 while 循环比,LangGraph 多了什么?

裸 while 循环 LangGraph
流程藏在代码逻辑里 流程显式画在图上
加分支要改多处 add_edge / 条件边声明式添加
不好做并行 Super-step 自动并行
中断恢复要自己实现 checkpointer + interrupt 内置支持
调试靠 print LangSmith 可观测

最小 Agent:LLM → 工具 → 循环

LangGraph 官方 Quickstart 的标准模式,也是大多数 Agent 项目的起点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def llm_call(state: dict):
llm_calls = state.get("llm_calls", 0) + 1
response = model_with_tools.invoke(
[SystemMessage(content="You are a helpful assistant.")]
+ state["messages"]
)
return {"messages": [response], "llm_calls": llm_calls}


def should_continue(state: MessagesState) -> Literal["tool_node", END]:
if state["messages"][-1].tool_calls:
return "tool_node"
return END


def tool_node(state: dict):
tool_calls = state["messages"][-1].tool_calls or []
result = []
for tool_call in tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=str(observation), tool_call_id=tool_call["id"]))
return {"messages": result}

这三段代码拼起来就是 Agent 的核心循环:LLM 思考 → 决定调工具 → 工具执行 → 结果写回 → LLM 再思考

用图来表示:


工具(Tools):Agent 的手脚

LangChain 里定义工具很简单,用 @tool 装饰器即可,框架会自动解析并加上 namedescription(description 来自函数 docstring):

1
2
3
4
5
6
7
@tool
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b

model_with_tools = model.bind_tools(tools)
model_with_tools.invoke(messages)

工具描述写得好不好,直接决定 Agent 能不能正确选择工具。 这是 Agent 工程里最容易被忽视、但影响最大的一环。

完整的工具绑定流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b

@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b

tools = [multiply, add]
tools_by_name = {t.name: t for t in tools}

model_with_tools = model.bind_tools(tools)

response = model_with_tools.invoke(messages)

if response.tool_calls:
for call in response.tool_calls:
tool = tools_by_name[call["name"]]
result = tool.invoke(call["args"])

@tool 装饰器会自动从 docstring 提取 description 给 LLM 看——所以 docstring 不是写给人看的,是写给 LLM 看的。描述越清晰,LLM 选工具的准确率越高。


Super-step:图的调度节拍

LangGraph 内部用 Super-step 来调度节点执行:

  • 同一个 Super-step 里,没有依赖关系的节点可以并行
  • 一个 Super-step 结束后,才进入下一个
  • 所有前驱节点都跑完,后继节点才会被触发

理解 Super-step 有助于排查「为什么这个节点还没跑」——大概率是它的前驱还没全部完成。


固定工作流 vs 动态 Agent

LangGraph 文档里区分两种模式,选型时要想清楚:

模式 特点 适用场景
固定工作流(Workflow) 路径 predetermined,每步做什么写死 流水线、ETL、固定审核流程
动态 Agent LLM 自己决定下一步调什么工具 开放式问答、故障诊断、研究助手

你的项目如果是 llm_call → tool_node → should_continue 这种循环,就是标准的动态 Agent 模式。


Agent 的本质:一个 while 循环

跳出 LangGraph 的框架视角,Agent 的核心范式其实很简单:

Agent = LLM + Tools + Memory + Planning

  • LLM 是大脑,负责推理和决策
  • Tools 是手脚,负责和外部世界交互
  • Memory 提供跨轮次的记忆力
  • Planning 决定任务怎么拆解、按什么顺序执行

Agent loop 本质上就是一个 while 循环:推理 → 调工具 → 写回上下文 → 再推理,直到达到目标或触发停止条件。

但裸写 while 循环有三个现实问题:

  1. 推理路径极易走偏 — LLM 一步错,后面全废,token 白烧
  2. 上下文越来越长 — 多轮工具调用后,Memory 管理变成难题
  3. 流程不可观测 — 出问题了不知道卡在哪一步

LangGraph 的价值在于把这个循环显式建模成图,让你能看清每一步、加分支、加并行、加重试、加持久化——而不是把所有逻辑塞进一个巨大的 prompt 里。

一个设计良好的 Agent 系统分三层:

通过 Context 和 Tools 层的编排,能显著提升 Agent 的能力上限。


compile():从图到可运行程序

节点和边定义好之后,还需要 compile() 才能变成可执行的图:

1
2
3
4
5
6
7
8
9
graph = StateGraph(State)
graph.add_node("llm_call", llm_call)
graph.add_node("tool_node", tool_node)
graph.add_conditional_edges("llm_call", should_continue)
graph.add_edge("tool_node", "llm_call")
graph.add_edge(START, "llm_call")

app = graph.compile(checkpointer=memory)
result = app.invoke({"messages": [HumanMessage(content="1+2=?")]})

compile() 之后得到的 app 才有 invoke() / stream() 方法。如果后续需要断点恢复或跨会话记忆,在 compile 时传入 checkpointer 即可。