第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有什么感受?评论区聊聊 👇