Agent的ReAct(推理+行动)模式

0 阅读10分钟

LLM(Reasoning engine)推理引擎

彻底搞懂Agent时代下的行动能力。为什么从文字描述到具有行动的能力。

LLM学习了大量的知识语料,不要只把它当成巨大的知识语料库像搜索引擎一样来使用搜索查询的功能,而是看成是一个推理引擎(Reasoning engine)

LLM会结合从它学习到的预料库和你提供给它的新数据来帮助回答内容或者推理内容。我们可以利用它强大的推理能力,通过设置提示,让它帮助我们拆分任务。

这里面涉及到了ThoughtActionObservation的概念,一种提示词框架ReAct诞生。

ReAct

ReAct 是一种让大语言模型"边想边做"的协作模式。  在这个模式下,LLM 不再是孤立的问答机器,而是一个能够使用工具的"大脑"。它通过 Thought(思考)  分析问题并规划步骤,然后输出 Action(行动)  指令来调用外部工具;框架执行工具后,将真实世界的 Observation(观察)  结果反馈给 LLM。这个循环可以重复多次,直到 LLM 认为信息足够,最终输出 Final Answer(最终答案)  给用户。本质上,Thought 是 LLM 的内心独白,Action 是它伸出的手,Observation 是它摸到的真实世界。

image.png

ReAct = Reasoning + Acting(推理 + 行动)

这是一种让 LLM 能够交替进行思考和行动的提示词框架。它不是让 LLM 一次性给出答案,而是让 LLM 像人类解决问题一样:

  1. 想一想要做什么(Reasoning)
  2. 然后执行一个动作(Acting)
  3. 看到结果后再想一想(Reasoning)
  4. 最后给出答案
角色谁产生在本次示例中的内容作用说明
💭 ThoughtLLM 生成"We can use the time tool to get today's date."LLM 的内部推理过程。它分析问题后,决定下一步要做什么。这个思考会被框架记录下来,用于后续的上下文。
🔧 ActionLLM 生成{"action": "time", "action_input": ""}LLM 发出的"命令"。它是一个结构化的 JSON,告诉框架:"我要调用哪个工具,传什么参数"。这是 LLM 和外部世界的接口。
👁️ Observation工具/Tool 生成"2026-04-13"工具执行后返回的真实结果。框架会把这个结果喂回给 LLM,作为新的信息输入。这是外部世界给 LLM 的反馈。
✅ Final AnswerLLM 生成"2026-04-13"LLM 综合所有 Thought 和 Observation 后,得出的最终结论。框架识别到这个词后,会停止循环并返回给用户。

Agent工作的案例

这里我用LangChain来快速实现一个查看当前系统时间案例,看看ThoughtActionObservation到底是什么。

from langchain.chat_models import ChatOpenAI
from langchain.agents import tool
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from datetime import date

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")

@tool
def time(text: str) -> str:
    """Returns todays date, use this for any \
    questions related to knowing todays date. \
    The input should always be an empty string, \
    and this function will always return todays \
    date - any date mathmatics should occur \
    outside this function."""
    return str(date.today())
    
agent= initialize_agent(
    [time], 
    llm, 
    agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    handle_parsing_errors=True,
    verbose = True)
    
try:
    result = agent("whats the date today?") 
except: 
    print("exception on external access")

执行过程看到的大概

> Entering new AgentExecutor chain...
Question: What's the date today?
Thought: We can use the time tool to get today's date.
Action:
\```
{
  "action": "time",
  "action_input": ""
}
\```

Observation: 2026-04-13
Thought:Final Answer: 2026-04-13

> Finished chain.
{'input': 'whats the date today?', 'output': '2026-04-13'}
步骤组件发生了什么(核心输入/输出)
1AgentExecutor接收问题"whats the date today?"
2LLMChain (第1次)构建提示词发给 GPT。提示词包含:系统角色、工具说明书、必须用 Action/Thought 格式的指令。
3ChatOpenAI (GPT)GPT 思考后回复: Thought: 用 time 工具查日期。 Action: {"action": "time", "action_input": ""}
4框架解析器拦截输出:发现 GPT 输出了 Action 字段,阻止其直接回复用户。
5Tool: time执行 Python 函数:输入 "",运行 date.today()返回 "2026-04-13"
6LLMChain (第2次)把结果塞回提示词:提示词里多了 Observation: 2026-04-13
7ChatOpenAI (GPT)GPT 总结回答: Final Answer: 2026-04-13
8AgentExecutor结束流程:输出最终结果 {'output': '2026-04-13'}

LangChain框架背后执行的流程

[用户]
   |
   | 输入: "whats the date today?"
   v
[1. AgentExecutor (大管家)]
   |
   | 打包数据: {input, 历史记录为空, 工具列表}
   v
[2. LLMChain (组装 Prompt)]
   |
   | 生成超大文本包含:
   | System: "你是助手,你可以用工具: time(...)"
   | Human:  "whats the date today?"
   | Assistant: (留白等GPT填)
   v
[3. LLM (GPT-3.5) - 第一次推理]
   |
   | GPT 看了说明,知道不能直接答,必须写指令。
   | 输出字符串:
   | "Thought: I need to check date.
   |  Action:
   |  {
   |    "action": "time",
   |    "action_input": ""
   |  }"
   v
[4. 框架核心逻辑 —— **这里是魔法的发生地!**]
   |
   | LangChain 有专门的正则表达式解析器 (OutputParser)
   | 它扫描 GPT 的输出,寻找 "Action:" 后面的 JSON 块。
   | --------------------------------------------------
   | 检测到: action = "time" , input = ""
   | 它去注册表里找名字叫 "time" 的 Python 函数。
   | --------------------------------------------------
   | 决策: 既然有 Action,就 **不** 返回给用户。
   | 执行: result = time("")  <-- Python 代码被触发!
   v
[5. @tool 装饰的函数]
   |
   | def time(text: str):
   |     return str(date.today())  // 返回 "2026-04-13"
   v
[6. AgentExecutor (拿到结果)]
   |
   | 大管家把结果包装成 "Observation: 2026-04-13"
   | 把它拼接到刚才的对话历史后面。
   v
[7. LLM (GPT-3.5) - 第二次推理]
   |
   | GPT 收到新 Prompt:
   | ... (之前的对话)
   | Observation: 2026-04-13  <-- 事实摆在这里了
   |
   | GPT 分析: 哦,小弟查到了,我要给 Final Answer。
   | 输出字符串:
   | "Final Answer: 2026-04-13"
   v
[8. 框架核心逻辑]
   |
   | 解析器扫描输出,发现 "Final Answer:"
   | 决策: 流程结束,提取后面的字符串。
   v
[最终输出]
   |
   | 返回给用户: "2026-04-13"

实践:代码生成器

使用ReAct(推理+行动)的模式,模拟现代的AI编辑器,生成代码

核心类ReActAgent.java

重点

  1. 写好提示词引导LLM进行有效的输出
  2. 通过引导LLM进行有效的输出,我们进行拦截,处理LLM的输出结果,方便我们后面调用工具

核心方法在run的运行:在while循环中让LLM拆解任务,我们调用工具,一步一步完成,直到最终结果。

image.png


public class ReActAgent{

    private final OpenAIClient apiClient;
    private final AgentTools agentTools;

    // 引导大模型进行思考
    private static final String REACT_PROMPT_TEMPLATE = """
      你是一个强大的AI助手,通过思考和使用工具来解决用户的问题。

      你的任务是你所能回答一下问题。你可以使用以下工具:

      {tools}

      请严格遵循以下规则和格式:

      1. 你的行动必须基于一个清晰的"Thought"过程。
      2. 你必须按照顺序使用 "Thought:","Action: ","Action Input: "。
      3. 在每次回复中,你只能生成**一个** Thought/Action/Action Input 组合
      4. **绝对不要**自己编造"Observation:"。系统会在你执行动作后,将真实的结果作为Observation提供给你。
      5. 当你拥有足够的信息来直接回答用户的问题时,请使用"Final Answer: "来输出最终答案。
      6. 每次回复中,"Thought:","Action: ","Action Input: "和"Final Answer: "不能同时出现。

      下面是你的思考和行动格式:
      Thought: 我需要做什么来解决问题?下一步是什么?
      Action: 我应该使用哪个工具?必须是[{tools_names}]中的一个。
      Action Input: 我应该给这个工具提供什么输入?**这必须是一个JSON对象**

      --- 开始 ---

      Question: {input}

      {agent_scratchpad}
    """;

    public ReActAgent(OpenAIClient apiClient,AgentTools agentTools){
        this.apiClient = apiClient;
        this.agentTools = agentTools;
    }

    public String run(String question,int maxIterations){
        // 模拟整个对话的历史记录
        StringBuilder agentScratchpad = new StringBuilder();

        for(int i = 0; i < maxIterations; i++){
            System.out.printf("迭代%d\n", i + 1);

            // ✨1. 构建 Prompt(不断的添加记忆和上下文)
            String promptString = buildPrompt(question,agentScratchpad.toString());

            // 2. 调用LLM(每一次调用都是一次无状态的访问)
            ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                .addUserMessage(promptString)
                .model(ModelConfig.LLM_NAME)
                .builder;

            ChatCompletion chatCompletion = apiClient.chat().completions().create(params);
            String rawLlmOutput = chatCompletion.choices().get(0).message().content().get();
        
            // 3. 解析LLM输出
            ParsedOutput parsedOutput = parseLlmOutput(rawLlmOutput);

            // 4. 根据解析结果进行决策
            if("final_answer".equals(parsedOutput.type())){
                return parsedOutput.answer();
            }

            if("error".equals(parsedOutput.type())){
                System.err.printf("解析错误: %s\n",parseOutput.message());
                String observation = String.format("解析错误: %s.请检查你的输出格式是否严格遵循要求。",parseOutput.message());
                agentScratchpad.append("Thought: 我之前的输出格式有误,需要修正。\nObservation: ")
                    .append(Observation)
                    .append("\n");
                continue;
            }

            String thought = parsedOutput.thought();
            String action = parsedOutput.action();
            String actionInputStr = parsedOutput.actionInputStr();

            // 我们解释之后,打印日志,看看大模型给我们输出的一套✨组合信息✨
            System.out.printf("思考:%s\n",thought);
            System.out.printf("行动:%s\n",action);
            System.out.printf("行动输入:%s\n",actionInputStr);


            // 5. 执行工具
            String observation = executeTool(action,actionInputStr);

            // 6. 构建历史记录
            agentScratchpad
                .append("Thought: ").append(thought).append("\n")
                .append("Action: ").append(action).append("\n")
                .append("Action Input: ").append(actionInputStr).append("\n")
                .append("Observation: ").append(observation).append("\n")
        
        }

        System.err.println("已达到最大迭代次数");
        return "Agent已停止,因为达到了最大迭代次数";
    }


    private String buildPrompt(String question,String agentScratchpad){

        // ✨1. 整理工具的描述信息
        List<String> toolNameList = new ArrayList<>();
        List<String> formattedToolList = new ArrayList<>();

        // 1.1 处理在AgentTools中定义的多个工具
        for(Method declaredMethod: AgentTools.class.getDeclaredMethods()){
            Tool toolAnnotation = declaredMethod.getAnnotation(Tool.class);
            String toolName = declaredMethod.getName();
            String toolDescription = toolAnnotation.description();

            String paramDescription = declaredMethod.getParameters()[0].getAnnotation(ToolParam.class).description();
            String formattedTool = String.format("- toolName=%s,toolDescription=%s,paramDescription=%s",toolName,toolDescription,paramDescription);
            formattedToolList.add(formattedTool);
            toolNameList.add(toolName);
        }

        // 1.2 组装所有的工具
        String formattedTools = String.join("/n/n",formattedToolList);
        String toolNames = String.join(",",toolNameList);


        return REACT_PROMPT_TEMPLATE
            .replace("{input}",question)
            .replace("{tools}",formattedTools)
            .replace("{tool_names}",toolNames)
            .replace("{agent_scratchpad}",agent_scratchpad);

    }


    // 处理LLM的输出
    private ParsedOutput parseLlmOutput(String llmOutput){
        if(llmOutput.contains("Final Answer:")){
            return new ParsedOutput("final_answer",llmOutput.split("Final Answer:")[1].strip(),null,null,null,null);
        }

        // ✨ 重要,拦截的处理
        Pattern actionPattern = Pattern.compile("Thought:(.*)Action:(.*?)Action Input:(.*)",Pattern.DOTALL);
        Matcher matcher = actionPattern.matcher(llmOutput);

        if(matcher.find()){
            String thought = matcher.group(1).trim();
            String action = matcher.group(2).trim();
            String actionInputStr = matcher.group(3).trim();

            // 处理一下actionInputStr的格式
            if(actionInputStr.startsWith("```json")){
                actionInputStr = actionInputStr.subString(7);
            }

            if(actionInputStr.endsWith("```")){
                actionInputStr = actionInputStr.subString(0,actionInputStr.length() - 3);
            }

            actionInputStr = actionInputStr.trim();

            return new ParsedOuput("action",null,thought,action,actionInputStr,null);
        }

        return new ParsedOuput("error",null,null,null,null,Stirng.format("解析LLM输出失败:'%s'",llmOutput));
    }

    // ✨  执行工具
    private String executeTool(String action,String actionInputStr){
        // 可以一般使用反射,这里为了方便直接写固定
        if("writeFile".equals(action)){
            return agentTools.writeFile(actionInputStr);
        }else{
            return String.format("错误: 找不到工具 '%s'。",action);
        }
    }


    private record ParsedOutput(
        String type,String answer,String thought,String action,String actionInputStr,String message
    ){}
}

工具类相关

两个注解用于描述工具以及工具需要的参数,通过反射拿到注解的信息,将信息加入提示词中

@Taget(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tool{
    String description();
}

@Taget(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolParam{
    String description();
}

定义工具

这里我们定义了一个写文件的方法,将大模型生成的代码写入我们的本地文件中。

public class AgentTools{
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将指定内容写入本地文件
     * @param jsonInput 一个包含'file_path'和'content'的JSON字符串
     * @return 执行结果的描述字符串
     */
    @Tool(description = "将指定内容写入本地文件。")
    public String writeFile(@ToolParam(description = "包含'file_path'和'content'的JSON字符串。")
                            String jsonInput){
        try{
            JsonNode rootNode = objectMapper.readTree(jsonInput);
            String filePath = rootNode.get("file_path").asText();
            String content = rootNode.get("content").asText();

            try(FileWriter writer = new FileWriter(filePath)){
                writer.write(content);
                return String.format("成功将内容写入文件 '%s' ",filePath);
            }catch(IOException e){
                return String.format("写入文件 '%s' 时发生错误:%s",filePath,e.getMessage());
            }
        }catch(Exception e){
            return String.format("解析Action Input或执行 writeFile工具时出错:%s",e.getMessage());
        }                        
    }
}

maven依赖

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.0</version>
    </dependency>

    <dependency>
        <groupId>com.openai</groupId>
        <artifactId>openai-java</artifactId>
        <version>0.32.0</version>
    </dependency>
</dependencies>

运行启动Main

public class Main{

    public static void main(String[] args){

        // 1. 初始哈API客户端和工具
        OpenAIClient apiClient = OpenAIOkHttpClient.builder()
         .apiKey(ModelConfig.API_KEY)
         .baseUrl(ModelConfig.BASE_URL)
         .build();

         AgentTools agentTools = new AgentTools();

         // 2. 创建ReActAgent
         ReActAgent agent = new ReActAgent(apiClient,agentTools);


        // 3. 定义问题并运行Agent
        String question = "请帮我用HTML、CSS、JS创建一个简单的贪吃蛇游戏,分成三个文件,分别是snake.html、snake.js、snake.css";
        String finalResult = agent.run(question,10);

        // 4. 打印最终结果
        System.out.println("\n\n==========");
        System.out.println("AGENT的执行结束,最终结果为:");
        System.out.println(finalResult);
        System.out.println("=============\n");
    }

}

运行结果。

从运行的结果来看,大模型已经在我们的引导之下一步一步将任务进行了拆解

第一次循环,大模型的输出

思考: 我需要为用户创建三个文件:snake.html,snake.css和snake.js,分别用于实现贪吃蛇游戏的结构,样式和逻辑。我将从创建snake.html文件开始
行动: writeFile
行动输入: {"file_path": "snake.html","content": "xxxx"}
观察: 成功将内容写入文件'snake.html'

第二次循环,大模型的输出

思考: 已经成功创建snake.html文件。接下来,我需要创建snake.css文件,为游戏画布和页面添加基本样式
行动: writeFile
行动输入: {"file_path": "snake.css","content": "xxxx"}
观察: 成功将内容写入文件'snake.css'