聊《LangGraph真能提效吗?先看流程里最慢的那一步》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。
摘要
摘要:业务方一句"加个审批流程",Demo 级别的 Agent 瞬间崩盘。这篇文章复盘我从脚本式 Agent 转向 LangGraph 图工作流的过程,核心观点是:大模型应用真正难的不是调用模型,而是把权限、日志、人工审批这些工程要素嵌进工作流里。通过一个报销审批系统的实战案例,展示 State、Node、Conditional Edge 的设计思路,以及为什么图结构比链式调用更适合生产环境。
---
目录
- 业务方一句话,Demo 就崩了
- 为什么脚本式 Agent 扛不住生产需求
- State 设计:先画地图再走路
- Node 与 Conditional Edge:用图表达业务逻辑
- 人工审批节点:Agent 里最难的那一步
- 工程化落地:权限、日志、可观测
- 总结
---
业务方一句话,Demo 就崩了

上个月接到一个内部需求:给客服团队做一个"工单分类助手"。业务方原话是"帮客服把工单自动分类,不用他们手动选类别"。
我花了两个晚上搭了个 Demo:用户输入工单内容,Agent 调用大模型判断类别,返回结果。代码不到 100 行,跑起来丝滑得很。业务方看完点点头,说"挺好的,上线吧"。
一周后,业务方又来了:
> "分类错了要能手动修正,修正后要有日志记录。另外,涉及客户隐私的工单,只有组长才能看详情。还有,分类结果超过 3 秒没响应要超时降级。"
我盯着屏幕,沉默了三分钟。
这还只是需求清单的前三条。
Demo 能跑,是因为我只考虑了" happy path "。真正上线需要的权限校验、日志追踪、超时处理、人工干预,在脚本式架构里完全是另一套代码。而且这几套逻辑互相嵌套,改一处崩一处。
我当时的判断是:这个需求用 LangChain 的 Chain 或者简单的 Agent 循环已经搞不定了,得换个思路。
---
为什么脚本式 Agent 扛不住生产需求

先说结论:脚本式 Agent 的问题不在于模型,而在于控制流。
在 Demo 阶段,我们的工作流是线性的:输入 → 模型调用 → 输出。这个过程看起来简单,但一旦业务需求增加分支、循环、人工介入、权限校验,线性结构就开始失控。
举个反例。之前看过一个团队做的"合同审核 Agent",用 LangChain 搭了一个 Pipeline:上传合同 → LLM 提取条款 → LLM 判断风险 → 生成报告。Demo 跑通了,上线后业务方要求"高风险合同必须人工复核"。
他们的做法是在 Pipeline 里加一个 if 判断:风险分数超过阈值就跳过自动输出,等待人工确认。看起来没问题,但实际运行中出现了几个 bug:
- 人工复核期间,系统不知道这个合同处于"等待复核"状态,下次请求进来又触发了一次审核
- 日志里没有记录"这个合同为什么被拦截",排查问题只能靠翻代码
- 权限控制是后加的,不同角色的查看范围没有在工作流层面体现
这些问题本质上都是控制流不清晰导致的。脚本式架构把业务逻辑散落在代码各处,没有统一的"状态"概念,一旦需求变化,修改就像拆东墙补西墙。
LangGraph 的核心价值,是把工作流显式地建模成一张图。节点代表处理逻辑,边代表流转条件,状态贯穿始终。这样权限、日志、人工审批都可以作为图的一部分来设计,而不是事后打补丁。
---
State 设计:先画地图再走路
写 LangGraph 工作流,第一步永远是定义 State。
State 不是随便一个 dict,它是整个工作流的"单点真相"。每个 Node 读什么、写什么,必须在 State 里说清楚。我之前踩过的坑是:State 定义得太松,Node 之间互相依赖隐含信息,最后调试的时候根本不知道数据从哪来的。
以报销审批系统为例,State 应该包含这些字段:
from typing import TypedDict, Annotated, Optional
import operator
class ApprovalState(TypedDict):
# 报销基本信息
claim_id: str
amount: float
description: str
# 权限相关
requester_role: str # employee / manager / finance
has_permission: bool
# 审批流程状态
approval_status: str # pending / approved / rejected / needs_review
approval_history: Annotated[list, operator.add]
# 人工审批节点等待信号
waiting_for_approval: bool
manual_decision: Optional[str]
# 日志与可观测
trace_id: str
log_entries: Annotated[list, operator.add]
# 最终输出
result: Optional[str]
这里有几个设计细节值得注意:
第一,用 Annotated 和 operator.add 处理列表累加。approval_history 和 log_entries 用这个模式,每个 Node 往里面追加记录,State 会自动合并,不用手动管理。
第二,权限字段放在 State 里,而不是在 Node 内部判断。这样权限校验逻辑可以封装成一个独立的 Node,其他 Node 不关心权限,只看 has_permission 这个字段。职责分离,改权限规则只改一个地方。
第三,waiting_for_approval 是人工审批节点的关键信号。这个字段告诉图:"当前流程暂停了,等外部信号恢复"。没有这个状态标记,图无法表达"等待人工介入"这个语义。
---

Node 与 Conditional Edge:用图表达业务逻辑
State 定好后,开始设计 Node 和 Edge。
Node 就是普通的 Python 函数,接收 State,返回更新后的 State。关键是要让每个 Node 只做一件事。
from langgraph.graph import StateGraph, START, END
def check_permission(state: ApprovalState) -> ApprovalState:
"""权限校验节点"""
role = state["requester_role"]
amount = state["amount"]
# 权限规则:金额超过 5000 需要 manager 以上角色
if amount > 5000 and role == "employee":
state["has_permission"] = False
state["log_entries"].append({
"step": "permission_check",
"action": "denied",
"reason": "amount_exceeds_role_limit"
})
else:
state["has_permission"] = True
state["log_entries"].append({
"step": "permission_check",
"action": "allowed"
})
return state
def classify_claim(state: ApprovalState) -> ApprovalState:
"""分类节点 - 调用大模型"""
if not state["has_permission"]:
state["result"] = "permission_denied"
return state
# 调用 LLM 进行分类
# ... 模型调用逻辑 ...
state["approval_status"] = "pending"
return state
Node 之间用 Edge 连接。普通 Edge 是无条件跳转,Conditional Edge 根据 State 决定走哪条路:
def route_approval(state: ApprovalState) -> str:
"""条件路由:根据金额和角色决定流程"""
if not state["has_permission"]:
return "denied"
if state["amount"] > 10000:
return "manual_review" # 大额需要人工审批
return "auto_approve" # 小额自动通过
graph = StateGraph(ApprovalState)
# 添加节点
graph.add_node("check_permission", check_permission)
graph.add_node("classify_claim", classify_claim)
graph.add_node("manual_review", manual_review_node)
graph.add_node("finalize", finalize_node)
# 添加边
graph.add_edge(START, "check_permission")
graph.add_edge("check_permission", "classify_claim")
graph.add_conditional_edges(
"classify_claim",
route_approval,
{
"denied": "finalize",
"manual_review": "manual_review",
"auto_approve": "finalize"
}
)
graph.add_edge("manual_review", "finalize")
graph.add_edge("finalize", END)
app = graph.compile()
这个设计的关键在于:业务规则被显式地表达在图的结构里,而不是隐藏在 Node 的内部逻辑中。route_approval 函数就是一个清晰的条件分支,改规则只改这个函数,不影响其他节点。
---
人工审批节点:Agent 里最难的那一步
人工审批是 Demo 和生产的分水岭。
在 Demo 里,"人工审批"通常是一个占位符:打印一句"请人工审批",然后假设审批通过了继续跑。在生产环境里,这个假设是危险的。
真实场景中,人工审批节点需要解决三个问题:
第一,流程暂停机制。图执行到人工审批节点时,应该停下来等待,而不是继续跑。LangGraph 支持通过 interrupt 机制实现:
def manual_review_node(state: ApprovalState) -> ApprovalState:
"""人工审批节点"""
# 记录等待状态
state["waiting_for_approval"] = True
state["log_entries"].append({
"step": "manual_review",
"action": "waiting_for_human"
})
# 中断执行,等待外部信号
return state
编译图时,通过 interrupt 配置,这个节点执行后会暂停,返回当前 State,等外部调用 update_state 恢复:
# 执行到人工审批节点时暂停
result = app.invoke(initial_state)
# 此时 result 包含 waiting_for_approval=True
# 等待人工审批...
# 模拟审批通过
app.update_state(result["config"], {"manual_decision": "approved"})
# 恢复执行
result = app.invoke(None, config=result["config"])
第二,审批权限校验。不是谁都能审批。在 manual_review_node 里应该校验审批人的角色权限,而不是假设调用方已经做过校验。权限校验应该贯穿整个工作流,而不是只在入口做一次。
第三,审批日志。每次人工操作都要记录:谁、什么时候、审批了什么、理由是什么。这些日志不是事后补的,而是工作流的一部分。
我之前见过一个反例:某个团队的"人工审核节点"只是简单地把数据存到数据库,没有记录操作人、没有时间戳、没有审计日志。上线后业务方要查"谁审批的这笔报销",翻代码翻了两小时。
---
工程化落地:权限、日志、可观测
把 Agent 从 Demo 推到生产,真正卡住的往往不是模型调用,而是这三个工程要素。
权限控制。权限不应该是一个单独的模块,而应该渗透进工作流的每一个节点。我的做法是:每个 Node 在执行前先检查当前用户的角色和 State 中的权限字段,不符合就直接返回错误状态,不继续执行后续逻辑。
def require_permission(state: ApprovalState, required_role: str):
"""权限校验装饰器"""
if not state["has_permission"]:
return {"result": "permission_denied"}
# 检查角色是否满足要求
role_hierarchy = {"employee": 1, "manager": 2, "finance": 3}
if role_hierarchy.get(state["requester_role"], 0) < role_hierarchy.get(required_role, 0):
return {"result": "insufficient_role"}
return {}
日志追踪。每个 Node 执行前后记录日志,日志里包含 trace_id、节点名称、输入输出摘要、执行时间。日志不是打到控制台就完事,要持久化到数据库,方便事后排查。
import time
from uuid import uuid4
def with_logging(node_func):
"""日志装饰器"""
def wrapper(state: ApprovalState) -> ApprovalState:
start = time.time()
trace_id = state.get("trace_id", str(uuid4()))
state["log_entries"].append({
"trace_id": trace_id,
"node": node_func.__name__,
"action": "start",
"timestamp": time.time()
})
result = node_func(state)
duration = time.time() - start
result["log_entries"].append({
"trace_id": trace_id,
"node": node_func.__name__,
"action": "complete",
"duration_sec": duration
})
return result
return wrapper
可观测性。除了日志,还要有 metrics:每个节点的执行次数、平均耗时、错误率。这些指标可以帮助定位瓶颈。比如某次上线后发现"classify_claim"节点的 P99 延迟从 2 秒涨到 8 秒,通过追踪发现是某个 LLM API 的限流导致的,及时调整了降级策略。
---
总结
从脚本式 Agent 到 LangGraph 图工作流,本质上是把隐式的控制流显式化。
Demo 阶段的 Agent 追求的是"能跑",生产阶段的 Agent 追求的是"可控"。可控意味着:流程清晰、状态可追踪、权限有校验、失败有兜底、人工可介入。
LangGraph 提供的图结构、State 管理、Conditional Edge、中断机制,正好对应了这些需求。但它不是银弹——用不好一样会写出难以维护的代码。关键是要先想清楚业务逻辑,再把逻辑映射到图的结构上,而不是先写代码再想办法加权限和日志。
我的建议是:如果你的 Agent 工作流还只有"输入 → 模型 → 输出"这一条路径,先用 LangChain 或者脚本搭 Demo 没问题。但一旦需求开始涉及分支判断、人工介入、权限控制,就该认真考虑图工作了。这时候改架构的成本,远低于上线后打补丁的成本。
最后说一句实在的:面试的时候,"我用 LangGraph 搭了一个报销审批系统"比"我用 LangChain 调了个 Agent"更有说服力。前者说明你考虑过生产环境的问题,后者只能证明你跑通了教程。
资料展示
下面是我整理的AI大模型学习资料和工具包预览,适合收藏后按主题逐步学习。



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

4041



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



