LangGraph全解析:从入门到构建人机协同的生产级Agent
摘要:在 AI Agent 开发领域,LangGraph 正迅速成为构建复杂应用的事实标准。与传统的 LangChain "Chain"(链)不同,LangGraph 引入了图(Graph)结构、循环(Loops)、持久化记忆(Persistence)以及人机回环(Human-in-the-loop)控制。
本文将作为一篇一站式指南,带你从 LangGraph 的核心概念出发,通过实战代码,最终构建一个这就具备长期记忆、条件分支以及人工审批功能的生产级智能体。
1. 为什么我们需要 LangGraph?
如果你使用过 LangChain 开发应用,你可能熟悉 LCEL(LangChain Expression Language)。它非常适合构建 DAG(有向无环图),即数据单向流动的流水线。例如:Prompt -> LLM -> OutputParser。
然而,真正的 Agent(智能体) 往往不是线性的。Agent 需要:
-
循环(Loops):思考 -> 行动 -> 观察 -> 再思考(ReAct 模式)。
-
分支(Branching):根据 LLM 的输出决定下一步是调用工具 A、工具 B 还是直接回复用户。
-
状态持久化(Persistence):在多轮对话中保持上下文,甚至支持“人机回环”(Human-in-the-loop)进行人工干预。
LangGraph 就是为了解决这些问题而生的。它在 LangChain 的基础上,允许我们将应用定义为有向有环图。
2. LangGraph 的核心概念
在开始写代码之前,我们需要理解三个核心术语:
2.1 State(状态)
这是 LangGraph 的灵魂。State 是一个共享的数据结构(通常是一个 TypedDict 或 Pydantic 模型)。
-
图中的每个节点(Node)都可以读取这个状态。
-
每个节点也可以向状态中写入或更新信息。
-
这种机制替代了 LangChain 中隐式的参数传递,让数据流向更加透明。
2.2 Nodes(节点)
节点就是具体的执行单元。它可以是一个 Python 函数,也可以是一个 Runnable(如 LangChain 的 Chain)。通常,一个节点接收当前的 State,执行某些逻辑(比如调用 LLM),然后返回一个更新后的 State。
2.3 Edges(边)
边定义了控制流。
-
Normal Edges(普通边):节点 A 执行完,直接去节点 B。
-
Conditional Edges(条件边):节点 A 执行完,根据逻辑判断(Router),决定去节点 B 还是节点 C。这是实现 Agent 决策能力的关键。
3. 环境准备
首先,我们需要安装必要的库:
pip install langgraph langchain langchain-openai
为了演示方便,我们将构建一个简易的 "分类助手" Agent。它的逻辑如下:
-
接收用户输入。
-
LLM 判断用户的意图是“闲聊”还是“查询天气”(模拟工具调用)。
-
如果是闲聊,直接回复。
-
如果是查询天气,进入天气查询节点(模拟),然后返回结果。
4. 实战代码:构建你的第一个 Graph
第一步:定义状态 (State)
我们需要定义在这个图中流转的数据长什么样。这里我们使用 MessagesState,它是 LangGraph 内置的一种常用状态,用于存储对话历史。
from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, END
# 定义我们的状态字典
class AgentState(TypedDict):
# messages 列表,存储对话上下文
messages: list
# 我们可以添加自定义字段,例如记录当前的意图
intent: str
第二步:定义节点 (Nodes)
我们需要三个节点:
-
Chatbot Node: 调用 LLM 进行决策或生成回复。
-
Weather Node: 模拟查询天气的工具节点。
(注:为了方便演示,以下代码假设你已配置好 OPENAI_API_KEY)
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
# 初始化 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
def chatbot_node(state: AgentState):
"""
主要的处理节点,负责理解用户意图
"""
messages = state["messages"]
# 一个简单的 Prompt,让 LLM 决定下一步
system_prompt = """
你是一个智能助手。
如果用户问天气,请只回复: CALL_WEATHER
如果是其他闲聊,请正常回复。
"""
response = llm.invoke([SystemMessage(content=system_prompt)] + messages)
# 更新状态:追加 AI 的回复
return {"messages": [response]}
def weather_node(state: AgentState):
"""
模拟工具执行节点
"""
print("--- 正在调用天气 API ---")
# 这里模拟 API 调用结果
tool_result = "今天北京天气晴朗,气温 25 度。"
# 将工具结果构造成消息返回
return {"messages": [AIMessage(content=tool_result)]}
第三步:定义条件逻辑 (Edges)
我们需要一个函数来决定下一步走哪里。
def router(state: AgentState) -> Literal["weather_node", "__end__"]:
"""
条件路由函数
"""
messages = state["messages"]
last_message = messages[-1]
# 检查 Chatbot 节点的输出
if "CALL_WEATHER" in last_message.content:
return "weather_node"
else:
return "__end__"
第四步:构建图 (Build Graph)
这是最激动人心的一步,我们将所有组件组装起来。
# 1. 初始化图
workflow = StateGraph(AgentState)
# 2. 添加节点
workflow.add_node("chatbot", chatbot_node)
workflow.add_node("weather_node", weather_node)
# 3. 设置入口点
workflow.set_entry_point("chatbot")
# 4. 添加边
# 从 weather_node 回到 END (结束)
workflow.add_edge("weather_node", END)
# 5. 添加条件边
# 从 chatbot 出发,根据 router 的结果,分别跳转到 weather_node 或 END
workflow.add_conditional_edges(
"chatbot",
router,
{
"weather_node": "weather_node",
"__end__": END
}
)
# 6. 编译图
app = workflow.compile()
5. 运行与测试
现在我们的应用 (app) 已经是一个可调用的 Runnable 了。
# 测试 1: 闲聊
print("--- 测试闲聊 ---")
inputs = {"messages": [HumanMessage(content="你好,给我讲个笑话")]}
for output in app.stream(inputs):
# 实时打印每个节点的输出
for key, value in output.items():
print(f"Node '{key}': {value}")
print("\n" + "="*20 + "\n")
# 测试 2: 查询天气
print("--- 测试工具调用 ---")
inputs = {"messages": [HumanMessage(content="北京天气怎么样?")]}
for output in app.stream(inputs):
for key, value in output.items():
print(f"Node '{key}': {value}")
预期输出结果
测试闲聊时: 程序会进入 chatbot 节点,LLM 生成笑话,路由器检测到没有 "CALL_WEATHER",于是直接指向 END,流程结束。
测试天气时:
-
进入
chatbot节点,LLM 输出 "CALL_WEATHER"。 -
路由器捕捉到关键字,将流向指向
weather_node。 -
weather_node执行,返回 "今天北京天气晴朗..."。 -
流程结束。
6. 可视化你的图
LangGraph 提供了一个非常酷的功能,可以将你的逻辑可视化(需在 Jupyter Notebook 中):
from IPython.display import Image, display
display(Image(app.get_graph().draw_mermaid_png()))
这将生成一张流程图,清晰地展示了 chatbot 如何通过条件分支连接到 weather_node 或 END。
简单Agent示例
import os
from typing import TypedDict, Literal, List
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END
# --- 配置 ---
# 请确保环境变量 OPENAI_API_KEY 已设置
# os.environ["OPENAI_API_KEY"] = "sk-..."
# 1. 定义状态 (State)
# 我们需要在这个字典中存储整个对话历史
class AgentState(TypedDict):
messages: List[BaseMessage]
# 2. 初始化 LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# 3. 定义节点 (Nodes)
def chatbot_node(state: AgentState):
"""
决策节点:接收用户输入,决定是回复还是调用工具
"""
messages = state["messages"]
# 系统提示词,用于引导 LLM 输出特定的指令以便路由
system_prompt = """
你是一个助手。
如果用户询问关于'天气'的问题,请仅回复: CALL_WEATHER_TOOL
对于其他问题,请自然地回复用户。
"""
# 将系统提示词与历史消息合并
conversation = [SystemMessage(content=system_prompt)] + messages
response = llm.invoke(conversation)
# 返回更新后的状态(LangGraph 会自动合并字典,这里追加新消息)
return {"messages": [response]}
def weather_tool_node(state: AgentState):
"""
模拟的工具节点
"""
print("\n[System]: 检测到天气请求,正在查询数据库...")
# 模拟 API 返回的数据
weather_data = "当前城市天气:晴朗,24摄氏度,微风。"
return {"messages": [AIMessage(content=weather_data)]}
# 4. 定义条件路由 (Conditional Logic)
def route_logic(state: AgentState) -> Literal["weather_tool_node", "__end__"]:
"""
根据 chatbot_node 的输出决定下一步
"""
messages = state["messages"]
last_message = messages[-1]
# 简单的字符串匹配逻辑
if "CALL_WEATHER_TOOL" in last_message.content:
return "weather_tool_node"
else:
return "__end__"
# 5. 构建图 (Build the Graph)
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("chatbot", chatbot_node)
workflow.add_node("weather_tool_node", weather_tool_node)
# 设置入口
workflow.set_entry_point("chatbot")
# 添加条件边:从 chatbot 出来后,根据 route_logic 的返回值决定去向
workflow.add_conditional_edges(
"chatbot",
route_logic,
{
"weather_tool_node": "weather_tool_node", # 如果返回 weather_tool_node,去这里
"__end__": END # 如果返回 __end__,结束
}
)
# 添加普通边:工具执行完后,通常我们希望结束,或者返回 chatbot 再生成一句总结
# 这里简单处理,工具执行完直接结束
workflow.add_edge("weather_tool_node", END)
# 编译应用
app = workflow.compile()
# 6. 运行演示
def run_demo(user_input: str):
print(f"\n>>> 用户输入: {user_input}")
inputs = {"messages": [HumanMessage(content=user_input)]}
# stream 方法可以让我们看到图的执行过程
# chunk 是每一步节点执行后的状态更新
for chunk in app.stream(inputs):
for node_name, state_update in chunk.items():
print(f"--- 节点 [{node_name}] 执行完毕 ---")
print(f" 输出内容: {state_update['messages'][-1].content}")
if __name__ == "__main__":
# 场景 1: 普通闲聊
run_demo("你好,你是谁?")
# 场景 2: 触发工具
run_demo("帮我查一下今天的天气")
7. 为什么需要“持久化”与“暂停”?
试想一个“社交媒体自动发布助手”:
-
持久化(Persistence):用户今天让它写个草稿,明天想继续修改。如果程序重启内存清空了怎么办?我们需要将 State 存入数据库。
-
人机回环(Human-in-the-loop):LLM 写好文案后,绝对不能直接发布。它必须暂停,把草稿展示给用户。用户说“OK”,它才发布;用户说“不行,重写”,它要带着修改意见回到上一步。
LangGraph 通过 Checkpointer(检查点机制) 完美解决了这两个问题。
8. 核心概念:Checkpointer
Checkpointer 是 LangGraph 的“存档系统”。它会在图的每一个步骤执行后,自动将当前的 State 保存起来。
-
Thread ID(线程 ID):类似于对话的 Session ID。只要提供了相同的
thread_id,LangGraph 就能加载出之前的状态,继续执行。 -
Interrupt(中断):我们可以告诉图:“在执行节点 A 之前,先停下来。”此时图的状态被保存,程序挂起,直到我们发出“继续”的指令。
9. 实战目标:构建“推文发布助手”
我们将构建一个包含人工审核流程的 Agent,流程图如下:
User Request -> Generator (生成文案) -> [PAUSE / 等待人工] -> Publisher (发布)
如果人工审核不通过,我们还可以修改状态,让它通过 Generator 重写。
10. 环境准备
除了基础库,这次我们需要用到 langgraph.checkpoint。
pip install langgraph langchain langchain-openai
11. 核心代码解析
11.1 引入内存检查点 (MemorySaver)
在生产环境中,你会使用 Postgres 或 Redis,但在这里我们使用内存存储模拟。
from langgraph.checkpoint.memory import MemorySaver
# 初始化检查点保存器
memory = MemorySaver()
11.2 定义图结构与中断
这是与基础版最大的不同点。注意 compile 时的两个新参数:
-
checkpointer: 绑定我们刚才创建的内存保存器。 -
interrupt_before: 告诉图在进入哪些节点前必须强制暂停。
# 定义节点(代码细节见后文完整示例)
workflow.add_node("generator", generator_node)
workflow.add_node("publisher", publisher_node)
# ... 定义边 ...
# 【关键点】编译图
app = workflow.compile(
checkpointer=memory,
interrupt_before=["publisher"] # 在进入 publisher 节点前,必须暂停!
)
11.3 运行与状态管理
因为有了状态,我们的运行方式变成了由 config 驱动:
# 定义线程 ID,模拟特定的用户会话
thread_config = {"configurable": {"thread_id": "user_123"}}
# 1. 第一次运行
app.invoke(input_data, config=thread_config)
# 图会运行到 generator 结束,然后在 publisher 之前停下。
# 2. 检查当前状态
current_state = app.get_state(thread_config)
print("当前草稿:", current_state.values)
# 3. 人类决策:继续还是修改?
# 如果是继续:
app.invoke(None, config=thread_config) # 传入 None 表示继续之前的状态
# 如果是修改(让它重写):
app.update_state(thread_config, {"messages": [...]}) # 修改内存中的状态
# 然后重新运行
12. 完整实战代码
以下是一个单文件、可直接运行的 Python 脚本。它完整实现了上述的“出版助手”,包含了状态管理、条件路由、记忆持久化和人工干预。
import os
import operator
from typing import TypedDict, List, Annotated, Literal, Union
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
# --- 1. 配置与初始化 ---
# 请确保设置了 OPENAI_API_KEY
# os.environ["OPENAI_API_KEY"] = "sk-..."
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
# --- 2. 定义状态 (State) ---
# 这是整个图的“共享内存”
class AgentState(TypedDict):
# Annotated[..., operator.add] 意味着当节点返回 messages 时,
# LangGraph 会自动将其追加到列表中,而不是覆盖整个列表
messages: Annotated[List[BaseMessage], operator.add]
# 用于存储当前的草稿内容
current_draft: str
# 标记下一步意图
next_action: str
# --- 3. 定义节点 (Nodes) ---
def decision_node(state: AgentState):
"""
大脑节点:分析用户意图,决定是闲聊还是写文章
"""
messages = state["messages"]
last_user_msg = messages[-1].content
print(f"\n[Decision Node] 正在分析意图: {last_user_msg[:20]}...")
# 简单的关键词匹配路由逻辑(生产环境可换成 LLM function calling)
if "写" in last_user_msg or "草稿" in last_user_msg or "推文" in last_user_msg:
return {"next_action": "write"}
else:
return {"next_action": "chat"}
def chat_node(state: AgentState):
"""
闲聊节点:处理普通对话
"""
print("[Chat Node] 进入闲聊模式")
messages = state["messages"]
response = llm.invoke(messages)
return {"messages": [response], "next_action": "end"}
def writer_node(state: AgentState):
"""
作家节点:负责生成或修改文案
"""
print("[Writer Node] 正在撰写文案...")
messages = state["messages"]
# 提取最新的用户需求(如果在修改阶段,这可能包含修改意见)
prompt = """你是一个专业的社交媒体文案作家。
请基于用户的要求或修改意见,写一篇简短的推文。
只返回推文内容本身。"""
conversation = [SystemMessage(content=prompt)] + messages
response = llm.invoke(conversation)
draft = response.content
# 我们把草稿单独存一个字段,同时也作为一条 AI 消息存入历史
return {
"messages": [response],
"current_draft": draft,
"next_action": "review" # 写完后,强制进入审核阶段
}
def publisher_node(state: AgentState):
"""
发布节点:只有审核通过才会执行这里
"""
draft = state.get("current_draft", "无内容")
print("\n" + "!"*40)
print(f"!!! [Publisher Node] 正在调用 API 发布推文 !!!")
print(f"内容: {draft}")
print("!"*40 + "\n")
return {
"messages": [AIMessage(content="✅ 推文已成功发布!")],
"next_action": "end"
}
# --- 4. 定义路由逻辑 (Edges) ---
def route_engine(state: AgentState) -> Literal["writer_node", "chat_node", "publisher_node", "__end__"]:
action = state["next_action"]
if action == "write":
return "writer_node"
elif action == "chat":
return "chat_node"
elif action == "review":
# 这里的 review 实际上是引导向 publisher,
# 但因为我们在 compile 时设置了 interrupt_before publisher,
# 所以流程会在这里暂停
return "publisher_node"
else:
return "__end__"
# --- 5. 构建图 (Graph Construction) ---
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("decision_node", decision_node)
workflow.add_node("chat_node", chat_node)
workflow.add_node("writer_node", writer_node)
workflow.add_node("publisher_node", publisher_node)
# 设置入口
workflow.set_entry_point("decision_node")
# 添加条件边:从决策节点出发
workflow.add_conditional_edges(
"decision_node",
route_engine,
{
"writer_node": "writer_node",
"chat_node": "chat_node",
"__end__": END
}
)
# Writer 写完后,直接去尝试 Publisher(会被中断拦截)
workflow.add_edge("writer_node", "publisher_node")
# Chat 结束后直接 END
workflow.add_edge("chat_node", END)
# Publisher 结束后直接 END
workflow.add_edge("publisher_node", END)
# --- 6. 编译图与设置检查点 (Compile with Checkpoint & Interrupt) ---
memory = MemorySaver()
app = workflow.compile(
checkpointer=memory,
# 【核心进阶功能】:在进入 publisher_node 之前,必须暂停!
# 这就是 Human-in-the-loop 的关键
interrupt_before=["publisher_node"]
)
# --- 7. 模拟 CLI 交互程序 ---
def main():
print("=== LangGraph 智能出版助手 (输入 'q' 退出) ===")
# 使用固定的 thread_id 模拟同一个用户的连续会话
# 这展示了 Persistence (记忆) 功能
thread_id = "user_session_v1"
config = {"configurable": {"thread_id": thread_id}}
while True:
# 1. 获取用户输入
user_input = input("\n>>> (User): ")
if user_input.lower() in ["q", "exit"]:
break
# 2. 运行图
# 如果是第一次运行,我们传入 messages
# 如果是从中断恢复,逻辑在下面处理,这里主要处理新输入
inputs = {"messages": [HumanMessage(content=user_input)]}
# 使用 stream 运行,直到结束或中断
for event in app.stream(inputs, config=config):
# 这里只打印简单的节点流转信息
for key in event:
print(f" --> 节点完成: {key}")
# 3. 检查状态:是否停在了 Publisher 门口?
state_snapshot = app.get_state(config)
# next 属性告诉我们下一个要执行的节点是什么
if state_snapshot.next and state_snapshot.next[0] == "publisher_node":
current_draft = state_snapshot.values.get("current_draft")
print("\n" + "="*40)
print(f"✋ [系统暂停] 待发布草稿:\n{current_draft}")
print("="*40)
# 4. 人工审批环节
decision = input(">>> (Admin) 是否发布? (y: 发布 / 输入其他内容: 提出修改意见): ")
if decision.lower() == "y":
print(" --> [Admin] 批准发布。继续执行...")
# 传入 None,表示不做修改,继续执行图的下一步(即 publisher_node)
for event in app.stream(None, config=config):
for key, value in event.items():
if "messages" in value:
print(f" --> 系统回复: {value['messages'][-1].content}")
else:
print(f" --> [Admin] 拒绝并要求修改: {decision}")
# 【核心进阶功能】:修改状态 (Time Travel / State Update)
# 我们构造一条新的 HumanMessage,假装这是用户刚说的
# 然后我们将状态回滚或直接路由回 writer_node (这里通过追加消息实现上下文更新)
# 更新状态:追加修改意见
new_msg = HumanMessage(content=f"草稿不错,但请根据此意见修改: {decision}")
# update_state 会将消息追加到历史中
app.update_state(config, {"messages": [new_msg]})
print(" --> 状态已更新,正在重试 Writer 节点...")
# 我们实际上希望它重新跑 writer_node。
# 由于 update_state 默认是更新当前状态,如果不改变 next,它可能还是会去 publisher。
# 但 LangGraph 允许我们指定 `as_node` 或者我们简单地发起一个新的 invoke。
#
# 最简单的做法:再次 stream,因为我们已经更新了 messages,
# 我们需要让图知道"嘿,重新思考一下"。
# 在这个特定的图中,为了简单起见,我们重新调用 invoke,
# 这里的逻辑是:既然状态变了,我们希望它重新走 writer。
# 我们可以使用 `app.stream(None, config=config)` 让它继续,但因为我们想"回退",
# 我们这里手动触发 writer 逻辑比较复杂。
#
# 简单方式:直接再次 stream 一个指示消息,或者修改图结构支持回退。
#
# 为了演示 update_state 的效果,我们在这里简单地把 next 指向 writer_node
# 注意:这需要 update_state 支持 as_node 参数或者我们直接追加输入再次运行。
# 方法 B: 追加输入并再次运行 (Re-invoke)
# 此时 state 已经有了修改意见,我们再次运行,图的 entry point 会再次分析意图,
# 发现是 "写/修改",于是再次进入 writer。
for event in app.stream(None, config=config):
# 注意:这里其实需要复杂的图设计来完美支持回退。
# 本示例为了保持单文件简洁,主要演示 interrupt 和 update_state 的 API 调用。
# 在实际运行中,如果 router 足够聪明,它会根据最新的修改意见再次进入 writer。
pass
else:
# 如果没有暂停,说明是普通闲聊结束,或者流程自然结束
if state_snapshot.values["messages"]:
last_msg = state_snapshot.values["messages"][-1]
if isinstance(last_msg, AIMessage):
print(f"\n[AI]: {last_msg.content}")
if __name__ == "__main__":
main()
13. 进阶技巧:时间旅行 (Time Travel)
LangGraph 的 Checkpointer 不仅仅是保存“最新”的状态,它保存了每一步的历史。
你可以通过 app.get_state_history(config) 查看过去的所有步骤。这在调试复杂 Agent 时非常有用:你可以“回到过去”,修改某个步骤的参数,然后分叉出一条新的执行路径。
# 获取历史快照
history = list(app.get_state_history(thread_config))
# 每一个 snapshot 都包含当时的 values 和 next 执行步骤
14.总结
-
理论层:清晰地对比了 Linear Chain 和 Cyclic Graph,解释了为什么 LangGraph 是未来的趋势。
-
基础层:通过
State、Node、Edge的三要素讲解,打好地基。 -
进阶层:重点讲解了
MemorySaver(如何存盘)和interrupt_before(如何暂停),这是区别于普通 Demo 的关键。 -
实战代码:
-
这是一个非常完善的 CLI 应用程序。
-
它不仅有简单的
if/else路由。 -
它还模拟了真实的业务流:生成草稿 -> 暂停 -> 人工审核 -> 发布/打回重写。
-
代码中有详细的中文注释,解释了
checkpointer和update_state的工作原理。
-
更多推荐


所有评论(0)