从零手写一个 ReAct Agent:让大模型自己调用工具

发布时间:2026/7/29 2:34:47
从零手写一个 ReAct Agent:让大模型自己调用工具 从零手写一个 ReAct Agent让大模型自己调用工具这篇文章不会用 LangChain、不会用 Spring AI也不会依赖任何第三方库。我们将用纯 Java 8 手写一个最小化的 ReAct Agent并让它真的跑起来。一、什么是 ReActReAct 是Reason推理和Act行动两个词的组合。它的核心思想很简单让大语言模型在回答问题的时候不直接给出答案而是先写下自己的思考过程再决定要不要调用外部工具。如果调用了工具就把工具返回的结果再写回上下文让模型继续思考直到得到最终答案。这个过程可以抽象成一个循环模型看到问题输出一个Thought模型根据思考输出Action和Action InputAgent 执行对应的工具得到Observation把Observation追加到上下文再次交给模型循环直到模型输出Final Answer。这就像是给大模型配了一张草稿纸和一组工具。它可以在草稿纸上反复演算而不是靠一次性推理强行回答。二、为什么不用框架市面上有很多现成的 Agent 框架比如 LangChain、LangGraph、Spring AI 等。它们功能强大但对初学者来说有几个问题框架封装了很多细节你很难看清 Agent 到底是怎么运转的框架版本更新快API 容易变学习成本不低很多框架默认依赖较新的 JDK而企业里常见的还是 JDK 8。所以这篇文章的目标是用最少的代码、最底层的工具把 ReAct 的本质表达出来。理解了本质之后再去看框架会有一种“原来它只是把这些步骤封装了”的感觉。三、我们要实现什么我们将实现一个支持数学计算的 Agent。你可以问它(25 15) * 2 - 10 等于多少Agent 的思考过程大致如下Thought: 我需要计算这个表达式。 Action: calculator Action Input: (25 15) * 2 - 10 Observation: 70.0 Thought: 工具已经返回了结果这就是最终答案。 Final Answer: 70.0整个项目只需要一个 Java 文件、一个pom.xml、一个.env文件没有任何第三方依赖。四、核心代码结构完整代码只有一个文件ReActAgent.java。它可以分成四个部分1. 工具接口interfaceTool{Stringname();Stringdescription();Stringrun(Stringinput);}每个工具都需要有一个名字、一段自然语言描述以及一个执行方法。名字是模型在Action:后面要填的内容描述会写进 Prompt 里告诉模型这个工具是干什么的。2. 计算器工具staticclassCalculatorToolimplementsTool{OverridepublicStringname(){returncalculator;}OverridepublicStringdescription(){returnUseful for calculating math expressions. Input should be a valid math expression like 2 2.;}OverridepublicStringrun(Stringinput){Stringexprinput.trim();if(!expr.matches([0-9\\-*/().\\s])){returnError: expression contains invalid characters.;}doubleresultevaluate(expr);returnString.valueOf(result);}}计算器做了两件事用正则白名单过滤输入防止模型把不安全的字符串传进来用双栈算法求值四则表达式支持括号。这里的双栈求值虽然和学习 Agent 没有直接关系但它体现了“工具必须自己保证安全和正确”这件事。Agent 不能把一切都交给模型。3. Prompt 设计ReAct 的关键不是代码有多复杂而是 Prompt 能不能把模型约束在固定格式里。privateStringbuildPrompt(Stringquestion){returnAnswer the following questions as best you can. You have access to the following tools:\n\ntoolDesc\n\nUse the following format:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [calculator]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: question\nThought:;}Prompt 的最后停在Thought:后面什么都不写。这是为了让模型接着写第一个思考。4. ReAct 循环publicStringrun(Stringquestion,intmaxIterations)throwsException{StringBuilderscratchpadnewStringBuilder();scratchpad.append(buildPrompt(question));for(inti0;imaxIterations;i){StringllmOutputcallLlm(scratchpad.toString());ActionactionparseAction(llmOutput);if(action.isFinalAnswer){returnaction.content;}Stringobservationtools.get(action.toolName).run(action.toolInput);scratchpad.append(\n).append(llmOutput);scratchpad.append(\nObservation: ).append(observation);scratchpad.append(\nThought: );}returnReached max iterations without final answer.;}这是整个 Agent 的心脏。scratchpad是一个不断变长的字符串它就是模型的“工作记忆”。每一轮循环我们都会把上一轮模型说的话、工具返回的 Observation、以及一个新的Thought:都追加进去。这样模型在下一轮就能看到完整的历史。parseAction()负责从模型输出里提取Action和Action Input。如果模型输出了Final Answer:就直接结束循环。maxIterations是一个保险丝防止模型陷入死循环。五、一个完整运行示例假设我们问(25 15) * 2 - 10 等于多少第一次调用模型Prompt 里只有问题和格式说明模型输出Thought: I need to calculate this expression. Action: calculator Action Input: (25 15) * 2 - 10Agent 解析出Action calculator、Action Input (25 15) * 2 - 10调用计算器工具得到70.0。于是scratchpad变成... 前面的 Prompt ... Question: (25 15) * 2 - 10 等于多少 Thought: I need to calculate this expression. Action: calculator Action Input: (25 15) * 2 - 10 Observation: 70.0 Thought:第二次调用模型模型看到Observation: 70.0就知道计算已经完成于是输出The calculator returned 70.0. Final Answer: 70.0Agent 检测到Final Answer:把70.0返回给用户循环结束。六、这个例子能说明什么通过这个最简实现我们可以看到 ReAct 的几个本质特征模型只负责规划和决策真正的执行由外部工具完成。 Observation 必须由代码生成不能由模型编造。上下文是 Agent 的记忆。如果没有scratchpad把历史保留下来模型每轮都是无状态的无法做多步推理。格式约束是 Prompt 工程的核心。ReAct 的威力来自“让模型按固定格式输出”而不是来自什么高深的算法。工具可以是任何东西。现在只是一个计算器后面可以换成搜索引擎、数据库查询、文件读写、API 调用等等。只要符合Tool接口Agent 就能调用它。七、局限和下一步这个例子是最小化的距离生产环境还很远。如果要继续深入可以考虑支持多个工具让模型根据问题自动选择把输出格式从纯文本改成 JSON解析更健壮加入异常处理和重试机制用向量数据库存储历史对话支持更长的上下文引入反思机制让模型在出错时自我纠正。但这些都是建立在你已经理解 ReAct 循环的基础上。先把循环跑通比一上来就追框架重要得多。八、总结ReAct 不是什么神秘的框架特性而是一种让大模型“边想边做”的协作模式。它的核心只有三件事给模型固定格式让它输出Thought、Action、Action InputAgent 解析这些输出调用真实工具得到Observation把Observation写回上下文让模型继续下一轮直到输出Final Answer。用 Java 手写一遍之后你会发现 Agent 的实现并没有想象中复杂。复杂的不是代码而是怎么让模型稳定地按你期望的格式思考。希望这个例子能帮你把 Agent 的学习从“调用框架 API”推进到“理解底层机制”的层面。