ReAct
最经典的一个智能体范式ReAct (Reason + Act)。ReAct由Shunyu Yao于2022年提出,其核心思想是模仿人类解决问题的方式,将推理 (Reasoning) 与行动 (Acting) 显式地结合起来,形成一个“思考-行动-观察”的循环。
ReAct 的工作流程
在ReAct诞生之前,主流的方法可以分为两类:一类是“纯思考”型,如思维链 (Chain-of-Thought),它能引导模型进行复杂的逻辑推理,但无法与外部世界交互,容易产生事实幻觉;另一类是“纯行动”型,模型直接输出要执行的动作,但缺乏规划和纠错能力。
ReAct的巧妙之处在于,它认识到思考与行动是相辅相成的。思考指导行动,而行动的结果又反过来修正思考。为此,ReAct范式通过一种特殊的提示工程来引导模型,使其每一步的输出都遵循一个固定的轨迹:
- Thought (思考): 这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。
- Action (行动): 这是智能体决定采取的具体动作,通常是调用一个外部工具,例如 Search['华为最新款手机']。
- Observation (观察): 这是执行Action后从外部工具返回的结果,例如搜索结果的摘要或API的返回值。
智能体将不断重复这个 Thought -> Action -> Observation 的循环,将新的观察结果追加到历史记录中,形成一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。这个过程形成了一个强大的协同效应:推理使得行动更具目的性,而行动则为推理提供了事实依据。
这种机制特别适用于以下场景:
- 需要外部知识的任务:如查询实时信息(天气、新闻、股价)、搜索专业领域的知识等。
- 需要精确计算的任务:将数学问题交给计算器工具,避免LLM的计算错误。
- 需要与API交互的任务:如操作数据库、调用某个服务的API来完成特定功能。
工具
如果说大语言模型是智能体的大脑,那么工具 (Tools) 就是其与外部世界交互的“手和脚”。为了让ReAct范式能够真正解决我们设定的问题,智能体需要具备调用外部工具的能力。
一个良好定义的工具应包含以下三个核心要素:
- 名称 (Name): 一个简洁、唯一的标识符,供智能体在 Action 中调用,例如 Search。
- 描述 (Description): 一段清晰的自然语言描述,说明这个工具的用途。这是整个机制中最关键的部分,因为大语言模型会依赖这段描述来判断何时使用哪个工具。
- 执行逻辑 (Execution Logic): 真正执行任务的函数或方法。
当智能体需要使用多种工具时(例如,除了搜索,还可能需要计算、查询数据库等),我们需要一个统一的管理器来注册和调度这些工具。
将所有独立的组件,LLM客户端和工具执行器组装起来,构建一个完整的 ReAct 智能体。
系统提示词设计
提示词是整个 ReAct 机制的基石,它为大语言模型提供了行动的操作指令。我们需要精心设计一个模板,它将动态地插入可用工具、用户问题以及中间步骤的交互历史。
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。
可用工具如下:
{tools}
请严格按照以下格式进行回应:
Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。
现在,请开始解决以下问题:
Question: {question}
History: {history}
"""
这个模板定义了智能体与LLM之间交互的规范:
- 角色定义: “你是一个有能力调用外部工具的智能助手”,设定了LLM的角色。
- 工具清单 (tools): 告知LLM它有哪些可用的“手脚”。
- 格式规约 (Thought/Action): 这是最重要的部分,它强制LLM的输出具有结构性,使我们能通过代码精确解析其意图。
- 动态上下文 (question/history): 将用户的原始问题和不断累积的交互历史注入,让LLM基于完整的上下文进行决策。
核心循环
ReActAgent 的核心是一个循环,它不断地“格式化提示词 -> 调用LLM -> 执行动作 -> 整合结果”,直到任务完成或达到最大步数限制。
输出解析
LLM 返回的是纯文本,我们需要从中精确地提取出Thought和Action。
工具调用与执行
它首先检查是否为Finish指令,如果是,则流程结束。否则,它会通过tool_executor获取对应的工具函数并执行,得到observation。
观测结果的整合
最后一步,也是形成闭环的关键,是将Action本身和工具执行后的Observation添加回历史记录中,为下一轮循环提供新的上下文。
通过将Observation追加到 history,智能体在下一轮生成提示词时,就能“看到”上一步行动的结果,并据此进行新一轮的思考和规划。
import OpenAI from 'openai';
/**
* 搜索工具
* @param {*} query
* @returns
*/
function search(query) {
return `今天天气晴朗,气温25摄氏度`
}
/**
* 工具执行器
*/
class ToolExecutor {
constructor(tools) {
this.tools = tools;
}
registerTool(toolName, toolDesc, toolFunction) {
this.tools.push({
name: toolName,
description: toolDesc,
function: toolFunction
});
}
getTool(toolName) {
return this.tools.find(tool => tool.name === toolName);
}
executeTool(toolName, input) {
const tool = this.getTool(toolName);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
return tool.function(input);
}
getAvailableTools() {
return this.tools.map(tool => `${tool.name}: ${tool.description}`).join('\n');
}
execute(toolName, toolInput) {
const tool = this.getTool(toolName);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
return tool.function(toolInput);
}
}
function TestToolExecutor() {
const toolExecutor = new ToolExecutor([]);
toolExecutor.registerTool('search', '搜索天气', search);
console.log('Available tools: ', toolExecutor.getAvailableTools());
const tool = toolExecutor.getTool('search');
console.log('Execute search: ', tool.function('北京天气'));
}
// TestToolExecutor();
const getPrompt = (tools, question, history) => `
请注意,你是一个有能力调用外部工具的智能助手。
可用工具如下:
${tools}
请严格按照以下格式进行回应:
Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- tool_name[tool_input]:调用一个可用工具。
- Finish[最终答案]:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。
Finish 时也要按照指定格式输出
现在,请开始解决以下问题:
Question: ${question}
History: ${history}
`
class ReActAgent {
constructor(llm, toolExecutor, maxSteps = 10) {
this.llm = llm;
this.toolExecutor = toolExecutor;
this.maxSteps = maxSteps;
this.history = [];
}
async run(question) {
this.history = []
let currentStep = 0
while (currentStep++ < this.maxSteps) {
console.log(`-------- Step ${currentStep}`)
const tools = this.toolExecutor.getAvailableTools();
const historyStr = this.history.join('\n');
const prompt = getPrompt(tools, question, historyStr);
console.log('-------prompt', prompt)
const messages = [
{
role: 'user',
content: prompt
}
]
const response = await this.llm.chat.completions.create({
model: "Qwen3-30B-A3B-AWQ",
messages,
temperature: 0,
});
const { thought, action } = this.parseOutput(response);
console.log('Thought:', thought)
console.log('Action:', action)
if (!action) {
console.error('No action found in response');
break
}
if (action.startsWith('Finish')) {
const result = action.replace('Finish', '');
console.log('Result:', result);
break
}
const toolMatch = action.match(/(.*)\[(.*)\]/);
if (toolMatch) {
const toolName = toolMatch[1];
const toolInput = toolMatch[2];
const result = this.toolExecutor.executeTool(toolName, toolInput);
console.log('Tool result:', result);
this.history.push(`Action: ${action}`);
this.history.push(`Result: ${result}`);
} else {
console.log('Invalid action:', action);
break;
}
}
return 'Max steps reached';
}
parseOutput(response) {
let content = response.choices[0].message.content;
// 去掉 <think> </think> 之间的内容(支持多行)
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '');
console.log('-------content', content)
const thought = this.parseThought(content);
const action = this.parseAction(content);
return { thought, action };
}
parseThought(response) {
const match = response.match(/Thought: (.*)/);
return match ? match[1] : '';
}
parseAction(response) {
const match = response.match(/Action: (.*)/);
return match ? match[1] : response.trim();
}
}
async function tesetAgent() {
const toolExecutor = new ToolExecutor([]);
toolExecutor.registerTool('search', '搜索天气', search);
const llmClient = new OpenAI({
baseURL: 'http://192.168.3.202:30001/v1',
apiKey: '11'
});
const agent = new ReActAgent(llmClient, toolExecutor);
await agent.run('北京天气');
}
tesetAgent();