LangGraph多步工作流:Agent不够用的时候,我踩了4个坑

第6篇写了Agent------让AI自己决定调哪个工具。当时觉得挺完美:AI自己判断、自己调工具、自己回答,还要啥?

直到产品提了个需求: "用户问退货,先查知识库,再查订单状态,如果订单已发货还要调物流追踪,最后把所有信息整合成一条回复。"

用Agent试了一下------AI经常跳步:查了知识库就忘了查订单,或者查了订单就忘了整合。Agent能选工具,但管不了流程。 它不知道"先做什么再做什么",每次都像新手一样乱撞。

LangGraph就是干这个的------给Agent加流程控制。 不是让AI自己瞎选工具,而是你画好流程图,AI按图走。

我花了一周从零搭了一个LangGraph工作流,踩了4个坑。每个坑都让我重新理解了"工作流和Agent到底什么关系"。


先说结论

维度 Agent(第6篇) LangGraph
流程控制 AI自己决定,不可控 你画流程图,AI按图走
多步骤编排 靠Prompt引导,经常跳步 节点+边,流程100%确定
状态管理 无,每次调用独立 有状态图,步骤间可传数据
复杂度 中,要定义节点和边
适合场景 简单工具选择 多步骤、有条件分支的工作流

用Java人的理解:Agent ≈ 一个没有流程控制的Service,调哪个DAO全靠AI猜;LangGraph ≈ Spring StateMachine + 责任链模式,流程节点和转换条件你都定义好,AI只负责每个节点里的执行。

一句话:选工具用Agent,管流程用LangGraph。


坑1:以为LangGraph很复杂,其实核心就3个概念

我的第一反应

看LangGraph文档,上来就是StateGraph、Nodes、Edges、Conditional Edges......我脑子里的画面:

复制代码
图论 + 状态机 + 消息传递 + 条件路由......这不就是写了一个BPMN引擎吗?

完全不是。LangGraph核心就3个东西:

复制代码
State(状态) + Node(节点) + Edge(边)
概念 是什么 Java类比
State 在节点之间传递的数据对象 OrderContext------整个流程共享的上下文
Node 一个处理步骤,接收State,返回新State @Transactional 里的一个Service方法
Edge 节点之间的连线,可以加条件 if-else路由,或者StateMachine的转换条件

最小可用的LangGraph,大概30行代码。

最简示例:一个两步工作流

python 复制代码
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
​
# 1. 定义State------节点之间传递的数据
class MyState(TypedDict):
    question: str
    search_result: str
    answer: str
​
# 2. 定义节点------每个节点是一个函数
def search_node(state: MyState) -> MyState:
    """第一步:搜索知识库"""
    # 这里调RAG检索,简化为模拟
    result = f"关于「{state['question']}」的检索结果:退货需在7天内申请......"
    return {"search_result": result}  # 只返回修改的字段,会自动合并到State
​
def answer_node(state: MyState) -> MyState:
    """第二步:基于检索结果生成回答"""
    answer = f"根据查询:{state['search_result'][:20]}......完整回答如下:退货需在7天内申请。"
    return {"answer": answer}
​
# 3. 构建图------把节点和边连起来
graph = StateGraph(MyState)
graph.add_node("search", search_node)      # 添加节点
graph.add_node("answer", answer_node)
​
graph.add_edge(START, "search")             # 开始 → search
graph.add_edge("search", "answer")          # search → answer
graph.add_edge("answer", END)               # answer → 结束
​
# 4. 编译并运行
app = graph.compile()
result = app.invoke({"question": "退货流程是什么?"})
print(result["answer"])

运行过程:

scss 复制代码
[START] → search("退货流程是什么?") → answer(检索结果) → [END]

流程完全确定,不会跳步,不会遗漏。 这就是LangGraph和Agent的核心区别------Agent是"AI自己选路",LangGraph是"你画好路,AI走"。


坑2:条件分支写错,工作流跑飞了

翻车现场

需求:用户问退货,先判断是问"退货流程"还是"查订单状态"------问流程走知识库,查订单走API。

我这样写的:

python 复制代码
# 错误写法:条件路由函数返回的不是节点名
def route_intent(state: MyState) -> str:
    """判断用户意图"""
    question = state["question"]
    if "流程" in question:
        return "知识库"      # 中文节点名?或者英文?
    else:
        return "查订单API"   # 这到底对应哪个节点?

结果:KeyError: '知识库'------LangGraph找不到叫"知识库"的节点。

条件路由函数返回的必须是已注册的节点名称(字符串),一个字都不能错。

正确做法:路由函数返回节点名,节点名用常量

python 复制代码
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
​
class MyState(TypedDict):
    question: str
    intent: str
    knowledge_result: str
    order_result: str
    answer: str
​
# 节点1:判断意图
def classify_node(state: MyState) -> MyState:
    question = state["question"]
    # 实际项目中这里调LLM判断意图
    if "流程" in question or "怎么退" in question:
        return {"intent": "refund_process"}
    else:
        return {"intent": "order_status"}
​
# 节点2:查知识库
def knowledge_node(state: MyState) -> MyState:
    result = "退货流程:登录账户→我的订单→选择退货→填写原因→提交,3个工作日内审核。"
    return {"knowledge_result": result}
​
# 节点3:查订单API
def order_node(state: MyState) -> MyState:
    result = "订单ORD20250601:已发货,顺丰SF123456,预计明天送达。"
    return {"order_result": result}
​
# 节点4:生成回答
def answer_node(state: MyState) -> MyState:
    if state["intent"] == "refund_process":
        return {"answer": f"关于退货:{state['knowledge_result']}"}
    else:
        return {"answer": f"关于订单:{state['order_result']}"}
​
# 条件路由函数------返回值必须是已注册的节点名
def route_intent(state: MyState) -> Literal["knowledge", "order"]:
    if state["intent"] == "refund_process":
        return "knowledge"
    else:
        return "order"
​
# 构建图
graph = StateGraph(MyState)
graph.add_node("classify", classify_node)
graph.add_node("knowledge", knowledge_node)
graph.add_node("order", order_node)
graph.add_node("answer", answer_node)
​
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_intent)  # 条件分支
graph.add_edge("knowledge", "answer")
graph.add_edge("order", "answer")
graph.add_edge("answer", END)
​
app = graph.compile()

运行效果:

bash 复制代码
# 问流程
app.invoke({"question": "退货流程是什么?"})
# 路线:classify → knowledge → answer
​
# 问订单
app.invoke({"question": "我的订单到哪了?"})
# 路线:classify → order → answer

条件路由 = Java里的策略模式。 route_intent就是策略选择器,根据State里的字段决定走哪个分支。区别是Java里你写if-else,LangGraph里你写路由函数+add_conditional_edges

避坑要点:

要点 说明
路由函数返回值 必须是add_node时注册的节点名 ,用Literal类型约束
节点名统一 建议用英文小写+下划线,和Python函数命名一致
每条路径都要有终点 条件分支的每条路最终都要连到END,不然图不完整

坑3:State设计不合理,节点之间数据传丢

翻车现场

需求升级:查完知识库和订单后,两个结果都要传给answer节点。我这样设计State:

python 复制代码
class BadState(TypedDict):
    question: str
    intent: str
    result: str       # 问题:知识库和订单都用这个字段?
    answer: str

结果:

ini 复制代码
路线:classify → knowledge → answer
State里result = "退货流程:......"
​
路线:classify → order → answer
State里result = "订单状态:......"

看起来没问题?但如果以后需要两个都查呢? order的结果会把knowledge的结果覆盖掉,因为它们写的是同一个字段。

正确做法:每个节点的输出用独立字段

python 复制代码
class GoodState(TypedDict):
    question: str
    intent: str
    knowledge_result: str    # 知识库检索结果,独立字段
    order_result: str        # 订单查询结果,独立字段
    tracking_result: str     # 物流追踪结果,独立字段
    answer: str

State设计原则:

原则 说明 反例
每个节点输出独立字段 不要多个节点写同一个字段 result同时存知识库和订单
按需设字段 只定义真正需要的字段 把所有可能的字段都加进去
节点只返回修改的字段 未修改的字段自动保留 返回整个State副本

用Java人的理解:State ≈ 你的OrderContext对象。如果两个Service都往context.result里写数据,后者覆盖前者------这不是LangGraph的问题,是数据模型设计的问题。

一个更复杂的State设计------支持对话历史:

python 复制代码
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
​
class ChatState(TypedDict):
    messages: Annotated[list, add_messages]  # 对话历史,自动追加
    question: str
    intent: str
    knowledge_result: str
    order_result: str
    answer: str

Annotated[list, add_messages]的意思是:新消息追加而不是覆盖。这在多轮对话场景下必须用,不然每轮对话历史都被清空。


坑4:循环工作流写成死循环,调了半天找不到原因

翻车现场

需求又升级了:如果AI判断信息不够,要回去重新检索。于是我在图里加了个循环:

bash 复制代码
graph.add_conditional_edges("answer", should_retry, {
    "retry": "knowledge",   # 回去重新检索
    "end": END,
})

然后测试:

ruby 复制代码
问:退货要多久?
​
classify → knowledge → answer → retry → knowledge → answer → retry → knowledge → ...

无限循环! AI每次都觉得"信息不够"要重试,永远不停。

根本原因

LangGraph的循环和while True一样------必须有退出条件 。我的should_retry函数只判断了"信息够不够",没限制重试次数。

正确做法:在State里加重试计数器

python 复制代码
class MyState(TypedDict):
    question: str
    intent: str
    knowledge_result: str
    order_result: str
    answer: str
    retry_count: int       # 重试计数器
    max_retries: int       # 最大重试次数
​
def answer_node(state: MyState) -> MyState:
    """生成回答,信息不足时标记需要重试"""
    if not state.get("knowledge_result"):
        return {"answer": "信息不足", "retry_count": state.get("retry_count", 0) + 1}
​
    answer = f"根据查询结果:{state['knowledge_result'][:30]}......"
    return {"answer": answer}
​
def should_retry(state: MyState) -> Literal["retry", "end"]:
    """判断是否需要重试"""
    retry_count = state.get("retry_count", 0)
    max_retries = state.get("max_retries", 3)
​
    # 关键:超过最大重试次数必须退出
    if retry_count >= max_retries:
        return "end"
    if "信息不足" in state.get("answer", ""):
        return "retry"
    return "end"
​
# 构建图
graph = StateGraph(MyState)
graph.add_node("classify", classify_node)
graph.add_node("knowledge", knowledge_node)
graph.add_node("answer", answer_node)
​
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_intent)
graph.add_edge("knowledge", "answer")
graph.add_edge("order", "answer")
graph.add_conditional_edges("answer", should_retry, {
    "retry": "knowledge",
    "end": END,
})
​
app = graph.compile()
​
# 运行时传入max_retries
result = app.invoke({
    "question": "退货要多久?",
    "max_retries": 3,  # 最多重试3次
})

循环工作流的三条铁律:

铁律 说明
必须有最大重试次数 retry_count >= max_retries时强制退出
每次循环必须有新输入 别用同样的条件重试同样的操作------结果不会变
打日志 每次循环打印当前State,不然出了循环你都不知道卡在哪

用Java人的理解:这和写while循环一样------没有退出条件的循环就是死循环,不管是在Java里还是LangGraph里。Spring StateMachine也要你定义guard条件来防止死循环。


完整代码:一个退货智能客服工作流

python 复制代码
"""
LangGraph退货智能客服工作流
依赖:pip install langgraph langchain langchain-openai
"""
from typing import TypedDict, Literal, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
​
# ============ State定义 ============
​
class ServiceState(TypedDict):
    messages: Annotated[list, add_messages]  # 对话历史
    question: str
    intent: str                              # refund_process / order_status / unknown
    knowledge_result: str                    # 知识库检索结果
    order_result: str                        # 订单查询结果
    answer: str
    retry_count: int
    max_retries: int
​
# ============ 节点定义 ============
​
def classify_node(state: ServiceState) -> ServiceState:
    """意图识别"""
    question = state["question"]
    if "流程" in question or "怎么退" in question or "退货" in question:
        intent = "refund_process"
    elif "订单" in question or "到哪了" in question or "发货" in question:
        intent = "order_status"
    else:
        intent = "unknown"
    return {"intent": intent}
​
def knowledge_node(state: ServiceState) -> ServiceState:
    """查知识库"""
    # 实际项目:这里调RAG链
    knowledge = {
        "refund_process": "退货流程:登录→我的订单→申请退货→填原因→提交,3个工作日审核。退货需在收货后7天内,商品需保持原包装。",
        "unknown": "常见问题:1.退货需在7天内 2.换货请联系客服 3.退款3-5天到账",
    }
    result = knowledge.get(state["intent"], "未找到相关信息")
    return {"knowledge_result": result}
​
def order_node(state: ServiceState) -> ServiceState:
    """查订单API"""
    # 实际项目:这里调订单微服务
    return {"order_result": "订单ORD20250601:已发货,顺丰SF123456,预计明天送达。"}
​
def answer_node(state: ServiceState) -> ServiceState:
    """生成回答"""
    parts = []
    if state.get("knowledge_result"):
        parts.append(state["knowledge_result"])
    if state.get("order_result"):
        parts.append(state["order_result"])
​
    if parts:
        answer = "\n".join(parts)
    else:
        answer = "抱歉,我暂时无法回答这个问题,建议转接人工客服。"
​
    return {"answer": answer, "retry_count": state.get("retry_count", 0)}
​
# ============ 路由函数 ============
​
def route_intent(state: ServiceState) -> Literal["knowledge", "order", "answer"]:
    """意图路由"""
    intent = state.get("intent", "unknown")
    if intent == "refund_process":
        return "knowledge"
    elif intent == "order_status":
        return "order"
    else:
        return "answer"  # 未知意图直接回答
​
def should_retry(state: ServiceState) -> Literal["retry", "end"]:
    """重试判断"""
    retry_count = state.get("retry_count", 0)
    max_retries = state.get("max_retries", 3)
​
    if retry_count >= max_retries:
        return "end"
​
    if not state.get("knowledge_result") and not state.get("order_result"):
        return "retry"
​
    return "end"
​
# ============ 构建图 ============
​
graph = StateGraph(ServiceState)
​
# 添加节点
graph.add_node("classify", classify_node)
graph.add_node("knowledge", knowledge_node)
graph.add_node("order", order_node)
graph.add_node("answer", answer_node)
​
# 添加边
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_intent)
graph.add_edge("knowledge", "answer")
graph.add_edge("order", "answer")
graph.add_conditional_edges("answer", should_retry, {
    "retry": "knowledge",
    "end": END,
})
​
app = graph.compile()
​
# ============ 运行 ============
​
if __name__ == "__main__":
    print("退货智能客服已启动,输入问题开始提问(q退出):")
    while True:
        q = input("\n问:")
        if q.lower() == "q":
            break
        result = app.invoke({"question": q, "max_retries": 3})
        print(f"答:{result['answer']}")

运行效果:

css 复制代码
退货智能客服已启动,输入问题开始提问(q退出):
​
问:退货流程是什么?
答:退货流程:登录→我的订单→申请退货→填原因→提交,3个工作日审核。退货需在收货后7天内,商品需保持原包装。
​
问:我的订单到哪了?
答:订单ORD20250601:已发货,顺丰SF123456,预计明天送达。
​
问:今天天气怎么样?
答:抱歉,我暂时无法回答这个问题,建议转接人工客服。

3种不同意图,走3条不同路线------流程完全可控。


4个坑的总结

# 错误做法 正确做法 一句话
1 以为LangGraph很复杂 不敢下手 核心3件套:State+Node+Edge LangGraph = 你画路,AI走
2 条件分支节点名写错 中文/随意命名 Literal类型约束+英文小写统一 路由函数返回值=注册的节点名
3 State字段冲突 多节点写同一字段 每个节点输出独立字段 State设计=数据模型设计
4 循环工作流死循环 无退出条件 max_retries计数器+强制退出 没有退出条件的循环=死循环

Agent vs LangGraph:什么场景用哪个

场景 用Agent 用LangGraph
用户随便问,工具不多(3-5个) ✅ Agent够用 杀鸡用牛刀
固定流程,步骤有先后顺序 ❌ Agent会跳步 ✅ 流程图保证顺序
需要条件分支(A走这,B走那) ❌ Agent不知道分支 ✅ conditional_edges
需要循环重试 ❌ Agent容易死循环 ✅ 有计数器控制
需要多步骤间传数据 ❌ Agent无状态 ✅ State全局传递

一句话:简单选工具用Agent,复杂管流程用LangGraph。 别什么都上LangGraph------Agent能解决80%的问题,剩下20%才需要LangGraph。


LangGraph还能做什么

这篇只讲了最基础的工作流,LangGraph更强大的能力:

能力 说明 适合场景
多Agent协作 多个Agent分工合作,一个负责检索一个负责写作 需要不同角色配合的复杂任务
人机协作 在关键节点暂停,等人确认后继续 审批流程、高风险操作
持久化状态 State可以存到数据库,中断后恢复 长时间运行的工作流
流式输出 支持逐token输出,像ChatGPT一样 对话式AI应用

我接下来的计划: 用LangGraph的多Agent协作搭一个"调研助手"------一个Agent负责搜索,一个Agent负责总结,一个Agent负责审核总结质量。三个Agent流水线作业,比单个Agent效率高3倍。


你用过LangGraph吗?对比Agent有什么感受?评论区聊聊 👇

相关推荐
是小李呀1 小时前
Java集合扩容机制解析:ArrayList与HashMap底层原理及性能差异
java
SimonKing1 小时前
FreeLLMAPI:24 家厂商、128 个模型、1 个 Key智能切换所有可用的模型
java·后端·程序员
阿pin1 小时前
Java随笔-JVM内存模型
java·开发语言·jvm·内存模型
曲幽1 小时前
从卡顿到丝滑:FastAPI 调用外部 API 的正确姿势(httpx 实战)
python·fastapi·web·async·httpx·client·await·requests·aiohttp
AI 大模型学习不踩坑1 小时前
Harness 架构原理与工程实践详解
java·人工智能·神经网络·机器学习·计算机视觉·架构
不瘦80斤不改名1 小时前
HalfCart:基于LBS的1\.5km本地拼单系统全栈实践与踩坑复盘
python·docker·typescript·pycharm
深盾科技_Virbox2 小时前
软件交付保护如何从加壳走向代码虚拟化
java·python·安全
学计算机的计算基2 小时前
二叉树算法下篇:递归核心技巧与高频面试题详解
java·笔记·算法