概述
前面几篇我们已经学过 LCEL、Tool、结构化输出、create_agent() 和 Middleware。
如果你的任务只是:
text
Prompt -> Model -> Parser
那么 LCEL 非常合适。
它简单、直观、组合性强:
python
chain = prompt | model | parser
result = chain.invoke({"question": "LangGraph 是什么?"})
但真实 Agent 应用经常不是一条直线。
它可能是这样:
text
用户问题
|
v
分类意图
|
+-- 普通问答 --> RAG 检索 --> 生成答案
|
+-- 数据查询 --> 生成 SQL --> 安全校验 --> 执行查询 --> 解释结果
|
+-- 高风险操作 --> 工具调用前人工审批 --> 执行动作
|
+-- 模型不确定 --> 反思修正 --> 再回答
这时你会遇到 Chain 不擅长表达的问题:
- 分支很多,
if/else到处散落。 - 某些节点要循环执行。
- 多个节点要并行执行。
- 某些节点要等待人工输入。
- 中间状态要被持久化。
- 失败后要从中断处恢复。
- 需要清晰观测每一步做了什么。
LangGraph 解决的就是这个问题:
用图来描述复杂 LLM 应用的执行流程,把"状态、节点、边、分支、循环、恢复"变成一等公民。
Chain 适合线性流程,Graph 适合有分支、有循环、有状态、有恢复需求的复杂 Agent 工作流。
先建立心智模型:State、Node、Edge
LangGraph 最核心的三个概念是:
| 概念 | 中文理解 | 作用 |
|---|---|---|
State |
状态 | 记录图运行过程中的共享数据 |
Node |
节点 | 执行具体逻辑的函数 |
Edge |
边 | 决定下一个执行哪个节点 |
可以把一个 LangGraph 程序理解成:
text
State 是工作台
Node 是工人
Edge 是流程规则
例如一个"写作 Agent":
text
State:
topic: 文章主题
draft: 初稿
review: 审查意见
final: 终稿
Nodes:
plan_node: 生成提纲
write_node: 写初稿
review_node: 审查初稿
revise_node: 修改文章
Edges:
plan -> write
write -> review
review 如果通过 -> END
review 如果不通过 -> revise -> review
这已经不是简单的 prompt | model | parser。
它是一个带状态和决策的流程图。
LangGraph 的本质不是"更复杂的 Chain",而是"状态驱动的工作流引擎"。
最小 Graph:三个节点串起来
先写一个不调用大模型的最小例子,理解基本结构。
python
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
topic: str
outline: str
draft: str
final: str
def plan_node(state: State) -> dict:
return {
"outline": f"围绕《{state['topic']}》生成三段式提纲"
}
def write_node(state: State) -> dict:
return {
"draft": f"根据提纲写初稿:{state['outline']}"
}
def polish_node(state: State) -> dict:
return {
"final": f"润色后的文章:{state['draft']}"
}
builder = StateGraph(State)
builder.add_node("plan", plan_node)
builder.add_node("write", write_node)
builder.add_node("polish", polish_node)
builder.add_edge(START, "plan")
builder.add_edge("plan", "write")
builder.add_edge("write", "polish")
builder.add_edge("polish", END)
graph = builder.compile()
result = graph.invoke({"topic": "LangGraph 状态图引擎"})
print(result["final"])
这段代码有固定套路:
text
1. 定义 State
2. 定义 Node 函数
3. 创建 StateGraph
4. add_node 添加节点
5. add_edge 连接节点
6. compile 编译图
7. invoke 执行图
执行路径是:
#mermaid-svg-3lcCX4Toz90ynlOY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3lcCX4Toz90ynlOY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3lcCX4Toz90ynlOY .error-icon{fill:#552222;}#mermaid-svg-3lcCX4Toz90ynlOY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3lcCX4Toz90ynlOY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3lcCX4Toz90ynlOY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3lcCX4Toz90ynlOY .marker.cross{stroke:#333333;}#mermaid-svg-3lcCX4Toz90ynlOY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3lcCX4Toz90ynlOY p{margin:0;}#mermaid-svg-3lcCX4Toz90ynlOY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3lcCX4Toz90ynlOY .cluster-label text{fill:#333;}#mermaid-svg-3lcCX4Toz90ynlOY .cluster-label span{color:#333;}#mermaid-svg-3lcCX4Toz90ynlOY .cluster-label span p{background-color:transparent;}#mermaid-svg-3lcCX4Toz90ynlOY .label text,#mermaid-svg-3lcCX4Toz90ynlOY span{fill:#333;color:#333;}#mermaid-svg-3lcCX4Toz90ynlOY .node rect,#mermaid-svg-3lcCX4Toz90ynlOY .node circle,#mermaid-svg-3lcCX4Toz90ynlOY .node ellipse,#mermaid-svg-3lcCX4Toz90ynlOY .node polygon,#mermaid-svg-3lcCX4Toz90ynlOY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3lcCX4Toz90ynlOY .rough-node .label text,#mermaid-svg-3lcCX4Toz90ynlOY .node .label text,#mermaid-svg-3lcCX4Toz90ynlOY .image-shape .label,#mermaid-svg-3lcCX4Toz90ynlOY .icon-shape .label{text-anchor:middle;}#mermaid-svg-3lcCX4Toz90ynlOY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3lcCX4Toz90ynlOY .rough-node .label,#mermaid-svg-3lcCX4Toz90ynlOY .node .label,#mermaid-svg-3lcCX4Toz90ynlOY .image-shape .label,#mermaid-svg-3lcCX4Toz90ynlOY .icon-shape .label{text-align:center;}#mermaid-svg-3lcCX4Toz90ynlOY .node.clickable{cursor:pointer;}#mermaid-svg-3lcCX4Toz90ynlOY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3lcCX4Toz90ynlOY .arrowheadPath{fill:#333333;}#mermaid-svg-3lcCX4Toz90ynlOY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3lcCX4Toz90ynlOY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3lcCX4Toz90ynlOY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3lcCX4Toz90ynlOY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3lcCX4Toz90ynlOY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3lcCX4Toz90ynlOY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3lcCX4Toz90ynlOY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3lcCX4Toz90ynlOY .cluster text{fill:#333;}#mermaid-svg-3lcCX4Toz90ynlOY .cluster span{color:#333;}#mermaid-svg-3lcCX4Toz90ynlOY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3lcCX4Toz90ynlOY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3lcCX4Toz90ynlOY rect.text{fill:none;stroke-width:0;}#mermaid-svg-3lcCX4Toz90ynlOY .icon-shape,#mermaid-svg-3lcCX4Toz90ynlOY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3lcCX4Toz90ynlOY .icon-shape p,#mermaid-svg-3lcCX4Toz90ynlOY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3lcCX4Toz90ynlOY .icon-shape .label rect,#mermaid-svg-3lcCX4Toz90ynlOY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3lcCX4Toz90ynlOY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3lcCX4Toz90ynlOY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3lcCX4Toz90ynlOY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} START
plan
write
polish
END
最小 LangGraph = 一个 State schema + 一组节点函数 + 一组边 + compile。
StateGraph:为什么先定义状态?
在 LangGraph 里,State 是所有节点共享的"运行时快照"。
每个节点都接收当前状态:
python
def some_node(state: State) -> dict:
...
然后返回状态更新:
python
return {"draft": "新的初稿"}
注意:节点通常不需要返回完整 state,只返回本节点要更新的字段。
例如当前状态是:
python
{
"topic": "LangGraph",
"outline": "三段式提纲",
"draft": "",
"final": "",
}
某个节点返回:
python
{"draft": "这里是初稿"}
合并后状态变成:
python
{
"topic": "LangGraph",
"outline": "三段式提纲",
"draft": "这里是初稿",
"final": "",
}
没有被返回的字段会保留原值。
这和很多函数式 pipeline 不同。
在普通 Chain 里,你更关注:
text
input -> output
在 LangGraph 里,你更关注:
text
state -> partial update -> new state
LangGraph 节点返回的不是"最终结果",而是对共享状态的一次增量更新。
State Schema:TypedDict、dataclass、Pydantic 怎么选?
LangGraph 的 state schema 常见写法有三类:
| 写法 | 优点 | 适合场景 |
|---|---|---|
TypedDict |
轻量、清晰、性能好 | 大多数图 |
dataclass |
可以定义默认值 | 需要默认状态值 |
Pydantic BaseModel |
校验强 | 需要递归校验或严格数据模型 |
多数项目建议从 TypedDict 开始:
python
from typing_extensions import TypedDict
class AgentState(TypedDict):
question: str
documents: list[str]
answer: str
如果某些字段不是一开始就存在,可以用 NotRequired:
python
from typing_extensions import NotRequired, TypedDict
class AgentState(TypedDict):
question: str
documents: NotRequired[list[str]]
answer: NotRequired[str]
如果你希望字段有默认值,可以考虑 dataclass:
python
from dataclasses import dataclass, field
@dataclass
class AgentState:
question: str
documents: list[str] = field(default_factory=list)
answer: str = ""
使用 Pydantic 则要权衡性能:
python
from pydantic import BaseModel, Field
class AgentState(BaseModel):
question: str
documents: list[str] = Field(default_factory=list)
answer: str = ""
如果只是普通 Agent 流程,不要为了"看起来更严谨"一上来就用 Pydantic。
状态会在图执行中频繁读写,过重的数据校验可能带来额外开销。
默认用 TypedDict,需要默认值用 dataclass,确实需要强校验再用 Pydantic。
Reducer:状态更新到底怎么合并?
LangGraph 的 state 每个字段都有自己的合并规则。
默认规则很简单:
新值覆盖旧值。
例如:
python
from typing_extensions import TypedDict
class State(TypedDict):
count: int
logs: list[str]
如果当前状态是:
python
{"count": 1, "logs": ["start"]}
节点返回:
python
{"logs": ["node_a finished"]}
默认合并后是:
python
{"count": 1, "logs": ["node_a finished"]}
注意,logs 被覆盖了,不是追加。
如果你想追加,就要给字段声明 reducer:
python
from operator import add
from typing import Annotated
from typing_extensions import TypedDict
class State(TypedDict):
count: int
logs: Annotated[list[str], add]
现在节点返回:
python
{"logs": ["node_a finished"]}
会合并成:
python
{"count": 1, "logs": ["start", "node_a finished"]}
Reducer 是 LangGraph 里非常关键但容易被忽略的概念。
尤其是以下字段:
- 消息列表。
- 检索结果。
- 工具调用日志。
- 多个并行节点的输出。
- 反思循环中的历史记录。
都要认真决定是覆盖还是追加。
State 定义字段,Reducer 定义字段如何被更新。
消息状态:为什么推荐 add_messages?
LLM 应用里最常见的状态字段是 messages。
错误写法是:
python
class State(TypedDict):
messages: list
这样每次节点返回新的 messages,都可能覆盖历史消息。
你可能会改成:
python
from operator import add
from typing import Annotated
class State(TypedDict):
messages: Annotated[list, add]
这样可以追加,但仍然不完美。
因为对话消息有 id,有时你不是想追加一条新消息,而是想更新已有消息。
LangGraph 提供了专门的 reducer:add_messages。
python
from typing import Annotated
from typing_extensions import TypedDict
from langchain.messages import AnyMessage
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
还可以直接用内置的 MessagesState:
python
from langgraph.graph import MessagesState
class ChatState(MessagesState):
documents: list[str]
MessagesState 默认包含:
python
messages: Annotated[list[AnyMessage], add_messages]
这对聊天 Agent 非常常用。
只要 state 里有聊天消息,优先用 MessagesState 或 add_messages,不要裸写 list。
Node:节点就是函数,但要保持单职责
LangGraph 的节点可以是普通 Python 函数。
它通常接收 state,返回 dict:
python
def retrieve_node(state: State) -> dict:
docs = retriever.invoke(state["question"])
return {"documents": docs}
节点也可以接收 runtime:
python
from dataclasses import dataclass
from langgraph.runtime import Runtime
@dataclass
class Context:
user_id: str
def node_with_runtime(state: State, runtime: Runtime[Context]) -> dict:
user_id = runtime.context.user_id
return {"answer": f"当前用户是 {user_id}"}
一个节点应该尽量只做一类事情:
- 分类节点只负责分类。
- 检索节点只负责检索。
- 生成节点只负责生成。
- 审查节点只负责审查。
- 工具节点只负责执行工具。
不要写成:
text
mega_node:
分类
检索
生成
审查
重试
发邮件
记日志
这样虽然能跑,但你会失去 Graph 的价值。
Graph 的价值在于:
- 每一步可观察。
- 每一步可测试。
- 每一步可替换。
- 每一步可恢复。
- 每一步可单独加边界。
节点可以是普通函数,但设计上应该是"可命名、可测试、可观测"的步骤。
Edge:边决定流程怎么走
Edge 分两类:
| 类型 | 方法 | 适合场景 |
|---|---|---|
| 普通边 | add_edge |
固定从 A 到 B |
| 条件边 | add_conditional_edges |
根据 state 动态决定下一个节点 |
普通边最简单:
python
builder.add_edge("retrieve", "generate")
意思是:
text
retrieve 执行完,一定执行 generate
条件边适合分支:
python
from typing import Literal
from langgraph.graph import END
def route_after_review(state: State) -> Literal["revise", "__end__"]:
if state["approved"]:
return END
return "revise"
builder.add_conditional_edges("review", route_after_review)
也可以把路由函数的返回值映射到节点名:
python
def route_by_intent(state: State) -> str:
return state["intent"]
builder.add_conditional_edges(
"classify",
route_by_intent,
{
"rag": "retrieve",
"sql": "generate_sql",
"chat": "chat",
},
)
这段代码表达:
text
classify 节点执行后
如果 intent = rag -> retrieve
如果 intent = sql -> generate_sql
如果 intent = chat -> chat
官方文档里有一个重要建议:
同一个节点最好只选择一种路由机制:要么普通边,要么条件边 /
Command。不要在同一节点上混用静态边和动态路由,否则执行路径会变得难推理。
普通边表达确定流程,条件边表达动态决策。
START 和 END:图的入口与出口
LangGraph 用两个特殊节点表达开始和结束:
START: 虚拟起点,表示用户输入进入图后第一个执行哪里。END: 终点,表示流程结束。
常见写法:
python
from langgraph.graph import START, END
builder.add_edge(START, "classify")
builder.add_edge("generate", END)
你也可能看到旧写法:
python
builder.set_entry_point("classify")
builder.set_finish_point("generate")
这两种都能表达入口和出口。
但当前更推荐使用:
python
builder.add_edge(START, "classify")
builder.add_edge("generate", END)
因为它更统一,所有路由都通过边来表达。
START 和 END 不是业务节点,而是用来声明图从哪里开始、在哪里结束的虚拟节点。
条件边实战:分类后走不同处理链路
下面写一个最小"问题路由"图。
目标:
- 用户问知识库问题,走 RAG。
- 用户问数据库问题,走 SQL。
- 其他问题,走普通聊天。
python
from typing import Literal
from typing_extensions import NotRequired
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
question: str
route: NotRequired[Literal["rag", "sql", "chat"]]
answer: NotRequired[str]
def classify_node(state: State) -> dict:
question = state["question"]
if "数据库" in question or "SQL" in question:
return {"route": "sql"}
if "文档" in question or "知识库" in question:
return {"route": "rag"}
return {"route": "chat"}
def rag_node(state: State) -> dict:
return {"answer": f"从知识库检索后回答:{state['question']}"}
def sql_node(state: State) -> dict:
return {"answer": f"生成并执行 SQL 后回答:{state['question']}"}
def chat_node(state: State) -> dict:
return {"answer": f"直接聊天回答:{state['question']}"}
def route_by_classification(state: State) -> str:
return state["route"]
builder = StateGraph(State)
builder.add_node("classify", classify_node)
builder.add_node("rag", rag_node)
builder.add_node("sql", sql_node)
builder.add_node("chat", chat_node)
builder.add_edge(START, "classify")
builder.add_conditional_edges(
"classify",
route_by_classification,
{
"rag": "rag",
"sql": "sql",
"chat": "chat",
},
)
builder.add_edge("rag", END)
builder.add_edge("sql", END)
builder.add_edge("chat", END)
graph = builder.compile()
result = graph.invoke({"question": "帮我查一下知识库里的部署文档"})
print(result["answer"])
图结构如下:
#mermaid-svg-BJcufDAZ3YrrIhIR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BJcufDAZ3YrrIhIR .error-icon{fill:#552222;}#mermaid-svg-BJcufDAZ3YrrIhIR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BJcufDAZ3YrrIhIR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BJcufDAZ3YrrIhIR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BJcufDAZ3YrrIhIR .marker.cross{stroke:#333333;}#mermaid-svg-BJcufDAZ3YrrIhIR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BJcufDAZ3YrrIhIR p{margin:0;}#mermaid-svg-BJcufDAZ3YrrIhIR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster-label text{fill:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster-label span{color:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster-label span p{background-color:transparent;}#mermaid-svg-BJcufDAZ3YrrIhIR .label text,#mermaid-svg-BJcufDAZ3YrrIhIR span{fill:#333;color:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR .node rect,#mermaid-svg-BJcufDAZ3YrrIhIR .node circle,#mermaid-svg-BJcufDAZ3YrrIhIR .node ellipse,#mermaid-svg-BJcufDAZ3YrrIhIR .node polygon,#mermaid-svg-BJcufDAZ3YrrIhIR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BJcufDAZ3YrrIhIR .rough-node .label text,#mermaid-svg-BJcufDAZ3YrrIhIR .node .label text,#mermaid-svg-BJcufDAZ3YrrIhIR .image-shape .label,#mermaid-svg-BJcufDAZ3YrrIhIR .icon-shape .label{text-anchor:middle;}#mermaid-svg-BJcufDAZ3YrrIhIR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BJcufDAZ3YrrIhIR .rough-node .label,#mermaid-svg-BJcufDAZ3YrrIhIR .node .label,#mermaid-svg-BJcufDAZ3YrrIhIR .image-shape .label,#mermaid-svg-BJcufDAZ3YrrIhIR .icon-shape .label{text-align:center;}#mermaid-svg-BJcufDAZ3YrrIhIR .node.clickable{cursor:pointer;}#mermaid-svg-BJcufDAZ3YrrIhIR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BJcufDAZ3YrrIhIR .arrowheadPath{fill:#333333;}#mermaid-svg-BJcufDAZ3YrrIhIR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BJcufDAZ3YrrIhIR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BJcufDAZ3YrrIhIR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BJcufDAZ3YrrIhIR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BJcufDAZ3YrrIhIR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BJcufDAZ3YrrIhIR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster text{fill:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR .cluster span{color:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BJcufDAZ3YrrIhIR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BJcufDAZ3YrrIhIR rect.text{fill:none;stroke-width:0;}#mermaid-svg-BJcufDAZ3YrrIhIR .icon-shape,#mermaid-svg-BJcufDAZ3YrrIhIR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BJcufDAZ3YrrIhIR .icon-shape p,#mermaid-svg-BJcufDAZ3YrrIhIR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BJcufDAZ3YrrIhIR .icon-shape .label rect,#mermaid-svg-BJcufDAZ3YrrIhIR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BJcufDAZ3YrrIhIR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BJcufDAZ3YrrIhIR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BJcufDAZ3YrrIhIR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} rag
sql
chat
START
classify
rag
sql
chat
END
这个例子没有调用 LLM,但结构就是生产系统常见的路由骨架。
你可以把 classify_node 换成结构化输出模型:
python
classification = classifier.invoke(state["question"])
return {"route": classification.route}
条件边把 if/else 从业务主流程里抽出来,让图结构直接表达分支逻辑。
循环:Agent 为什么天然适合 Graph?
Agent 的本质通常不是一次模型调用,而是循环:
text
模型思考
|
v
是否需要工具?
|
+-- 是 --> 执行工具 --> 把结果写回状态 --> 再次思考
|
+-- 否 --> 输出最终答案
用 LangGraph 表达就是:
#mermaid-svg-p2hb8lD2ia3uCiLH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p2hb8lD2ia3uCiLH .error-icon{fill:#552222;}#mermaid-svg-p2hb8lD2ia3uCiLH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p2hb8lD2ia3uCiLH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p2hb8lD2ia3uCiLH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p2hb8lD2ia3uCiLH .marker.cross{stroke:#333333;}#mermaid-svg-p2hb8lD2ia3uCiLH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p2hb8lD2ia3uCiLH p{margin:0;}#mermaid-svg-p2hb8lD2ia3uCiLH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster-label text{fill:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster-label span{color:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster-label span p{background-color:transparent;}#mermaid-svg-p2hb8lD2ia3uCiLH .label text,#mermaid-svg-p2hb8lD2ia3uCiLH span{fill:#333;color:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH .node rect,#mermaid-svg-p2hb8lD2ia3uCiLH .node circle,#mermaid-svg-p2hb8lD2ia3uCiLH .node ellipse,#mermaid-svg-p2hb8lD2ia3uCiLH .node polygon,#mermaid-svg-p2hb8lD2ia3uCiLH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p2hb8lD2ia3uCiLH .rough-node .label text,#mermaid-svg-p2hb8lD2ia3uCiLH .node .label text,#mermaid-svg-p2hb8lD2ia3uCiLH .image-shape .label,#mermaid-svg-p2hb8lD2ia3uCiLH .icon-shape .label{text-anchor:middle;}#mermaid-svg-p2hb8lD2ia3uCiLH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p2hb8lD2ia3uCiLH .rough-node .label,#mermaid-svg-p2hb8lD2ia3uCiLH .node .label,#mermaid-svg-p2hb8lD2ia3uCiLH .image-shape .label,#mermaid-svg-p2hb8lD2ia3uCiLH .icon-shape .label{text-align:center;}#mermaid-svg-p2hb8lD2ia3uCiLH .node.clickable{cursor:pointer;}#mermaid-svg-p2hb8lD2ia3uCiLH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p2hb8lD2ia3uCiLH .arrowheadPath{fill:#333333;}#mermaid-svg-p2hb8lD2ia3uCiLH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p2hb8lD2ia3uCiLH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p2hb8lD2ia3uCiLH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p2hb8lD2ia3uCiLH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p2hb8lD2ia3uCiLH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p2hb8lD2ia3uCiLH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster text{fill:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH .cluster span{color:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p2hb8lD2ia3uCiLH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p2hb8lD2ia3uCiLH rect.text{fill:none;stroke-width:0;}#mermaid-svg-p2hb8lD2ia3uCiLH .icon-shape,#mermaid-svg-p2hb8lD2ia3uCiLH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p2hb8lD2ia3uCiLH .icon-shape p,#mermaid-svg-p2hb8lD2ia3uCiLH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p2hb8lD2ia3uCiLH .icon-shape .label rect,#mermaid-svg-p2hb8lD2ia3uCiLH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p2hb8lD2ia3uCiLH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p2hb8lD2ia3uCiLH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p2hb8lD2ia3uCiLH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} tools
end
START
model
should_continue
tools
END
这和上一篇 create_agent() 背后的执行循环是一致的。
手写 Graph 的好处是:
- 你能控制每个节点的输入输出。
- 你能决定什么时候继续循环。
- 你能插入审查、限流、人工审批。
- 你能把工具执行拆成多个节点。
- 你能精确观测循环次数和状态变化。
只要流程里出现"判断后可能回到前一步",Graph 通常比 Chain 更自然。
手写一个极简 ReAct 图:模型节点 + 工具节点
下面写一个极简 ReAct 风格 Agent。
它包含两个节点:
llm_call: 调模型,决定是否调用工具。tool_node: 执行工具,把工具结果写回 messages。
python
from typing import Literal
from typing_extensions import Annotated, TypedDict
from langchain.chat_models import init_chat_model
from langchain.messages import AnyMessage, HumanMessage, SystemMessage, ToolMessage
from langchain.tools import tool
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
tools = [multiply]
tools_by_name = {tool.name: tool for tool in tools}
model = init_chat_model("openai:gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
def llm_call(state: AgentState) -> dict:
response = model_with_tools.invoke(
[
SystemMessage(
content="你是一个数学助手。需要计算时调用工具,不要心算。"
)
]
+ state["messages"]
)
return {"messages": [response]}
def tool_node(state: AgentState) -> dict:
last_message = state["messages"][-1]
results = []
for tool_call in last_message.tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
results.append(
ToolMessage(
content=str(observation),
tool_call_id=tool_call["id"],
)
)
return {"messages": results}
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return END
builder = StateGraph(AgentState)
builder.add_node("llm_call", llm_call)
builder.add_node("tools", tool_node)
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", should_continue)
builder.add_edge("tools", "llm_call")
agent = builder.compile()
result = agent.invoke(
{"messages": [HumanMessage(content="12 乘以 9 等于多少?")]}
)
print(result["messages"][-1].content)
这就是一个手写版的 Agent loop。
你可以清楚看到:
- 模型节点负责生成 AIMessage。
- 工具节点负责执行 tool call。
- 条件边负责判断继续工具还是结束。
add_messages负责把每轮消息追加到状态里。
create_agent() 帮你自动组装了这个循环,手写 LangGraph 则让你完全掌控这个循环。
从 Chain 到 Graph:同一个需求的两种写法
假设你要做一个"生成文章并审查"的流程:
text
生成初稿 -> 审查质量 -> 如果不合格就修改 -> 再审查
用 Chain 写,你可能会写成:
python
draft = write_chain.invoke({"topic": topic})
review = review_chain.invoke({"draft": draft})
while not review["approved"]:
draft = revise_chain.invoke({
"draft": draft,
"feedback": review["feedback"],
})
review = review_chain.invoke({"draft": draft})
这不是不能写。
问题是当流程变复杂后,你会把控制逻辑、状态管理、观测和恢复全堆在业务代码里。
用 Graph 写,结构会更明确:
text
write -> review
|
+-- approved --> END
|
+-- rejected --> revise -> review
代码骨架:
python
from typing import Literal
from typing_extensions import NotRequired, TypedDict
from langgraph.graph import StateGraph, START, END
class WritingState(TypedDict):
topic: str
draft: NotRequired[str]
feedback: NotRequired[str]
approved: NotRequired[bool]
revision_count: NotRequired[int]
def write_node(state: WritingState) -> dict:
return {
"draft": f"围绕 {state['topic']} 写出的初稿",
"revision_count": 0,
}
def review_node(state: WritingState) -> dict:
draft = state["draft"]
if len(draft) > 30:
return {"approved": True, "feedback": "质量通过"}
return {
"approved": False,
"feedback": "内容太短,需要补充细节",
}
def revise_node(state: WritingState) -> dict:
count = state.get("revision_count", 0) + 1
return {
"draft": state["draft"] + f"\n第 {count} 次补充:增加案例和解释。",
"revision_count": count,
}
def route_after_review(state: WritingState) -> Literal["revise", "__end__"]:
if state["approved"]:
return END
return "revise"
builder = StateGraph(WritingState)
builder.add_node("write", write_node)
builder.add_node("review", review_node)
builder.add_node("revise", revise_node)
builder.add_edge(START, "write")
builder.add_edge("write", "review")
builder.add_conditional_edges("review", route_after_review)
builder.add_edge("revise", "review")
graph = builder.compile()
这个结构一眼就能看出来:
- 哪些步骤会执行。
- 哪些步骤可能循环。
- 循环退出条件在哪里。
- 状态字段由谁产生。
Chain 把流程藏在代码控制流里,Graph 把流程显式画出来。
Command:当节点既要更新状态,又要决定下一步
条件边把"节点逻辑"和"路由逻辑"分开。
很多时候这很好。
但有些节点天然会同时做两件事:
text
1. 计算新的状态
2. 根据计算结果决定下一步
这时可以用 Command。
python
from typing import Literal
from langgraph.graph import END
from langgraph.types import Command
def review_node(state: WritingState) -> Command[Literal["revise", "__end__"]]:
if len(state["draft"]) > 100:
return Command(
update={"approved": True, "feedback": "通过"},
goto=END,
)
return Command(
update={
"approved": False,
"feedback": "内容太短,请补充案例",
},
goto="revise",
)
然后图里就不需要单独写 add_conditional_edges("review", ...):
python
builder.add_node("review", review_node)
Command 常见于:
- 审批节点。
- 路由节点。
- 人工中断恢复后的分支。
- 工具执行后直接跳转。
- 子图返回父图。
不过不要滥用。
如果你的路由逻辑很复杂,或者希望图结构更直观,把路由函数单独写出来通常更清晰。
Command 适合把"状态更新 + 下一步跳转"合并在一个节点返回值里。
并行执行:多个下游节点可以同时跑
LangGraph 支持一个节点连接多个下游节点。
如果一个节点有多个出边,下游节点可以在下一轮 super-step 中并行执行。
例如文章审查:
text
draft
|
+-- check_facts
|
+-- check_style
|
+-- check_security
|
v
merge_review
示例代码:
python
from operator import add
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class ReviewState(TypedDict):
draft: str
issues: Annotated[list[str], add]
final_decision: str
def check_facts(state: ReviewState) -> dict:
return {"issues": ["事实检查:未发现明显问题"]}
def check_style(state: ReviewState) -> dict:
return {"issues": ["风格检查:标题可以更具体"]}
def check_security(state: ReviewState) -> dict:
return {"issues": ["安全检查:未发现敏感信息"]}
def merge_review(state: ReviewState) -> dict:
return {
"final_decision": "\n".join(state["issues"])
}
builder = StateGraph(ReviewState)
builder.add_node("check_facts", check_facts)
builder.add_node("check_style", check_style)
builder.add_node("check_security", check_security)
builder.add_node("merge_review", merge_review)
builder.add_edge(START, "check_facts")
builder.add_edge(START, "check_style")
builder.add_edge(START, "check_security")
builder.add_edge(
["check_facts", "check_style", "check_security"],
"merge_review",
)
builder.add_edge("merge_review", END)
graph = builder.compile()
这里 issues 必须用追加 reducer。
否则三个并行节点都写 issues,最后可能互相覆盖。
并行节点写同一个 state 字段时,必须认真设计 reducer。
Super-step:LangGraph 怎么执行图?
LangGraph 的执行模型受 Pregel 思想影响,可以粗略理解成一轮一轮的 super-step。
不用把这个概念想得太复杂。
它大概表达:
text
第 1 轮:执行所有当前活跃节点
第 2 轮:执行上一轮发出更新后被激活的节点
第 3 轮:继续执行新的活跃节点
...
直到没有节点需要继续执行
对于线性图:
text
A -> B -> C
它基本就是顺序执行。
对于并行图:
text
-> B ->
A --- ---> D
-> C ->
B 和 C 可以处在同一个 super-step。
理解 super-step 的意义在于:
- 你会知道为什么并行节点要靠 reducer 合并状态。
- 你会知道为什么 checkpoint 通常发生在步骤边界。
- 你会知道为什么节点内部副作用要考虑幂等。
LangGraph 不是简单递归调用节点,而是按活跃节点和状态更新逐步推进图执行。
Checkpoint:节点副作用要幂等
LangGraph 支持 checkpoint,也就是在执行过程中保存状态。
这对以下场景非常关键:
- 人工审批。
- 中断后恢复。
- 长任务继续执行。
- 失败后重试。
- 多轮对话记忆。
但是 checkpoint 有一个工程细节必须注意:
如果图从某个 checkpoint 恢复,某个节点可能会从头重新执行。
这意味着节点里的副作用要幂等。
危险写法:
python
def create_order_node(state: State) -> dict:
order_id = external_api.create_order(state["payload"])
return {"order_id": order_id}
如果这个节点因为恢复被执行两次,可能会创建两个订单。
更稳的写法:
python
def create_order_node(state: State) -> dict:
idempotency_key = state["request_id"]
order = external_api.create_order(
state["payload"],
idempotency_key=idempotency_key,
)
return {"order_id": order.id}
对于外部写操作,建议:
- 使用幂等 key。
- 先查再写。
- 使用 upsert。
- 把不可逆操作放到人工审批之后。
- 在 state 中记录外部系统返回的业务 ID。
Graph 可以恢复执行,但你的外部副作用必须能承受重放。
可视化:把图画出来
LangGraph 的一个重要优势是可以可视化。
在 Notebook 里可以这样:
python
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
也可以打印 Mermaid:
python
print(graph.get_graph().draw_mermaid())
对于复杂 Agent,建议每次改图结构后都看一眼:
- 是否有孤立节点。
- 是否有意外分支。
- 是否存在无法到达 END 的循环。
- 是否同一个节点混用了静态边和动态边。
- 是否高风险工具路径经过了审批节点。
图不是为了好看,而是为了降低复杂系统的认知成本。
能画出来的 Agent,才更容易被调试、审查和交接。
实战:手写"思考 -> 执行 -> 反思"三阶 Agent
现在写一个更接近真实 Agent 的三阶段工作流:
text
think 思考:分析问题,制定行动
act 执行:根据行动调用工具或生成草稿
reflect 反思:判断是否满意,不满意就回到 think
为了让代码容易理解,我们先用规则模拟 LLM。
状态设计:
python
from typing import Literal
from typing_extensions import NotRequired, TypedDict
class AgentState(TypedDict):
question: str
plan: NotRequired[str]
action_result: NotRequired[str]
critique: NotRequired[str]
final_answer: NotRequired[str]
approved: NotRequired[bool]
iteration: NotRequired[int]
字段含义:
question: 用户问题。plan: 思考阶段生成的计划。action_result: 执行阶段产生的结果。critique: 反思阶段的审查意见。final_answer: 最终答案。approved: 反思是否通过。iteration: 循环次数,防止无限循环。
节点实现:
python
from typing import Literal
from typing_extensions import NotRequired, TypedDict
from langgraph.graph import StateGraph, START, END
class AgentState(TypedDict):
question: str
plan: NotRequired[str]
action_result: NotRequired[str]
critique: NotRequired[str]
final_answer: NotRequired[str]
approved: NotRequired[bool]
iteration: NotRequired[int]
def think_node(state: AgentState) -> dict:
iteration = state.get("iteration", 0) + 1
return {
"iteration": iteration,
"plan": (
f"第 {iteration} 轮计划:先识别问题类型,"
f"再给出结构化回答。问题是:{state['question']}"
),
}
def act_node(state: AgentState) -> dict:
plan = state["plan"]
if state["iteration"] == 1:
return {
"action_result": f"根据计划执行,但回答还比较粗糙。plan={plan}"
}
return {
"action_result": f"根据反思意见补充细节后执行。plan={plan}"
}
def reflect_node(state: AgentState) -> dict:
result = state["action_result"]
iteration = state["iteration"]
if iteration >= 2:
return {
"approved": True,
"critique": "答案已经包含必要细节,可以结束。",
"final_answer": result,
}
return {
"approved": False,
"critique": "答案过于粗糙,需要补充步骤和边界条件。",
}
def route_after_reflect(state: AgentState) -> Literal["think", "__end__"]:
if state["approved"]:
return END
return "think"
builder = StateGraph(AgentState)
builder.add_node("think", think_node)
builder.add_node("act", act_node)
builder.add_node("reflect", reflect_node)
builder.add_edge(START, "think")
builder.add_edge("think", "act")
builder.add_edge("act", "reflect")
builder.add_conditional_edges("reflect", route_after_reflect)
graph = builder.compile()
result = graph.invoke({"question": "如何设计一个可恢复的客服 Agent?"})
print(result["final_answer"])
图结构:
#mermaid-svg-JWXzSrUxKWJg5kZx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JWXzSrUxKWJg5kZx .error-icon{fill:#552222;}#mermaid-svg-JWXzSrUxKWJg5kZx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JWXzSrUxKWJg5kZx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JWXzSrUxKWJg5kZx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JWXzSrUxKWJg5kZx .marker.cross{stroke:#333333;}#mermaid-svg-JWXzSrUxKWJg5kZx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JWXzSrUxKWJg5kZx p{margin:0;}#mermaid-svg-JWXzSrUxKWJg5kZx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster-label text{fill:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster-label span{color:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster-label span p{background-color:transparent;}#mermaid-svg-JWXzSrUxKWJg5kZx .label text,#mermaid-svg-JWXzSrUxKWJg5kZx span{fill:#333;color:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx .node rect,#mermaid-svg-JWXzSrUxKWJg5kZx .node circle,#mermaid-svg-JWXzSrUxKWJg5kZx .node ellipse,#mermaid-svg-JWXzSrUxKWJg5kZx .node polygon,#mermaid-svg-JWXzSrUxKWJg5kZx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JWXzSrUxKWJg5kZx .rough-node .label text,#mermaid-svg-JWXzSrUxKWJg5kZx .node .label text,#mermaid-svg-JWXzSrUxKWJg5kZx .image-shape .label,#mermaid-svg-JWXzSrUxKWJg5kZx .icon-shape .label{text-anchor:middle;}#mermaid-svg-JWXzSrUxKWJg5kZx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JWXzSrUxKWJg5kZx .rough-node .label,#mermaid-svg-JWXzSrUxKWJg5kZx .node .label,#mermaid-svg-JWXzSrUxKWJg5kZx .image-shape .label,#mermaid-svg-JWXzSrUxKWJg5kZx .icon-shape .label{text-align:center;}#mermaid-svg-JWXzSrUxKWJg5kZx .node.clickable{cursor:pointer;}#mermaid-svg-JWXzSrUxKWJg5kZx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JWXzSrUxKWJg5kZx .arrowheadPath{fill:#333333;}#mermaid-svg-JWXzSrUxKWJg5kZx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JWXzSrUxKWJg5kZx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JWXzSrUxKWJg5kZx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JWXzSrUxKWJg5kZx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JWXzSrUxKWJg5kZx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JWXzSrUxKWJg5kZx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster text{fill:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx .cluster span{color:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JWXzSrUxKWJg5kZx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JWXzSrUxKWJg5kZx rect.text{fill:none;stroke-width:0;}#mermaid-svg-JWXzSrUxKWJg5kZx .icon-shape,#mermaid-svg-JWXzSrUxKWJg5kZx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JWXzSrUxKWJg5kZx .icon-shape p,#mermaid-svg-JWXzSrUxKWJg5kZx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JWXzSrUxKWJg5kZx .icon-shape .label rect,#mermaid-svg-JWXzSrUxKWJg5kZx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JWXzSrUxKWJg5kZx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JWXzSrUxKWJg5kZx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JWXzSrUxKWJg5kZx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} not approved
approved
START
think
act
reflect
END
这个三阶 Agent 的关键不是示例里的字符串拼接,而是结构:
think只负责规划。act只负责执行。reflect只负责评价。- 条件边负责决定继续还是结束。
iteration防止无限循环。
如果换成真实 LLM:
think_node可以调用模型生成计划。act_node可以调用工具或执行 RAG。reflect_node可以用结构化输出返回approved和critique。
"思考 -> 执行 -> 反思"是 Graph 表达 Agent 循环控制的经典模式。
给三阶 Agent 加上 LLM 结构化输出
真实项目里,reflect_node 不应该只靠字符串判断。
可以让模型返回结构化审查结果:
python
from pydantic import BaseModel, Field
class Reflection(BaseModel):
approved: bool = Field(description="当前结果是否可以作为最终答案")
critique: str = Field(description="如果不通过,说明需要改进什么")
然后在反思节点里使用:
python
reflection_model = model.with_structured_output(Reflection)
def reflect_node(state: AgentState) -> dict:
reflection = reflection_model.invoke(
[
{
"role": "system",
"content": "你是一个严格的答案审查员。",
},
{
"role": "user",
"content": (
f"用户问题:{state['question']}\n"
f"当前答案:{state['action_result']}\n"
"请判断当前答案是否足够具体、准确、可执行。"
),
},
]
)
update = {
"approved": reflection.approved,
"critique": reflection.critique,
}
if reflection.approved:
update["final_answer"] = state["action_result"]
return update
这和第 09 篇结构化输出是连起来的。
Graph 负责流程,结构化输出负责让流程决策稳定。
不要让模型自由发挥返回:
text
我觉得差不多可以了
而是让它返回:
json
{
"approved": true,
"critique": "答案已经覆盖关键步骤"
}
Graph 的条件边最好基于结构化字段决策,而不是解析自然语言。
何时用 create_agent(),何时手写 LangGraph?
很多人学到这里会纠结:
既然可以手写 Graph,那还要
create_agent()吗?
要。
二者不是替代关系。
| 场景 | 推荐 |
|---|---|
| 普通工具调用 Agent | create_agent() |
| 快速做一个可用助手 | create_agent() |
| 只需要加摘要、HITL、限流等通用能力 | create_agent() + Middleware |
| 流程有明确业务节点 | 手写 LangGraph |
| 有复杂分支、循环、并行、审批 | 手写 LangGraph |
| 多个 Agent 作为子流程协作 | 手写 LangGraph |
| 要精确控制每一步状态和恢复 | 手写 LangGraph |
可以这样理解:
text
create_agent() = 官方预制的 Agent Graph
手写 LangGraph = 你自己定义 Graph
前者快,后者可控。
真实项目里经常混用:
text
外层业务流程:手写 LangGraph
某个节点内部:调用 create_agent() 创建的子 Agent
例如:
text
classify_ticket
|
+-- faq_agent 使用 create_agent
|
+-- refund_workflow 手写 Graph + HITL
|
+-- bug_report 手写 Graph
简单 Agent 用 create_agent(),复杂业务编排用手写 LangGraph。
常见问题一:State 里存了太多格式化 Prompt
错误做法:
python
class State(TypedDict):
user_input: str
prompt_for_classifier: str
prompt_for_writer: str
prompt_for_reviewer: str
这样会让状态越来越脏。
更好的原则是:
State 存原始数据和中间结果,Prompt 在节点内部按需格式化。
例如:
python
class State(TypedDict):
user_input: str
classification: str
draft: str
review: str
然后在节点里:
python
def classify_node(state: State) -> dict:
prompt = f"请分类用户问题:{state['user_input']}"
result = model.invoke(prompt)
return {"classification": result.content}
这样做的好处是:
- 改 Prompt 不影响 state schema。
- trace 里能看到干净的数据。
- 下游节点可以用不同方式格式化同一份数据。
- 状态持久化更稳定。
State 是数据层,不是 Prompt 垃圾桶。
常见问题二:忘记 reducer,导致状态被覆盖
这是新手最常见的坑。
你以为自己在追加日志:
python
class State(TypedDict):
logs: list[str]
多个节点都返回:
python
return {"logs": ["xxx finished"]}
结果最后只剩最后一次更新。
正确写法:
python
from operator import add
from typing import Annotated
class State(TypedDict):
logs: Annotated[list[str], add]
对于 messages:
python
from langgraph.graph import MessagesState
class State(MessagesState):
route: str
凡是"多次写入同一字段"的 state key,都要先想清楚 reducer。
常见问题三:图里有循环,但没有退出条件
危险结构:
text
think -> act -> reflect -> think
如果 reflect 永远不通过,图会一直跑。
至少要有一个退出保护:
python
def route_after_reflect(state: AgentState):
if state["approved"]:
return END
if state.get("iteration", 0) >= 3:
return END
return "think"
也可以在运行时配置 recursion limit。
但更好的方式是业务上就明确:
- 最多反思几轮。
- 超过后返回当前最佳答案。
- 或升级到人工处理。
- 或输出"无法可靠完成"。
所有 Agent 循环都必须有业务退出条件。
常见问题四:在节点里做不可重放副作用操作
节点里可以调用外部系统,但要谨慎。
高风险动作包括:
- 发邮件。
- 扣款。
- 退款。
- 删除数据。
- 修改权限。
- 创建订单。
- 执行写 SQL。
这些动作不要直接混在普通推理节点里。
建议:
- 单独做 action node。
- action node 前加审批。
- 使用 idempotency key。
- 把外部系统返回 ID 写入 state。
- 失败时能明确重试或补偿。
Graph 让流程可恢复,但不可逆动作必须自己做幂等和审批。
一张图总结 LangGraph 工作方式
#mermaid-svg-Jpa4QL28n14mWDEi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jpa4QL28n14mWDEi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jpa4QL28n14mWDEi .error-icon{fill:#552222;}#mermaid-svg-Jpa4QL28n14mWDEi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jpa4QL28n14mWDEi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jpa4QL28n14mWDEi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jpa4QL28n14mWDEi .marker.cross{stroke:#333333;}#mermaid-svg-Jpa4QL28n14mWDEi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jpa4QL28n14mWDEi p{margin:0;}#mermaid-svg-Jpa4QL28n14mWDEi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Jpa4QL28n14mWDEi .cluster-label text{fill:#333;}#mermaid-svg-Jpa4QL28n14mWDEi .cluster-label span{color:#333;}#mermaid-svg-Jpa4QL28n14mWDEi .cluster-label span p{background-color:transparent;}#mermaid-svg-Jpa4QL28n14mWDEi .label text,#mermaid-svg-Jpa4QL28n14mWDEi span{fill:#333;color:#333;}#mermaid-svg-Jpa4QL28n14mWDEi .node rect,#mermaid-svg-Jpa4QL28n14mWDEi .node circle,#mermaid-svg-Jpa4QL28n14mWDEi .node ellipse,#mermaid-svg-Jpa4QL28n14mWDEi .node polygon,#mermaid-svg-Jpa4QL28n14mWDEi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Jpa4QL28n14mWDEi .rough-node .label text,#mermaid-svg-Jpa4QL28n14mWDEi .node .label text,#mermaid-svg-Jpa4QL28n14mWDEi .image-shape .label,#mermaid-svg-Jpa4QL28n14mWDEi .icon-shape .label{text-anchor:middle;}#mermaid-svg-Jpa4QL28n14mWDEi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Jpa4QL28n14mWDEi .rough-node .label,#mermaid-svg-Jpa4QL28n14mWDEi .node .label,#mermaid-svg-Jpa4QL28n14mWDEi .image-shape .label,#mermaid-svg-Jpa4QL28n14mWDEi .icon-shape .label{text-align:center;}#mermaid-svg-Jpa4QL28n14mWDEi .node.clickable{cursor:pointer;}#mermaid-svg-Jpa4QL28n14mWDEi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Jpa4QL28n14mWDEi .arrowheadPath{fill:#333333;}#mermaid-svg-Jpa4QL28n14mWDEi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Jpa4QL28n14mWDEi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Jpa4QL28n14mWDEi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jpa4QL28n14mWDEi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa4QL28n14mWDEi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa4QL28n14mWDEi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Jpa4QL28n14mWDEi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Jpa4QL28n14mWDEi .cluster text{fill:#333;}#mermaid-svg-Jpa4QL28n14mWDEi .cluster span{color:#333;}#mermaid-svg-Jpa4QL28n14mWDEi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Jpa4QL28n14mWDEi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Jpa4QL28n14mWDEi rect.text{fill:none;stroke-width:0;}#mermaid-svg-Jpa4QL28n14mWDEi .icon-shape,#mermaid-svg-Jpa4QL28n14mWDEi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jpa4QL28n14mWDEi .icon-shape p,#mermaid-svg-Jpa4QL28n14mWDEi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Jpa4QL28n14mWDEi .icon-shape .label rect,#mermaid-svg-Jpa4QL28n14mWDEi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa4QL28n14mWDEi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Jpa4QL28n14mWDEi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Jpa4QL28n14mWDEi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
定义 State Schema
编写 Node 函数
用 add_node 注册节点
用 add_edge / add_conditional_edges 连接流程
compile 编译图
invoke / stream 执行
节点返回 partial update
Reducer 合并 State
还有活跃节点?
结束并返回最终 State
这张图背后的关键点:
- 图执行围绕 state 推进。
- 节点只返回状态更新。
- 边决定下一个节点。
- reducer 决定状态怎么合并。
compile()之后图才可运行。invoke()返回最终状态。
总结
本文从 Chain 讲到 Graph,核心是想建立一个判断标准:
当 LLM 应用开始出现分支、循环、并行、状态持久化、人工审批和恢复需求时,就应该考虑 LangGraph。
需要记住这些结论:
StateGraph是 LangGraph Graph API 的核心入口。State是共享状态,节点读 state、返回 partial update。Node是执行逻辑的函数,应该保持单职责。Edge决定流程走向,普通边用于固定流程,条件边用于动态分支。START和END是图的虚拟入口和出口。- Reducer 决定同一个 state 字段如何合并多次更新。
- 聊天消息优先用
MessagesState或add_messages。 - 有循环就必须有退出条件。
- 有外部副作用就必须考虑幂等。
create_agent()是预制 Graph,手写 LangGraph 是自定义 Graph。