LangGraph 实战:搭建带状态的多步骤 AI Agent
告别 if-else 硬编码,从零用 LangGraph 搭建可循环、带状态管理、支持条件路由的多步骤 AI 代理。学完能独立编写包含自动决策、工具调用和持久化状态的复杂工作流,并避开递归溢出、状态丢失等常见坑点。

LangGraph 实战:搭建带状态的多步骤 AI Agent
你遇到过的痛点
跟团队聊 AI 应用落地时,很多人卡在同一个问题:「我想让 Agent 根据用户输入动态决定下一步,而不是写死一串 if-else,怎么做?」
比如做一个研究助手,用户输入 "对比 Kafka 和 RabbitMQ",理想流程是:
- 先查资料,拿到双方特性
- 判断是否需要补充最新社区数据
- 需要就调搜索工具,不需要就直接整理
- 汇总输出
用普通 prompt chain 能写,但流程一旦变复杂——需要循环、条件跳转、记住中间状态——代码就会变成一团 spaghetti。状态到处飘,调试靠 print,加新分支就得重写大段逻辑。
LangGraph 就是为了解决这个问题。它把 AI Agent 的逻辑表达成图(Graph):节点是执行单元,边是流转条件,状态自动在节点间传递。GitHub 上 18.5k Star 已验证了这条路的实用性。
这篇教程带你从零开始,搭一个能自主决策、带状态、可循环的多步骤 Agent。不是玩具 demo,是能跑、能调试、能扩展的真实工作流。
前置条件
- Python 3.9+(推荐 3.11)
- 一个可用的 LLM API Key(OpenAI / 智谱 / 通义等均可,本文以 OpenAI 为例)
- 了解基础的 Python 异步编程(
async/await),不了解也没关系,LangGraph 同步写法完全支持
不需要读过 LangGraph 源码,也不需要是 AI 专家。后端转 AI 开发的同学,照着走一遍就能上手。
第一步:环境准备与安装
LangGraph 本身是个轻量库,依赖 langchain-core 做底层。用虚拟环境隔离依赖,避免污染全局。
bash
## 创建虚拟环境
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
## 安装核心库
pip install langgraph langchain-openai duckduckgo-search
为什么分开装?
langgraph只负责图的定义和状态管理,实际调 LLM 需要langchain-openai等 Chat Model 实现。拆开装的好处是换模型时(比如从 GPT 切到通义千问),图逻辑一行不用改。
设好环境变量,确保 API Key 生效:
bash
export OPENAI_API_KEY="sk-your-key-here"
第二步:理解核心概念——状态图(StateGraph)
用之前,先搞清 LangGraph 的三个核心词:
- State(状态):一个 TypedDict,存着工作流里所有中间结果。比如用户的原始问题、已查到的资料、当前步骤标识。状态是节点间通信的唯一桥梁。
- Node(节点):一个普通 Python 函数,接收当前状态,返回状态更新(partial state)。LangGraph 会自动合并更新。
- Edge(边):决定下一步走向哪个节点。可以是固定边(无条件),也可以是条件边(根据返回值决定跳转)。
理解这三点,就掌握了 LangGraph 80% 的用法。
第三步:快速上手——定义最小可运行图
先定义最简图:一个节点、一条边、一个状态。跑通后再加复杂度。
python
from typing import TypedDict
from langgraph.graph import StateGraph, END
## 1. 定义状态
class AgentState(TypedDict):
question: str
answer: str
## 2. 定义节点:收状态、返回状态更新的函数
def answer_node(state: AgentState):
return {"answer": f"你问的是:{state['question']}"}
## 3. 建图
graph = StateGraph(AgentState)
graph.add_node("answer", answer_node)
graph.set_entry_point("answer") # 入口
graph.add_edge("answer", END) # 固定走到终点
compiled = graph.compile()
## 4. 运行
result = compiled.invoke({"question": "Python 还是 Java?"})
print(result["answer"])
跑一下,你会看到 你问的是:Python 还是 Java?
为什么这样写? 节点函数只返回要更新的字段,剩下的原封不动保留。这种 partial update 机制让状态管理变得可预测,不会出现某个节点意外覆盖其他节点数据的情况。
第四步:实战——搭建「可循环搜索的研究助手」
现在加真实能力:
- 调用 LLM
- 根据判断决定要不要搜索
- 最多循环 3 次避免死循环
- 最终生成完整回答
4.1 完整代码
python
import os
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from duckduckgo_search import DDGS
## --- 状态定义 ---
class ResearchState(TypedDict):
messages: list
search_count: int
final_answer: str
## --- 工具函数 ---
def web_search(query: str) -> str:
"""用 DuckDuckGo 搜索并返回前 3 条摘要"""
results = DDGS().text(query, max_results=3)
return "\n".join([f"- {r['title']}: {r['body']}" for r in results])
## --- 节点1: 决定下一步 ---
def decide_next(state: ResearchState) -> str:
# 如果已经搜过 3 次,直接结束
if state.get("search_count", 0) >= 3:
return "summarize"
# 否则继续搜索
return "search"
## --- 节点2: 执行搜索 ---
def search_node(state: ResearchState) -> dict:
last_msg = state["messages"][-1].content
results = web_search(last_msg)
return {
"messages": [AIMessage(content=f"[搜索结果]\n{results}")],
"search_count": state.get("search_count", 0) + 1
}
## --- 节点3: 生成最终回答 ---
def summarize_node(state: ResearchState) -> dict:
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = "基于以下搜索结果,生成一份结构化对比总结:\n" + state["messages"][-1].content
response = llm.invoke([HumanMessage(content=prompt)])
return {"final_answer": response.content}
## --- 建图 ---
graph = StateGraph(ResearchState)
graph.add_node("search", search_node)
graph.add_node("summarize", summarize_node)
graph.set_entry_point("search")
## 条件路由
graph.add_conditional_edges("search", decide_next, {
"search": "search",
"summarize": "summarize"
})
graph.add_edge("summarize", END)
workflow = graph.compile()
## --- 运行 ---
result = workflow.invoke({
"messages": [HumanMessage(content="对比 Kafka 和 RabbitMQ 的核心差异")],
"search_count": 0,
"final_answer": ""
})
print(result["final_answer"])
4.2 拆解几个关键点
条件路由(Conditional Edge):add_conditional_edges 允许节点执行后,根据返回值动态决定下一步。路由逻辑和数据逻辑分离,比 if-else 链清晰得多。
循环控制:用 search_count 手动计数,达到 3 次后路由到 summarize。LangGraph 也内置了 recursion_limit 参数,但显式计数能在日志里看到真实轮次,调试更方便。
状态合并:每个节点只返回它要改的字段,messages 是 list,LangGraph 默认追加而不是覆盖。历史对话天然保留,不需要自己写 state["messages"].append()。
常见问题 & 踩坑提醒
1. 图跑起来报错 Recursion limit exceeded
LangGraph 默认最大递归深度是 25。忘了加退出条件或条件判断写反了,就会触发这个保护。解决办法:检查条件边返回值,确保有路径能走到 END;或者编译时加 checkpointer=None, recursion_limit=50。
2. 状态字段丢失
常见于:节点函数里直接修改了 state 对象(比如 state["x"] = 1),但没有 return。节点必须返回一个 dict,哪怕是空的 {} 也要返回,否则图会认为该节点没产生任何更新。
3. LLM 返回格式不稳定
让 LLM 输出 JSON 或特定格式时,建议用 output_parser 或直接开 response_format={"type": "json_object"}。别在 prompt 里祈祷它听话,用结构化输出。
下一步可以做什么
跑通这个 demo 后,可以往这些方向扩展:
- 加 Checkpointer(持久化):把状态存到 SQLite/Postgres,实现聊天机器人断点续聊
- 加 Human-in-the-loop:在关键节点暂停,等人确认后继续走
- 接入真实业务系统:把
web_search换成内部 API,将 Agent 嵌入工单审批或客服流程
LangGraph 的价值不在于「能画图」,而在于它把 AI 工作流的状态、路由、循环、暂停统一成了一套可测试、可调试的抽象。不用再用全局变量和回调硬凑了。
照着走一遍有问题,或者想看看接入内部系统的完整案例,留言告诉我卡在哪一步。