从第一张图到生产部署,Module 1 构建了 LangGraph 的完整基础认知体系。
LangGraph 的最小运行单元:一张由 State、Node、Edge 构成的有向图。这是理解后续所有课程的基础骨架。
在图中接入真实 LLM,引入消息对话系统。这是从"演示图"到"真实 AI 应用"的关键一步。
messages: Annotated[list, add_messages],开箱即用的对话状态MessagesState 是 LangGraph 对话型 Agent 的标配起点,它预装了 add_messages Reducer,无需手动配置即可正确累积对话历史。让图根据 LLM 的输出自动决定"调用工具"还是"直接结束",是构建工具调用 Agent 的核心路由机制。
ToolNode + tools_condition 是 LangGraph 工具调用的"标准套件"——二者搭配可以用 4 行代码完成工具路由,无需手动解析工具调用结果。在 Router 基础上加一条回边,构成真正的 Agent 循环:LLM 推理 → 调用工具 → 观察结果 → 再推理,直到问题解决。
tools → agent 的回边让图形成环,支持无限次工具调用迭代tools_condition 路由到 END为 Agent 加装"记忆",让它在多轮对话中记住之前的内容,在同一次运行的不同步骤间保持状态。
MemorySaver 仅是入门版——它把状态存在内存里,进程重启就丢失。Module 2 会学习将其升级为 SqliteSaver(持久化到磁盘),这才是生产可用的记忆方案。将本地开发的图部署为生产服务。LangGraph Studio 提供可视化调试,LangGraph Cloud 提供一键 API 部署。
langgraph dev 启动本地服务,langgraph build 打包 Docker 镜像6 个课程的核心 API、使用场景与关键说明
| 概念 / API | 所属课程 | 核心作用 | 关键用法 |
|---|---|---|---|
| StateGraph | L1 | 图的构建器,接受 Schema 类 | StateGraph(MyState) |
| add_node / add_edge | L1 | 向图中添加节点和边 | builder.add_node("n", fn) |
| compile / invoke | L1 | 编译图 / 执行图 | graph.invoke({"key": val}) |
| MessagesState | L2 | 内置对话状态,含 add_messages | class S(MessagesState): ... |
| bind_tools | L2 | 将工具列表注入 LLM | llm.bind_tools([tool1, tool2]) |
| ToolNode | L3 | 自动执行 LLM 请求的工具 | ToolNode([tool1, tool2]) |
| tools_condition | L3 | 检测是否有工具调用,路由决策 | add_conditional_edges("agent", tools_condition) |
| ReAct 回边 | L4 | tools→agent 的循环边,实现迭代推理 | add_edge("tools", "agent") |
| MemorySaver | L5 | 内存级 Checkpointer,保存 State 快照 | compile(checkpointer=MemorySaver()) |
| thread_id | L5 | 标识对话会话,相同 ID 共享历史 | {"configurable": {"thread_id": "x"}} |
| langgraph.json | L6 | 部署配置,声明图入口和依赖 | {"graphs": {"agent": "./graph.py:graph"}} |
| langgraph dev | L6 | 启动 Studio 可视化本地调试服务 | CLI 命令 |
LangGraph 用 Node + Edge 描述 AI 的执行流程,比 if/else 更直观,比 LangChain Chain 更灵活——图天然支持分支、循环、并行。
MessagesState + add_messages Reducer 是所有对话型 Agent 的起点。理解它如何追加消息,是写出正确 Chatbot 的前提。
ToolNode + tools_condition 自动处理工具调用的解析和执行,无需手动提取 tool_calls 字段,显著减少样板代码。
Agent = Router + add_edge("tools", "agent")。这条回边让图形成环,LLM 得以持续推理直到自主判断任务完成。
每次 invoke 传入相同 thread_id,图就能从上次结束的地方继续。不同用户用不同 ID,状态完全隔离。
LangGraph Studio 能实时展示图结构、每步 State、工具调用详情。开发复杂 Agent 时,它比任何 print 语句都有效。
一个能够多步推理、自动调用工具、跨轮记忆用户偏好的旅行规划助手。它将 Module 1 的全部 6 个核心知识点串联成一个完整的可运行项目。
# ── 依赖导入 ───────────────────────────────────────────
from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
# ════════════════════════════════════════════════════
# 【L3】工具定义:@tool 装饰器自动生成 JSON Schema
# LLM 通过 Schema 了解每个工具的参数和用途
# ToolNode 根据 LLM 输出的 tool_call 自动匹配并执行
# ════════════════════════════════════════════════════
@tool
def get_weather(city: str) -> str:
"""获取指定城市的当前天气信息。"""
# 实际项目中调用真实天气 API,此处模拟返回
weather_data = {
"北京": "晴,26°C,微风",
"上海": "多云,22°C,东南风3级",
"成都": "阴,18°C,无风",
}
return weather_data.get(city, f"{city}:天气数据暂不可用")
@tool
def search_attractions(city: str, category: str = "all") -> str:
"""搜索城市的热门景点。category 可为 'nature'(自然)、'culture'(文化)或 'all'(全部)。"""
attractions = {
"北京": ["故宫", "长城", "颐和园", "天坛"],
"上海": ["外滩", "豫园", "东方明珠", "田子坊"],
"成都": ["大熊猫基地", "宽窄巷子", "武侯祠", "锦里"],
}
spots = attractions.get(city, [])
return f"{city}热门景点:{', '.join(spots)}" if spots else f"暂无{city}景点数据"
@tool
def estimate_budget(city: str, days: int, style: str = "standard") -> str:
"""估算旅行预算。style 可为 'budget'(经济)、'standard'(标准)、'luxury'(豪华)。"""
per_day = {"budget": 300, "standard": 600, "luxury": 1500}
daily_cost = per_day.get(style, 600)
total = daily_cost * days
return f"{city} {days}天{style}旅行预计费用:¥{total}(每天约¥{daily_cost})"
# 工具列表:注册所有可用工具
tools = [get_weather, search_attractions, estimate_budget]
@tool 做了两件事:①读取函数的类型注解和 docstring,自动生成 LLM 能理解的 JSON Schema;②将普通函数包装成 BaseTool 对象,供 ToolNode 和 bind_tools 使用。函数的 docstring 就是工具描述,写得越清晰,LLM 越能准确判断何时调用该工具。
# ════════════════════════════════════════════════════
# 【L1 + L2】State 定义
# · 继承 MessagesState(L2):内置 messages 字段 +
# add_messages Reducer,开箱即用
# · 扩展自定义字段(L1):TypedDict 风格添加额外状态
# ════════════════════════════════════════════════════
class TravelState(MessagesState):
# MessagesState 已内置:messages: Annotated[list[AnyMessage], add_messages]
# 下面是扩展字段
destination: str # 用户确定的目的地(默认覆盖策略)
travel_days: int # 旅行天数
# ════════════════════════════════════════════════════
# 【L2】LLM 初始化 + bind_tools
# · bind_tools 将工具的 JSON Schema 注入 LLM 的系统提示
# · LLM 输出 tool_call 时,内容是 JSON 格式的参数
# ════════════════════════════════════════════════════
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools) # 【L2】核心:让 LLM 知道有哪些工具
# ════════════════════════════════════════════════════
# 【L2 + L4】Agent 节点:ReAct 循环的推理端
# · 读取完整消息历史,带入工具上下文,调用 LLM
# · 返回 {"messages": [新的 AIMessage]}
# · add_messages Reducer 自动追加,不覆盖历史
# ════════════════════════════════════════════════════
def agent(state: TravelState):
system_prompt = SystemMessage(content=(
"你是一个专业的旅行规划助手。你可以查询天气、搜索景点、估算预算。"
"请根据用户需求,综合使用工具制定完整的旅行建议。"
))
# state["messages"] 包含完整的对话历史(由 add_messages Reducer 维护)
response = llm_with_tools.invoke([system_prompt] + state["messages"])
return {"messages": [response]} # 返回字典,Reducer 自动追加
继承 MessagesState 比从零定义 TypedDict 省去了 Annotated[list, add_messages] 的配置。同时支持自由扩展字段——destination 和 travel_days 使用默认覆盖策略,节点每次写入都会替换旧值。
llm.bind_tools(tools) 在每次请求时把工具的 JSON Schema 附加到 API 调用的 tools 参数里。LLM 看到工具列表后,可以输出一个特殊的 AIMessage,其 tool_calls 字段包含工具名和参数——这就是触发 ToolNode 执行的信号。
# ════════════════════════════════════════════════════
# 【L1】StateGraph:图的构建器
# 【L3】ToolNode:自动执行工具调用的内置节点
# ════════════════════════════════════════════════════
tool_node = ToolNode(tools) # 【L3】传入工具列表,自动匹配 tool_call 并执行
builder = StateGraph(TravelState) # 【L1】创建图,传入 State Schema
# ── 注册节点 ────────────────────────────────────────
builder.add_node("agent", agent) # 【L1】LLM 推理节点
builder.add_node("tools", tool_node) # 【L3】工具执行节点
# ── 连接边 ──────────────────────────────────────────
builder.add_edge(START, "agent") # 【L1】入口 → agent
# 【L3】条件边:LLM 有工具调用 → tools,否则 → END
builder.add_conditional_edges(
"agent",
tools_condition, # 内置路由函数,检测 tool_calls 是否存在
)
# 【L4】关键!这条回边让图形成环,实现 ReAct 循环
# tools 执行完工具后,把结果(ToolMessage)追加进 messages,
# 然后重新路由到 agent,让 LLM 看到工具结果继续推理
builder.add_edge("tools", "agent")
# ── 【L5】挂载 MemorySaver,实现跨轮对话记忆 ──────
memory = MemorySaver()
graph = builder.compile(checkpointer=memory) # 【L5】编译时注入 Checkpointer
没有 add_edge("tools", "agent") 时,图在调用一次工具后就结束。加上这条回边后:工具结果(ToolMessage)追加进 messages → agent 节点重新读取完整消息历史(含工具结果)→ LLM 基于新信息再次推理 → 可能继续调用工具或直接回答。这个循环持续到 LLM 不再输出工具调用为止,tools_condition 此时路由到 END。
# ════════════════════════════════════════════════════
# 【L5】thread_id:会话标识
# · 相同 thread_id → 共享同一对话历史
# · 不同 thread_id → 完全独立,互不干扰
# ════════════════════════════════════════════════════
config = {"configurable": {"thread_id": "user_alice_trip_001"}}
# ── 第 1 轮:询问北京旅行建议 ─────────────────────
result_1 = graph.invoke(
{"messages": [HumanMessage(content="我想去北京旅行3天,帮我规划一下")]},
config=config,
)
# Agent 会自动调用:get_weather("北京") → search_attractions("北京") →
# estimate_budget("北京", 3, "standard"),然后综合给出回答
# ── 第 2 轮:追问(自动记得第 1 轮内容)────────────
result_2 = graph.invoke(
{"messages": [HumanMessage(content="如果改成豪华出行,费用大概是多少?")]},
config=config, # 同一 thread_id → 记得"北京3天"的上下文
)
# LLM 知道上下文是北京3天,直接调用 estimate_budget("北京", 3, "luxury")
# ── 多用户隔离示例 ──────────────────────────────────
alice_config = {"configurable": {"thread_id": "alice_001"}}
bob_config = {"configurable": {"thread_id": "bob_001"}}
# alice 和 bob 的对话历史完全隔离,互不影响
# ── 查看完整消息历史 ────────────────────────────────
for msg in result_2["messages"]:
role = msg.__class__.__name__.replace("Message", "")
print(f"[{role}] {msg.content[:80]}")
每次节点执行完毕,LangGraph 将当前 State 序列化并用 (thread_id, checkpoint_id) 为 key 存入 MemorySaver(内存字典)。下次 invoke 传入相同 thread_id 时,LangGraph 先加载最新快照作为初始 State,再执行新的节点。这就是为什么第 2 轮能"记得"第 1 轮的内容。
messages 字段由 add_messages Reducer 维护,它会把每轮的 HumanMessage、AIMessage、ToolMessage 全部追加进历史列表。第 2 轮开始时,agent 节点拿到的 state["messages"] 包含第 1 轮所有消息——LLM 看到完整上下文,自然知道目的地是北京。
# ── langgraph.json(项目根目录)──────────────────────
# 【L6】LangGraph Studio / Cloud 的部署配置文件
# · "graphs":声明图的名称 → 对应的 Python 模块路径
# · "dependencies":项目依赖,打包时自动安装
# · "env":环境变量文件路径
# langgraph.json
{
"graphs": {
"travel_agent": "./travel_agent.py:graph"
// ↑ 文件路径 ↑ 变量名
},
"dependencies": ["."],
"env": ".env"
}
# ── 本地启动 LangGraph Studio ────────────────────────
# 【L6】在项目根目录运行,会自动打开 Studio 界面
# $ langgraph dev
# → 启动本地 API 服务(默认 http://127.0.0.1:2024)
# → 自动打开 Studio 可视化界面
# → 可在 Studio 里直接输入消息测试图的运行
# ── Studio 的调试能力 ────────────────────────────────
# 【L6】每次执行可以看到:
# · 图结构可视化(节点 + 边,含 ReAct 循环)
# · 每个节点执行前后的 State 快照对比
# · 工具调用的输入参数和返回结果
# · 每条消息的完整内容(AIMessage 含 tool_calls)
# · 可以在任意步骤"时间旅行"(查看历史快照)
# ── 云端部署(可选)─────────────────────────────────
# $ langgraph build -t my-travel-agent # 构建 Docker 镜像
# $ langgraph up # 启动生产服务
langgraph.json 是 Studio 和 Cloud 识别项目的唯一入口。"graphs" 键声明了"图名 → Python 文件中的变量"的映射关系——只要 travel_agent.py 里有一个叫 graph 的已编译图对象,Studio 就能找到并可视化它。langgraph dev 命令会在后台启动 FastAPI 服务并暴露标准 LangGraph API,Studio 通过这个 API 与图交互。
项目各部分与 6 个子课程的精确映射
| 项目组成 | 对应课程 | 核心 API | 解决的问题 |
|---|---|---|---|
| TravelState(MessagesState) | L1 + L2 | class S(MessagesState) | 定义数据契约 + 内置对话消息管理 |
| @tool 工具定义 | L2 + L3 | @tool 装饰器 | 自动生成 JSON Schema,供 LLM 理解和调用 |
| llm.bind_tools(tools) | L2 | bind_tools([...]) | 将工具信息注入 LLM,让其知道可以调什么 |
| agent 节点函数 | L2 + L4 | llm_with_tools.invoke(messages) | 读取完整消息历史,LLM 推理决定下一步 |
| ToolNode(tools) | L3 | ToolNode([...]) | 自动解析 tool_calls,执行对应工具,返回结果 |
| tools_condition 条件边 | L3 | add_conditional_edges("agent", tools_condition) | 检测是否有工具调用,路由到 tools 或 END |
| add_edge("tools", "agent") | L4 | add_edge("tools", "agent") | 构成 ReAct 循环——工具结果重新送回 LLM 推理 |
| MemorySaver checkpointer | L5 | compile(checkpointer=MemorySaver()) | 每步 State 快照,支持跨轮多步对话记忆 |
| thread_id 配置 | L5 | {"configurable": {"thread_id": "x"}} | 标识会话,相同 ID 恢复历史,不同 ID 隔离 |
| langgraph.json 配置 | L6 | {"graphs": {"name": "./file.py:var"}} | 声明部署入口,Studio / Cloud 识别项目 |