揭秘AI Agent开发的底层逻辑:ReAct思想的魅力与实践(附智能客服完整案例代码)
1、ReAct Agent基本理论【AI大模型教程】
ReAct Agent,也称为ReAct框架,是一个用于提示大语言模型(LLM)的创新方法。它首次出现在2022年10月的论文《ReAct:Synergizing Reasoning and Acting in Language Models》(arxiv.org/pdf/2210.03…) 中,并于2023年3月进行了修订。该框架旨在将大语言模型中的推理(Reasoning)和行动(Acting)有机结合,从而使AI代理(Agent)变得更加强大、通用且易于解释。通过交替进行推理和行动,ReAct允许智能体动态地在生成想法和执行任务特定行动之间切换,这类似于人类在解决问题时如何思考并采取实际步骤。
ReAct框架的核心在于两个过程的融合:Reason(推理)和Act(行动)。这种方法受到了人类认知过程的启发——我们常常在思考问题时会结合实际行动来验证想法,而不是单纯依赖于内部推理。例如,当我们计划旅行时,我们不仅会脑补路线,还会实际查询地图或天气来调整计划。ReAct正是将这种“思考-行动”循环应用到AI代理中,帮助它们处理复杂任务。
Reason部分:基于思想链(CoT)的推理
ReAct的Reason部分建立在一种名为**思想链(Chain of Thought, CoT)**的提示工程技术之上。CoT最早在2022年的论文《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》(arxiv.org/pdf/2201.11…
CoT的核心优势包括:
- 分解问题:将大问题拆分成小步骤,每个步骤聚焦于一个子问题,便于模型处理。
- 顺序思维:每个步骤基于前一步的结果,形成一条连续的推理链,确保逻辑连贯。
让我们通过一个简单例子来理解CoT的应用。假设问题是:“如果一个盒子里有3个红球和2个蓝球,你随机抽取一个球,然后不放回再抽取一个球,抽到两个红球的概率是多少?”
- 步骤1:计算第一次抽到红球的概率。盒子里总共有5个球,其中3个是红球,所以概率是3/5。
- 步骤2:假设第一次抽到红球,现在盒子里剩下4个球,其中红球剩2个。所以第二次抽到红球的概率是2/4 = 1/2。
- 步骤3:两个事件是连续的,所以总概率是第一次概率乘以第二次概率:(3/5) × (1/2) = 3/10。
- 结论:抽到两个红球的概率是3/10,或者说30%。
通过这种逐步分解,CoT帮助模型避免跳跃式错误,并提供可追踪的推理路径。然而,CoT并非完美。经过实际使用,人们发现它容易产生“幻觉”(hallucinations),即模型在中间步骤生成不准确的信息,或者错误在链条中传播。例如,在处理实时数据或外部知识时,模型可能基于过时或虚构的信息推理,导致最终结果偏差。
引入Act:弥补CoT的局限性
为了解决CoT的这些问题,Google DeepMind团队开发了ReAct框架。它引入了思考-行动-观察(Thought-Action-Observation)循环,允许代理不仅仅停留在内部推理,还能通过行动与外部环境互动,从而验证和修正想法。这个迭代过程使代理能够根据行动结果动态调整策略,直到任务完成。
ReAct的流程可以简单描述为:
- Question:用户提出的任务或问题,作为起点。
- Thought:代理的推理步骤,用于规划下一步行动或调整计划。
- Action:代理执行的具体行动,通常涉及调用外部工具(如API、搜索引擎)。
- Action Input:行动的具体参数,例如查询关键词。
- Observation:行动执行后的结果反馈,用于更新代理的知识。
- 该循环重复进行,直到代理得出最终答案。
以下是ReAct流程的示意图:
在这个循环中,代理像一个“思考者+执行者”:它先思考(Thought),然后行动(Action),观察结果(Observation),并据此完善下一次思考。这种机制大大提升了代理的鲁棒性和准确性,尤其在需要外部知识的任务中。
ReAct代理的基本流程示例:
基于ReAct思想构建的代理工程,通常通过精心设计的提示词来实现。以下是一个基础提示词示例,展示了如何指导大语言模型在循环中运行:
您在一个由“Thought、Action、Observation、Answer”组成的循环中运行。在循环的最后,您输出一个答案。使用“Thought”来描述您对问题的思考。使用“Action”来执行您可用的动作之一。“Observation”将是执行这些动作的结果。“Answer”将是分析“Observation”结果后得出的最终答案。您可用的动作包括:calculate(计算):例如:calculate: 4 * 7 / 3执行计算并返回数字 - 使用Python,如有必要请确保使用浮点数语法。wikipedia(维基百科):例如:wikipedia: Django返回从维基百科搜索的摘要。如果有机会,请始终在维基百科上查找信息。示例会话:Question:法国的首都是什么?Thought:我应该在维基百科上查找关于法国的信息。Action:wikipedia: FranceObservation:法国(正式名称:法兰西共和国)是一个位于西欧的主权国家。其首都为巴黎,是法国最大的城市和主要的文化、经济中心。Thought:现在我知道答案了。Answer:法国的首都是巴黎。
在这个示例中,ReAct框架明确定义了代理的任务执行过程。面对不同场景,我们只需调整两点:1. 代理的身份设定,通常通过System Prompt来定义(如“您是一个数学助手”);2. 代理所需的工具,通过大模型的Function Calling功能来集成外部API或工具。这些工具的使用方法需要根据任务需求进行调整,例如添加搜索引擎、数据库查询等。
通过这种方式,ReAct不仅提升了代理的性能,还使其响应更具可解释性——用户可以追踪每个Thought和Observation,理解代理的决策过程。
2、构建 ReAct Agent 流程
要实现一个功能完整的ReAct Agent,我们需要遵循一个清晰的步骤框架。这不仅仅是理论上的循环,还涉及实际的代码实现和工具集成。总体来说,有三项核心工作:
- 设计代理的完整提示词:这包括在System Prompt中设定代理的角色、行为规范和ReAct循环规则,确保代理的行为与任务一致。
- 处理用户输入:将用户的问题作为动态变量注入提示词中,使代理能实时响应具体需求。
- 构建和整合工具:定义代理可用的外部工具(如API调用),并将它们嵌入提示词中,以便在Action阶段调用。
下面,我们详细说明如何用Python实现一个基础但完整的ReAct Agent流程。我们假设使用OpenAI的API(如GPT模型)作为大语言模型后端,因为它支持Function Calling功能。整个流程可以分为准备阶段、循环执行阶段和终止阶段。
步骤1:准备代理的System Prompt和工具定义
首先,定义System Prompt,这是ReAct Agent的“大脑”。它需要包含ReAct循环的格式、可用工具的描述,以及代理的角色。例如,如果我们构建一个通用查询代理,Prompt可以这样设计:
system_prompt = """您是一个智能代理,使用ReAct框架(Thought-Action-Observation循环)来解决问题。始终遵循这个格式:Thought: 您的推理过程。Action: 要执行的行动(从可用工具中选择)。Action Input: 行动的具体输入。Observation: (由系统提供)行动结果。重复循环直到您有足够信息输出最终答案。最终使用:Answer: [最终答案]可用工具:- search: 使用搜索引擎查询信息。输入:查询字符串。- calculate: 执行数学计算。输入:Python表达式,如 '4 * 5'。"""
同时,定义工具函数。这些函数对应Action阶段的实际执行。例如:
def search(query): # 模拟搜索引擎调用(实际中可集成Google API或类似) return f"搜索结果 for '{query}': 示例信息..."def calculate(expression): try: return eval(expression) # 注意安全:在生产环境中使用安全执行器 except: return "计算错误"
工具定义后,需要将工具名称和描述注入Prompt中,以指导模型调用。
步骤2:实现ReAct循环
使用一个循环来模拟Thought-Action-Observation过程。每次迭代:
- 将当前状态(包括用户问题和历史记录)发送给LLM。
- 解析LLM响应,提取Thought、Action和Action Input。
- 执行Action,获取Observation。
- 将Observation追加到历史中,继续循环。
- 当检测到“Answer”时,终止循环。
以下是Python代码示例(假设使用openai库):
import openaiopenai.api_key = "your-api-key"def run_react_agent(user_question): history = [] # 保存Thought/Action/Observation历史 prompt = system_prompt + f"\nQuestion: {user_question}\nThought:" while True: response = openai.ChatCompletion.create( model="gpt-4", messages=[{"role": "system", "content": prompt + "\n".join(history)}] ).choices[0].message.content # 解析响应(假设响应格式严格) if "Answer:" in response: final_answer = response.split("Answer:")[1].strip() return final_answer # 提取Thought, Action, Action Input thought = response.split("Action:")[0].replace("Thought:", "").strip() action = response.split("Action:")[1].split("Action Input:")[0].strip() action_input = response.split("Action Input:")[1].strip() # 执行行动 if action == "search": observation = search(action_input) elif action == "calculate": observation = calculate(action_input) else: observation = "未知行动" # 更新历史 history.append(f"Thought: {thought}") history.append(f"Action: {action}") history.append(f"Action Input: {action_input}") history.append(f"Observation: {observation}")# 示例使用print(run_react_agent("法国首都是什么?"))
在这个循环中,代理会持续迭代直到得出答案。注意,在实际开发中,需要处理解析错误和添加更多工具支持。
步骤3:测试和优化
运行代理后,测试不同场景。例如,对于数学问题,代理可能直接使用calculate工具;对于事实查询,则调用search。优化点包括:
- 错误处理:如果Observation无效,代理可在下次Thought中调整。
- 工具扩展:添加更多工具,如数据库查询或文件读取。
- 性能监控:限制循环次数,避免无限循环。
就目前的AI Agent现状而言,流行的代理框架都有内置的ReAct变体,比如LangChain、LlamaIndex中的代理,或者新兴的CrewAI框架。这些框架简化了实现过程,让开发者无需从零构建循环。
例如,LangChain内置的的ReAct代理提示工程描述如下:
Answer the following questions as best you can. You have access to the following tools:{tools}Use the following format:Question: the input question you must answerThought: you should always think about what to doAction: the action to take, should be one of [{tool_names}]Action Input: the input to the actionObservation: the result of the action... (this Thought/Action/Action Input/Observation can repeat N times)Thought: I now know the final answerFinal Answer: the final answer to the original input questionBegin!Question: {input}Thought:{agent_scratchpad}
这个提示中有三个占位符{tools}、{input}和{agent_scratchpad},在发送给LLM前会被替换:
- {tools}:工具描述。
- {tool_names}:工具名称列表。
- {input}:用户原始问题。
- {agent_scratchpad}:历史记录,用于维护上下文。
因此,基于ReAct的代理工程并非一成不变,其调用的工具也不局限于单一类型。这种灵活性为AI Agent执行复杂任务开启了无限可能性,例如集成自定义API或多代理协作。初级开发者可以从LangChain起步,快速构建原型,然后逐步自定义ReAct逻辑。
3、基于ReAct Agent 实现智能客服完整案例
在了解了ReAct的基本理论和构建流程后,我们来通过一个实际案例来加深理解。本节将介绍一个基于ReAct思想实现的电商智能客服代理项目。该案例展示如何将ReAct框架应用到真实场景中,帮助处理用户咨询、查询商品、检索优惠并计算价格。整个案例使用Python实现,结合了数据库、文件读取和计算工具,适合作为入门实践项目。
项目概述
本项目构建了一个电商客服智能代理(AI Agent),专注于体育用品商店的客户服务。它利用ReAct框架,让代理能够通过推理和行动循环处理用户查询,例如检查商品库存、查询促销政策并计算最终价格。代理支持多轮对话,维护上下文记忆,确保在复杂交互中保持一致性。
核心功能包括:
- 产品查询:使用
query_by_product_name工具从SQLite数据库中检索商品信息,如价格、库存等。 - 优惠政策检索:通过
read_store_promotions工具读取促销政策文本文件(例如store_promotions.txt),提取与指定商品相关的优惠细节。 - 价格计算:借助
calculate工具,根据商品原价和优惠规则计算最终交易价格,支持简单数学表达式。 - 多轮对话支持:代理维护对话历史(messages),允许用户在后续查询中引用先前信息,例如“刚才那个足球的优惠还适用吗?”。
这个项目假设了一个小型电商场景:数据库存储商品数据,促销文件记录临时优惠。代理使用大语言模型(如OpenAI的GPT或本地Ollama模型)作为推理引擎。通过ReAct循环,代理能动态决策,避免纯CoT的幻觉问题。
ReAct 流程详解:
ReAct框架的核心是“思想-行动-观察”循环,特别适合如客服这种需要外部交互的任务。
核心思想:
- Thought:代理分析上下文和用户问题,规划下一步(例如“先查商品,再查优惠”)。
- Action:指定工具和参数(例如“query_by_product_name: 足球”)。
- Observation:工具返回真实数据,注入上下文。
- Repeat or Answer:如果信息不足,继续循环;否则输出最终答案。
在项目中的实现:
- 用户输入添加到messages列表。
- 模型基于系统提示生成响应(包含Thought和Action)。
- 主程序解析Action,执行工具,获取Observation。
- Observation追加到messages,继续调用模型。
- 检测到Answer时,结束循环,返回用户友好响应。
这种设计使Agent运行比较稳健,而且有最大循环次数限制,避免ReAct陷入无限循环。而且,如果工具失败,Agent也可在下轮Thought中调整(例如重试或告知用户)等。
关键代码模块解析:
项目代码结构简单明了,主要文件包括agent.py(代理核心)、tools/目录(工具函数)、main.py(入口程序)、config.json(配置)和.env(环境变量)。下面逐一介绍。
1. CustomerServiceAgent 类
位于agent.py文件中,这是ReAct代理的核心实现类。主要职责是模拟一个智能客服助手,能够根据用户的提问提供及时、准确的回答。它支持两种类型的回答模式:直接回答:当问题与具体产品无关时,直接生成回复。基于工具链的回答:当涉及具体产品信息时,通过一系列分析步骤(如查询数据库、读取促销政策、计算价格等)生成最终答案。
主要职责:
- 管理对话历史:使用
self.messages列表存储系统提示、用户输入、Thought、Action、Observation等,确保上下文连贯。 - call方法:使类实例可以像函数一样被调用,主要功能是调用execute方法生成答复。
- execute方法:根据用户输入生成回复,支持两种语言模型(Ollama 和 OpenAI),调用大语言模型,并提取大语言模型的答复内容。
- 模型集成:支持OpenAI或Ollama,通过配置加载模型。
关键代码如下:主要system的prompt构成了ReAct的整体功能框架(这个prompt很重要):
class CustomerServiceAgent: def __init__(self, client, config): self.client = client self.config = config self.messages = [] self.system_prompt = """ You are a Intelligent customer service assistant for e-commerce platform. It is necessary to answer the user's consultation about the product in a timely manner. If it has nothing to do with the specific product, you can answer it directly. output it as Answer: [Your answer here]. Example : Answer: Is there anything else I can help you with If specific information about the product is involved, You run in a loop of Thought, Action, Observation. Use Thought to describe your analysis process. Use Action to run one of the available tools - then wait for an Observation. When you have a final answer, output it as Answer: [Your answer here]. Available tools: 1. query_by_product_name: Query the database to retrieve a list of products that match or contain the specified product name. This function can be used to assist customers in finding products by name via an online platform or customer support interface 2. read_store_promotions: Read the store's promotion document to find specific promotions related to the provided product name. This function scans a text document for any promotional entries that include the product name. 3. calculate: Calculate the final transaction price by combining the selling price and preferential information of the product When using an Action, always format it as: Action: tool_name: argument1, argument2, ... Example : Human: Do you sell football in your shop? If you sell soccer balls, what are the preferential policies now? If I buy it now, how much will I get in the end? Thought: To answer this question, I need to check the database of the background first. Action: query_by_product_name: football Observation: At present, I have checked that the ball is in stock, and I know its price is 120 yuan. Thought: I need to further inquire about the preferential policy of football Action: read_store_promotions: football Observation: The current promotional policy for this ball is: 10% discount upon purchase Thought: Now I need to combine the selling price and preferential policies of the ball to calculate the final transaction price Action: calculate: 120 * 0.9 Observation: The final price of the ball was 108.0 yuan Thought: I now have all the information needed to answer the question. Answer: According to your enquiry, we do sell soccer balls in our store, the current price is 120 yuan. At present, we offer a 10% discount on the purchase of football. Therefore, if you buy now, the final transaction price will be 108 yuan. Note: You must reply to the final result in Chinese Now it's your turn: """.strip() self.messages.append({"role": "system", "content": self.system_prompt}) # __call__ 方法可以使得一个类的实例可以被像函数那样调用,提供了类实例的“可调用”能力。 # 当使用类实例后面跟着括号并传递参数时,就会触发 __call__ 方法。 def __call__(self, message): self.messages.append({"role": "user", "content": message}) print(f"__call__: message={message}") response = self.execute() if not isinstance(response, str): raise TypeError(f"Expected string response from execute, got {type(response)}") self.messages.append({"role": "assistant", "content": response}) print(f"__calll__: response={response}") return response def execute(self): # 检查 self.client 是否是 OllamaClient 类的一个实例。这是类型安全的一种做法,确保 self.client 具有执行接下来代码所需的方法和属性。 if isinstance(self.client, OllamaClient): completion = self.client.chat_completions_create( model=self.config["ollama"]['model_name'], messages=self.messages, temperature=self.config["ollama"]['temperature'] ) # 如果 completion 是一个字典并且包含一个键为 message 的项,则尝试从 message 中提取 content 键对应的值。如果没有 content,则返回一个空字符串。 if isinstance(completion, dict) and 'message' in completion: return completion['message'].get('content', '') # 如果 completion 直接是一个字符串,则直接返回这个字符串。 elif isinstance(completion, str): return completion else: raise ValueError(f"Unexpected response structure from OllamaClient: {completion}") else: # 使用 OpenAI 的 GPT 系列模型 completion = self.client.chat.completions.create( model=self.config['openai']['model_name'], messages=self.messages, ) response = completion.choices[0].message.content if response != None: return completion.choices[0].message.content else: return "当前没有正常的生成回复,请重新思考当前的问题,并再次进行尝试"
2. 工具模块
位于tools/目录下,这些是ReAct中Action阶段调用的函数。每个工具简单高效,专注于单一职责。
query_by_product_name
- 功能:模糊查询SQLite数据库(例如
SportsEquipment.db)中的商品信息,返回匹配的行数据(如名称、价格、描述)。 - 为什么需要:模拟真实电商后台,避免模型幻觉商品信息。
- 关键代码:
def query_by_product_name(product_name): # 连接 SQLite 数据库 conn = sqlite3.connect('SportsEquipment.db') cursor = conn.cursor() # 使用SQL查询按名称查找产品。'%'符号允许部分匹配。 cursor.execute("SELECT * FROM products WHERE product_name LIKE ?", ('%' + product_name + '%',)) # 获取所有查询到的数据 rows = cursor.fetchall() # 关闭连接 conn.close() return rows
read_store_promotions
- 功能:读取促销政策文件(
store_promotions.txt),过滤包含指定商品的行,返回优惠描述。 - 关键代码:
def read_store_promotions(product_name): # 指定优惠政策文档的文件路径 file_path = 'store_promotions.txt' try: # 打开文件并按行读取内容 with open(file_path, 'r', encoding='utf-8') as file: promotions_content = file.readlines() # 搜索包含产品名称的行 filtered_content = [line for line in promotions_content if product_name in line] # 返回匹配的行,如果没有找到,返回一个默认消息 if filtered_content: return ''.join(filtered_content) else: return "没有找到关于该产品的优惠政策。" except FileNotFoundError: # 文件不存在的错误处理 return "优惠政策文档未找到,请检查文件路径是否正确。" except Exception as e: # 其他潜在错误的处理 return f"读取优惠政策文档时发生错误: {str(e)}"
calculate
- 功能:安全执行数学表达式,用于价格计算。
- 关键代码:
def calculate(what): return eval(what)
3. 主程序入口
位于main.py文件中,负责项目启动和用户交互循环(ReAct的交互循环)。
主要流程:
- 加载
config.json(定义模型类型、温度等)。 - 初始化客户端(OpenAI需从
.env加载API密钥)。 - 创建
CustomerServiceAgent实例,传入工具字典。 - 进入交互循环:读取用户输入,调用大模型的,并得到大模型的答复,根据大模型答复,进行调用工具或直接回复输出答案。
示例流程:
假设用户查询:“店里有足球卖吗?如果买现在有什么优惠?最终价格是多少?”
- Thought:需要先检查后台数据库是否有足球。
- Action:query_by_product_name: 足球
- Observation:数据库显示足球在售,价格为120元,库存充足。
- Thought:还需要查询足球的促销政策。
- Action:read_store_promotions: 足球
- Observation:当前促销政策为购买足球享受9折优惠。
- Thought:结合售价和促销政策计算最终价格。
- Action:calculate: 120 * 0.9
- Observation:计算结果为108。
- Answer:我们店里有足球出售,当前售价为120元。现在购买可享受9折优惠,最终价格为108元。
关键代码(这是ReAct循环的核心部分,展示了ReAct的本质,这个循环通过反复调用模型生成响应,主程序解析并执行Action,注入Observation继续循环。直到响应中出现"Answer:"标志):
# 主循环用于多次用户输入 while True: query = input("输入您的问题或输入 '退出' 来结束: ") if query.lower() == '退出': break iteration = 0 max_iterations = get_max_iterations(config) while iteration < max_iterations: # 内部循环用于处理每一条 query try: result = agent(query) # print(f"AI原始答复: {result}") action_re = re.compile('^Action: (\w+): (.*)$') actions = [action_re.match(a) for a in result.split('\n') if action_re.match(a)] if actions: action_parts = result.split("Action:", 1)[1].strip().split(": ", 1) tool_name = action_parts[0] tool_args = action_parts[1] if len(action_parts) > 1 else "" if tool_name in tools: try: observation = tools[tool_name](tool_args) query = f"Observation: {observation}" except Exception as e: query = f"Observation: Error occurred while executing the tool: {str(e)}" else: query = f"Observation: Tool '{tool_name}' not found" elif "Answer:" in result: print(f"客服回复:{result.split('Answer:', 1)[1].strip()}") break # 收到答案后结束内部循环 else: query = "Observation: No valid action or answer found. Please provide a clear action or answer." except Exception as e: print(f"An error occurred while processing the query: {str(e)}") print("Please check your configuration and ensure the AI service is running.") break iteration += 1 if iteration == max_iterations: print("Reached maximum number of iterations without a final answer.")
4. 配置管理
- 配置文件:
config.json示例,同时支持openai和本地ollama模型部署。通过use_model为true或false进行控制。
{ "ollama": { "use_model": false, "model_name": "llama3.1:8b", "temperature": 1.0, "max_iterations": 20 }, "openai": { "use_model": true, "model_name": "gpt-4o", "temperature": 1.0, "max_iterations": 20 }}
- 环境变量:
.env存储敏感信息,如OPENAI_API_KEY=sk-...。使用dotenv加载,确保安全。
**执行效果:**执行main.py主程序,效果如下:
如果我们想看ReAct过程,可以把调用大模型的过程(输入和响应)都打印出来:
添加打印信息后,输出效果如下,可以很清晰看到模型的ReAct过程。
(备注:在调试过程中,发现openai的gpt模型能够按预想的逐步输出过程,而一些国产的模型,会一次性全部输出ReAct过程)
本项目展示了ReAct框架在智能客服中的实际应用,通过简单工具集成实现了高效查询和计算。开发者可以参考代码,逐步修改工具或添加新功能(如集成更多API)。ReAct的优势在于其可解释性和灵活性——每个步骤 traceable,便于调试。未来可扩展到多代理协作或更复杂场景,如结合图像识别的商品推荐。实践这个项目,你将掌握从理论到代码的完整ReAct开发流程。
总结
综上所述,本文从ReAct Agent的基本理论入手,详细阐述了其结合推理与行动的核心框架,并通过Python代码示例展示了构建ReAct代理的实际流程。最后,以智能客服案例作为实践落地,演示了ReAct在处理复杂任务时的强大能力。作为Agent开发者,掌握ReAct思想将帮助你构建更智能、可解释的AI系统,推动从单一模型向多工具协作代理的演进。未来,随着大语言模型的进步,ReAct及其变体将继续在AI Agent开发中发挥关键作用,鼓励读者通过动手实践深化理解并探索更多应用场景。