J-LangChain - Agent - 编排一个 ReAct + Function Call 反应链

系列文章索引
J-LangChain 入门

介绍

j‑langchain 是一款基于 Java 的 AIGC 编排框架,致力于集成多种大模型(LLM)调用及 RAG 工具。自 1.0.8 版本起,我们引入了工具函数(Function Call)调用能力,正式实现了 Tools 功能,并将其与 ReAct(Reasoning + Acting) 模式结合,从而构建出功能丰富、交互智能的 Agent 系统。

在本文中,我们将通过一个详实的实例,展示如何利用 Tools 功能编排一个具备 ReAct 反应链的 Agent。不仅能够体验 j‑langchain 的 Function Call 功能,还能深入了解大模型在 ReAct 反应链中如何一步步调用外部工具,实现复杂推理与动态响应。

示例场景

假设我们需要构建一个智能代理,能够回答类似于"上海的天气如何?"的等问题。为此,我们需要:

  • 定义一个提示模板(Prompt Template),指导代理如何处理问题。

  • 配置工具(Tools),例如获取天气和时间的函数。

  • 实现一个带有工具调用的循环逻辑,确保代理能在必要时获取外部信息。

  • 解析并返回最终答案。

以下是实现这一功能的完整代码解析。

代码解析

1. 定义提示模板

提示模板是代理的核心,它告诉模型如何思考和行动。我们使用 PromptTemplate 类定义了一个结构化的模板:

java 复制代码
PromptTemplate prompt = PromptTemplate.fromTemplate(
                """
                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 answer
                Thought: Consider whether you already have enough information to answer the question. If so, proceed directly to the final answer.
                
                If additional information is needed, take the following steps:
                - Identify what specific information is missing.
                - Call the appropriate tool to obtain that information.
                - Analyze the new information and determine if the question can now be answered.
                
                When using tools, follow this structured approach:
                Action: the action to take, should be one of [${toolNames}]
                Action Input: the input to the action
                Observation: the result of the action
                
                - You may use tools **up to 3 times**. If you still lack a complete answer after 3 attempts, summarize the best possible response.
                - If a tool's result is **irrelevant or does not improve understanding**, do not call the same tool again. Instead, attempt to derive an answer from available information.
                
                Thought: Based on the gathered information, determine if you can now provide a final answer. If yes, proceed to:
                Final Answer: the final answer to the original input question.
                If not, provide the best possible answer with a note on any remaining uncertainties.
                
                Begin!
                
                Question: ${input}
                Thought:
                """);
  • ${tools}${toolNames} 是占位符,会在运行时被替换为实际的工具列表和工具名称。
  • ${input} 使用户提出的问题。
  • 模板中明确了代理的思考步骤:先判断是否需要更多信息,若需要则调用工具,最后给出答案。

2. 配置语言模型

我们使用 ChatOllama 作为语言模型,设置 temperature 为 0 以确保输出稳定:

java 复制代码
ChatOllama llm = ChatOllama.builder().model("llama3:8b").temperature(0f).build();

当然你可以调试更聪明的模型,但此实例中 llama3:8b 已经可以达到效果。

3. 定义工具

工具是代理获取外部信息的关键。我们定义了两个简单工具:获取天气和获取时间:

java 复制代码
Tool getWeather = Tool.builder()
    .name("get_weather")
    .params("location: String")
    .description("Get city weather information and enter the city name")
    .func(location -> String.format("The weather in %s is sunny with a temperature of 25°C", location))
    .build();

Tool getTime = Tool.builder()
    .name("get_time")
    .params("city: String")
    .description("Get city the current time and enter the city name")
    .func(location -> String.format("%s The current time is 12:00 PM", location))
    .build();

List<Tool> tools = List.of(getWeather, getTime);
prompt.withTools(tools);
  • Tool.builder() 提供了简洁的方式来定义工具的名称、参数、描述和执行逻辑。
  • prompt.withTools(tools) 将工具注入提示模板。

4. 辅助节点

为了实现 ReAct 形式交互,我们需要设计一个循环,并实现一些节点处理中间结果,和循环的退出判断,辅助流程:

4.1 处理工具调用的中间结果
java 复制代码
TranslateHandler<AIMessage, AIMessage> cut = new TranslateHandler<>(llmResult -> {
    if (llmResult == null || StringUtils.isEmpty(llmResult.getContent()) || !llmResult.getContent().contains("Observation:")) {
        return llmResult;
    }
    String prefix = llmResult.getContent().substring(0, llmResult.getContent().indexOf("Observation:"));
    llmResult.setContent(prefix);
    return llmResult;
});
  • cut 处理器截取模型输出中工具调用前的部分,确保后续逻辑只处理必要内容。
4.2 解析模型输出
java 复制代码
TranslateHandler<Map<String, String>, AIMessage> trans = new TranslateHandler<>(llmResult -> PromptUtil.stringToMap(llmResult.getContent()));
  • trans 将模型生成的文本解析为键值对(如 Action 和 Action Input)。
4.3 循环控制
java 复制代码
int limit = 10;
Function<Integer, Boolean> isFinish = i -> {
    Map<String, String> map = ContextBus.get().getResult(trans.getNodeId());
    return i < limit && (map == null || (map.containsKey("Action") && map.containsKey("Action Input")));
};
  • isFinish 定义了循环退出条件:最多迭代 10 次,或模型不再需要调用工具。
4.4 执行工具调用
java 复制代码
// 入参为trans节点键值对结果
TranslateHandler<Object, Map<String, String>> call = new TranslateHandler<>(map -> {
	// 获取原promt模型,用于追加
    StringPromptValue promptResult = ContextBus.get().getResult(prompt.getNodeId());
    // 获取cut节点结果,用于追加
    AIMessage cutResult = ContextBus.get().getResult(cut.getNodeId());

    Tool useTool = tools.stream().filter(t -> t.getName().toLowerCase().equals(map.get("Action"))).findAny().orElse(null);
    if (useTool == null) {
        promptResult.setText(promptResult.getText().trim() + "again");
        return promptResult;
    }
    String observation = (String) useTool.getFunc().apply(map.get("Action Input"));
    System.out.println("Observation: " + observation);

    String prefix = cutResult.getContent();
    String agentScratchpad = prefix.substring(prefix.indexOf("Thought:") + 8).trim() + "\nObservation:" + observation + "\nThought:";
    promptResult.setText(promptResult.getText().trim() + agentScratchpad);
    return promptResult;
});
  • call 处理器执行工具调用,并将结果(Observation)追加到提示中,供下一次循环使用。

5. 组装完整流程

使用 ChainActor 构建完整的执行链:

java 复制代码
FlowInstance chain = chainActor.builder()
      .next(sPrint) // 打印开始标记
      .next(prompt) // 构建prompt
      .loop(
	       // 循环是否退出
	       isFinish,
	
	       // 循环执行
	       llm,
	       chainActor.builder() // 嵌入中间结果执行链
               .next(cut).next(trans) // 处理每次模型输出
               .next(
               		Info.c(isCall, call), // 判断需要调用工具
                    Info.c(input -> ContextBus.get().getResult(llm.getNodeId())) // 判断不需要调用工具,直接输出模型结果
               )
               .build()
      )
      .next(parser) // 解析结果
      .next(answer) // 再次处理模型输出结果,截取
      .next(ePrint) // 打印结束标记
      .build();
6. 执行并测试
> Entering new AgentExecutor chain...
A straightforward question! Let's see if we can get an answer without using any tools.

Thought: We don't have any information about Shanghai's weather yet. But we can try to use the `get_weather` tool to find out!

Action: get_weather
Action Input: Shanghai

Observation: The weather in Shanghai is sunny with a temperature of 25°C
Thought: Now that we have the weather information for Shanghai, let's proceed to answer the question.

Final Answer: The weather in Shanghai is sunny with a temperature of 25°C.
> Finished chain.
The weather in Shanghai is sunny with a temperature of 25°C.

完整代码实例

https://github.com/flower-trees/j-langchain-example/blob/master/src/main/java/org/salt/jlangchain/demo/rag/tools/ZeroShotReactDescription.java

java 复制代码
@Component
public class ZeroShotReactDescription {

    @Autowired
    ChainActor chainActor;

    public void run() {

        PromptTemplate prompt = PromptTemplate.fromTemplate(
                """
                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 answer
                Thought: Consider whether you already have enough information to answer the question. If so, proceed directly to the final answer.
                
                If additional information is needed, take the following steps:
                - Identify what specific information is missing.
                - Call the appropriate tool to obtain that information.
                - Analyze the new information and determine if the question can now be answered.
                
                When using tools, follow this structured approach:
                Action: the action to take, should be one of [${toolNames}]
                Action Input: the input to the action
                Observation: the result of the action
                
                - You may use tools **up to 3 times**. If you still lack a complete answer after 3 attempts, summarize the best possible response.
                - If a tool's result is **irrelevant or does not improve understanding**, do not call the same tool again. Instead, attempt to derive an answer from available information.
                
                Thought: Based on the gathered information, determine if you can now provide a final answer. If yes, proceed to:
                Final Answer: the final answer to the original input question.
                If not, provide the best possible answer with a note on any remaining uncertainties.
                
                Begin!
                
                Question: ${input}
                Thought:
                """);

        ChatOllama llm = ChatOllama.builder().model("llama3:8b").temperature(0f).build();

        Tool getWeather = Tool.builder()
                .name("get_weather")
                .params("location: String")
                .description("Get city weather information and enter the city name")
                .func(location -> String.format("The weather in %s is sunny with a temperature of 25°C", location))
                .build();

        Tool getTime = Tool.builder()
                .name("get_time")
                .params("city: String")
                .description("Get city the current time and enter the city name")
                .func(location -> String.format("%s The current time is 12:00 PM", location))
                .build();

        List<Tool> tools = List.of(getWeather, getTime);

        prompt.withTools(tools);

        TranslateHandler<AIMessage, AIMessage> cut = new TranslateHandler<>(llmResult -> {
            if (llmResult == null || StringUtils.isEmpty(llmResult.getContent()) || !llmResult.getContent().contains("Observation:")) {
                if (llmResult != null) {
                    System.out.println(llmResult.getContent());
                }
                return llmResult;
            }
            String prefix = llmResult.getContent().substring(0, llmResult.getContent().indexOf("Observation:"));
            System.out.println(prefix);
            llmResult.setContent(prefix);
            return llmResult;
        });

        TranslateHandler<Map<String, String>, AIMessage> trans = new TranslateHandler<>(llmResult -> PromptUtil.stringToMap(llmResult.getContent()));

        int limit = 10;
        Function<Integer, Boolean> isFinish = i -> {
            Map<String, String> map = ContextBus.get().getResult(trans.getNodeId());
            return i < limit && (map == null || (map.containsKey("Action") && map.containsKey("Action Input")));
        };

        Function<Object, Boolean> isCall = map -> ((Map<String, String>) map).containsKey("Action") && ((Map<String, String>) map).containsKey("Action Input");

        TranslateHandler<Object, Map<String, String>> call = new TranslateHandler<>(map -> {
            StringPromptValue promptResult = ContextBus.get().getResult(prompt.getNodeId());
            AIMessage cutResult = ContextBus.get().getResult(cut.getNodeId());

            Tool useTool = tools.stream().filter(t -> t.getName().toLowerCase().equals(map.get("Action"))).findAny().orElse(null);
            if (useTool == null) {
                promptResult.setText(promptResult.getText().trim() + "again");
                return promptResult;
            }
            String observation = (String) useTool.getFunc().apply(map.get("Action Input"));
            System.out.println("Observation: " + observation);

            String prefix = cutResult.getContent();
            String agentScratchpad = prefix.substring(prefix.indexOf("Thought:") + 8).trim() + "\nObservation:" + observation + "\nThought:";
            promptResult.setText(promptResult.getText().trim() + agentScratchpad);
            return promptResult;
        });

        StrOutputParser parser = new StrOutputParser();

        TranslateHandler<Object, Object> answer = new TranslateHandler<>(input -> {
            ChatGeneration generation = (ChatGeneration) input;
            String content = generation.getText();
            if (content.contains("Final Answer:")) {
                int start = content.indexOf("Final Answer:") + 13;
                int end = content.indexOf("\n", start);
                if (end > 0) {
                    generation.setText(content.substring(start, end).trim());
                } else {
                    generation.setText(content.substring(start).trim());
                }
            }
            return generation;
        });

        ConsumerHandler<?> sPrint = new ConsumerHandler<>(input -> System.out.println("> Entering new AgentExecutor chain..."));
        ConsumerHandler<?> ePrint = new ConsumerHandler<>(input -> System.out.println("> Finished chain."));

        FlowInstance chain = chainActor.builder()
                .next(sPrint) // print start
                .next(prompt)
                .loop(
                        // Loop Exit Conditions
                        isFinish,

                        // Loop Flow
                        llm,
                        chainActor.builder()
                                .next(cut).next(trans) // convert content generated by mll
                                .next(
                                        Info.c(isCall, call), // need call function
                                        Info.c(input -> ContextBus.get().getResult(llm.getNodeId())) // else no need, return mll result
                                )
                                .build()

                )
                .next(parser)
                .next(answer) // deal result
                .next(ePrint) // print end
                .build();

        ChatGeneration result = chainActor.invoke(chain, Map.of("input", "What's the weather like in Shanghai?"));
        System.out.println(result);
    }
}

总结

综上所述,我们完整的演示了如何用 j‑langchain 一步步编排一个 React + function call 形式的 Agent,欢迎大家 clone 体验,后续会有更多的编排实例和封装,期待反馈~~

相关推荐
shangxianjiao1 小时前
Javaweb后端全局异常处理器
java·springboot
奋进的小暄1 小时前
贪心算法(5)(java)k次取反后最大化的数组和
java·算法·贪心算法
chenchihwen2 小时前
ITSM统计分析:提升IT服务管理效能 实施步骤与操作说明
java·前端·数据库
肉肉不吃 肉2 小时前
TONGYI Lingma(通义灵码),GitHub Copilot和Cursor 对比
github·visual studio code
东东oyey2 小时前
Prompt 工程
python·llm·prompt·提示词工程
java技术小馆3 小时前
责任链模式如何减少模块之间的耦合
java·数据库·设计模式·责任链模式
大溪地C3 小时前
Spring Boot3整合Knife4j(4.5.0)
java·数据库·spring boot
Java&Develop3 小时前
java项目springboot 项目启动不了解决方案
java·开发语言·spring boot
飞奔的马里奥4 小时前
30天学习Java第四天——JVM规范
java·jvm·学习
LeeZhao@4 小时前
【AIGC】计算机视觉-YOLO系列家族
yolo·计算机视觉·aigc