聊《我把LangGraph接进项目后,先推翻了几个想当然》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。
摘要
最近有个业务方提需求,要做一个订单自动审核系统。Demo阶段跑得很顺,模型能判断评论质量、能根据金额决定审批路径。结果上线第一天就翻车了——风控说没有权限校验,审计说没有操作日志,运维说根本不知道哪一步卡住了。
我接手后花了两周排查,发现核心问题不在模型,而在工程化。今天复盘一下LangGraph工作流是怎么帮我们把这个系统从脚本变成可控系统的。
目录
- 为什么需要图工作流
- State 与 Node
- Edge 与条件分支
- 人工审批节点
- 工程化落地
- 适用边界
- 总结
为什么需要图工作流

先说一个真实场景。我们有个电商订单审核Agent,需求是这样的:
输入一个订单,包含商品描述、用户评论、金额。Agent需要:
1. 调用模型生成评论摘要
2. 判断评论质量(正面/负面/中立)
3. 如果金额>1000元,进入人工审批
4. 否则自动批准或拒绝
5. 记录完整的操作日志
最初我用脚本写,逻辑大概是这样:
def process_order(order):
# 步骤1:生成评论摘要
summary = call_model(order["comment"])
# 步骤2:判断质量
quality = judge_quality(summary)
# 步骤3:根据金额路由
if order["amount"] > 1000:
# 人工审批
result = manual_review(order, quality)
else:
# 自动审批
result = auto_approve(order, quality)
# 步骤4:记录日志
log_audit(result)
return result
这个脚本能跑通Demo,但有几个致命问题:
第一个问题是不可观测。 如果审核失败,你不知道是模型调用失败了,还是金额判断逻辑有问题,还是数据库写入失败了。日志是事后追加的,不是流程的一部分。
第二个问题是权限混乱。 人工审批节点需要特定角色才能执行,但脚本里没有任何权限检查。谁都能调这个接口,审核结果直接被覆盖。
第三个问题是状态丢失。 如果某个节点执行了一半服务重启,整个流程从头开始。订单审核这种场景,状态管理是刚需。
这时候就需要图工作流。LangGraph的核心思想是:把Agent的逻辑抽象成图,节点是执行单元,边是流转逻辑,状态是全局共享的。这样每个节点都可以独立测试、独立监控、独立重试。
State 与 Node

在LangGraph里,首先要定义State。State不是简单的字典,而是一个 TypedDict,它描述了整个工作流运行过程中所有节点共享的数据结构。
from typing import TypedDict, Annotated, Sequence
import operator
class OrderState(TypedDict):
# 输入数据
order_id: str
order_amount: float
user_comment: str
# 中间结果
comment_summary: Annotated[str, operator.add]
quality_score: float
quality_label: str
# 审批结果
approval_status: str # pending/approved/rejected/manual_review
reviewer: str
review_comment: str
# 审计信息
audit_log: Annotated[list, operator.add]
这里用到了Annotated和operator.add,这是LangGraph的状态合并机制。当一个节点返回部分状态时,operator.add会让新值追加到旧值后面,而不是覆盖。这对于日志记录特别有用。
接下来定义Node。每个Node就是一个普通函数,接收State,返回部分State更新:
def generate_summary(state: OrderState) -> dict:
"""调用模型生成评论摘要"""
comment = state["user_comment"]
# 调用模型
response = call_llm(f"请总结以下评论:{comment}")
summary = response["content"]
# 记录审计日志
audit_entry = {
"step": "generate_summary",
"order_id": state["order_id"],
"timestamp": datetime.now().isoformat(),
"status": "success"
}
return {
"comment_summary": [summary],
"audit_log": [audit_entry]
}
def judge_quality(state: OrderState) -> dict:
"""判断评论质量"""
summary = state["comment_summary"][-1]
# 调用模型打分
response = call_llm(f"请对以下评论摘要打分(0-1):{summary}")
score = float(response["content"])
# 根据分数打标签
if score > 0.7:
label = "positive"
elif score < 0.3:
label = "negative"
else:
label = "neutral"
audit_entry = {
"step": "judge_quality",
"order_id": state["order_id"],
"score": score,
"label": label,
"timestamp": datetime.now().isoformat(),
"status": "success"
}
return {
"quality_score": score,
"quality_label": label,
"audit_log": [audit_entry]
}
这里有个关键设计:每个节点都向audit_log追加记录。这样不管工作流走到哪一步,完整的操作历史都在State里。
排查过程:有一次上线后,有用户反馈订单审核超时。我查看audit_log发现,generate_summary节点成功执行了,但judge_quality节点没有日志记录。进一步排查发现,模型调用超时了,但异常没有被捕获,导致整个节点失败且没有日志。修复方式是在每个节点加try-catch,失败时记录错误日志并返回默认值:
def judge_quality(state: OrderState) -> dict:
try:
summary = state["comment_summary"][-1]
response = call_llm(f"请对以下评论摘要打分(0-1):{summary}")
score = float(response["content"])
if score > 0.7:
label = "positive"
elif score < 0.3:
label = "negative"
else:
label = "neutral"
status = "success"
except Exception as e:
# 异常时返回默认值,避免整个工作流崩溃
score = 0.5
label = "neutral"
status = f"error: {str(e)}"
logger.error(f"Quality judgment failed for order {state['order_id']}: {e}")
audit_entry = {
"step": "judge_quality",
"order_id": state["order_id"],
"score": score,
"label": label,
"status": status,
"timestamp": datetime.now().isoformat()
}
return {
"quality_score": score,
"quality_label": label,
"audit_log": [audit_entry]
}
Edge 与条件分支
定义了节点之后,需要定义边来控制流转逻辑。LangGraph支持两种边:普通边和条件边。
普通边是固定的流转,比如A节点完成后一定进入B节点。条件边则根据State的值决定下一步走哪条路:
def route_by_amount(state: OrderState) -> str:
"""根据金额决定走自动审批还是人工审批"""
amount = state["order_amount"]
quality_label = state["quality_label"]
# 金额>1000或评论质量负面,需要人工审批
if amount > 1000 or quality_label == "negative":
return "manual_review"
else:
return "auto_approve"
这个条件函数会返回下一个节点的名称。LangGraph会根据返回值动态选择路径。
完整的图构建代码如下:
from langgraph.graph import StateGraph, END
# 构建图
graph = StateGraph(OrderState)
# 添加节点
graph.add_node("generate_summary", generate_summary)
graph.add_node("judge_quality", judge_quality)
graph.add_node("auto_approve", auto_approve)
graph.add_node("manual_review", manual_review)
# 添加边
graph.add_edge("generate_summary", "judge_quality")
graph.add_conditional_edges(
"judge_quality",
route_by_amount,
{
"auto_approve": "auto_approve",
"manual_review": "manual_review"
}
)
graph.add_edge("auto_approve", END)
graph.add_edge("manual_review", END)
# 设置入口点
graph.set_entry_point("generate_summary")
# 编译
app = graph.compile()
代码解释:这里add_conditional_edges是关键。第一个参数是源节点,第二个参数是条件函数,第三个参数是条件到节点的映射。当route_by_amount返回"manual_review"时,图会跳转到manual_review节点;返回"auto_approve"时,跳转到auto_approve节点。
这个设计的好处是:条件逻辑和业务逻辑分离。route_by_amount只负责判断走哪条路,不负责执行审批。每个节点只做一件事,易于测试和维护。

人工审批节点
人工审批是Agent工作流中最复杂的场景之一。它涉及权限控制、状态持久化、超时处理等问题。
def manual_review(state: OrderState) -> dict:
"""人工审批节点"""
order_id = state["order_id"]
reviewer = get_current_user() # 获取当前登录用户
# 权限检查:只有风控团队才能执行人工审批
if not has_permission(reviewer, "order_review"):
audit_entry = {
"step": "manual_review",
"order_id": order_id,
"status": "permission_denied",
"user": reviewer,
"timestamp": datetime.now().isoformat()
}
return {"audit_log": [audit_entry]}
# 将订单放入待审批队列
approval_request = {
"order_id": order_id,
"amount": state["order_amount"],
"quality_label": state["quality_label"],
"reviewer": reviewer,
"status": "pending",
"created_at": datetime.now().isoformat()
}
# 持久化到数据库
save_to_approval_queue(approval_request)
audit_entry = {
"step": "manual_review",
"order_id": order_id,
"status": "submitted",
"reviewer": reviewer,
"timestamp": datetime.now().isoformat()
}
return {
"approval_status": "pending",
"audit_log": [audit_entry]
}
这里有两个关键设计:
第一,权限检查是节点的一部分,不是外围装饰。 很多团队会把权限检查放在API层,但这样如果工作流被其他地方调用,权限检查就失效了。把权限检查放在节点内部,保证无论从哪里触发,权限规则都生效。
第二,人工审批是异步的。 节点执行完后,工作流并不直接返回结果,而是将订单放入待审批队列。人工审批完成后,会通过回调或轮询的方式继续工作流。这涉及到图的状态持久化,后面工程化部分会详细讲。
失败原因:有一次人工审批节点一直卡住,订单状态一直是pending。排查发现是数据库连接池配置错误,save_to_approval_queue调用失败但异常被吞掉了。后来在每个数据库操作都加了明确的异常处理和日志,问题才解决。
区分失败原因的关键是看audit_log:
- 如果某一步有日志记录但状态是"error",说明是业务逻辑错误
- 如果某一步完全没有日志记录,说明是配置或环境问题
- 如果日志记录完整但结果不对,说明是模型或算法问题
工程化落地
把LangGraph工作流从Demo变成生产系统,需要解决三个问题:状态持久化、可观测性、权限控制。
状态持久化:默认情况下,LangGraph的状态只存在于内存中。服务重启后状态丢失。生产环境需要把状态持久化到数据库:
from langgraph.checkpoint.sqlite import SqliteSaver
# 使用SQLite作为检查点存储
checkpointer = SqliteSaver.from_conn_string("./checkpoints.db")
app = graph.compile(checkpointer=checkpointer)
这样每次节点执行后,State都会被保存到数据库。即使服务重启,也能从检查点恢复执行。
可观测性:给每个节点添加日志和指标:
import time
from functools import wraps
def instrument_node(node_func):
@wraps(node_func)
def wrapper(state: OrderState, config=None):
start_time = time.time()
node_name = node_func.__name__
order_id = state.get("order_id", "unknown")
try:
result = node_func(state, config)
duration = time.time() - start_time
# 记录成功日志
logger.info(f"Node {node_name} completed for order {order_id} in {duration:.2f}s")
return result
except Exception as e:
duration = time.time() - start_time
# 记录失败日志
logger.error(f"Node {node_name} failed for order {order_id} after {duration:.2f}s: {e}")
raise
return wrapper
用装饰器包裹每个节点,自动记录执行时间和异常。这样在监控面板上就能看到每个节点的耗时分布,快速定位瓶颈。
权限控制:除了节点内部的权限检查,还需要在图的入口处做全局校验:
def run_with_auth(state: OrderState, user: str):
"""带权限校验的入口函数"""
# 全局权限检查
if not is_authorized(user, "order_processing"):
raise PermissionError(f"User {user} is not authorized")
# 执行工作流
result = app.invoke({**state, "audit_log": []}, config={"configurable": {"user": user}})
return result
这样无论从哪里调用工作流,都需要经过权限校验。
适用边界
LangGraph工作流适合以下场景:
- 多步骤的复杂业务流程,需要条件分支
- 需要人工干预的审批流程
- 对可观测性有要求的生产系统
- 需要状态持久化和恢复的场景
但不适合:
- 简单的单步调用,直接用函数即可
- 实时性要求极高的场景,图工作流的开销可能不划算
- 逻辑非常简单且固定的流程,过度设计会增加维护成本
取舍:引入图工作流会增加代码复杂度,需要学习新的概念(State、Node、Edge、Checkpointer)。如果你的项目只是Demo或者逻辑很简单,不要为了用而用。但当流程复杂度超过3个节点、需要权限控制和审计日志时,图工作流的价值就体现出来了。
总结
从Demo到生产,差的不只是代码质量,更是工程化能力。权限、日志、可观测性,这些看似" boring"的东西,才是决定Agent项目能否上线的关键。
LangGraph工作流的价值不在于它能调用模型,而在于它能把复杂的业务流程抽象成图,让每个节点独立可控、状态可追溯、异常可定位。
我的建议是:先写一个简单的工作流跑通核心逻辑,然后逐步添加权限检查、日志记录、状态持久化。不要一开始就追求完美架构,但也不要只停留在Demo阶段就上线。
Demo能跑通只是开始,能解释失败、能追踪状态、能控制权限,才算真正入门。
资料展示
下面是我整理的AI大模型学习资料和工具包预览,适合收藏后按主题逐步学习。



如果你想看完整资料目录,可以在评论区留言「资料」;也欢迎告诉我你更关注AI大模型里的哪类内容。

342



被折叠的 条评论
为什么被折叠?



