AgentExecutor:告别手写 ReAct 循环,一行代码启动 Java Agent
标签JavaReActAgentj-langchainLLMAgentExecutor工具调用前置阅读Java 实现 ReAct Agent工具调用与推理循环适合人群理解了 ReAct 原理、想在项目中高效落地的 Java 开发者一、上篇留下的问题上篇文章从零实现了一个完整的 ReAct Agent核心代码大致是这样的FlowInstanceagentChainchainActor.builder().next(prompt).loop(shouldContinue,llm,chainActor.builder().next(cutAtObservation)// 截断 Observation 之后的内容.next(parseAction)// 解析 Action / Action Input.next(Info.c(needsToolCall,executeTool),Info.c(input-ContextBus.get().getResult(llm.getNodeId()))).build()).next(newStrOutputParser()).next(extractFinalAnswer).build();ChatGenerationresultchainActor.invoke(agentChain,Map.of(input,上海现在的天气怎么样));把推理循环拆开来写对理解原理非常有帮助。但如果项目中每个 Agent 都这样写这些样板代码会占据大量篇幅而且每次改动都要同步好几个处理器。j-langchain 提供了两个工具来解决这个问题AgentExecutor把上面所有样板代码封装进去Builder API 一行启动AgentTool注解替代Tool.builder()用注解描述工具更简洁直观本文演示这两个工具的用法以及在什么情况下该退回手动模式。二、最简示例AgentExecutor 替代手写循环同样是查天气的场景用AgentExecutor重写TestpublicvoidreactAgentWithExecutor(){// 1. 定义工具与上篇完全相同这部分不变ToolgetWeatherTool.builder().name(get_weather).params(location: String).description(获取城市天气信息输入城市名称).func(location-String.format(%s 天气晴气温 25°C,location)).build();ToolgetTimeTool.builder().name(get_time).params(city: String).description(获取城市当前时间输入城市名称).func(city-String.format(%s 当前时间 14:30,city)).build();// 2. 构建 AgentExecutor —— ReAct 循环由框架处理不用再手写AgentExecutoragentAgentExecutor.builder(chainActor).llm(ChatAliyun.builder().model(qwen-plus).temperature(0f).build()).tools(getWeather,getTime).maxIterations(10).onThought(System.out::print)// 每轮推理结果回调.onObservation(obs-System.out.println(Observation: obs))// 工具执行结果回调.build();// 3. 执行ChatGenerationresultagent.invoke(上海现在的天气怎么样);System.out.println(result.getText());}与手写版本相比去掉了截断处理器、Action 解析器、scratchpad 拼接逻辑、Final Answer 提取器——这些统统由框架内部完成执行结果和内部机制与手写版完全一致只是不用再关心实现细节。三、进一步简化用 AgentTool 注解定义工具Tool.builder()虽然直观但字段较多写起来还是有些冗余。AgentTool注解提供了更简洁的替代方式。单参数工具publicclassCityTools{AgentTool(获取城市天气信息输入城市名称)publicStringgetWeather(Stringlocation){returnString.format(%s 天气晴气温 25°C,location);}AgentTool(获取城市当前时间输入城市名称)publicStringgetTime(Stringcity){returnString.format(%s 当前时间 14:30,city);}}注解的值就是工具描述方法名自动转成 snake_case 作为工具名getWeather→get_weather方法返回值的toString()作为 Observation。多参数工具当工具需要多个参数时用Param描述每个参数LLM 会生成 JSON 格式的 Action Input框架自动按参数名解析注入AgentTool(订机票需要出发城市、目的城市和日期)publicStringbookFlight(Param(出发城市)StringfromCity,Param(目的城市)StringtoCity,Param(日期格式 YYYY-MM-DD)Stringdate){returnString.format(已成功预订 %s → %s日期 %s 的机票,fromCity,toCity,date);}使用注解工具构建 Agent把工具类实例直接传给tools()框架自动扫描所有带AgentTool的方法TestpublicvoidreactAgentWithToolAnnotation(){AgentExecutoragentAgentExecutor.builder(chainActor).llm(ChatAliyun.builder().model(qwen-plus).temperature(0f).build()).tools(newCityTools())// 传对象框架自动扫描 AgentTool 方法.maxIterations(10).onThought(System.out::print).onObservation(obs-System.out.println(Observation: obs)).build();// 单参数工具调用ChatGenerationresult1agent.invoke(上海现在的天气怎么样);System.out.println(result1.getText());// 多参数工具调用ChatGenerationresult2agent.invoke(帮我订一张明天从上海飞北京的机票日期是2024-03-15);System.out.println(result2.getText());}四、推理过程长什么样单参数工具的推理轨迹Thought: 需要查询上海的天气调用 get_weather 工具。 Action: get_weather Action Input: 上海 Observation: 上海 天气晴气温 25°C Thought: 已获得天气信息可以直接回答。 Final Answer: 上海现在天气晴朗气温 25°C。多参数工具的推理轨迹LLM 生成 JSON 格式的 Action Input框架解析后按参数名注入方法Thought: 需要订机票调用 book_flight 工具。 Action: book_flight Action Input: {fromCity: 上海, toCity: 北京, date: 2024-03-15} Observation: 已成功预订 上海 → 北京日期 2024-03-15 的机票 Thought: 订票已完成可以告知用户。 Final Answer: 已为您预订了 2024-03-15 从上海飞往北京的机票。五、AgentTool 注解规则速查特性说明工具名默认取方法名转 snake_case如getWeather→get_weather可用AgentTool(name...)覆盖单参数Action Input 直接传字符串多参数Action Input 须为 JSON框架按参数名自动解析支持 camelCase 和 snake_case参数描述Param(描述)注入到 Prompt帮助 LLM 理解每个参数的含义返回值方法返回值toString()作为 ObservationBuilder 参数速查参数必填说明llm(...)✅任意BaseChatModel实现Ollama、阿里云、OpenAI 等tools(Tool...)✅直接传Tool对象可变参数tools(ListTool)✅传Tool列表tools(Object)✅传带AgentTool注解的对象自动扫描maxIterations(n)可选最大推理轮次默认 10promptTemplate(...)可选自定义 ReAct PromptonThought(...)可选每轮 Thought/Action 生成后的回调onObservation(...)可选工具执行完成后的回调六、与其他框架对比特性LangChain PythonLangChain4jj-langchain底层机制ReAct PromptFunction CallingReAct Prompt工具定义tool PydanticTool注解AgentToolParam多参数处理Pydantic → JSON Schema注解反射 → JSONParam→ JSON Schema →method.invoke简洁入口create_react_agentAiServicesAgentExecutor.builder()手动控制循环较难几乎不能chainActor.builder().loop(...)模型依赖任意模型需支持 Function Calling任意模型推理过程透明度中低高度封装高全流程可见有一点值得特别说明LangChain4j 的AiServices走的是 Function Calling 路线不是真正的 ReAct。模型直接输出结构化 JSON 决定调用哪个工具框架捕获后执行再以ToolMessage注入对话历史。两者底层机制不同代码简洁度的对比不在同一维度上。七、什么时候该退回手动模式AgentExecutor覆盖了大多数场景但以下情况建议退回上篇的手动构建方式多 LLM 协作第一轮用轻量模型做意图分类后续轮次换强模型推理AgentExecutor只支持单一 LLM。自定义终止条件除是否还有 Action之外还需要检查外部状态如任务队列、数据库标记才能决定是否继续循环。循环内插入审计每次工具调用前后需要写审计日志、触发告警或做风控拦截。非标准 Prompt 格式对接某些私有化部署模型时ReAct 的 Prompt 格式需要做特殊适配。这两种方式可以共存先用AgentExecutor快速验证业务逻辑确认需要深度定制时再切换到手动模式不需要从头重写工具定义部分。八、总结AgentExecutor和AgentTool解决的是同一个问题把重复的样板代码从业务逻辑中剥离出去。上篇的手动实现让你看清了 ReAct 的每一个细节本篇的封装方式让你在实际项目中少写冗余代码。两者都有必要掌握前者帮助理解后者用于落地。 相关资源完整示例Article09AgentExecutor.java对应方法reactAgentWithExecutor()和reactAgentWithToolAnnotation()j-langchain GitHubhttps://github.com/flower-trees/j-langchainj-langchain Gitee 镜像https://gitee.com/flower-trees-z/j-langchain