聊《LangGraph并不难,难的是知道什么时候不该用》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。
摘要
上个月我们团队把 LangGraph 接进了一条审批流 Agent,Demo 跑得很顺,模型返回正确、工具调用没问题。结果上线第一天,运维群里炸了——一个普通测试账号触发了管理员权限的工具,日志里找不到关键节点的执行痕迹,接手的新同学看了三小时代码还搞不懂状态流转到哪一步断了。
这件事让我意识到,LangGraph 本身不难,难的是从"能跑"到"能交接"之间的工程化鸿沟。今天我们不聊概念,聊几个真实踩过的坑。
---
目录
- 从脚本到图:为什么 Demo 上线就翻车
- State 与 Node:状态是交接的货币
- Edge 与条件分支:排查时才发现的问题
- 人工审批节点:权限问题的根源
- 工程化落地:日志、权限、可观测性
- 失败原因分类:业务错误、配置错误、环境错误
- 适用边界:什么时候不该用 LangGraph
- 总结
从脚本到图:为什么 Demo 上线就翻车

很多团队做 Agent 的第一步是写脚本:
def run_agent(user_input):
response = llm.invoke(user_input)
if "审批" in response:
result = call_approval_tool(response)
return result
这段代码跑起来没问题,但有几个致命问题:
- 状态不可追踪:你不知道
response是从哪里来的,中间经过了什么逻辑 - 分支难管理:每加一个条件,函数就膨胀一分
- 无人工介入点:审批流里经理要确认,脚本里根本塞不进去
我们当时遇到的第一个翻车场景是:用户输入"帮我审批张三的报销单",Agent 直接调用了 approve_expense 工具,没有经过任何人工确认。原因很简单——脚本里没有"等待人工审批"这个节点。
图工作流的本质,是把 Agent 的执行路径显式化。每个节点做什么、状态怎么流转、什么条件下走哪条边,全都写在代码里,而不是藏在函数调用链里。
---
State 与 Node:状态是交接的货币

LangGraph 的核心抽象是 State 和 Node。很多开发者只关注 Node 里的逻辑,忽略了 State 的设计。
我们踩过的一个坑:State 里只存了 user_input 和 final_response,没有记录中间的工具调用结果。结果排查问题时,只能靠打印日志猜,而且日志量爆炸,关键信息淹没在里面。
正确的做法是,State 应该包含所有需要跨节点传递的信息:
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
# 用户原始输入
user_input: str
# 工具调用历史(用 operator 合并列表)
tool_calls: Annotated[list, operator.add]
# 当前执行节点
current_node: str
# 审批状态:pending / approved / rejected
approval_status: str
# 最终输出
final_response: str
这里的 Annotated[list, operator.add] 是关键——它告诉 LangGraph,每次写入 tool_calls 时,是追加而不是覆盖。这个设计细节决定了你能不能回溯完整的执行路径。
代码解释:TypedDict 定义了状态的结构,Annotated 是 LangGraph 特有的类型标记,operator.add 指定了合并策略。current_node 字段是排查问题的核心——出问题时你只需要查这个字段,就能知道 Agent 卡在哪一步。
---
Edge 与条件分支:排查时才发现的问题
条件分支是图工作流最强大的地方,也是最容易出问题的地方。
我们有一次遇到的故障现象是:用户输入"查看我的报销记录",Agent 却进入了"审批流程"分支,而不是"查询流程"分支。排查过程如下:
1. 复现问题:用同一个输入反复测试,确认问题稳定复现
2. 检查 State:发现 user_input 字段被之前的节点修改过,变成了"审批我的报销记录"
3. 定位原因:某个 Node 在执行时,把原始输入覆盖了,导致条件分支的判断依据出错
4. 修复方案:在 State 里分离 original_input 和 processed_input,条件分支只读 original_input
from langgraph.graph import StateGraph, END
# 定义条件路由函数
def route_by_intent(state: AgentState) -> str:
original = state["original_input"]
if "审批" in original or "批准" in original:
return "approval_node"
elif "查看" in original or "查询" in original:
return "query_node"
else:
return "fallback_node"
# 构建图
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("approval", approval_node)
graph.add_node("query", query_node)
graph.add_node("fallback", fallback_node)
# 条件边:根据 State 动态路由
graph.add_conditional_edges(
"agent",
route_by_intent,
{
"approval_node": "approval",
"query_node": "query",
"fallback_node": "fallback"
}
)
代码解释:add_conditional_edges 是条件分支的核心 API。第一个参数是当前节点名,第二个是路由函数,第三个是路由结果到目标节点的映射。路由函数接收完整的 State,返回字符串,LangGraph 根据返回值决定下一步走哪个节点。
注意路由函数应该只读 State,不修改 State——这是排查问题时最容易违反的规则。
---

人工审批节点:权限问题的根源
我们团队权限翻车的那次,问题就出在人工审批节点的设计上。
最初的实现是:Agent 检测到需要审批的意图后,直接调用工具完成审批。这在 Demo 阶段没问题,但上线后暴露了两个问题:
1. 权限越界:任何用户输入包含"审批"关键字,Agent 就会尝试调用管理员工具
2. 无法审计:审批操作没有记录操作人和审批时间
正确的做法是引入一个等待人工确认的节点:
import time
from langgraph.types import Command
def approval_wait_node(state: AgentState) -> Command:
"""等待人工审批的节点"""
# 记录审批请求到数据库
record = {
"request_id": generate_id(),
"user_id": state["user_id"],
"action": state["user_input"],
"status": "pending",
"created_at": time.time()
}
db.insert("approval_requests", record)
# 返回 Command 让图暂停,等待外部触发
return Command(
goto="wait_for_approval",
update={"approval_status": "pending", "approval_request": record}
)
def approval_confirm_node(state: AgentState) -> AgentState:
"""人工确认后执行"""
if state["approval_status"] == "approved":
result = call_approval_tool(state["approval_request"])
return {"final_response": f"审批通过: {result}", "approval_status": "completed"}
else:
return {"final_response": "审批已拒绝", "approval_status": "rejected"}
代码解释:Command 是 LangGraph 的高级特性,允许节点在返回状态的同时控制图的执行流程。goto="wait_for_approval" 让图跳转到等待状态,直到外部通过 graph.invoke(..., config={"configurable": {"approval_status": "approved"}}) 触发继续执行。这个模式天然支持权限控制——只有经过鉴权的外部调用才能推进审批流程。
---
工程化落地:日志、权限、可观测性
Demo 跑通和团队能接手,之间隔着三座山:日志、权限、可观测性。
日志:不要只打 print
很多开发者在 Node 里加 print 做日志,这在排查时几乎没用。正确的做法是利用 LangGraph 的回调机制:
from langchain_core.callbacks import BaseCallbackHandler
class TracingCallback(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
# 记录节点开始
log(f"[START] node={serialized.get('name')} inputs={inputs}")
def on_chain_end(self, outputs, **kwargs):
# 记录节点结束
log(f"[END] outputs={outputs}")
def on_tool_start(self, serialized, inputs, **kwargs):
# 记录工具调用
log(f"[TOOL] name={serialized.get('name')} inputs={inputs}")
然后在构建图时注入:
graph.compile(
callbacks=[TracingCallback()]
)
这样每次执行都会产生结构化的日志,包含节点名、输入、输出、工具调用信息。排查问题时,只需要搜索节点名,就能看到完整的执行轨迹。
权限:在 State 里记录操作人
权限问题不能靠"相信用户不会乱输入"来解决。正确的做法是在 State 里显式记录当前操作人,并在每个 Node 里做权限校验:
def check_permission(state: AgentState, required_role: str):
user_role = state.get("user_role", "guest")
role_hierarchy = {"guest": 0, "user": 1, "manager": 2, "admin": 3}
if role_hierarchy.get(user_role, 0) < role_hierarchy.get(required_role, 0):
raise PermissionError(f"需要 {required_role} 权限")
def approval_node(state: AgentState) -> AgentState:
check_permission(state, "manager") # 权限校验
# ... 审批逻辑
交付文档:比代码更重要的交接物
我们团队接手过一个 LangGraph 项目,代码写了 500 行,但没有一张图的结构说明。新同学花了两天时间才搞懂 State 里每个字段的含义。
正确的交付物应该包括:
- State 字段说明:每个字段的含义、来源、去向
- 图结构图:可以用 Mermaid 画出来,清晰展示节点和边的关系
- 路由逻辑说明:条件分支的判断依据
- 异常处理策略:每个节点可能的失败原因和兜底方案
---
失败原因分类:业务错误、配置错误、环境错误
排查 LangGraph 问题时,首先要区分错误的类型:
业务错误:逻辑写错了,比如条件分支的判断依据不对、State 字段被错误修改。这类错误通常表现为"结果不对",可以通过检查 State 的中间值来定位。
配置错误:回调没有正确注入、编译参数传错了、configurable 字段没设置。这类错误通常表现为"图跑不起来"或"行为不符合预期",可以通过检查编译后的图结构来定位。
环境错误:模型 API 超时、工具调用失败、数据库连接问题。这类错误通常有明确的异常信息,可以通过日志中的异常堆栈来定位。
我们当时踩的权限翻车问题,属于业务错误——我们错误地认为"能跑通 Demo 就等于实现了权限控制",忽略了 Agent 的执行路径里根本没有权限校验节点。
---
适用边界:什么时候不该用 LangGraph
LangGraph 不是银弹。以下几个场景,用简单的脚本或状态机可能更合适:
- 单次调用、无状态:如果 Agent 只是一次 LLM 调用加一次工具调用,不需要图结构
- 流程固定、无分支:如果执行路径是线性的,不需要条件分支
- 团队没有运维能力:LangGraph 的调试和排查需要一定的工程能力,小团队可能承受不起
取舍建议:如果你的 Agent 需要多轮对话、条件分支、人工介入、权限控制,那么 LangGraph 是值得投入的。如果只是一个简单的问答机器人,不要为了用而用。
---
总结
LangGraph 让 Agent 从脚本变成了可控的系统,但这只是第一步。从"能跑"到"能交接",中间隔着日志、权限、可观测性这些工程化细节。
我们团队那次翻车,根本原因不是 LangGraph 不会用,而是没有意识到 Demo 和上线之间的差距。权限问题、日志缺失、State 设计不合理——这些问题在 Demo 阶段都不明显,一旦上线、一旦换人接手,就会全部暴露。
写代码容易,写可交接的代码难。LangGraph 给了你构建复杂 Agent 的能力,但能不能用好,取决于你对工程化细节的重视程度。
资料展示
下面是我整理的AI大模型学习资料和工具包预览,适合收藏后按主题逐步学习。



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

4698



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



