从 Workflow 到 Agent:理解 ReAct 和 Tool Calling

1. 从 Workflow 到 Agent:为什么需要新的架构

过去很长一段时间里,大模型的使用主要是围绕 Prompt 工程展开。随着 LangChain、LangGraph 等框架的发展,越来越多的应用开始采用工作流(Workflow)模式,将复杂任务拆解为多个步骤,再按照固定顺序执行。这种模式能够有效提升系统的稳定性和可维护性,但当任务复杂度进一步提高时,仅靠预定义流程可能无法满足需求。Agent 正是在这种背景下出现的一种新架构。

本文相关代码放在 reAct_tool_calling/ 目录下。依赖 LangGraph、Chroma、数据库或外部检索服务,沿用之前的 get_llm() 写法,通过 DashScope 的 OpenAI compatible 接口调用 deepseek-v4-pro

1
2
3
4
5
6
7
8
9
common.py

get_llm() 创建 ChatOpenAI

定义 Message、Tool、ToolCall

定义旅游、天气、住宿三个工具

LangChainTravelModel 通过 bind_tools() 把工具注册给模型

1.1 Workflow 的本质是预定义流程

在前面的 Web Research Chain 中,整个研究报告生成流程是一个典型的工作流。

1
用户问题 → 搜索词生成 → 网页搜索 → 网页抓取 → 内容总结 → 报告生成

虽然中间可能存在条件判断或分支选择,但这些路径在系统设计阶段就已经确定。例如:

1
2
3
4
if question_type == "weather":
weather_node()
else:
search_node()

无论用户输入什么内容,系统最终都只能在预先设计好的路径中运行。这种方式有两个明显特点:

  • 执行步骤固定
  • 路由规则固定

因此 Workflow 更像是一条流水线。对于结构明确的问题,这种方式效果很好。例如网页研究、文档分析、RAG 问答等场景,本质上都属于确定性较强的任务,只要按照既定流程执行即可获得结果。

1.2 Workflow 的边界

但现实中的任务并不总是能够提前规划完整流程。例如:

1
苏州有哪些值得游玩的地方?

这种问题可以直接走知识库检索流程。但如果问题变成:

1
苏州最近天气怎么样?

系统需要调用天气服务。再进一步:

1
2
苏州哪里值得玩?
最近天气如何?

此时既需要旅游知识库,又需要天气工具。如果继续采用传统工作流,代码很快会变成:

1
2
3
4
5
6
7
8
9
10
11
if contains_weather:
weather_tool()

if contains_travel:
travel_tool()

if contains_map:
map_tool()

if contains_hotel:
hotel_tool()

工具越多,条件分支越多。随着系统能力扩展,维护成本会呈指数级增长。更重要的是,很多复杂问题根本无法提前预测所有组合情况。例如:

1
2
3
上海到苏州怎么去?
天气适合什么时候出发?
附近有哪些值得停留的景点?

这类问题可能同时涉及:

1
2
3
4
地图工具
天气工具
旅游知识库
交通服务

在设计阶段很难穷举所有路径,这也是 Workflow 的边界。

1.3 Agent 与 Workflow 的本质区别

Agent 的出现,本质上是改变了决策方式:

1
2
3
4
5
# Workflow
程序决定下一步做什么

# Agent
模型决定下一步做什么

这是两种完全不同的思路。Workflow 的控制权掌握在代码。

1
2
3
4
5
# Workflow 的控制权掌握在代码
程序 → 决定执行路径

# Agent 的控制权则部分转移给了模型
问题 → LLM 分析 → 决定调用什么工具 → 决定是否继续执行

例如面对:

1
我想看江南园林,应该去哪里?

Agent 可能先调用旅游知识库。而面对:

1
苏州明天适合逛园林吗?

Agent 可能先查询天气,再查询景点信息。面对:

1
推荐一个周末江南旅行行程

Agent 又可能连续调用多个工具,最后汇总生成结果。这些执行路径并不是开发阶段写死的,而是在运行过程中由模型动态决定。这也是 Agent 与 Workflow 最核心的区别。

1.4 Agent 不等于更智能

Agent 的能力来源于:

1
大模型 + 工具 + 动态决策机制

其中大模型负责推理和规划。工具负责获取真实世界信息。动态决策机制负责决定何时使用工具。因此 Agent 的优势不在于模型参数更多,而在于能够访问模型本身无法获得的信息。例如:

1
2
3
4
5
天气数据
数据库记录
企业知识库
搜索结果
代码执行结果

这些内容原本并不在模型训练数据中。如果没有工具支持,大模型只能根据已有知识进行推测。生成的内容可能看起来合理,但看起来真实并不等于真实存在。而当 Agent 调用了外部工具之后,回答便建立在真实数据基础之上,这样可以获得更高的准确性和可验证性。

1.5 从工作流到智能体

回顾整个过程,可以看到一条路线:

1
Prompt → Workflow → Tool Calling → ReAct → Tool Agent

Prompt 解决的是模型表达问题,Workflow 解决的是流程组织问题。Tool Calling 解决的是外部能力接入问题。ReAct 解决的是动态决策问题。而 Agent,则是在这些能力基础上形成的一种新应用架构。接下来将进一步讨论 Tool Calling 的工作机制,以及大模型如何借助工具完成自主决策,从而构建真正意义上的 Agent。

2. Tool Calling:让大模型获得外部能力

前一章已经提到,Agent 与 Workflow 的最大区别在于流程控制权发生了变化。Workflow 由程序决定下一步做什么,而 Agent 则由大模型决定下一步应该执行什么操作。但这里会产生一个新的问题。大模型即使能够决定下一步要做什么,也不意味着它真的具备执行这些操作的能力。例如:

1
2
3
4
5
查询天气
搜索网页
访问数据库
执行代码
读取知识库

这些都属于模型训练过程之外的能力。如果没有额外机制,大模型仍然只能生成文本。Tool Calling 的出现,就是为了让模型能够借助外部系统完成这些工作。

2.1 大模型为什么需要工具

假设直接向模型提问:

1
杭州明天的天气怎么样?

模型可能会生成一段看起来合理的回答:

1
杭州明天多云,最高气温28℃。

问题是这个结果未必来自真实天气数据。模型只是根据训练过程中见过的大量天气描述,推测出一个概率最高的答案。从语言表达上看没有问题,但无法保证真实性。这也是大模型应用中最常见的问题之一。

1
看起来真实 ≠ 真实存在

模型擅长生成语言,却无法主动访问外部世界。因此很多实际业务系统都需要接入:

1
2
3
4
5
数据库
搜索引擎
企业知识库
第三方 API
代码执行环境

通过这些工具获取真实数据,再由模型负责理解和组织结果。此时系统结构就需要调整。

1
用户问题 → LLM → 工具 → 真实数据 → LLM → 最终答案

这就是 Tool Calling 的基础思路。

2.2 Tool 的本质是什么

对于程序来说,一个 Tool 就是一个普通函数。例如:

1
2
3
4
5
def get_weather(city: str):
...

def search_documents(query: str):
...

这些函数原本只能由程序主动调用。而 Tool Calling 做的事情,是把这些函数描述给大模型,让模型知道系统里存在哪些能力可以使用。因此 Tool 更准确的定义应该是,可以被大模型主动调用的外部能力。能力的来源并不重要。可能来自:

1
2
3
4
5
6
Python 函数
REST API
数据库查询
搜索服务
向量检索
代码执行器

只要能够接收参数并返回结果,都可以被包装成 Tool。

2.3 从 Function Calling 到 Tool Calling

早先 OpenAI 提供的是 Function Calling。模型并不会直接执行函数,而是返回一个函数调用请求。例如:

1
2
3
4
5
6
{
"name": "get_weather",
"arguments": {
"city": "Hangzhou"
}
}

程序收到这段结构化数据后:

1
2
3
4
找到函数
执行函数
获得结果
返回模型

完成一次调用。后来随着能力扩展,调用对象已经不再局限于函数。可能是:

1
2
3
4
5
数据库
浏览器
搜索引擎
代码执行器
知识库

因此 OpenAI 将概念统一升级为 Tool Calling。对于模型来说,它并不关心底层实现。只需要知道:

1
2
3
工具名称
工具用途
参数定义

即可。

2.4 LangChain 如何定义 Tool

在 LangChain 中,最常见的写法是使用 @tool 装饰器。例如旅游知识库工具:

1
2
3
4
5
6
7
8
@tool
# search_travel_info 工具名称
# query: str 调用时需要提供的数据
def search_travel_info(query: str) -> str:
# 告诉模型这个工具是做什么的
"""
Search travel information.
"""

这里有一个容易忽略的细节。模型实际上看不到函数实现。模型能够看到的只有:

1
2
3
工具名称
工具说明
参数定义

因此 Tool 的描述质量会直接影响工具调用效果。很多 Tool 调用失败,并不是工具本身有问题,而是说明文档没有准确表达工具用途。

2.5 bind_tools 的实现机制

工具定义完成后,还需要注册给模型。例如:

1
2
3
llm_with_tools = llm.bind_tools(
TOOLS
)

内部发生了一次转换。LangChain 会自动读取:

1
2
3
函数名称
参数类型
Docstring

并生成对应的 Tool Schema。最终发送给模型的信息更接近:

1
2
3
4
5
6
7
{
"name": "search_travel_info",
"description": "Search travel information",
"parameters": {
"query": "string"
}
}

从这一刻开始,模型便知道系统中存在这样一个能力。当用户提出问题时,模型会自行判断:

1
2
3
是否需要调用工具
调用哪个工具
如何构造参数

因此:

1
@tool + bind_tools()

完成了一次能力注册过程。

1
Python 函数 → Tool Schema → 注册给模型

2.6 Tool Calling 不是执行工具

这里还有一个重要概念容易混淆。模型发起 Tool Call,并不意味着工具已经执行。例如模型返回:

1
2
3
4
5
6
{
"name": "search_travel_info",
"args": {
"query": "江南园林 苏州 杭州"
}
}

这只是一个调用请求,真正执行工具的仍然是程序,执行流程实际上是:

1
LLM → 生成 Tool Call → 程序执行 Tool → 获得结果 → 返回 LLM

模型负责决策,程序负责执行,这是 Tool Calling 的核心工作机制。

2.7 Tool Calling:从对话模型走向智能体

当系统只有一个工具时,Tool Calling 看起来只是一次普通函数调用。但随着工具数量增加,情况开始发生变化。例如:

1
2
3
4
5
旅游知识库
天气服务
地图服务
酒店查询
机票查询

面对同一个问题:

1
2
苏州哪里值得玩?
最近天气如何?

模型可能需要:

1
2
3
先调用旅游知识库
再调用天气服务
最后综合结果

此时 Tool Calling 已经不再是单纯的函数执行机制,而开始承担任务规划和能力调度的职责。这也是 Agent 架构能够成立的基础。下一章将进一步讨论 ReAct 模式,以及大模型如何通过“思考—行动—观察”的循环机制,将多个 Tool 串联起来完成复杂任务。

3. ReAct:现代 Agent 的运行基础

Tool Calling 解决了一个关键问题,让大模型拥有调用外部能力的入口。但仅有工具还不够。如果系统中同时存在天气查询、知识库检索、地图搜索等多个工具,大模型还需要知道:

1
2
3
4
什么时候调用工具
调用哪个工具
调用几次
什么时候结束

如果这些决策仍然提前编写规则,那么系统本质上还是 Workflow。真正让 Agent 具备自主决策能力的,是 ReAct(Reason + Act)模式。

3.1 ReAct 解决的问题

传统聊天模式非常简单。

1
用户问题 → LLM → 最终答案

整个过程只有一次模型调用。例如:

1
法国首都是哪里?

模型直接回答:

1
巴黎

这种模式适合模型已经掌握答案的问题。但对于需要外部信息的问题,就会出现问题。例如:

1
杭州明天会下雨吗?

模型无法直接知道真实天气。此时最合理的流程应该是:

1
分析问题 → 查询天气 → 获得结果 → 组织答案

也就是说,在回答之前需要先执行动作。ReAct 正是为了解决这种场景提出的。

3.2 Reason 与 Act

ReAct 这个名字来自两个单词,Reason(推理)、Act(行动),核心思想是先思考,再行动。获得结果后继续思考,再决定是否继续行动。整个过程形成循环。

1
Question → Reason(负责规划) → Act(负责调用工具) → Observation(负责接收工具结果) → Reason(负责规划) → Act(负责调用工具) → Observation(负责接收工具结果) → Final Answer

这很像人解决问题时的思考过程。

3.3 一个完整例子

假设问题是:

1
我想看江南园林,应该去哪里?

第一次进入模型。此时模型没有任何资料。它可能产生如下推理:

1
需要查询江南园林相关的旅游信息

随后生成 Tool Call。

1
2
3
search_travel_info(
"江南园林 苏州 杭州"
)

工具执行完成后返回结果。

1
苏州是江南园林最典型的目的地之一

模型再次收到信息。这时候会重新分析:

1
已经获得地点信息、可以生成答案

最终返回:

1
如果想看江南园林,首推苏州;如果还想结合湖景和茶文化,可以把杭州一起放进行程……

整个过程中发生了两次推理。第一次决定调用工具。第二次决定结束任务。这就是一个标准的 ReAct 循环。

3.4 ReAct 为什么比 Workflow 更灵活

Workflow 的特点是路径固定。例如:

1
搜索 → 总结 → 回答

所有问题都会经过同样流程。而 ReAct 的特点是动态决策。面对不同问题,可以产生完全不同的执行路径。例如:

1
杭州有哪些景点?

可能只调用知识库。

1
知识库 → 回答

而:

1
苏州最近天气如何?

则可能变成:

1
天气工具 → 回答

如果问题是:

1
2
苏州哪里值得玩?
最近天气如何?

执行过程又会变成:

1
知识库 → 天气工具 → 汇总结果 → 回答

执行路径由模型实时决定。这也是 ReAct 相比传统 Workflow 最大的优势。

3.5 ReAct 并不是无限循环

Agent 并不会一直循环下去,每次进入模型时,都需要做出一个判断:

1
是否还需要工具

如果需要:

1
继续调用 Tool

如果不需要:

1
生成最终答案

因此 Agent 的结束条件其实非常简单。

1
存在 Tool Call → 继续执行 → 不存在 Tool Call → 结束任务

后面 LangGraph 中的 tools_condition 节点,本质上就是在完成这个判断。

3.6 Query Rewrite:Agent 的隐藏能力

在实际运行过程中,可能出现这样一个现象。用户输入:

1
推荐适合看江南园林的城市

模型生成的检索请求往往不是原始问题,而是:

1
2
3
江南园林城市推荐
苏州古典园林路线
杭州苏州江南旅行攻略

也就是说,模型会主动改写查询。这种能力本质上与 RAG 中的 Query Rewrite 十分相似。不同的是,以前需要额外设计查询改写模块。而在 Agent 中,这项工作往往由模型自行完成。这也是为什么很多 Agent 在检索场景下比简单 RAG 效果更好的原因之一。

3.7 ReAct 为什么会是 Agent 的标准模式

今天几乎所有主流 Agent 框架都建立在 ReAct 思想之上。包括:

1
2
3
4
5
LangGraph
LangChain Agent
OpenAI Agent
AutoGen
CrewAI

虽然具体实现不同,但底层逻辑几乎一致。

1
Reason → Act → Observation → Reason

因为 ReAct 解决了 Agent 最核心的问题:让模型能够基于当前状态动态决定下一步行动,而不是按照预先写好的流程执行。 从某种意义上说,Tool Calling 提供了能力入口,而 ReAct 则提供了能力调度机制。有了这两部分之后,一个真正意义上的 Tool Agent 才开始形成。接下来将结合 reAct_tool_calling 里的可运行代码,拆解一个完整 ReAct Agent 的内部结构,看看 Tool 执行节点、Message History 和停止条件是如何协同工作的。

4. 用 Python 手写一个 ReAct Agent

理解了 ReAct 的运行逻辑之后,接下来看看这套循环到底如何落地成代码?02_manual_react_single_tool.py 并没有直接使用现成 Agent,而是先手动实现一个最小可运行的 ReAct Agent。这样做的目的是为了彻底理解 Agent 内部到底发生了什么。

本章对应的代码主要分布在两个文件中:

1
2
common.py
02_manual_react_single_tool.py

common.py 负责准备 Agent 运行所需的基础对象,包括 MessageToolCallTool、旅游知识库、模型封装和工具执行函数。02_manual_react_single_tool.py 则负责把这些对象组合成一个最小 ReAct 循环。

这一章先按代码依赖顺序拆开看

1
本地知识库 → 普通查询函数 → Tool → 带工具的模型 → Message History → ReAct 循环

4.1 从旅游知识库开始

首先在 common.py 中准备了一个很小的中国城市旅行知识库。真实项目里,这里可以来自网页、企业文档、数据库或向量库。这个 demo 为了聚焦 Agent 机制,工具数据直接放在本地列表中;需要联网的是模型调用,而不是旅游资料查询。

代码结构类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TRAVEL_KNOWLEDGE = [
{
"title": "杭州",
"text": (
"杭州适合第一次江南旅行。西湖、灵隐寺、京杭大运河和龙井村可以组成两到三天的轻松路线。"
"城市节奏相对舒缓,适合看湖景、喝茶、散步和短途文化游。"
),
"keywords": {"杭州", "hangzhou", "西湖", "灵隐寺", "龙井", "运河", "江南", "湖景", "茶"},
},
{
"title": "苏州",
"text": (
"苏州以古典园林、平江路、山塘街和博物馆见长。"
"如果想看园林、老城街巷和江南水乡气质,苏州比大城市更适合慢慢走。"
),
"keywords": {"苏州", "suzhou", "园林", "平江路", "山塘街", "博物馆", "水乡", "江南"},
},
]

这里每条资料都包含三个字段:

字段 含义
title 城市名称
text 可返回给模型的事实资料
keywords 用于本地匹配的关键词集合

这一步的核心是准备一个可被工具查询的数据源。后面 Agent 并不会直接读取 TRAVEL_KNOWLEDGE,而是通过查询函数访问它。查询函数是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def search_travel_info(query: str) -> str:
"""Search local China travel notes."""
query_terms = normalize(query)
query_lower = query.lower()
ranked = []
for item in TRAVEL_KNOWLEDGE:
score = sum(
1
for keyword in item["keywords"]
if keyword.lower() in query_terms or keyword.lower() in query_lower
)
if score:
ranked.append((score, item))

if not ranked:
ranked = [(1, item) for item in TRAVEL_KNOWLEDGE[:2]]

ranked.sort(key=lambda pair: pair[0], reverse=True)
return "\n".join(f"- {item['title']}: {item['text']}" for _, item in ranked[:3])

这个函数的输入是模型生成的查询词:

1
"江南 园林 水乡 苏州 杭州"

输出是一段可直接放回上下文的文本:

1
2
- 苏州: 苏州以古典园林、平江路、山塘街和博物馆见长。如果想看园林、老城街巷和江南水乡气质,苏州比大城市更适合慢慢走。
- 杭州: 杭州适合第一次江南旅行。西湖、灵隐寺、京杭大运河和龙井村可以组成两到三天的轻松路线。城市节奏相对舒缓,适合看湖景、喝茶、散步和短途文化游。
1
TRAVEL_KNOWLEDGE → normalize(query) → 关键词匹配 → 返回相关中国城市旅行资料

其中最重要的不是存储方式,而是统一查询接口。真实项目中,这个接口可能是:

1
2
3
4
Chroma
FAISS
Milvus
Pinecone

Agent 真正需要的只是:

1
search_travel_info(query)

这样的可调用能力。因此在 Agent 体系中,经常会看到这样一层抽象:

1
数据源 → 查询函数 → Tool → Agent

知识库负责提供事实。查询函数负责检索。Tool 负责向 Agent 暴露能力。

4.2 把查询函数封装成 Tool

知识库准备完成后,需要把检索能力暴露给模型。最简单的方式就是封装成 Tool。例如:

1
2
3
4
5
6
7
8
TOOLS = {
"search_travel_info": Tool(
name="search_travel_info",
description="Search local travel information about Chinese cities, sights, routes, food, and culture.",
parameters={"query": "string"},
fn=search_travel_info,
)
}

这段代码在 common.py。它做的事情不是执行查询,而是声明一个工具。可以把它拆成四个字段看:

字段 作用
name 模型调用工具时使用的名字
description 告诉模型这个工具适合解决什么问题
parameters 告诉模型调用时需要传哪些参数
fn 真正被程序执行的 Python 函数

底层函数仍然很普通:

1
2
def search_travel_info(query: str) -> str:
...

但被 Tool(...) 包装之后,它就拥有了新的身份。此时模型能够通过工具名称、说明和参数定义感知到这个能力的存在。

这里有一个容易忽略的细节:对于模型来说,真正重要的并不是函数实现,而是工具描述。在 common.py 中,工具描述写在这里:

1
2
3
4
5
6
Tool(
name="search_travel_info",
description="Search local travel information about Chinese cities, sights, routes, food, and culture.",
parameters={"query": "string"},
fn=search_travel_info,
)

模型无法读取函数内部代码。能够看到的只有:

1
2
3
工具名称
工具说明
参数定义

封装成 Tool 后,工具的调用方式也统一了。外部不再直接写:

1
search_travel_info("江南 园林")

而是写:

1
2
3
TOOLS["search_travel_info"].invoke({
"query": "江南 园林"
})

这一步看起来只是多包了一层,但它让所有工具都变成同一种形状:

1
工具名 + 参数dict → 执行结果

后面的 execute_tool_calls() 正是依靠这个统一接口执行工具。

4.3 Tool Schema 背后的工作机制

Tool 定义完成后,还需要注册给模型。在 LangChain 中,常见写法是:

1
2
3
llm_with_tools = llm.bind_tools(
TOOLS
)

在代码里,工具同时提供了两层表示。第一层是 Tool.schema(),用于在 01_tool_calling_schema.py 中直接打印工具描述,帮助观察模型能看到什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
def schema(self) -> dict[str, Any]:
return {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
name: {"type": type_name}
for name, type_name in self.parameters.items()
},
"required": list(self.parameters),
},
}

01_tool_calling_schema.py 会直接打印这个结构:

1
2
3
tool = TOOLS["search_travel_info"]
print("Tool schema sent to the model:")
print(tool.schema())

输出结构大致是:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "search_travel_info",
"description": "Search local travel information about Chinese cities, sights, routes, food, and culture.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
}

这就是模型能够看到的工具说明。注意,这里没有函数体,也没有 TRAVEL_KNOWLEDGE。模型只知道自己可以调用一个叫 search_travel_info 的工具,并且调用时要传入 query

第二层是 Tool.to_langchain_tool(),用于把本地工具转换成 LangChain 可以注册的 StructuredTool

1
2
3
4
5
6
7
8
def to_langchain_tool(self):
from langchain_core.tools import StructuredTool

return StructuredTool.from_function(
func=self.fn,
name=self.name,
description=self.description,
)

然后 LangChainTravelModel 使用 get_llm() 创建模型,并通过 bind_tools() 注册工具:

1
2
3
4
5
6
7
class LangChainTravelModel:
def __init__(self, enabled_tools: list[str]) -> None:
require_api_key()
self.tool_map = {name: TOOLS[name] for name in enabled_tools}
self.llm = get_llm().bind_tools(
[tool.to_langchain_tool() for tool in self.tool_map.values()]
)

这里的输入是工具名列表:

1
enabled_tools=["search_travel_info"]

构造函数先从全局 TOOLS 中取出对应工具:

1
self.tool_map = {name: TOOLS[name] for name in enabled_tools}

再把这些工具转换成 LangChain 工具:

1
[tool.to_langchain_tool() for tool in self.tool_map.values()]

最后绑定到模型上:

1
self.llm = get_llm().bind_tools(...)

所以 bind_tools()是一次 Tool Schema 转换和注册,LangChain 会自动读取函数名称、参数类型、Docstring,然后生成标准 Tool Schema。最终发送给模型的内容更接近:

1
2
3
4
5
6
7
{
"name": "search_travel_info",
"description": "...",
"parameters": {
"query": "string"
}
}

然后模型便知道系统中存在一个可以查询旅游信息的能力。之后用户提问时,模型会自行判断:

1
2
3
是否需要调用
何时调用
如何构造参数

因此,真实框架里的:

1
@tool + bind_tools()

和当前 demo 里的:

1
Tool(...) + tool.to_langchain_tool() + get_llm().bind_tools(...)

本质上都是在完成一次能力注册过程。

4.4 Messages 如何承载 Agent 的运行状态

接下来需要定义 Agent 状态。在 LangGraph 里,常见写法是:

1
2
class AgentState(TypedDict):
messages: ...

common.py 中,为了减少框架概念,直接用 Message 数据类表示消息:

1
2
3
4
5
6
7
@dataclass
class Message:
role: str
content: str
tool_calls: list[ToolCall] = field(default_factory=list)
tool_call_id: str | None = None
tool_name: str | None = None

这个类同时表示三种消息:

role 表示什么 关键字段
user 用户输入 content
assistant 模型输出 contenttool_calls
tool 工具返回结果 contenttool_call_idtool_name

第一次看到时容易产生疑问。为什么状态里只有一个字段?事实上,对于 ReAct Agent 来说,消息历史已经包含了全部上下文。例如:

1
Human → AI → Tool → AI

一次完整执行过程中产生的所有信息都会进入消息列表。例如:

1
2
3
4
5
6
[
Message(role="user", content="我想看江南园林,应该去哪里?"),
Message(role="assistant", content="", tool_calls=[...]),
Message(role="tool", content="检索结果...", tool_name="search_travel_info"),
Message(role="assistant", content="最终回答...")
]

其中:

1
2
3
4
用户问题
工具调用请求
工具返回结果
最终回答

全部都会保留。因此 Agent 的记忆并不是单独维护的变量,而是整条 Message History。这也是现代 Agent 框架普遍采用的设计方式。这也解释了为什么 02_manual_react_single_tool.py 里只需要初始化一条消息:

1
messages = [Message(role="user", content=question)]

后续所有中间结果都会追加到这个列表中,而不是分散保存在多个变量里。

4.5 ToolNode:Agent 执行动作的核心节点

很多人在学习 Tool Calling 时,会误以为模型负责执行工具。实际上并不是。模型只能生成调用请求。真正执行工具的是程序里的工具执行节点。在 LangGraph 中,这个角色通常叫 ToolNode。在 common.py 中,对应的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
def execute_tool_calls(ai_message: Message, tools: dict[str, Tool]) -> list[Message]:
tool_messages: list[Message] = []
for call in ai_message.tool_calls:
result = tools[call.name].invoke(call.args)
tool_messages.append(
Message(
role="tool",
content=str(result),
tool_call_id=call.id,
tool_name=call.name,
)
)
return tool_messages

这个函数的输入是上一轮模型输出的 assistant message:

1
2
3
4
5
6
7
8
9
10
11
Message(
role="assistant",
content="",
tool_calls=[
ToolCall(
name="search_travel_info",
args={"query": "江南 园林 水乡 苏州 杭州"},
id="call_travel_1",
)
],
)

以及当前 Agent 允许使用的工具表:

1
2
3
tools = {
"search_travel_info": TOOLS["search_travel_info"]
}

函数会遍历 tool_calls,根据 call.name 找到对应工具,再把 call.args 传进去:

1
result = tools[call.name].invoke(call.args)

执行完成后,输出不是普通字符串,而是一组 tool 消息:

1
2
3
4
5
6
7
8
[
Message(
role="tool",
content="- 苏州: ...",
tool_call_id="call_travel_1",
tool_name="search_travel_info",
)
]

例如模型返回:

1
2
3
AIMessage(
tool_calls=[...]
)

这个函数会读取:

1
tool_calls

找到对应工具。

1
tool.invoke(args)

执行函数。获得结果后再封装成:

1
ToolMessage(...)

重新放回消息列表。整个过程如下:

1
AIMessage → Tool Call → 工具执行节点 → 执行 Tool → ToolMessage

因此 Agent 的运行过程实际上是一系列消息不断累积的过程。

4.6 LLM Node 是 Agent 的决策中心

与 ToolNode 相对应的是 LLM Node。它的职责只有一个,思考下一步做什么。每次进入 LLM Node 时,模型都会读取当前 Message History。然后做出判断。第一种情况,信息不足,生成 Tool Call。

1
2
3
AIMessage(
tool_calls=[...]
)

第二种情况是信息已经足够,直接生成答案。

1
2
3
AIMessage(
content="..."
)

因此在 ReAct 体系中:

1
2
3
LLM Node = Reason

Tool 执行节点 = Act

这正好对应 ReAct 中最核心的两个环节。在代码中,LLM Node 对应的是:

1
ai_message = model.invoke(messages)

LangChainTravelModel.invoke() 内部还做了一次消息格式转换。因为当前代码中使用自己的 Message 数据类,而 LangChain 模型需要的是 HumanMessageAIMessageToolMessage

1
2
lc_messages = self._to_langchain_messages(messages)
response = self.llm.invoke(lc_messages)

如果模型返回了工具调用,代码会把 LangChain 的 tool_calls 转回本地 ToolCall

1
2
3
4
5
6
7
8
tool_calls = [
ToolCall(
name=call["name"],
args=call.get("args", {}),
id=call.get("id", f"call_{index}"),
)
for index, call in enumerate(getattr(response, "tool_calls", []) or [], start=1)
]

因此 model.invoke(messages) 的输出始终是本地 Message。这样外层 ReAct 循环不需要关心 LangChain 的具体消息类型,只需要判断:

1
2
if ai_message.tool_calls:
...

4.7 tools_condition:驱动 Agent 决策流转的关键机制

到这里还缺少最后一个问题。Agent 如何知道什么时候结束?答案就在一个简单条件判断里:

1
2
3
4
if ai_message.tool_calls:
...
else:
return messages

02_manual_react_single_tool.py 中,这个条件判断写在循环内部:

1
2
3
4
5
6
7
8
while True:
ai_message = model.invoke(messages)
messages.append(ai_message)

if not ai_message.tool_calls:
return messages

messages.extend(execute_tool_calls(ai_message, tools))

这段代码可以按输入输出拆成三步。

1
2
3
4
5
6
7
8
9
# 1. 拿当前消息历史调用模型
ai_message = model.invoke(messages)

# 2. 把模型输出追加到历史中
messages.append(ai_message)

# 3. 检查模型是否还要求调用工具
if not ai_message.tool_calls:
return messages

如果没有工具调用,说明模型已经给出最终答案,Agent 结束。如果还有工具调用,就执行工具,并把工具结果继续追加回 messages

1
messages.extend(execute_tool_calls(ai_message, tools))

真实 LangGraph 中,这个判断通常封装为 tools_condition()。它的逻辑非常简单。

1
2
3
4
if tool_calls:
goto ToolNode
else:
goto END

如果模型生成了 Tool Call。继续执行工具。如果没有生成 Tool Call。说明模型已经获得足够信息。流程结束。因此整个 Agent 图最终变成:

flowchart LR
    LLM1["LLM
Reason"]     TC{"是否存在
Tool Call"}     TOOL["ToolNode
Act"]     LLM2["LLM
Reason"]     END["END
Final Answer"]     LLM1 --> TC     TC -- 是 --> TOOL     TOOL --> LLM2     LLM2 --> TC     TC -- 否 --> END

这张图已经具备了完整 ReAct Agent 的全部能力。到这里一个真正意义上的 Tool Agent 已经构建完成。下一章将结合实际运行过程,逐步跟踪一次完整 Agent 调用,观察 Message、Tool Call、Tool Result 在系统内部是如何流转的。

5. 调试 Agent:看懂一次完整执行过程

前面已经完成了一个最小可运行的 ReAct Agent,它的结构并不复杂:

1
LLM Node → ToolNode → LLM Node

Agent 需要理解的核心是运行时消息是如何流转的,以及模型为什么有时调用工具、有时直接回答。02_manual_react_single_tool.py 接下来通过打印消息列表展示了一次完整执行过程。这一段很关键,因为它把抽象的 ReAct 循环变成了可以观察的实际状态变化。

5.1 Chat Loop 只是外层入口

为了运行 Agent,代码中实现了一个最小循环。完整函数在 02_manual_react_single_tool.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
def run_manual_react(question: str) -> list[Message]:
tools = {"search_travel_info": TOOLS["search_travel_info"]}
model = LangChainTravelModel(enabled_tools=list(tools))
messages = [Message(role="user", content=question)]

while True:
ai_message = model.invoke(messages)
messages.append(ai_message)

if not ai_message.tool_calls:
return messages

messages.extend(execute_tool_calls(ai_message, tools))

输入调用:

1
2
messages = run_manual_react("我想看江南园林,应该去哪里?")  
print_messages(messages)

输出是完整消息历史:

1
2
3
4
5
6
7
8
{message.role}: {message.content}
assistant tool_calls: {calls}
tool {message.tool_name}: {message.content}
assistant tool_calls: {calls}
tool {message.tool_name}: {message.content}
assistant tool_calls: {calls}
tool {message.tool_name}: {message.content}
{message.role}: {message.content}

也就是说,它不会只返回最终回答,而是把中间的模型决策、工具调用和工具结果全部保留下来。而不是只打印最后一条消息。这个循环本身不是 Agent 的核心。它只是完成三件事:

1
2
3
调用模型
执行工具
把工具结果追加回消息列表

真正的 Agent 逻辑就藏在这个循环里。也就是说,外层看起来只是一次普通问答:

1
输入问题 → 输出答案

但内部已经经历了多轮模型调用和工具调用。

5.2 Agent 的首次决策是如何产生

按照上面的输入问题:

1
我想看江南园林,应该去哪里?

也就是让 Agent 推荐适合看江南园林的城市。第一次进入模型节点时,状态中只有一条消息。

1
2
3
4
5
6
[
Message(
role="user",
content="我想看江南园林,应该去哪里?"
)
]

此时模型还没有外部资料。如果是普通 ChatBot,它可能会直接生成答案。但在 Tool Agent 中,模型会先判断是否需要调用工具。因为问题涉及具体旅游信息,模型很可能不会直接回答,而是生成 Tool Call。

5.3 LLM 如何决定调用工具

第一次模型输出通常不是最终答案,而是类似这样的结构:

1
2
3
4
5
6
7
8
9
10
Message(
role="assistant",
tool_calls=[
ToolCall(
name="search_travel_info",
args={"query": "江南园林推荐 苏州扬州 著名园林景点"},
id="call_...",
)
]
)

这说明模型判断当前信息不足,需要查询旅游知识库。模型生成的查询不一定等于用户原始问题。在真实大模型里,它可能一次生成多个 Tool Call。例如:

1
2
3
江南园林推荐 苏州扬州 著名园林景点
苏州古典园林 拙政园 留园 狮子林 网师园
江南园林旅游攻略 最佳路线 最佳季节

在代码,这个查询改写由真实模型产生。LangChainTravelModel.invoke() 会把本地 Message 转成 LangChain 的 HumanMessageAIMessageToolMessage,再调用:

1
response = self.llm.invoke(lc_messages)

如果模型返回 tool_calls,再把它们转换回本地 ToolCall。这体现出 Agent 的一个隐含能力:模型会主动改写查询,提高检索召回率。用户的问题是自然语言,检索工具需要的是更适合搜索的关键词。模型在两者之间做了一次转换。这和 RAG 中常见的 Query Rewrite、Multi Query Retrieval 思路非常接近,只是在 Agent 中,这件事由模型自动完成。

5.4 ToolNode 调用工具并生成 ToolMessage

当模型节点返回 Tool Call 后,循环会检查到:

1
存在 tool_calls

于是流程进入工具执行节点。execute_tool_calls() 会读取上一条 assistant message 中的调用请求。

1
tool_calls = last_message.tool_calls

然后逐个执行对应工具。

1
2
3
search_travel_info(
query="江南园林推荐 苏州扬州 著名园林景点"
)

工具内部会调用本地检索函数。

1
query → normalize(query) → 匹配 TRAVEL_KNOWLEDGE → 返回相关资料

检索结果返回后,并不会直接跳到最终答案。它会被封装成 ToolMessage,继续加入消息历史。

1
2
3
4
5
6
[
Message(role="user"),
Message(role="assistant", tool_calls=[...]),
Message(role="tool"),
Message(role="assistant")
]

这里是理解 Agent 的关键点。工具结果也是消息。 它不是临时变量,也不是隐藏状态,而是作为上下文的一部分交回给模型。

5.5 Agent 如何利用观察结果继续推理

工具执行完成后,流程会再次回到模型节点。此时模型看到的上下文已经变成:

1
2
3
用户问题
模型的工具调用请求
工具返回的检索结果

模型终于拥有了回答问题所需的资料。于是这一次,它通常不会再生成 Tool Call,而是直接生成最终答案。例如:

1
看江南园林,首推苏州。苏州以古典园林、平江路、山塘街和博物馆见长,如果想看园林、老城街巷和江南水乡气质,苏州比大城市更适合慢慢走。

这时返回的是普通 assistant message。

1
2
3
4
Message(
role="assistant",
content="..."
)

没有 tool_calls

5.6 Agent 为什么会自动停止

第二次模型节点执行完成后,循环再次检查 tool_calls。这一次判断结果不同。

1
没有 tool_calls

于是图进入 END。

1
LLM Node → 无 Tool Call → END

这就是 ReAct Agent 的停止机制。并不需要额外写复杂逻辑。只需要判断模型这次输出中是否还有工具调用请求即可。

5.7 Agent 的运行本质是 Message 流转

通过这次调试,可以得到一个非常重要的结论:

1
Agent 的运行过程 = Message 不断累积的过程

从外部看,只是一次问答。从内部看,实际经历了:

1
HumanMessage → AIMessage(tool_calls) → ToolMessage → AIMessage(final answer)

不同类型的消息共同组成上下文。模型每次决策,都不是凭空发生的,而是基于当前完整 Message History。这也是 Agent 状态通常只需要维护 messages 的原因。LangGraph 把这件事封装成状态类型。

5.8 调试 Agent

调试这类 Agent 时,重点在于观察三个点。第一个是模型节点。它决定是否调用工具。第二个是工具执行节点。它负责执行工具并返回结果。第三个是 Message History。它记录了整个决策链路。只要这三部分清晰,Agent 的运行过程就清晰。从工程角度看,这也能帮助定位问题。如果模型没有调用工具,通常要检查:

1
2
3
Prompt 是否明确
Tool 描述是否清楚
用户问题是否触发工具需求

如果工具调用了但结果不好,通常要检查:

1
2
3
参数是否合理
检索质量是否稳定
工具返回内容是否足够

如果最终答案不准确,则需要检查:

1
2
3
ToolMessage 是否包含真实有效信息
模型是否正确引用工具结果
是否混入了模型自己的猜测

这也说明,在 Agent 系统中,最终答案不能只看语言是否流畅,更要看它是否真正来自工具结果。看起来真实不等于真实存在,只有可追踪到工具或数据源的信息,才更适合作为可靠答案。

6. 从单工具 Agent 到多工具 Agent

前面构建的 Agent 已经具备完整的 ReAct 循环能力。但它实际上只有一个工具。

1
search_travel_info()

因此无论用户提出什么问题,模型最终都只能调用旅游知识库。这种架构能够验证 ReAct 原理,却无法体现 Agent 真正的价值。因为 Agent 最重要的能力并不是调用工具,而是:在多个工具之间进行选择和协调。

本章对应的代码03_multi_tool_agent.py,它和 02_manual_react_single_tool.py 的结构几乎一样,变化的是工具集合:

1
2
3
4
5
tools = {
"search_travel_info": TOOLS["search_travel_info"],
"weather_forecast": TOOLS["weather_forecast"],
"check_bnb_availability": TOOLS["check_bnb_availability"],
}

也就是说,多工具 Agent 并没有改 ReAct 循环,而是把可选能力从一个扩展到了三个。

6.1 单工具 Agent 的局限性

假设系统中只有旅游知识库。下面的问题都可以处理:

1
2
3
杭州有哪些景点?
推荐苏州周末路线
哪里适合看江南园林?

因为这些问题都属于知识库覆盖范围。但如果问题变成:

1
苏州明天天气怎么样?

知识库无法回答。即使模型调用了工具,也只能获得旅游信息。这意味着:

1
Agent ≠ 万能

Agent 的能力边界仍然取决于工具集合。模型再聪明,也无法凭空获取不存在的数据。

6.2 增加第二个 Tool

为了展示多工具协作,03_multi_tool_agent.py 增加了天气和住宿两个工具。示例代码类似:

1
2
3
4
5
6
Tool(
name="weather_forecast",
description="Get a mock weather forecast, given a Chinese city name.",
parameters={"town": "string"},
fn=weather_forecast,
)

这个工具负责根据地点返回天气信息。为了避免依赖真实天气接口,代码中还是使用 common.py 里的本地模拟数据。返回内容大致如下:

1
2
3
4
5
{
"town": "苏州",
"weather": "小雨",
"temperature": 22
}

重点在于 Agent 开始拥有多种能力。此时工具集合变成:

1
2
3
4
5
tools = {
"search_travel_info": TOOLS["search_travel_info"],
"weather_forecast": TOOLS["weather_forecast"],
"check_bnb_availability": TOOLS["check_bnb_availability"],
}

系统结构也发生变化。

flowchart TB

    LLM["LLM"]
    Tool1["旅游知识库"]
    Tool2["天气服务"]
    Tool3["住宿查询"]

    LLM --> Tool1
    LLM --> Tool2
    LLM --> Tool3

从这一刻开始,模型需要做选择。完整函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def run_multi_tool_agent(question: str) -> list[Message]:
tools = {
"search_travel_info": TOOLS["search_travel_info"],
"weather_forecast": TOOLS["weather_forecast"],
"check_bnb_availability": TOOLS["check_bnb_availability"],
}
model = LangChainTravelModel(enabled_tools=list(tools))
messages = [Message(role="user", content=question)]

while True:
ai_message = model.invoke(messages)
messages.append(ai_message)

if not ai_message.tool_calls:
return messages

messages.extend(execute_tool_calls(ai_message, tools))

和单工具版本相比,循环部分完全没有变化:

1
2
3
while True:
ai_message = model.invoke(messages)
...

变化只发生在初始化阶段:

1
model = LangChainTravelModel(enabled_tools=list(tools))

这里 enabled_tools 会把三个工具都注册给模型。模型看到的就不再只有 search_travel_info,还包括 weather_forecastcheck_bnb_availability

6.3 Tool Routing 是如何发生的

很多传统系统会使用条件判断实现工具路由。例如:

1
2
3
4
5
if "weather" in query:
weather_tool()

elif "travel" in query:
travel_tool()

这种方式的问题非常明显。随着工具增加,例如地图、酒店、机票等,判断逻辑会越来越复杂。最终变成大量:

1
2
3
4
if...
elif...
elif...
elif...

而 Agent 的思路完全不同。程序不再决定工具路由。模型负责决定。例如:

1
杭州有哪些景点?

模型可能选择:

1
search_travel_info()

而:

1
苏州天气怎么样?

模型则会选择:

1
weather_forecast()

开发时不需要提前编写这些规则。模型会根据工具描述和用户问题自行判断。这也是 Tool Calling 的核心价值之一。

6.4 一个问题调用多个 Tool

真正体现 Agent 能力的场景,是一个问题同时涉及多个领域。例如:

1
2
3
推荐一个适合看江南园林的城市
查一下苏州天气
帮我找一个民宿房间

此时模型可能先执行:

1
search_travel_info()

获得旅游信息。然后继续执行:

1
weather_forecast()

获得天气数据。然后还可以继续执行:

1
check_bnb_availability()

获得住宿数据。最后将这些内容综合。

1
景点推荐 + 天气情况 + 住宿选项 + 出行建议

生成最终回答。此时执行路径已经不再固定,这也是 ReAct 真正发挥作用的地方。在 03_multi_tool_agent.py 中,示例问题:

1
question = "推荐一个适合看江南园林的城市,查一下苏州天气,并找一个民宿房间。"

这句话同时包含三个需求:

需求 适合的工具
推荐江南园林城市 search_travel_info
查询苏州天气 weather_forecast
找民宿房间 check_bnb_availability

因此模型可能一次返回多个 Tool Call:

1
2
3
4
5
6
7
8
Message(
role="assistant",
tool_calls=[
ToolCall(name="search_travel_info", args={"query": "江南 园林 水乡 苏州 杭州"}, id="call_1"),
ToolCall(name="weather_forecast", args={"town": "苏州"}, id="call_2"),
ToolCall(name="check_bnb_availability", args={"town": "苏州", "rooms": 1}, id="call_3"),
],
)

随后 execute_tool_calls() 会逐个执行这些调用,并生成三条 tool 消息:

1
2
3
4
5
[
Message(role="tool", tool_name="search_travel_info", content="..."),
Message(role="tool", tool_name="weather_forecast", content="..."),
Message(role="tool", tool_name="check_bnb_availability", content="..."),
]

第二次进入模型节点时,模型看到的是用户问题加上三份工具观察结果,于是可以综合生成最终答案。

6.5 从单一能力到能力编排

只有一个 Tool 时,其实很难感受到 Agent 的优势。因为模型根本不存在选择空间。调用即可。但当工具数量不断增加时,情况开始发生变化。如果继续采用传统 Workflow。每增加一个能力,都要修改流程图和分支逻辑。而 Agent 架构下:

1
新增 Tool  → 注册 Tool → 更新 Prompt

即可。系统整体结构几乎不需要变化。因此 Agent 最大的优势并不是单个工具能力,而是:随着工具数量增长,复杂度增长速度远低于传统流程系统。

6.6 Tool 不决定答案,决定信息来源

很多人刚接触 Agent 时容易产生一种误解。认为 Tool 越多,模型就越聪明。实际上 Tool 并不会增强推理能力。Tool 只是提供信息来源。真正负责理解和组织这些内容的仍然是模型。因此可以把两者理解为:

1
2
3
4
5
Tool
负责获取事实

LLM
负责解释事实

如果 Tool 返回的数据有问题,模型很难纠正。如果 Tool 返回的是虚构数据,那么最终答案也会建立在错误基础上。因此在 Agent 系统中,工具质量往往比 Prompt 更重要。

6.7 从 Tool Agent 到 Multi-Agent

当工具数量继续增加时,又会出现新的问题。假设系统拥有:

1
2
3
20 个 Tool
30 个 Tool
50 个 Tool

此时单个 Agent 的 Prompt 会越来越长。工具选择也会越来越困难。这时开始出现新的架构方向:

1
Supervisor Agent + Specialized Agents

即:

1
总控 Agent → 多个专业 Agent → 各自管理工具

例如:

1
2
3
4
旅游 Agent
天气 Agent
地图 Agent
酒店 Agent

每个 Agent 只负责自己的领域。而总控 Agent 负责协调。这也是后续 Multi-Agent 架构产生的原因。从这里开始,讨论重点将不再是如何调用 Tool,而是如何组织越来越复杂的 Agent 系统。下一章将回到工程实践层面,看看为什么在实际开发中很少反复手写 ReAct 循环,而更多使用 create_react_agent() 这类封装快速构建标准 Agent。

7. create_react_agent:从原理验证走向实践

前面用了大量篇幅手动搭建 ReAct Agent。从 Tool 定义开始,到模型节点、工具执行节点、条件判断,再到完整循环,最终实现了一个能够自主调用工具的 Agent。这个过程对于理解原理非常重要。但如果把这些代码直接带到实际项目中,会发现有大量重复代码,因为绝大部分 ReAct Agent 的底层结构几乎完全一样。

本章对应的代码是04_create_react_agent_style.py,用来说明真实封装到底隐藏了什么。

7.1 为什么实际项目很少手写 ReAct 循环

回顾前面的实现过程。需要定义:

1
2
3
4
5
6
7
8
Message

model.invoke(messages)

execute_tool_calls()

if ai_message.tool_calls:
...

整个流程加起来已经接近几十行代码。而且无论做什么 Agent,底层结构都高度相似。其核心运行逻辑都是:

1
LLM → Tool → LLM → END

区别仅仅在于:

1
2
使用什么工具
Prompt 怎么写

因此真实框架通常会把这种固定模式封装起来。

7.2 create_react_agent 封装了哪些能力

LangGraph 提供了一个预构建 Agent。

1
2
3
from langgraph.prebuilt import (
create_react_agent
)

创建方式非常简单。

1
2
3
4
agent = create_react_agent(
llm,
tools=TOOLS
)

看起来只有一行代码。但内部已经自动完成:

1
2
3
4
5
StateGraph
ToolNode
Message State
Conditional Edge
ReAct Loop

全部构建过程。在 04_create_react_agent_style.py 中,也实现了一个简单封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def create_react_agent(model: LangChainTravelModel, tools: list[Tool]) -> Callable[[str], list[Message]]:
tool_map = {tool.name: tool for tool in tools}

def agent(question: str) -> list[Message]:
messages = [Message(role="user", content=question)]
while True:
ai_message = model.invoke(messages)
messages.append(ai_message)

if not ai_message.tool_calls:
return messages

messages.extend(execute_tool_calls(ai_message, tool_map))

return agent

这段代码没有实现完整 LangGraph,只是把前面手写的固定 ReAct 循环包成函数。它的输入是两个对象:

输入 含义
model 已经绑定好工具的模型
tools 当前 Agent 允许调用的工具列表

输出不是一次执行结果,而是一个可复用的 agent 函数:

1
agent = create_react_agent(model, tools)

之后调用时只需要传问题:

1
messages = agent("我想去一个有历史感的中国城市旅行,再查一下西安天气。")

这样,使用者不需要每次都写:

1
2
3
messages = [Message(role="user", content=question)]
while True:
...

换句话说,前面亲手搭建的 Agent 最终可以浓缩成:

1
create_react_agent(...)

这里有一个关键点:create_react_agent() 封装的是“标准循环”,不是“业务逻辑”。业务差异仍然来自:

1
2
3
传入什么模型
传入什么工具
写什么 Prompt

7.3 create_react_agent 背后的运行结构

如果把内部结构展开,本质上仍然是:

flowchart LR

    LLM["LLM"]

    DECISION{"是否存在
Tool Call"} TOOL["Tool 执行节点
ToolNode"] END_NODE(["END"]) LLM_NEXT["LLM"] LLM --> DECISION DECISION -- 是 --> TOOL DECISION -- 否 --> END_NODE TOOL --> LLM_NEXT

也就是说:

1
create_react_agent()

并没有发明新的 Agent。只是把标准 ReAct Agent 预先封装好了。因此:

1
create_react_agent = 标准 ReAct Agent 模板

理解这一点就可以知道,这只是把前面已经实现过的结构隐藏起来。这样就不用每次都创建工具表、模型和消息列表,运行时只剩下用户问题:

1
messages = agent("我想去一个有历史感的中国城市旅行,再查一下西安天气。")

这就是工程封装的意义:不是改变 Agent 原理,而是减少重复样板代码,让开发者把注意力放回工具设计和 Prompt 设计。

7.4 Prompt 如何影响 Agent 的行为决策

前面几章讨论的是 Graph 如何搭建、节点如何连接、状态如何传递,而在实际项目里,真正影响 Agent 效果的往往不再是 Graph,而是 Prompt。例如:

1
2
3
SYSTEM_PROMPT = """
You are a travel assistant...
"""

对于 Agent 来说。Prompt 决定:

1
2
3
4
职责范围
回答风格
工具使用原则
任务边界

甚至会影响:

1
2
3
是否调用工具
调用哪个工具
调用次数

随着 Agent 架构逐渐成熟,关注重点开始从 Graph 转向 Prompt。

1
2
3
Tool 决定能力边界

Prompt 决定能力发挥

工具负责提供外部能力。Prompt 负责引导模型如何使用这些能力。

7.5 Tool Selection 本质上是语义路由

拥有多个 Tool 后。Agent 的核心工作逐渐变成Tool Selection,即选择哪个工具,例如:

  • 杭州有哪些景点? 模型会倾向于调用:search_travel_info()
  • 苏州明天气温多少? 则会调用:weather_forecast()

这里并不存在硬编码规则。模型依靠:

1
2
3
用户问题
Tool 描述
Prompt

三部分信息完成判断。从本质上看,Agent 实际上是在进行语义路由。

7.6 create_react_agent 的适用边界

虽然 create_react_agent() 十分方便。但它并不适合所有场景。标准 ReAct Agent 最适合:

1
2
3
单 Agent
少量 Tool
标准 ReAct 循环

例如:

1
2
3
客服助手
知识库助手
代码助手

这些应用通常直接使用即可。但当系统开始出现复杂状态、复杂路由、长流程任务、多 Agent 协作时,就需要重新回到 LangGraph。自己构建图结构。因为这些场景已经超出了标准 ReAct Agent 的能力范围。

7.7 从 Agent 到 Agent System

到了这里,本文其实已经完成了从 Workflow 到 Tool Agent 的演进,整个过程可以总结为:

1
Workflow → Tool Calling → ReAct → Tool Agent → create_react_agent

其中最大的变化不是增加了多少工具,而是流程控制权发生了转移。

1
2
3
Workflow = 程序控制流程

Agent = 模型控制流程

当系统规模继续扩大时,新的问题来了:

1
2
3
4
5
一个 Agent 能管理多少工具?

一个 Agent 能处理多复杂的任务?

多个 Agent 如何协作?

这些问题已经超出了单 Agent 范畴,也正是后续 Multi-Agent、Supervisor Agent 等架构要解决的问题。

8. 总结:Agent 本质上是一种动态工作流

从表面上看,Agent 像是在 Workflow 上增加了工具调用能力。但从架构角度看,两者最大的区别并不在工具,而在于决策机制。Workflow 中,开发者决定流程,而 Agent 中,模型决定流程。

Tool Calling 让模型能够访问外部能力。ReAct 让模型能够动态规划执行路径。LangGraph 则提供了组织这些能力的运行框架。最终形成:

1
Reason → Act → Observation → Reason

这一套标准循环,也是现代 Agent 系统几乎共同遵循的基础模式。无论是 LangGraph、OpenAI Agents、CrewAI 还是 AutoGen,其核心思想都没有脱离这一框架。理解了这一点,后续无论面对单 Agent、Multi-Agent 还是复杂 Agent Team,本质上都只是这套机制在不同规模下的扩展与组合。

9.备注

完整代码:https://github.com/keychankc/AI_agent_code/tree/main/reAct_tool_calling