从LLM到Agent:ReAct推理模式的底层原理
你用过ChatGPT,但你有没有想过:为什么它能回答"今天天气怎么样",却没法真的帮你查天气?这篇文章从零开始,带你理解"聊天机器人"和"智能助手"之间的那道坎------ReAct推理循环------到底是怎么回事。
一、LLM到底是什么?为什么它"四肢瘫痪"?
先从最基本的概念说起。LLM(大语言模型)本质上是一个数学函数:
输入文本 → 预测下一个字 → 拼回去 → 输出文本
它做的事就是"根据前面的文字,猜下一个字"。训练了海量数据后,这个"猜"变得极其精准,精准到看起来像是"理解"了问题、"思考"出了答案。但它实际上一次都没有离开过文字的世界。
所以LLM有个致命缺陷:它只能输出文字,不能和外界交互。你问它"今天天气怎么样",它能输出一段看起来很合理的文字,但那不是今天的天气,是它训练数据里见过的"天气预报示例"。
要解决这个问题,需要给LLM装上"手和脚"------让它能够调用外部的工具。比如:
- 一个"查天气"的工具
- 一个"发邮件"的工具
- 一个"搜索文件"的工具
LLM需要自己决定"什么时候调用哪个工具"。这就引出了ReAct。
二、ReAct:LLM的"思考-行动"循环
2.1 问题很简单:LLM怎么知道自己该调工具?
直觉上,我们可以写一个"规则引擎":如果用户说"天气",就调天气工具;如果用户说"计算",就调计算器。
但实际用户说的话千变万化。"这两天出门要不要带伞"------你看到"伞"会想到天气,但关键词匹配很难枚举所有可能。我们需要LLM自己理解 用户意图,自己决定调哪个工具。
这就是ReAct要解决的问题。
2.2 ReAct的核心思想
ReAct这个名字来自两个词的组合:
- Reasoning(推理):分析当前情况,想清楚该做什么
- Acting(行动):真正去执行
它不是一步到位,而是一个循环:
第1步:看用户问题 → 思考 → 判断需要查天气 → 调用天气工具
第2步:拿到天气结果 → 思考 → 判断信息够了 → 输出回答
这个循环看似简单,但它实现了一个很关键的能力:LLM不需要第一次就答对,它可以一步步来,先获取信息,再基于信息回答。
2.3 用大白话解释ReAct的每一步
假设用户问:"明天北京天气怎么样?适合户外运动吗?"
没有ReAct的LLM(纯文字模式):
→ 直接回答:"明天北京晴转多云,气温15-25度,适合户外运动。"
问题是:这个答案是编的!LLM根本不知道明天什么天气,它只是根据训练数据"猜"了一个合理的回答。
有ReAct的Agent:
第1轮:
用户:"明天北京天气怎么样?适合户外运动吗?"
LLM思考:我不知道明天天气,需要查一下。
LLM输出:{调用工具: getWeather, 参数: city="北京", date="明天"}
第2轮:
工具返回:{天气: "晴", 温度: "18-26度", 风力: "2级"}
LLM思考:天气晴朗,温度舒适,风力小。
LLM输出:"明天北京晴天,18到26度,风力2级,非常适合户外运动!"
LLM不再需要"编造"答案,因为它拿到了真实数据。
三、Function Calling:ReAct的工程落地
3.1 这不是"回调函数",这是"消息拼接"
很多初学者以为工具调用就是"LLM回调一个Java方法"。不是的。底层发生的事情更简单(也更精妙):
LLM的输入是一串消息。输出也是一串消息。工具的加入,不过是在这串消息中插入了新的消息。
整个对话的消息序列是这样的:
消息1 [system角色]: "你是智能助手,以下工具可用:{天气查询, 计算器, 文件搜索...}"
消息2 [user角色]: "明天北京天气怎么样?"
消息3 [assistant角色]: "{我想调天气工具} {工具名: getWeather, 参数: city=北京}" ← LLM输出
消息4 [tool角色]: "{天气: 晴, 18-26度, 风力2级}" ← 工具执行结果,插入到消息序列中
消息5 [assistant角色]: "明天北京晴天,18-26度,适合户外运动!" ← LLM基于消息1-4重新推理
关键点:工具调用结果不是"回调"给LLM的,是追加到消息历史里的。LLM看到上下文里多了一条消息(消息4),自然地基于此继续推理。
3.2 这为什么精妙?
因为LLM根本不需要任何架构改造。它还是做它最擅长的事------给定一串消息,预测下一条消息。多出来的工具结果只是消息序列中的一个新条目,对LLM来说和用户发的消息没有本质区别。
这个设计让ReAct的实现极其简单:不需要修改LLM,只需要在被调用的工具返回后,把结果塞进消息序列,然后让LLM再跑一次。
3.3 那LLM怎么知道有哪些工具可用?
每个工具都有一个"说明书"(JSON Schema),描述了它的名字、作用、参数。
比如天气工具的说明书:
json
{
"name": "get_weather",
"description": "查询指定城市明天的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
}
}
}
所有工具的说明书都放在System Prompt(消息1)里。LLM读取后,就知道有哪些工具、每个工具要什么参数。当它判断需要使用某个工具时,它输出的不是文字,而是一个结构化的JSON对象,指出了工具名和参数值。
四、System Prompt:Agent行为的"控制面板"
4.1 同样的工具,不同的行为
假设Agent有三个工具:查天气、计算器、搜索文件。
用户说:"帮我算3.14 × 256"。LLM可以:
- 选项A:直接心算"大概是800左右"
- 选项B:调用计算器,返回准确的"803.84"
选A还是选B,取决于概率。而System Prompt的作用,就是改变这个概率------让"正确的选项"变得更可能。
4.2 一个具体的例子
对比两种System Prompt:
Prompt A(模糊版):
你是智能助手,可以使用以下工具:get_weather, calculate, search_file。
Prompt B(精确版):
你是智能助手,严格遵守以下规则:
1. 任何数学计算必须调用calculate工具,禁止心算
2. 任何天气查询必须调用get_weather工具,禁止猜测
3. 任何文件操作必须调用search_file工具
4. 调用了工具后,只回复工具返回的精确结果,不要补充或修改
面对同样的用户输入"3.14 × 256":
- Prompt A下,LLM有约40%概率心算(因为心算"更方便")
- Prompt B下,LLM几乎100%调用计算器(因为被明确禁止心算)
这就是为什么写Prompt不是"锦上添花",而是Agent开发的核心工作。代码搭建了工具(手脚),Prompt决定了Agent怎么选择使用这些工具。
五、Single Agent vs Multi Agent:架构的分岔路口
理解了ReAct和Function Calling后,你会面临一个选择:所有工具放一个Agent,还是拆成多个专业Agent?
5.1 Single Agent
一个Agent,看到所有工具,自己决定调哪个。
优势是简单直接。只要工具数量不超过10个,这个方案工作得很好。
问题是:工具多了之后,LLM会"选择困难"。就像你面前摆了30个工具箱,每次要花很长时间才能找到对的------有时还会找错。
5.2 Multi Agent
一个"调度员" + 多个"专家"
调度员分析用户问题 → 决定派哪个专家 → 专家出结果 → 调度员汇总回答
每个专家只看到自己领域内的工具,选择准确率高。而且职责隔离------"搜索专家"根本不知道有"删除文件"这个工具,绝不可能误删文件。
5.3 如何选择?
一个经验法则:工具<10个用Single Agent;工具>15个、或者需要职责隔离(比如有些操作很危险),用Multi Agent。
我们的项目支持两种模式,通过一个配置项切换,方便你对比不同模式下的Agent行为。
六、总结
让我们串联一下今天学到的内容:
- LLM的局限:只能输出文字,不能和外界交互
- ReAct循环:思考→行动→观察→再思考,让LLM逐步逼近正确答案
- Function Calling:通过把工具结果追加到消息序列中,让LLM自然地基于真实数据推理
- System Prompt:控制Agent行为的"概率调节器",改变工具选择的准确性
- 架构选择:工具少用Single Agent,工具多用Multi Agent
理解了这些,你就理解了所有Agent框架的基石。后面几天的Skill系统、MCP协议、多Agent编排,都是在这个地基上搭建的更高层建筑------它们的本质都是"给LLM装更好的手和脚,让它更精确地判断什么时候用哪个"。
项目链接
项目代码:day6-agent