如果只是让模型生成一段文字,你完全不需要 LangGraph。真正让人崩溃的是,当业务要求变成“先判断意图,再分流到不同处理流程,中间要层层追问、失败重试、多轮对话还要记住上下文”的时候,用普通 Python 堆代码很快会把状态管理变成一场灾难。LangGraph 的核心价值就是把应用流程组织成一张有向图:状态是图上流动的数据,节点是执行动作,边决定下一步去哪,最终通过 compile() 得到可运行的图对象。这个“从模型定义到图运行”的过程,是入门 LangGraph 最重要的一条主线,理解透了,后面那些花哨的 agent 编排、多角色协作、人工审核中断都不再是黑盒。
这篇文章适合刚入门 LangGraph、被官方文档绕晕的开发者。我会用一个真实场景逐步搭建完整的图工作流,把状态、节点、边、编译、运行、记忆、条件路由拆开讲清楚,并附上踩坑记录。文中的案例和代码都来自我自己跑通的实践,不涉及任何特定公司的内部系统,请放心跟着做。
1. 搞懂 LangGraph 之前,先搞懂它要解决的痛点
1.1 当普通 Python 脚本开始失控时
两年前我带一个模拟项目X,做的是客服工单自动分类演示。第一版实现特别简单:用户输入进来,调用一次模型判断意图,再走一个 if-else 分支返回答案。大概二十几行 Python,跑得很顺。
后来需求加了:同一用户多轮对话不能断片;分类置信度低时要追问;账单类问题需要先验证身份;遇到技术排查要分步执行,中间还可能出错重试。我开始用全局变量保存会话状态,用字典维护不同流程的跳转表,用嵌套函数处理分支……半个月后,这个脚本变成了一坨只能靠注释和图谱才能理解的东西。最致命的问题是:流程和状态混在一起,你没法单独回答“当前对话到底进行到哪一步了”。
这不是我一个人遇到的问题。LLM 应用一旦进入真实业务,本质就变成了“带状态的流程编排”:输入是什么、已经完成哪些步骤、下一步走哪条路、失败了怎么重试、中途被打断怎么恢复。普通脚本把这些信息散落在变量和函数返回值里,而 LangGraph 把它们显式建模成了一张图。
1.2 LangGraph 的三个核心抽象:状态、节点、边
LangGraph 的底层抽象非常少,就三个:State、Node、Edge。
- State(状态):图在执行过程中共享的数据结构。它像一张流水线上的共享便签,每个工位可以往上面补充内容,后续工位读这张便签决定下一步。
- Node(节点):一个普通的 Python 函数,输入当前 State,输出要对 State 做的增量更新。它不负责“全局协调”,只负责“这一道工序”。
- Edge(边):定义节点之间的执行顺序。普通边是固定流向,条件边可以根据 State 里的内容动态选择下一个节点。
用一张对比表看会更清楚:
| 方案 | 状态管理方式 | 流程表达 | 中途恢复/重试 | 适合场景 |
|---|---|---|---|---|
| 普通 Python 脚本 | if-else + 全局变量 | 函数调用栈 | 基本靠手工 | 2-3 步固定流程 |
| LangChain 链式调用 | 链内部隐式传递 | 声明式链 | 弱 | 固定单链 |
| LangGraph | 显式 State 对象 | 有向图 | 检查点机制原生支持 | 复杂分支、多轮对话、人工介入 |
我当时把这个客服系统的逻辑画成图之后,才发现自己之前一直在“手写图引擎”。图里每个节点就是一个可测试的独立函数,每条边就是一次显式的流转,这个表达能力完全碾压二十个嵌套 if-else。所以我的建议是:入门 LangGraph 的第一件事不是学 API,而是先把流程图画出来,哪怕你用纸笔画都行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 状态、节点、边到底怎么理解,以及最小编码怎么落地
2.1 State:一张可以增量更新的共享便签
LangGraph 的 State 是用 TypedDict 定义的。为什么用 TypedDict?因为它能给字段加上类型,编辑器补全友好,同时也符合 Python 社区习惯。最简定义长这样:
python复制from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
第一次接触的人通常会忽略 Annotated[list, add_messages] 这里的门道。LangGraph 默认对 State 里的字段执行“覆盖”语义:节点返回什么值,就整个替换掉原字段。但对话消息是不断累积的,我们希望每次新消息“追加”而不是“覆盖”,所以必须给 messages 字段指定一个 reducer(归约函数),add_messages 就是官方提供的追加函数。
这个设定我后面会反复强调,因为它是 LangGraph 里最容易踩的坑:你以为返回 {"messages": [...]} 是追加,实际没配 reducer 时是覆盖,对话记录直接消失。
2.2 节点函数:只写增量,不碰全局状态
节点就是一个普通函数,签名固定为 (state: State) -> dict。它接收当前状态,返回需要更新的字段,而不是返回整个新状态。这种设计很像 React 里 setState 的思想:每个节点只声明自己贡献的部分,LangGraph 负责把各部分按 reducer 规则合并回状态。
一个最朴素的模型调用节点长这样:
python复制def chat_node(state: State):
response = llm.invoke(state["messages"])
return {"messages": [response]}
注意这里返回的是一个 list 包着一个响应消息。因为 add_messages 会遍历 list 将每个消息追加到 state["messages"] 末尾。如果后面还有别的内容要更新,可以同时返回多个字段:
python复制def chat_node(state: State):
response = llm.invoke(state["messages"])
return {
"messages": [response],
"last_model": "gpt-4o-mini",
}
2.3 边:固定流向是第一级,条件路由是第二级
图里边的种类虽然多,但核心只有两类:
- 固定边(Edge):
graph.add_edge("nodeA", "nodeB"),表示 A 执行完无条件走向 B。 - 条件边(Conditional Edge):
graph.add_conditional_edges(...),根据状态内容动态返回下一个节点名。
固定边适合流水线,条件边适合分岔路。真正复杂的工作流,往往是两者嵌套:先固定顺序跑几步,到某个节点做条件判断,分流后各走各的管道,最后再合流。
2.4 编译与运行:图对象才是真正的执行体
把节点和边都声明完后,调用 graph.compile() 得到 CompiledGraph 对象,这个对象才是可以 invoke 的执行体。为什么需要编译这一步?因为 LangGraph 要在运行前校验图结构:有没有节点未定义就被引用、有没有死循环路径、有没有非法并行结构。它还会生成内部执行计划,让你可以流式输出每一步结果。
python复制from langgraph.graph import StateGraph, START, END
graph = StateGraph(State)
graph.add_node("chat", chat_node)
graph.add_edge(START, "chat")
graph.add_edge("chat", END)
app = graph.compile()
这里 START 和 END 是特殊虚拟节点,分别代表入口和出口。入口到第一个真实节点之间必须有一条边,最后一个节点也要连到 END,否则编译会报错。
运行也很简单:
python复制result = app.invoke({
"messages": [{"role": "user", "content": "你好"}
})
print(result["messages"][-1].content)
到这里,一个最简的 LangGraph 工作流已经跑通了,完整链路就是:定义 State → 定义节点函数 → 组装成图 → 编译 → invoke。标题里说的“从模型定义到图运行”,第一步已经完成了。
3. 从模型定义到图运行的完整链路:一个能跑的写作助手
上一节的例子太基础,我换一个更能体现“图”价值的案例:一个文章写作助手工作流。它包含两个顺序节点:先分析用户给的选题,生成写作要点;再根据要点写初稿。两个节点在普通脚本里就是两个顺序函数调用,但在 LangGraph 里我们能看到状态如何在节点之间传递,也能体会流式输出的价值。
3.1 环境准备:版本搭配和安装顺序
我建议严格按下面的组合安装,我自己在这个组合下没有遇到兼容性冲突:
bash复制pip install langgraph langchain-openai langchain-core
Python 版本建议 3.10 或 3.11。我在 3.11 上测的是 langgraph 0.2.x 搭配 langchain-openai 0.2.x,整体稳定。如果你之前装过很老版本的 langchain,最好先升级再装,避免出现 ChatOpenAI 导入路径对不上的问题。
注意:langgraph 本身可以不依赖 LangChain 单独使用,但实际开发中几乎都会用 langchain-openai 来统一模型接口。所以这个安装组合基本是标配。
3.2 定义模型:别把模型直接塞进节点,先封装
模型定义通常是第一行:
python复制from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.3,
)
只要设置了 OPENAI_API_KEY 环境变量,这段代码就能直接跑。如果你用的是其它兼容 OpenAI 接口协议的模型服务,就在 ChatOpenAI 里显式传 base_url 和 api_key。
为什么要先定义一个全局的 llm 对象,而不是在每个节点函数内部重新实例化?两个原因:一是模型连接参数只维护一份,后续切换模型只需改这一行;二是节点函数保持简洁,核心逻辑一目了然。这个“封装”习惯,放到复杂图里收益非常大。
3.3 拼图、编译、运行:四个步骤跑通第一版
先写两个节点函数:
python复制def analyze_node(state: State):
user_message = state["messages"][-1].content
sys_prompt = f"""
你是一个资深编辑。用户给出的选题是:{user_message}
请输出写作要点,包括:
1. 目标读者
2. 文章核心论点
3. 3 个分论点
4. 需要的案例或数据方向
只输出要点本身,不要客套。
"""
response = llm.invoke([{"role": "user", "content": sys_prompt}])
return {
"messages": [response],
"outline": response.content,
}
python复制def write_node(state: State):
outline = state["outline"]
sys_prompt = f"基于以下写作要点,写一篇结构完整的文章。\n\n{outline}"
response = llm.invoke([{"role": "user", "content": sys_prompt}])
return {"messages": [response]}
注意 analyze_node 里我用 state["outline"] 做了字段更新,这个字段在 State 定义里需要存在,所以要把 State 改成:
python复制class State(TypedDict):
messages: Annotated[list, add_messages]
outline: str
然后组装成图:
python复制graph = StateGraph(State)
graph.add_node("analyze", analyze_node)
graph.add_node("write", write_node)
graph.add_edge(START, "analyze")
graph.add_edge("analyze", "write")
graph.add_edge("write", END)
app = graph.compile()
result = app.invoke({
"messages": [{"role": "user", "content": "如何用 LangGraph 写一个文章助手"}]
})
执行顺序就是:入口 → analyze → write → 出口。outline 这个字段会被 analyze 写入,然后被 write 读取,这正是节点之间传递数据的标准方式。
3.4 invoke 和 stream:一次性拿结果还是拿中间过程
invoke 适合你把整个图当作一个黑盒、只要最终答案的场景。但很多真实场景里,用户需要看到“写作要点”先出来,然后初稿再出来。这时候要用 stream:
python复制for step in app.stream(
{"messages": [{"role": "user", "content": "如何用 LangGraph 写一个文章助手"}]},
stream_mode="updates",
):
for node_name, node_output in step.items():
print(f"节点 {node_name} 完成")
stream_mode 有几种常用取值,区分清楚能少走弯路:
| stream_mode | 返回内容 | 适用场景 |
|---|---|---|
"updates" |
每个节点运行后返回的增量更新 | 知道哪一步完成 |
"values" |
每次超步之后的完整状态快照 | 追踪整张状态表 |
"messages" |
LLM 生成的 token 级流式输出 | 打字机效果 |
如果你要的是“用户看到流式打字”,应该用 stream_mode="messages";如果只是为了前端逐步展示节点进度,"updates" 更合适。
3.5 顺序节点串联:让“分析”和“写作”两个节点接力
上一节的 analyze_node → write_node 就是标准的顺序链路。我建议你在自己的项目里先把这个链路跑熟练,再碰条件路由。因为这个链路能让你理解一件事:图节点的返回值不是给用户看的,而是更新给状态里挂着的后续节点看的。表面上 analyze_node 返回了一个字符串,实际上它是把“分析结果”放进了共享便签,write_node 再从这个便签上取走。这个过程搞清楚,才算真正入了门。
3.6 结构化输出的配合:让模型节点输出可解析对象
很多节点不只是要一段文本,还希望模型输出结构化字段,比如意图分类、参数抽取。LangChain 里最方便的方式是 with_structured_output:
python复制from pydantic import BaseModel
class Outline(BaseModel):
target_reader: str
core_claim: str
points: list[str]
structured_llm = llm.with_structured_output(Outline)
在节点里可以直接:
python复制def analyze_node(state: State):
...
outline_obj: Outline = structured_llm.invoke([{"role": "user", "content": ...}])
return {
"messages": [AIMessage(content=outline_obj.model_dump_json())],
"outline": outline_obj.model_dump_json(),
}
这里有个小坑:with_structured_output 依赖 Pydantic 版本。如果你用的是较新的 Pydantic v2,langchain-openai 0.2.x 是兼容的;但如果你旧项目里残留了 Pydantic v1 片段,很可能会报 ValueError。遇到这类问题,优先检查 pydantic 和 langchain-core 的版本匹配。
4. 让图记住每轮对话:记忆机制与状态合并
4.1 没有记忆的图,每轮对话都是失忆现场
前面所有示例里,state["messages"] 虽然会随着一次调用内的节点执行而累积,但每次 invoke 都是从零开始的状态。对多轮对话来说,这等于用户每次提问你都像第一次见面。
LangGraph 解决记忆的方式很优雅:它不靠全局变量,而是靠 checkpointer(检查点)。所谓检查点,就是图在每个超步执行完后,把当前状态和待执行任务序列保存到某个存储里。下次用同一个会话 ID 继续运行时,图会从检查点恢复状态,而不是重头开始。
4.2 checkpointer:快照驱动的记忆
实现一个基于内存的检查点只需几行:
python复制from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
app = graph.compile(checkpointer=memory)
运行时要传一个 config,其中 thread_id 是会话标识:
python复制config = {"configurable": {"thread_id": "user-demo-001"}}
app.invoke(
{"messages": [{"role": "user", "content": "我叫小明"}]},
config=config,
)
app.invoke(
{"messages": [{"role": "user", "content": "我是谁"}]},
config=config,
)
第二次运行时,因为第一次的状态被保存了,图上某一步会恢复出“我叫小明”这段历史,模型就能回答“你叫小明”。
MemorySaver 适合开发和演示。生产环境如果服务重启就丢记忆,肯定不行,这时可以换成 SqliteSaver 或 PostgresSaver,接口用法基本一致:
python复制from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.db") as saver:
app = graph.compile(checkpointer=saver)
4.3 thread_id:区分会话的关键
thread_id 是谁?简单理解就是会话房间号。同一个房间号的所有调用共享一套状态,不同房间号互不干扰。这在多用户系统里特别重要。我见过有人不传 config 直接调用编译后的图,结果发现每轮对话还是失忆——原因就是没传 config,LangGraph 无法定位到该用哪个检查点。
4.4 覆盖与追加:为什么 messages 要 reducer
记住这个铁律:State 字段默认是覆盖。如果你在 State 里写了 messages: list,没有 Annotated,那么节点返回 {"messages": [新消息]} 的后果就是旧消息全部被清空,只剩新消息。
对比这两个定义:
python复制# 错误示范:每次都会覆盖
class State(TypedDict):
messages: list
# 正确示范:追加语义
class State(TypedDict):
messages: Annotated[list, add_messages]
add_messages 的追加也不是简单的 extend。它会按消息 id 做去重合并:如果消息 id 相同,会覆盖同 id 消息;如果 id 不同,追加到末尾。这个细节在重试节点、流式输出时很有用,不会搞出一堆重复消息。自定义字段如果需要类似“累加、取最大值”的归约逻辑,也可以自己写 reducer 传进去,这是 LangGraph 最有扩展性的地方之一。
5. 路由与并行:把图从流水线升级成真正的工作流
5.1 条件边:让图自己决定下一步
顺序链路解决的是“做完 A 做 B”,但真实业务经常是“做完 A 后,如果状态满足条件就做 B,否则做 C”。这时要用 add_conditional_edges。
条件边的核心是:传入一个函数,这个函数读取 State 并返回一个目标节点名(或节点名列表)。LangGraph 根据返回值走对应路径。
python复制graph.add_conditional_edges(
"router",
lambda state: state["intent"],
{
"tech": "tech",
"billing": "billing",
"other": END,
},
)
5.2 客服分流示例:意图识别到三个处理节点
完整示例这样写。先定义路由节点:
python复制def router_node(state: State):
user_message = state["messages"][-1].content
prompt = f"""
请判断用户输入属于哪一类,只输出一个词。
类别:tech(技术问题)、billing(账单问题)、other(其他)。
用户输入:{user_message}
"""
response = llm.invoke([{"role": "user", "content": prompt}])
intent = response.content.strip().lower()
# 防止模型输出多余内容时也能兜底
if intent not in ["tech", "billing", "other"]:
intent = "other"
return {"intent": intent}
再定义三个处理节点,这里用占位实现方便演示:
python复制def tech_node(state: State):
return {"messages": [AIMessage(content="收到,技术问题正在排查。")]}
def billing_node(state: State):
return {"messages": [AIMessage(content="收到,账单问题需要核对订单号。")]}
def other_node(state: State):
return {"messages": [AIMessage(content="收到,已转人工处理。")]}
组装图:
python复制graph = StateGraph(State)
graph.add_node("router", router_node)
graph.add_node("tech", tech_node)
graph.add_node("billing", billing_node)
graph.add_node("other", other_node)
graph.add_edge(START, "router")
graph.add_conditional_edges(
"router",
lambda state: state["intent"],
{"tech": "tech", "billing": "billing", "other": "other"},
)
graph.add_edge("tech", END)
graph.add_edge("billing", END)
graph.add_edge("other", END)
注意 State 里需要包含 intent 字段:
python复制class State(TypedDict):
messages: Annotated[list, add_messages]
intent: str
这里 intent 用默认覆盖语义就行,因为每次路由都会重新赋值,不需要累积。
5.3 条件路由的返回值机制
为什么 router_node 返回 intent,而条件边函数是 lambda state: state["intent"]?
我的理解是:LangGraph 把“产出判断结果”和“根据判断结果决定走向”分成两步。节点负责把判断结果写入状态,条件函数负责从状态里读并映射到节点名。这种分离让你可以随时手工修改状态里的 intent 来强制走某条路径,调试非常方便。
还有个易错点:映射字典的 key 必须与条件函数返回值完全一致,大小写敏感。它有更简单的写法:
python复制graph.add_conditional_edges(
"router",
lambda state: state["intent"],
{
"tech": "tech",
"billing": "billing",
"other": "other",
},
)
5.4 并行分支:同一超步内多个节点同时跑
LangGraph 支持一个节点同时出发到多个下游节点,这些节点会在同一个超步(super-step)内并行执行,LangGraph 会等所有并行路径完成后,再继续下一步。
一个简单场景:写稿时同时生成正文草稿和关键词列表,然后合并。中文场景下这个能力很适合做“氛围组速度加速器”。
python复制graph.add_node("draft", draft_node)
graph.add_node("keywords", keywords_node)
graph.add_node("merge", merge_node)
graph.add_edge(START, "draft")
graph.add_edge(START, "keywords")
graph.add_edge("draft", "merge")
graph.add_edge("keywords", "merge")
graph.add_edge("merge", END)
merge 节点会在 draft 和 keywords 都完成后才执行,因为两条边都指向它。用超步想:第一步是 START 到 draft、keywords 并行,第二步是 draft 和 keywords 到 merge,两路汇合后再进入 merge 节点。这个“同步屏障”机制让并行分支的汇合非常可控。
5.5 把图画出来:检查拓扑的最快方式
图越复杂,人越容易记不住结构。LangGraph 提供了非常方便的可视化方法:
python复制print(app.get_graph().draw_ascii())
它能直接打印出一张 ASCII 风格的拓扑图,节点和边的走向一目了然。我习惯每改完一次路由就打印一次,确认所有分支都连到了 END,避免出现“某个分流节点无路可走”的隐蔽问题。如果网络条件允许,还可以用 draw_mermaid() 生成 Mermaid 文本格式,放到支持 Mermaid 渲染的工具里看。
5.6 高阶:State 的 reducer 设计
最后提一个进阶思路:State 字段可以根据业务定制 reducer,不一定只有加消息和覆盖两种。比如你想让某个字段记录所有访问过它的节点名,可以自定义一个 list 累加器;想让某个计数字段递增,可以写一个求和 reducer。这相当于把状态的合并策略显式化,是 LangGraph 应对复杂业务的核心武器。入门阶段不用太深挖,先知道有这条路即可。
6. 入门期最容易翻车的五个场景与排查思路
我把这段时间遇到的典型问题整理成一份速查表。先看表格,后面详细展开排查过程:
| 症状 | 原因 | 排查思路/解法 |
|---|---|---|
| invoke 报 API key 错误 | 环境变量未设置或模型服务地址不对 | 先单独调用一次 llm.invoke() |
| 对话历史突然消失 | State 字段没有 reducer | 检查 TypedDict 是否有 Annotated[list, add_messages] |
| 条件路由报找不到节点 | 条件函数返回值与映射 key 不一致 | 打印 state["intent"],看有没有多余空格 |
| 报非法并行路径/图构建失败 | 多个并行边直接汇聚到同一节点 | 增加一个中间汇合节点 |
| async 环境卡死 | 在异步循环里用了同步 invoke |
换 ainvoke 或 astream |
6.1 第一类:环境变量和版本问题
新手最常遇到的是模型调用直接报错。我排查这类问题的固定流程是:
- 单独写一行
llm.invoke("hi"),排除 LangGraph 本身的干扰。 - 确认环境变量名称是
OPENAI_API_KEY还是自定义的 key 名。 - 如果用了第三方模型服务,检查
base_url是否以/v1结尾,很多兼容接口少写这个路径就 404。
版本问题也很隐蔽。我的建议是:先在一个干净的虚拟环境里用 pip install langgraph langchain-openai 装最新稳定版,跑通最小示例再往项目里搬。不要从旧项目里继承一个老版本 langchain,然后强行装新 langgraph,很容易出现接口不兼容的诡异问题。
6.2 第二类:State 字段被覆盖,对话历史凭空消失
我第三次跑多轮对话的时候,发现第二轮提问时模型完全不知道第一轮说了什么。我第一反应是检查 checkpointer,结果配置没错。
最后定位到问题出在 State 定义。我把 messages 定义成了普通的 list,没有加 add_messages reducer,所以 return {"messages": [response]} 直接把历史覆盖了。修复方法就是前面强调过的:
python复制messages: Annotated[list, add_messages]
排查思路很简单:在每次节点更新前后打印 len(state["messages"]),如果长度只增不增,就该怀疑 reducer 配置。或者用 app.get_state(config) 查看运行时快照里的消息数量,这一步能精确定位是哪条边之后丢的。
6.3 第三类:条件路由找不到节点
报错信息往往是 Invalid update 或者“node not found”之类的提示。我遇到过一次:意图识别模型输出的是 Tech(首字母大写),条件函数直接返回 state["intent"],映射字典里却只有 tech。大小写不匹配,图就不知道该走哪条路。
排查办法很简单:在条件函数里加一行打印:
python复制def route(state: State):
print(repr(state["intent"]))
return state["intent"]
repr 会显示字符串里隐藏的换行或空格,很多情况下模型会输出 "tech\n" 或 " tech"。处理这类问题,我在路由节点里做了清理:
python复制intent = response.content.strip().lower()
并且在映射时保留一个兜底分支,确保任何拦不住的情况都有去处。
6.4 第四类:非法并行路径的图结构报错
试并行分支时,我第一次这样写:
python复制graph.add_edge(START, "draft")
graph.add_edge(START, "keywords")
graph.add_edge("draft", "merge")
graph.add_edge("keywords", "merge")
这是合法的,前提是 draft 和 keywords 是两个独立节点。但如果你试图把一个节点并发成两条边直接指向同一个下游节点,比如:
python复制graph.add_edge("draft", "merge")
graph.add_edge("draft", "merge")
LangGraph 会认为这是非法路径,图结构无法编译。原因很简单:同一来源同一目标的两条边没有意义,也没法判断并行逻辑。这时要么删掉重复边,要么在中间拆出一个分发节点,把数据切成两路再分别汇聚。
6.5 第五类:async 环境里硬用同步 invoke
如果你的应用跑在 FastAPI 这类异步框架里,直接用 app.invoke() 会阻塞事件循环。LangGraph 提供了对应的异步接口 ainvoke、astream。
python复制result = await app.ainvoke(
{"messages": [{"role": "user", "content": "你好"}]},
config=config,
)
这个坑不复杂,但容易在压测时才暴露:单用户没事,并发一上来,所有请求好像在排队,大概率就是同步调用阻塞了事件循环。
6.6 调试工具:get_state 和 update_state
除了打印日志,LangGraph 还提供了两个调试利器。
app.get_state(config) 可以查看某个会话在当前检查点保存的状态快照,包含所有字段值,适合判断“状态到底走到哪了”。
python复制current = app.get_state(config)
print(current.values)
app.update_state(config, {...}) 可以手动修改状态,我经常用它来强制设置路由字段,跳过某个节点直接测试后续流程:
python复制app.update_state(config, {"intent": "tech"})
这两个命令配合起来,基本上能把“图跑到哪里、状态是什么、下一步会走哪条边”这三个问题可视化地排查一遍。
如果非要说一条最重要的经验,我会说:把 State 当成数据库表来设计,把节点当成事务来写。每个节点只更新自己负责的字段,reducer 明确合并语义,图结构自然稳定。遇到问题优先看状态快照,而不是猜哪条边写错了。LangGraph 本身并不复杂,复杂的是业务逻辑本身,而图恰恰是逼你把复杂逻辑显式化的工具。按这个思路从最简单的模型节点开始,把 invoke、stream、checkpointer、条件路由一个个加进去,你会发现自己对“LLM 应用编排”的理解会彻底不一样。
