LangGraph 实战:赋予 AI 手脚 —— 构建 ReAct Agent
LangGraph 实战(四):赋予 AI 手脚 —— 构建 ReAct Agent
1. 什么是 ReAct?为什么它很重要?
在上一篇中,我们写了一个“作家-审稿人”的循环。那个循环虽然看起来很智能,但其实非常死板:作家必须把稿子给审稿人,审稿人必须打分。AI 没有任何选择权。
ReAct (Reasoning + Acting) 是一种让 AI 变得真正智能的架构。它的运作模式非常像人类解决复杂问题时的过程:
- Reason (思考):用户问我“今天上海的天气适合穿短袖吗?” -> 我需要先知道今天上海的气温。
- Act (行动):调用“天气查询工具”去查上海的气温。
- Observe (观察):工具返回“上海今日气温 15度”。
- Reason (再思考):15度有点冷,不适合穿短袖。
- Final Answer (回答):建议穿长袖或外套,不建议短袖。
LangGraph 是实现这种“思考-行动-观察”循环的最佳框架。
2. 核心难点:状态记忆 (Memory)
在之前的例子中,我们的 State 是一个简单的字典(draft, revision_number)。但在 Agent 场景下,核心状态是对话历史(Message History)。
为什么?因为 Agent 需要记住:
- 用户刚开始问了什么。
- 它自己决定调用了什么工具。
- 工具返回了什么结果。
如果不记住这些,Agent 拿到工具结果后就会一脸懵逼:“我为什么要查天气来着?”
LangGraph 提供了一个关键的Reducer(归约器)函数:add_messages。它的作用是:当新的消息进来时,不要覆盖旧消息,而是追加到列表后面。
3. 环境准备
我们需要安装 LangGraph 的预构建组件库,它里面包含了一些现成的 ToolNode,能帮我们省很多代码。
1 | # 安装 LangGraph 及其预构建库 |
4. 实战:构建一个能查“股票”和“算数”的 Agent
我们将构建一个 Agent,它拥有两个工具:
get_stock_price: 一个模拟的查询股票价格的工具。multiply: 一个乘法计算器。
目标:用户问“苹果股票价格乘以 100 是多少?”,Agent 需要先查股价,再算乘法,最后回答。
第一步:定义工具 (Define Tools)
工具本质上就是 Python 函数,但需要用 @tool 装饰器标记,这样 LLM 才能理解这个函数是干嘛的。
1 | from langchain_core.tools import tool |
第二步:定义状态 (Define State)
这次我们使用 Annotated 和 add_messages。这是 LangGraph 处理对话的标准姿势。
1 | from typing import Annotated, TypedDict |
第三步:绑定工具到 LLM
我们需要告诉 GPT-3.5:“你有这些工具可用”。
1 | from langchain_openai import ChatOpenAI |
第四步:定义节点 (Nodes)
我们需要两个核心节点:
- Chatbot Node: 调用 LLM 进行思考。
- Tool Node: 如果 LLM 决定用工具,这个节点负责真正执行工具函数。
1 | from langgraph.prebuilt import ToolNode |
第五步:定义条件边 (Conditional Edge)
这是 ReAct 的灵魂。
当 chatbot_node 运行完,我们需要检查:LLM 是想说话(直接回答),还是想干活(调用工具)?
1 | def tools_condition(state: State): |
第六步:构建图 (Build Graph)
1 | from langgraph.graph import StateGraph, END |
第七步:运行与深度观察
我们问一个需要两步操作的问题。
1 | print("--- 开始运行 ReAct Agent ---") |
5. 深度解析:刚刚发生了什么?
如果你运行了上面的代码,你会看到一个精彩的“接力跑”:
- START -> Agent: Agent 接收到问题“AAPL 股价乘以 100”。它思考后发现自己不知道股价,于是生成了一个 AIMessage,里面包含
tool_calls=[{'name': 'get_stock_price', 'args': {'symbol': 'AAPL'}}]。 - Agent -> Condition: 条件边检测到
tool_calls存在,路由指向 Tools。 - Tools 节点:
ToolNode看到系统里有个请求要查get_stock_price,它自动运行了我们的 python 函数,拿到了150.0。然后它生成了一个 ToolMessage(这是关键,专门用来放工具结果的消息类型)。 - Tools -> Agent: 带着
ToolMessage(内容是 "150.0"),流程回到了 Agent。 - Agent 再次思考: 现在它的内存里有:
- 用户:查 AAPL 乘 100。
- 自己:我要查 AAPL。
- 工具:结果是 150.0。
- Agent 思考:好了,我知道股价是 150,现在我要算 150 * 100。
- Agent 生成新的 AIMessage,包含
tool_calls=[{'name': 'multiply', 'args': {'a': 150, 'b': 100}}]。
- Agent -> Condition -> Tools: 再次进入工具节点,执行乘法,得到 15000。
- Tools -> Agent: 结果 15000 返回给 Agent。
- Agent 最终思考: 结合所有信息,生成自然语言:“AAPL 的股价是 150,乘以 100 等于 15000。” 这一次,它没有生成
tool_calls。 - Agent -> Condition -> END: 任务结束。
6. 总结
在这一篇中,我们构建了一个真正的智能体。
与之前的 Chain 相比,最大的区别在于:我们没有硬编码执行顺序。 我们没有写代码说“先查股票,再算乘法”。我们只是给了工具。如果用户问“10 乘以 10 是多少”,Agent 就会跳过查股票,直接调乘法。如果用户问“你好”,Agent 就会直接回答,一个工具都不调。
这就是 ReAct 的魅力:通用性。
但是,现在的 Agent 还有一个致命弱点:它没有长时记忆。如果你关掉程序再打开,它就不记得刚才聊了什么。在下一篇(也是最后一篇),我们将探讨 LangGraph 的持久化(Persistence)和人工介入(Human-in-the-loop),这对于生产环境的应用至关重要。