915 字
2 分钟
LangGraph基础复盘
第 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, Annotatedfrom langchain_core.messages import BaseMessageimport 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, AIMessagefrom 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 asynciofrom typing import TypedDict, Annotated, Literalimport operator
from langchain_core.messages import BaseMessage, HumanMessagefrom 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 的 keydef 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 asynciofrom typing import TypedDict, Annotatedfrom langgraph.graph import StateGraph, ENDimport 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, Annotatedimport operator
from langchain_core.messages import BaseMessage, HumanMessagefrom 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 asynciofrom 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 七步
StateGraph(State类型)创建图构建器add_node(名称, 函数)注册节点set_entry_point(名称)指定入口add_edge/add_conditional_edges定义流转compile()编译为可执行图get_graph().draw_mermaid()可视化结构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基础复盘/ 部分信息可能已经过时
相关文章 智能推荐





