LangGraph 实战:搭建带状态的多步骤 AI Agent

124 次阅读 0 点赞 0 评论 11 分钟原创技术教程

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

#AI Agent # LangGraph # 多步骤工作流 # Python # LLM 应用开发 # 状态管理
LangGraph 实战:搭建带状态的多步骤 AI Agent

LangGraph 实战:搭建带状态的多步骤 AI Agent

你遇到过的痛点

跟团队聊 AI 应用落地时,很多人卡在同一个问题:「我想让 Agent 根据用户输入动态决定下一步,而不是写死一串 if-else,怎么做?」

比如做一个研究助手,用户输入 "对比 Kafka 和 RabbitMQ",理想流程是:

  1. 先查资料,拿到双方特性
  2. 判断是否需要补充最新社区数据
  3. 需要就调搜索工具,不需要就直接整理
  4. 汇总输出

用普通 prompt chain 能写,但流程一旦变复杂——需要循环、条件跳转、记住中间状态——代码就会变成一团 spaghetti。状态到处飘,调试靠 print,加新分支就得重写大段逻辑。

LangGraph 就是为了解决这个问题。它把 AI Agent 的逻辑表达成图(Graph):节点是执行单元,边是流转条件,状态自动在节点间传递。GitHub 上 18.5k Star 已验证了这条路的实用性。

这篇教程带你从零开始,搭一个能自主决策、带状态、可循环的多步骤 Agent。不是玩具 demo,是能跑、能调试、能扩展的真实工作流。

仓库地址:langchain-ai/langgraph


前置条件

  • 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 工作流的状态、路由、循环、暂停统一成了一套可测试、可调试的抽象。不用再用全局变量和回调硬凑了。

照着走一遍有问题,或者想看看接入内部系统的完整案例,留言告诉我卡在哪一步。

最后更新:2026-08-05T10:03:10

评论 (0)

发表评论

blog.comments.form.loading
0/500

暂无评论,快来发表第一条评论吧!