第2章 · 基础实例
不堆 API、不抄文档。这份教程只讲一件事:怎么把一个 Agent 想成一张会循环的状态图,并亲手把它搭出来。
读法:概念配最小可跑代码,每段都能复制到本地跑。建议边读边敲——LangGraph 是"手感型"框架,看十遍不如跑一遍。
0 · 为什么是 LangGraph(而不是 Chain / AgentExecutor)
很多人学 LangChain 第一步就走错路:学了一堆 LLMChain、AgentExecutor——它们正在被淘汰。现代建 Agent 的正确姿势是 LangGraph。先搞清它解决了什么问题。
传统的"链"(Chain)是一条直线:输入 → A → B → C → 输出。但真实的 Agent 需要:
- 循环:想一步、调个工具、看结果、再想一步……直到任务完成。直线结构天然做不到回头。
- 条件分支:根据模型输出决定"该调工具还是直接回答"。
- 状态:多步之间要记住已经发生了什么(对话历史、中间结果)。
- 可控:老
AgentExecutor是个黑盒,你很难在中间插手、暂停、人工审批。
LangGraph 把 Agent 建模成一张 有状态的图(Stateful Graph):节点是"做事的步骤",边是"下一步去哪",而且边可以指回去形成循环。这正好对上了 Agent 的本质。
LangChain Core 给你"积木"(模型、提示、工具),LangGraph 给你"把积木拼成会循环的 Agent 的骨架"。两者配合用,不是二选一。
1 · 核心心智模型:Agent 就是一张状态图
这一节是全篇地基。把这四个词刻进脑子,后面所有代码都是它们的组合。
| 概念 | 是什么 | 类比 |
|---|---|---|
| State 状态 | 一个贯穿全程的"共享数据包",每个节点读它、改它 | 白板:谁都能看、谁都能写 |
| Node 节点 | 一个函数,接收 State、干点事、返回要更新的字段 | 流水线上的一个工位 |
| Edge 边 | 规定"这个节点跑完去哪个节点" | 工位之间的传送带 |
| Conditional Edge 条件边 | 根据当前 State 动态决定下一步去哪(if/else) | 带岔路的传送带 |
运行时,LangGraph 做的事极其简单:从 START 出发,执行当前节点 → 节点返回的更新合并进 State → 沿着边走到下一个节点 → 直到走到 END。循环就是"边指回了前面的节点"。
上面这张图就是一个最经典的 ReAct Agent:agent 节点思考 → 条件边判断要不要调工具 → 要就去 tools 节点 → 工具结果写回 → 回到 agent 再想……不需要工具了就走 END。整篇教程都在把这张图一点点搭出来。
2 · 环境准备(含第一天就开 trace)
# 安装核心包(以 OpenAI 为例,换别的模型就换对应集成包)
pip install langgraph langchain langchain-openai
# 环境变量
export OPENAI_API_KEY="sk-..."
# 强烈建议:第一天就接 LangSmith,能看到图内部每一步在干啥
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="ls-..." # smith.langchain.com 免费注册
LangGraph 是多步循环,光看 print 很难知道"模型第二轮到底想了啥、为什么调了那个工具"。LangSmith 把每个节点的输入输出画成时间线,是学习期最值钱的辅助——它把黑盒变透明。
3 · 第一个图:从 0 手搭一个最小 Graph
先不碰工具、不碰循环。我们搭一个最简单的图:一个节点,调用模型回答问题。目的是把 StateGraph 的五步骨架跑通。
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
# ① 定义 State:这个图里要传递的数据。messages 用特殊 reducer(下一节细讲)
class State(TypedDict):
messages: Annotated[list, add_messages]
llm = ChatOpenAI(model="gpt-4o-mini")
# ② 定义节点:就是个普通函数,收 State,返回要更新的字段(dict)
def chatbot(state: State):
return {"messages": [llm.invoke(state["messages"])]}
# ③ 建图,挂节点
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
# ④ 连边:START → chatbot → END
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
# ⑤ 编译成可运行对象
graph = builder.compile()
# 跑!
result = graph.invoke({"messages": [{"role": "user", "content": "用一句话解释 LangGraph"}]})
print(result["messages"][-1].content)
定义 State → 写节点函数 → add_node → add_edge → compile。无论图多复杂,骨架永远是这五步。后面只是往里加节点和条件边。
节点 return {"messages": [...]} 时,返回的是要合并进 State 的增量,不是替换整个 State。怎么合并?由 State 字段上的 reducer 决定——这正是下一节的核心。
4 · State 与 Reducer:整个框架的灵魂
如果只能记一个 LangGraph 的核心概念,就记 Reducer。它决定"节点返回的更新,如何合并进 State"。
看回上面那行:messages: Annotated[list, add_messages]。这里 add_messages 就是一个 reducer。它的含义是:当节点返回新的 messages 时,不要覆盖,而是追加到已有列表后面。
对比:有没有 reducer 的区别
| 写法 | 合并行为 |
|---|---|
foo: str | 默认覆盖——新值直接替换旧值 |
history: Annotated[list, add_messages] | 追加——把新消息接到历史后面(对话场景必须这样) |
count: Annotated[int, operator.add] | 累加——每次节点返回的数字相加 |
为什么这事关键?因为 Agent 在循环里会反复经过同一个节点,你需要"积累"信息(对话越聊越长),而不是每轮把前面冲掉。Reducer 就是控制"积累 vs 覆盖"的开关。
import operator
from typing import Annotated
from typing_extensions import TypedDict
class State(TypedDict):
messages: Annotated[list, add_messages] # 追加:对话历史
step_count: Annotated[int, operator.add] # 累加:走了几步
final_answer: str # 覆盖:最终答案,只保留最新
没有共享 State 时,你得手动把上个函数的输出当参数传给下个函数,层层透传,很快就乱。LangGraph 让所有节点都读写同一个 State——节点之间解耦,只通过"白板"沟通。这也是它能优雅支持循环、分支、并行的根本原因。
5 · 条件边与循环:让图学会"自己决定下一步"
直线图没意思。Agent 的灵魂是"根据当前情况决定去哪"。这靠 add_conditional_edges 实现。
条件边 = 一个路由函数。它读 State,返回一个字符串(下一个节点的名字)。LangGraph 按这个名字决定往哪走。
from langgraph.graph import StateGraph, START, END
# 路由函数:看最后一条消息,决定继续还是结束
def route(state: State) -> str:
last = state["messages"][-1]
# 如果模型要求调用工具,就去 "tools" 节点;否则结束
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return END
builder.add_conditional_edges(
"agent", # 从哪个节点出发判断
route, # 路由函数
{"tools": "tools", END: END} # 返回值 → 目标节点的映射(可省略)
)
# 关键:tools 跑完,边指回 agent —— 这一条就是"循环"!
builder.add_edge("tools", "agent")
因为图能循环,理论上可能无限转圈。务必保证路由函数在某个条件下返回 END。生产里还会用 graph.invoke(inputs, {"recursion_limit": 25}) 设一个步数上限做兜底,防失控。
6 · 工具调用:ToolNode 让模型动手
Agent 之所以强,是因为它能调工具(搜索、算数、查数据库)。LangGraph 内置了 ToolNode,帮你省掉手写"解析 tool_calls → 执行 → 把结果包成消息"的样板代码。
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode, tools_condition
# ① 用 @tool 定义工具,docstring 会被模型当成"使用说明"
@tool
def get_weather(city: str) -> str:
"""查询某个城市的天气。"""
return f"{city} 今天晴,26°C"
tools = [get_weather]
# ② 把工具"绑"给模型,模型才知道有哪些工具可用
llm_with_tools = llm.bind_tools(tools)
def agent(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# ③ ToolNode 自动执行模型要求的工具调用
tool_node = ToolNode(tools)
builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
# tools_condition 是官方预置的路由:有 tool_calls 就去 tools,否则去 END
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
graph = builder.compile()
tools_condition = 上一节路由函数的官方版第 5 节我们手写了 route()。LangGraph 直接给了你一个等价的预置版 tools_condition——绝大多数 ReAct 场景直接用它就行,不用自己写。理解了手写版,你就知道它内部在干嘛。
7 · 完整 ReAct Agent:手搭 vs 一行预置
把前面所有零件拼起来,你就有了一个能思考、会调工具、自动循环的完整 Agent。其实第 6 节的代码已经是了。这里给你官方的一行版,以及"什么时候该用哪个"。
一行版:create_react_agent
from langgraph.prebuilt import create_react_agent
# 一行得到一个完整 ReAct Agent(内部就是第6节那张图)
agent = create_react_agent(llm, tools=[get_weather])
result = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
print(result["messages"][-1].content)
| 场景 | 用哪个 |
|---|---|
| 快速验证想法 / 标准 ReAct 流程 | create_react_agent 一行搞定 |
| 要自定义节点、加审批、改循环逻辑、多 Agent | 手搭 StateGraph(前 6 节) |
先用 create_react_agent 跑通找手感,再用 StateGraph 手搭一个等价的——你会真正理解"那一行背后到底是什么"。这正是面试时能讲清原理的关键,也是你 JD 里"懂核心架构"的体现。
8 · 记忆与持久化:Checkpointer + thread_id
到这里 Agent 还是"一次性"的——每次 invoke 都从零开始,不记得上次聊了啥。要做多轮对话,需要 Checkpointer(检查点)。
原理:每走完一步,LangGraph 把当前 State 存一份快照(checkpoint),用 thread_id 标识是哪个会话。下次带同样的 thread_id 进来,它自动把历史 State 加载回来接着聊。
from langgraph.checkpoint.memory import MemorySaver
# 开发用内存版;生产换 SqliteSaver / PostgresSaver 落盘
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
# 用 config 里的 thread_id 区分不同会话
config = {"configurable": {"thread_id": "user-42"}}
graph.invoke({"messages": [{"role":"user","content":"我叫小镰刀"}]}, config)
# 第二次,同一个 thread_id,它记得你的名字
r = graph.invoke({"messages": [{"role":"user","content":"我叫什么?"}]}, config)
print(r["messages"][-1].content) # → 你叫小镰刀
它存的是每一步的完整 State 快照,所以还顺带解锁了三个高级能力:① 断点续跑(崩了能从上一步恢复);② 时间旅行(回到任意历史步重跑);③ 下一节的人工介入。这是 LangGraph 比裸框架强的核心竞争力之一。
9 · 人工介入(Human-in-the-loop)
真实业务里,Agent 在执行"危险操作"(转账、删数据、发邮件)前,往往需要人点头。LangGraph 让你在图的任意位置暂停,等人工批准再继续。
最简单的方式:编译时声明"在某个节点前先停下"。
# 在进入 tools 节点前暂停,把控制权交回给你
graph = builder.compile(
checkpointer=memory,
interrupt_before=["tools"] # 执行工具前先停
)
config = {"configurable": {"thread_id": "t1"}}
graph.invoke({"messages":[{"role":"user","content":"帮我删除日志"}]}, config)
# 此刻图停在 tools 前。你检查它想干啥:
state = graph.get_state(config)
print(state.next) # → ('tools',) 说明它正等着调工具
# 人工确认 OK → 传 None 让它从断点继续
graph.invoke(None, config)
较新的 LangGraph 还提供在节点内部调用 interrupt(value) 主动暂停、并用 Command(resume=...) 把人工输入送回去的写法,更灵活。先掌握 interrupt_before 这个简单版,再升级。
10 · 流式输出:看 Agent 一步步在想什么
用 invoke 你只拿到最终结果。用 stream,你能实时看到每个节点跑完后的状态——调试和做"打字机效果"都靠它。
# stream_mode="values":每步后吐出完整 State
for chunk in graph.stream(
{"messages":[{"role":"user","content":"北京天气?"}]},
config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
| stream_mode | 吐什么 | 用途 |
|---|---|---|
"values" | 每步后的完整 State | 看整体进展 |
"updates" | 每步只吐"哪个节点改了啥" | 调试节点行为 |
"messages" | 逐 token 流式 | 前端打字机效果 |
11 · 观测与评测:对接"自进化"
这一节直接对应你 JD 里的"反思 / 数据飞轮 / 效果调优"。LangGraph + LangSmith 是落地自进化的天然组合。
① Trace:把循环看透
第 2 节配好 LANGCHAIN_TRACING_V2 后,每次运行都会在 LangSmith 里生成一条完整时间线:每个节点的输入输出、模型每轮的 token 消耗、调了哪些工具、为什么。排查"Agent 为什么绕圈/调错工具",看 trace 几秒钟定位。
② Eval:用数字说话
# 思路:建一个评估集,跑 Agent,用 LLM-as-judge 打分,量化成功率
from langsmith import Client
from langsmith.evaluation import evaluate
dataset = [
{"input":"北京天气?", "expect_tool":"get_weather"},
{"input":"1+1?", "expect_tool": None}, # 不该乱调工具
]
# 对每条 case 跑 agent → 判断行为是否符合预期 → 算通过率
# 改 prompt / 工具描述前后各跑一次,对比成功率 = A/B 调优
把每次运行的 trajectory 留下来 → 挑失败案例 → 改进 prompt/工具/路由 → 用同一评估集验证是否真的变好。这个闭环跑起来,你的 Agent 就"越用越好"——正是 JD 里 LearningLoop 的核心,也是你最该在简历上量化的东西("成功率从 X 提到 Y")。
12 · 多智能体:Supervisor 模式
单个 Agent 工具一多就容易"选择困难"。把任务拆给多个专精 Agent、由一个"主管"调度,往往更稳。这就是多智能体。
最常用的是 Supervisor(主管)模式:一个 supervisor 节点根据当前任务,决定把活派给哪个子 Agent(研究员 / 写手 / 审核),子 Agent 干完把结果交回 supervisor,它再决定下一步——本质还是"条件边 + 循环",只是节点变成了一个个 Agent。
多 Agent 调试成本高、容易失控。先把单 Agent(前 11 节)吃透,确实遇到"一个 Agent 扛不住"再拆。官方有 langgraph-supervisor 预置库,理解原理后直接用。
13 · 常见坑 & 最佳实践
| 坑 | 怎么破 |
|---|---|
还在学老的 AgentExecutor/LLMChain | 直接学 LangGraph,老 API 已是遗留 |
| State 字段忘了加 reducer,历史被覆盖 | 对话/累积类字段必须配 add_messages 或 operator.add |
| 循环停不下来 / 报 recursion 错 | 路由函数留 END 出口 + 设 recursion_limit |
| 工具不被调用 / 调错 | 工具 docstring 写清楚(模型靠它判断),并确认 bind_tools 了 |
| 多轮对话不记事 | compile 时传 checkpointer,invoke 时带 thread_id |
| 不知道 Agent 在干嘛 | 开 LangSmith trace,别靠 print 猜 |
14 · 学习路径 Checklist
按顺序打勾,每一项都"亲手敲一个能跑的 demo"才算过。
- 跑通最小 Graph(第 3 节五步骨架)
- 给 State 加一个
operator.add字段,理解 reducer - 手写一个条件边,做一个会循环的图(第 5 节)
- 用
ToolNode+tools_condition让模型调一个自定义工具 - 用
create_react_agent跑一遍,再用StateGraph手搭等价版 - 加
MemorySaver,做一个记得住名字的多轮对话 - 加
interrupt_before,做一个"工具执行前要人工批准"的流程 - 接 LangSmith,看一条完整 trace
- 建 10 条评估集,做一次"改前 vs 改后"的成功率对比(自进化起点)
- 读官方 LangChain Academy 的 LangGraph 课,补齐细节
① 官方文档 langchain-ai.github.io/langgraph ② 免费课 LangChain Academy(专教 LangGraph,质量高)③ 学每一节时把 Claude 当陪练:让它 review 你的图、出题考你、模拟面试官追问。
学完这份 checklist,你就具备了 JD 里"懂 Agent 核心架构 + 主流框架 + 自进化落地"的硬实力。下一步:把你某个 launchd 任务用 LangGraph 重写成带评测闭环的真 Agent,作为面试案例。