mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
915 字
2 分钟
LangGraph基础复盘
2026-09-09

第 10 章 复盘:LangGraph 核心概念 & 第一个 Graph#

知识脉络#

graph TD A[LCEL 管道只能线性执行] --> B[LangGraph: 把执行过程建模为有向图] B --> C[State 全局共享状态] B --> D[Node 处理函数] B --> E[Edge 普通边/条件边] C --> F[追加模式 vs 覆盖模式] E --> G[条件边实现分支与循环] D --> H[编译 Graph 并运行] H --> I[封装进项目正式代码]

10-1 为什么需要 LangGraph#

LCEL 管道(prompt | llm | parser)的根本限制:只能走直线,不能循环,数据只能从左到右流一遍。以下场景无法覆盖:

  • Agent 反复调用工具,直到拿到满意答案(循环)
  • 根据 LLM 输出内容走不同分支(条件判断)
  • 中途人工审批,确认后继续(暂停 + 恢复)
  • 多个 Agent 互相协作、交接任务(状态共享)

LangGraph 把执行过程建模成一张有向图:

概念含义
Node(节点)处理逻辑,本质是一个 Python 函数
Edge(边)节点之间的连接,决定下一步走哪里
State(状态)图执行过程中的全局数据,所有节点共享

由此支持循环、条件分支、暂停恢复。


10-2 三个核心概念:State、Node、Edge#

概念一:State(全局状态)#

from typing import TypedDict, Annotated
from langchain_core.messages import BaseMessage
import operator
# State 是 TypedDict,定义图执行过程中的全局数据结构,所有节点读写的数据都在这里
class ChatState(TypedDict):
# Annotated[list[BaseMessage], operator.add] 是 LangGraph 的状态合并语法:
# 每个节点返回的 messages 会【追加】到已有列表,而不是覆盖
# 若写成 messages: list[BaseMessage],节点返回时会覆盖整个列表
messages: Annotated[list[BaseMessage], operator.add]
# 普通字段直接写类型,节点返回时覆盖旧值
current_step: str

关键点:operator.add 就是 + 运算,用在列表上即拼接。节点返回 {"messages": [新消息]} 时,新消息追加到已有 messages 后面,而非替换整个列表。

概念二:Node(处理节点)#

from langchain_core.messages import HumanMessage, AIMessage
from app.ai.llm import llm
# Node 就是普通 Python 函数:接收当前 State,返回要更新的部分 State(字典)
# 不需要返回完整 State,只返回变化的字段
def preprocess_node(state: ChatState) -> dict:
"""预处理节点:打印当前状态,模拟前处理逻辑"""
print(f"[预处理节点] 当前消息数:{len(state['messages'])}")
return {"current_step": "preprocess_done"}
async def llm_node(state: ChatState) -> dict:
"""LLM 节点:调用模型生成回复"""
print("[LLM 节点] 正在生成回复...")
response = await llm.ainvoke(state["messages"])
print(f"[LLM 节点] 生成完成:{response.content[:30]}...")
# messages 为追加模式,只返回新增消息即可
return {"messages": [response], "current_step": "llm_done"}

概念三:Edge(节点连接)#

  • 普通边:A 执行完固定走向 B
  • 条件边:A 执行完,根据 State 内容决定走 B 还是 C(10-4 展开)

10-3 构建并运行第一个 Graph#

文件:scripts/ch10/test_basic_graph.py

import asyncio
from langgraph.graph import StateGraph, END
async def build_and_run_simple_graph():
# 第一步:创建 StateGraph,传入 State 类型
builder = StateGraph(ChatState)
# 第二步:添加节点 add_node(节点名称, 节点函数)
builder.add_node("preprocess", preprocess_node)
builder.add_node("llm", llm_node)
# 第三步:设置入口节点
builder.set_entry_point("preprocess")
# 第四步:添加边(定义执行顺序)
builder.add_edge("preprocess", "llm") # preprocess 执行完 → llm
builder.add_edge("llm", END) # llm 执行完 → 结束(END 为内置终止节点)
# 第五步:编译,得到可执行的 CompiledGraph
graph = builder.compile()
# 第六步:打印 Mermaid 图,可视化结构(可粘贴到 https://mermaid.live)
print("Graph 结构(Mermaid 格式):")
print(graph.get_graph().draw_mermaid())
# 第七步:ainvoke 传入初始 State 运行
initial_state = {
"messages": [HumanMessage(content="你好,用一句话介绍一下 LangGraph")],
"current_step": "start",
}
print("\n开始运行 Graph...")
result = await graph.ainvoke(initial_state)
print(f"\n最终 State:")
print(f" current_step: {result['current_step']}")
print(f" messages 数量: {len(result['messages'])}")
print(f" 最后一条消息: {result['messages'][-1].content}")
asyncio.run(build_and_run_simple_graph())

运行:

uv run python scripts/ch10/test_basic_graph.py

图结构:

graph LR START([START]) --> preprocess preprocess --> llm llm --> END([END])

输出包含三部分:Mermaid 图结构、节点按序执行的日志、最终 State。


10-4 条件边:根据 State 决定走哪条路#

条件边是 LangGraph 实现循环和分支的核心特性。

文件:scripts/ch10/test_conditional_graph.py

import asyncio
from typing import TypedDict, Annotated, Literal
import operator
from langchain_core.messages import BaseMessage, HumanMessage
from langgraph.graph import StateGraph, END
from app.ai.llm import llm
class AgentState(TypedDict):
# 消息列表:追加模式
messages: Annotated[list[BaseMessage], operator.add]
# 下一步动作标记:need_tool / need_refine / direct
next_action: str
# 工具调用次数:防止工具环节无限循环
tool_call_count: int
async def analyze_node(state: AgentState) -> dict:
"""分析节点:判断该走哪条路径"""
print("[分析节点] 判断问题类型...")
question = state["messages"][-1].content
# 这里用规则模拟路由判断;实际项目中可由 LLM 输出动作标记
if "天气" in question:
return {"next_action": "need_tool"}
return {"next_action": "direct"}
async def direct_answer_node(state: AgentState) -> dict:
"""直接回答节点"""
print("[直接回答节点] 生成回复...")
response = await llm.ainvoke(state["messages"])
return {"messages": [response], "next_action": "done"}
async def tool_node(state: AgentState) -> dict:
"""工具节点:模拟调用外部工具"""
print("[工具节点] 调用工具获取数据...")
tool_result = HumanMessage(content="(工具返回)武汉今天晴,25~32℃")
return {
"messages": [tool_result],
"next_action": "need_refine",
"tool_call_count": state.get("tool_call_count", 0) + 1,
}
async def refine_node(state: AgentState) -> dict:
"""优化回答节点:基于工具结果再次生成回复"""
print("[优化节点] 基于工具结果优化回复...")
response = await llm.ainvoke(state["messages"])
return {"messages": [response], "next_action": "done"}
# 条件边函数:根据 State 返回下一个节点名称
# 返回值必须是 add_conditional_edges 中 path_map 的 key
def route_after_analyze(state: AgentState) -> Literal["direct_answer", "tool", "refine"]:
"""分析节点之后的路由"""
action = state["next_action"]
if action == "need_tool":
return "tool"
elif action == "need_refine":
return "refine"
else:
return "direct_answer"
def route_after_tool(state: AgentState):
"""工具节点之后的路由:调用次数过多则强制结束,防止死循环"""
if state.get("tool_call_count", 0) >= 3:
return END
return "refine"
async def main():
builder = StateGraph(AgentState)
builder.add_node("analyze", analyze_node)
builder.add_node("direct_answer", direct_answer_node)
builder.add_node("tool", tool_node)
builder.add_node("refine", refine_node)
builder.set_entry_point("analyze")
# add_conditional_edges(起始节点, 路由函数, 路由映射)
# 路由函数返回字符串,路由映射把字符串映射到实际节点名
builder.add_conditional_edges(
"analyze",
route_after_analyze,
{
"direct_answer": "direct_answer",
"tool": "tool",
"refine": "refine",
},
)
# 工具节点同样用条件边:超过 3 次强制结束,否则进入 refine
builder.add_conditional_edges(
"tool",
route_after_tool,
{"refine": "refine", END: END},
)
builder.add_edge("direct_answer", END)
builder.add_edge("refine", END)
graph = builder.compile()
print("Graph 结构:")
print(graph.get_graph().draw_mermaid())
# 不同输入走不同路径
test_cases = [
"你好,介绍一下 Python", # direct_answer 路径
"武汉今天天气怎么样", # tool → refine 路径
]
for question in test_cases:
print(f"\n{'=' * 50}")
print(f"问题:{question}")
result = await graph.ainvoke({
"messages": [HumanMessage(content=question)],
"next_action": "",
"tool_call_count": 0,
})
last_msg = result["messages"][-1]
print(f"最终回复:{last_msg.content[:100]}")
asyncio.run(main())

运行:

uv run python scripts/ch10/test_conditional_graph.py

分支结构:

graph TD START([START]) --> analyze analyze -->|direct| direct_answer analyze -->|need_tool| tool analyze -->|need_refine| refine tool -->|未超次数| refine tool -->|>=3 次强制结束| END1([END]) direct_answer --> END2([END]) refine --> END3([END])

10-5 State 的更新规则详解#

文件:scripts/ch10/test_state_update.py

import asyncio
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
import operator
class DemoState(TypedDict):
# 追加模式:每次返回的值追加到列表
log: Annotated[list[str], operator.add]
# 覆盖模式:每次返回的值覆盖旧值
counter: int
last_node: str
def node_a(state: DemoState) -> dict:
print(f"Node A 执行,当前 counter={state['counter']}")
return {
"log": ["Node A 执行了"], # 追加
"counter": state["counter"] + 1, # 覆盖
"last_node": "A",
}
def node_b(state: DemoState) -> dict:
print(f"Node B 执行,当前 counter={state['counter']}")
return {
"log": ["Node B 执行了"], # 追加
"counter": state["counter"] + 10, # 覆盖
"last_node": "B",
}
async def main():
builder = StateGraph(DemoState)
builder.add_node("a", node_a)
builder.add_node("b", node_b)
builder.set_entry_point("a")
builder.add_edge("a", "b")
builder.add_edge("b", END)
graph = builder.compile()
result = await graph.ainvoke({
"log": ["初始日志"],
"counter": 0,
"last_node": "",
})
print("\n最终结果:")
print(f"log(追加,保留全部历史):{result['log']}")
print(f"counter(覆盖,只留最新值):{result['counter']}")
print(f"last_node(覆盖):{result['last_node']}")
asyncio.run(main())

运行:

uv run python scripts/ch10/test_state_update.py

两种更新模式对比:

模式写法行为适用数据
追加Annotated[list, operator.add]新值拼接到旧列表后消息列表、日志等需保留历史的数据
覆盖普通类型 int / str新值直接替换旧值”当前步骤”等只需最新值的标记字段

10-6 把 Graph 封装进项目#

新建目录 app/ai/graphs/,文件 app/ai/graphs/chat_graph.py:

from typing import TypedDict, Annotated
import operator
from langchain_core.messages import BaseMessage, HumanMessage
from langgraph.graph import StateGraph, END
from app.ai.llm import llm
class ChatState(TypedDict):
# 对话消息:追加模式,保留完整历史
messages: Annotated[list[BaseMessage], operator.add]
async def chat_node(state: ChatState) -> dict:
"""对话节点:调用 LLM 生成回复"""
response = await llm.ainvoke(state["messages"])
return {"messages": [response]}
def build_chat_graph():
builder = StateGraph(ChatState)
builder.add_node("chat", chat_node)
builder.set_entry_point("chat")
builder.add_edge("chat", END)
# 编译一次即可,后续全局复用
return builder.compile()
chat_graph = build_chat_graph()
async def chat_once(question: str) -> str:
"""对外暴露的简单调用接口"""
result = await chat_graph.ainvoke({
"messages": [HumanMessage(content=question)]
})
return result["messages"][-1].content

快速验证脚本:

import asyncio
from app.ai.graphs.chat_graph import chat_once
async def main():
answer = await chat_once("用一句话解释什么是状态图")
print(answer)
asyncio.run(main())

复盘清单#

1. LangGraph vs LCEL#

  • LCEL:只能线性执行,数据从左到右流一遍
  • LangGraph:有向图模型,支持循环、条件分支、暂停恢复、状态共享

2. 构建 Graph 七步#

  1. StateGraph(State类型) 创建图构建器
  2. add_node(名称, 函数) 注册节点
  3. set_entry_point(名称) 指定入口
  4. add_edge / add_conditional_edges 定义流转
  5. compile() 编译为可执行图
  6. get_graph().draw_mermaid() 可视化结构
  7. ainvoke(初始State) 异步运行

3. 条件边三要素#

  • 路由函数:接收 State,返回下一个节点的 key 字符串
  • 路由映射(path_map):把 key 字符串映射到实际节点名
  • 终止保护:循环路径上用计数器(如 tool_call_count >= 3 时返回 END)防止死循环

4. State 更新规则#

  • Annotated[list, operator.add] → 追加,保留历史
  • 普通类型字段 → 覆盖,只留最新值
  • 节点只需返回发生变化的字段,不用返回完整 State

5. Node 要点#

  • 普通 Python 函数(同步或 async)
  • 入参是当前 State,返回值是部分 State 的字典
  • 追加型字段只返回新增元素列表

下一章衔接#

第 11 章用 LangGraph 实现 ReAct Agent:在「思考 → 调用工具 → 看结果 → 再思考」之间循环,直到给出最终答案——条件边 + 状态更新正是其实现基础。 #(注:内容由AI生成)

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

LangGraph基础复盘
https://mizuki-master-1vt.pages.dev/posts/langgraph基础复盘/
作者
汪凝~
发布于
2026-09-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录