2.2 保姆级教程:手把手带你构建第一个 LangGraph 应用

3 阅读1分钟

导语:在上一讲中,我们理解了 LangGraph 的革命性思想——用“图”来编排 Agent。理论总是让人兴奋,但真正的掌握源于实践。本篇文章将是一份“保姆级”的教程,我们将暂时抛开复杂的理论,从零开始,手把手、一步步地带你构建一个功能完整、但逻辑清晰的 LangGraph 应用。我们将构建一个“智能工具助手”,它能根据你的问题,从多个可用工具中(例如,网页搜索、计算器)智能地选择一个来执行。跟随本教程,你将获得第一次完整的、端到端的 LangGraph 开发体验,为你后续构建更复杂的系统奠定最坚实的基础。

目录

  1. 项目目标:构建一个“智能工具助手” Agent
    • 功能需求:能够使用网页搜索,也能进行数学计算
    • 技术选型:LangGraph + Tavily Search API + LangChain
  2. 步骤一:环境准备与工具定义
    • 安装必要的库:langgraph, langchain-openai, tavily-python
    • 获取 API Keys:OpenAI/DeepSeek API Key 和 Tavily API Key
    • 定义我们的工具集:TavilySearchResults 和一个自定义的 python_calculator
  3. 步骤二:定义图的状态(State)
    • 使用 TypedDict 创建 AgentState
    • 状态中需要包含哪些关键信息?(messages, sender, etc.)
  4. 步骤三:创建图的“节点”(Nodes)
    • 设计节点 call_model:作为 Agent 的“大脑”,负责决策
    • 设计节点 call_tool:作为 Agent 的“双手”,负责执行工具
    • 代码实现:编写每个节点的具体函数
  5. 步骤四:定义图的“边”(Edges),连接工作流
    • 设计入口:从哪里开始?
    • 设计条件边 should_continue:如何根据模型输出来决定下一步走向?
    • 连接所有节点:add_node, set_entry_point, add_conditional_edges, add_edge
  6. 步骤五:编译并运行你的图
    • app = workflow.compile():从图定义到可执行应用
    • 传入输入,并使用 .stream() 进行可视化调试
    • 观察 Agent 如何在搜索和计算之间做出正确选择
  7. 代码复盘与关键点解析
    • Annotated[List[BaseMessage], operator.add] 的含义是什么?
    • bind_tools 的作用是什么?
    • 条件边 should_continue 是如何实现路由的?
  8. 总结:恭喜!你已掌握构建 Agent 工作流的基本功

1. 项目目标:构建一个“智能工具助手” Agent

我们今天的目标是构建一个 Agent,它不仅仅能调用一个工具,而是能从多个工具中,根据用户的问题,智能地选择一个合适的工具来使用。

功能需求:

  1. 当用户提出事实性、需要网络查询的问题时(例如“马斯克最近在忙啥?”),Agent 应该调用网页搜索工具。
  2. 当用户提出需要数学计算的问题时(例如“34 * 5.67 等于多少?”),Agent 应该调用计算器工具。
  3. 如果用户的问题不需要工具,Agent 应该能直接回答。

技术选型:

  • 图编排langgraph
  • LLMlangchain_openai (可接入 OpenAI GPT 系列或 DeepSeek 等兼容模型)
  • 网页搜索tavily-python (Tavily 是一个专为 LLM 设计的搜索引擎,效果很好,提供免费 API)
  • 计算器:我们将自定义一个简单的 Python 函数作为计算器工具。

2. 步骤一:环境准备与工具定义

安装必要的库

打开你的终端,运行以下命令:

pip install langgraph langchain_openai tavily-python

获取 API Keys

  1. LLM API Key:你需要一个 OpenAI 或 DeepSeek 的 API Key。在终端中设置环境变量:

    # 如果使用 DeepSeek
    export OPENAI_API_KEY="YOUR_DEEPSEEK_API_KEY"
    export OPENAI_API_BASE="https://api.deepseek.com/v1"
    
    # 如果使用 OpenAI
    # export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
    
  2. Tavily API Key

    • 访问 Tavily AI 官网 并注册一个账户。
    • 在你的账户仪表盘中,找到你的 API Key。
    • 在终端中设置环境变量:
      export TAVILY_API_KEY="YOUR_TAVILY_API_KEY"
      

定义我们的工具集

现在,我们来创建 Agent 可以使用的“双手”。

# multi_tool_agent.py

from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.tools import tool

# 1. Tavily 搜索工具
# TavilySearchResults 是一个预置好的工具类,可以直接实例化
# description 会被自动填充,你也可以自定义
search_tool = TavilySearchResults(max_results=2)

# 2. 自定义计算器工具
@tool
def python_calculator(code: str):
    """
    A Python REPL tool that can execute simple Python code for calculations.
    Use this for any math questions. The input must be a valid Python expression.
    Example: `2 * 3`, `5**4`, `10 / 2`.
    """
    try:
        # 使用 exec 可能有安全风险,这里简化处理,生产环境需要沙箱
        result = eval(code)
        return result
    except Exception as e:
        return f"Error: {str(e)}"

# 将所有工具放入一个列表
tools = [search_tool, python_calculator]

我们现在拥有了一个工具列表 tools,里面包含了两个功能截然不同的工具。

3. 步骤二:定义图的状态(State)

State 是 LangGraph 的核心,它像一个在流水线上流转的“篮子”,每个工位(节点)都可以往里面添加或读取东西。我们的“篮子”需要能装下对话的全部历史记录。

# multi_tool_agent.py (续)

from typing import TypedDict, Annotated, List
from langchain_core.messages import BaseMessage
import operator

# `Annotated` 和 `operator.add` 是一个特殊的组合
# 它告诉 LangGraph,当多个节点都返回 'messages' 时
# 不要用新的覆盖旧的,而是将新旧消息列表“相加”
class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], operator.add]

我们定义了一个名为 AgentStateTypedDict。它只有一个字段 messagesAnnotated 的用法我们会在最后的“代码复盘”中详细解释,现在你只需要知道,这是一种让消息历史可以被累加的魔法。

4. 步骤三:创建图的“节点”(Nodes)

我们的 Agent 工作流需要两个核心的“工位”:

  1. agent 节点:负责调用 LLM 进行思考和决策。
  2. tool_executor 节点:负责接收 LLM 的指令,并执行相应的工具。

让我们来实现这两个节点函数。

# multi_tool_agent.py (续)

from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode

# --- 1. 初始化 LLM 和工具执行器 ---
llm = ChatOpenAI(model="deepseek-chat") # 或者 "gpt-4-turbo"

# 将我们的工具列表绑定给 LLM,这样 LLM 才知道它有哪些工具可用
llm_with_tools = llm.bind_tools(tools)

# ToolNode 是 LangGraph 提供的一个预置好的节点,专门用来执行工具
# 它会自动解析 tool_calls,调用相应的工具,并返回 ToolMessage
tool_executor_node = ToolNode(tools)

# --- 2. 定义我们的节点函数 ---

# a. Agent 节点:负责调用 LLM
def agent_node(state: AgentState):
    """
    The primary node that drives the agent's decision-making process.
    It takes the current state (conversation history) and calls the LLM.
    """
    print("--- Calling Agent Node (LLM) ---")
    response = llm_with_tools.invoke(state['messages'])
    # 返回一个字典,其 key 必须是 AgentState 中定义的字段
    return {"messages": [response]}

# b. 工具执行节点
# 我们直接使用预置的 ToolNode,无需自己编写函数
# def tool_node(state: AgentState):
#   ... (ToolNode 替我们完成了这部分工作)

这里我们做了几件重要的事情:

  • 通过 llm.bind_tools(tools),我们将工具的 JSON Schema 信息“注入”到了 LLM 的每一次调用中。这样,LLM 在思考时,就会知道 search_toolpython_calculator 的存在和用法。
  • 我们直接使用了 langgraph.prebuilt.ToolNode。这是一个非常有用的预置节点,它帮我们完成了“解析 tool_calls -> 循环执行工具 -> 构造 ToolMessage”这一套标准流程,大大简化了我们的代码。
  • 我们编写了 agent_node 函数,它的职责非常纯粹:就是调用 LLM。

5. 步骤四:定义图的“边”(Edges),连接工作流

现在我们有了“篮子”(State)和“工位”(Nodes),是时候用“传送带”(Edges)把它们连接起来,形成一条完整的流水线了。

# multi_tool_agent.py (续)

from langgraph.graph import StatefulGraph, END

# --- 1. 定义条件边函数 ---
def should_continue(state: AgentState):
    """
    A router function that determines the next step based on the LLM's output.
    """
    print("--- Checking for Tool Calls ---")
    last_message = state['messages'][-1]
    
    # 如果没有工具调用,说明 LLM 已经给出了最终答案,结束流程
    if not last_message.tool_calls:
        print("No tool calls. Ending.")
        return "end"
    
    # 如果有工具调用,继续执行工具
    else:
        print("Tool calls found. Continuing.")
        return "continue"

# --- 2. 组装图 ---
workflow = StatefulGraph(AgentState)

# 添加节点
workflow.add_node("agent", agent_node)
workflow.add_node("tool_executor", tool_executor_node)

# 设置入口点
workflow.set_entry_point("agent")

# 添加条件边
workflow.add_conditional_edges(
    # 决策的起点是 'agent' 节点
    "agent",
    # 决策的函数是 should_continue
    should_continue,
    # 决策的路径映射
    {
        "continue": "tool_executor", # 如果返回 "continue",则走向 'tool_executor' 节点
        "end": END,                 # 如果返回 "end",则结束 (END 是特殊标识)
    },
)

# 添加常规边
# 当 'tool_executor' 节点执行完毕后,工作流总是应该回到 'agent' 节点
# 让 Agent 基于工具的执行结果,进行下一步的思考
workflow.add_edge("tool_executor", "agent")

让我们来解读一下这张“流水线设计图”:

  1. 入口 (set_entry_point):所有任务都从 agent 节点开始。
  2. agent 节点执行:调用 LLM。
  3. 条件路由 (add_conditional_edges):在 agent 节点之后,调用 should_continue 函数进行判断。
    • 路径 A:如果 LLM 的回复不包含 tool_callsshould_continue 返回 "end",流程直接走向终点 END
    • 路径 B:如果 LLM 的回复包含 tool_callsshould_continue 返回 "continue",流程走向 tool_executor 节点。
  4. tool_executor 节点执行:执行 LLM 请求的工具。
  5. 返回循环 (add_edge)tool_executor 节点执行完毕后,流程无条件地返回到 agent 节点,形成一个完美的“思考 -> 行动 -> 观察 -> 再思考”的循环。

6. 步骤五:编译并运行你的图

我们的流水线已经设计完毕,现在只需要按下“启动”按钮。

# multi_tool_agent.py (续)

from langchain_core.messages import HumanMessage

# 编译图,得到一个可执行的 app
app = workflow.compile()

# --- 现在,让我们来测试它! ---

# 测试一:网页搜索
print("\n\n--- Test Case 1: Web Search ---")
inputs1 = {"messages": [HumanMessage(content="What's on Elon Musk's mind recently?")]}
for output in app.stream(inputs1, {"recursion_limit": 5}): # 设置递归限制以防意外死循环
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print(value)
        print("---")
    print("\n")


# 测试二:数学计算
print("\n\n--- Test Case 2: Calculator ---")
inputs2 = {"messages": [HumanMessage(content="What is 34 * 5.67?")]}
for output in app.stream(inputs2, {"recursion_limit": 5}):
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print(value)
        print("---")
    print("\n")


# 测试三:直接回答
print("\n\n--- Test Case 3: Direct Answer ---")
inputs3 = {"messages": [HumanMessage(content="Hello!")]}
for output in app.stream(inputs3, {"recursion_limit": 5}):
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print(value)
        print("---")
    print("\n")

将以上所有代码片段整合到一个 multi_tool_agent.py 文件中,然后在终端运行 python multi_tool_agent.py

你将会看到非常详细的输出,清晰地展示了 Agent 的每一步行动:

  • 测试一中,agent 节点会决策调用 search_tool,然后 tool_executor 节点会执行它。
  • 测试二中,agent 节点会决策调用 python_calculator,然后 tool_executor 节点会执行它。
  • 测试三中,agent 节点会直接生成回复,should_continue 将返回 "end",流程直接结束。

你已经成功构建了一个可以在多个工具间进行智能选择的 Agent!

7. 代码复盘与关键点解析

  • Annotated[List[BaseMessage], operator.add] 的含义是什么?

    • Annotated 是 Python typing 模块中的一个功能,它允许我们为类型添加额外的元数据。
    • operator.add 就是我们添加的元数据。
    • LangGraph 在处理状态更新时,会检查这个元数据。如果它发现了 operator.add,它就会执行“加法”操作(对于列表就是 list.extend),而不是默认的“覆盖”操作。这确保了 messages 列表是持续累加的,而不是每次都被新消息替换。
  • bind_tools 的作用是什么?

    • 这是一个便捷的方法,等同于在每次调用 .invoke() 时,手动将工具的 JSON Schema 传入 tools 参数。
    • llm.bind_tools(tools) 返回一个新的 Runnable 对象,这个对象在被调用时,会自动将工具信息附加上去。这让我们的 agent_node 代码更简洁。
  • 条件边 should_continue 是如何实现路由的?

    • 这是 LangGraph 控制流的核心。add_conditional_edgespath_function(即我们的 should_continue)是实现动态路由的关键。
    • 它就像一个铁路上的“道岔”,根据 State 中的当前信息(最新的消息是否包含 tool_calls),决定将工作流的“列车”引向哪条“轨道”(tool_executor 节点或 END)。

8. 总结:恭喜!你已掌握构建 Agent 工作流的基本功

通过这个“保姆级”的教程,你不仅构建了你的第一个真正意义上的 LangGraph 应用,更重要的是,你亲身体验了用图的思维来设计和编排 Agent 工作流的全过程。

你学会了:

  1. 如何定义一个可累加的状态(AgentState)。
  2. 如何将 Agent 的思考(agent_node)和行动(tool_executor_node)拆分成独立的节点。
  3. 如何使用强大的条件边(add_conditional_edges)来实现 Agent 的动态决策。
  4. 如何将这一切编译成一个可执行、可观测的 app

你已经掌握了构建复杂、可控、可预测的 Agent 所需的最核心的基本功。在接下来的课程中,我们将基于这些基本功,去探索更令人兴奋的应用,比如多 Agent 协作、人机交互等。