02、Langchain4j tools原理与核心实践(含自定义http插件)

文章目录

前言

博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

CSDN搜索:长路

视频平台:b站-Coder长路

文章配套源码

gitee:https://gitee.com/changluJava/demo-exer/tree/master/ai/langchain4j

Github:https://github.com/changluya/Java-Demos/tree/master/ai

介绍

本章节将会介绍下langchain4j中如何实现与ai的function tools调用实现,在langchain4j中给我们提供了多种方式去进行实现function tools。

初始环境搭建

技术栈选用

后端:JDK17、Langchain4j

前端:vite+vue2

pom依赖导入

在当前的demo中我们导入了相应的langchain4j starter以及对接了阿里百炼与基于Xorbits Inference部署的大模型,补充了前后端接口文档工具包:

xml 复制代码
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.2.6</spring-boot.version>
    <knife4j.version>4.3.0</knife4j.version>
    <langchain4j.version>1.3.0</langchain4j.version>
    <mybatis-plus.version>3.5.11</mybatis-plus.version>
    <langchain4j.community.version>1.3.0-beta9</langchain4j.community.version>
</properties>

<dependencies>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-core</artifactId>
    </dependency>

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
    </dependency>

    <!-- web应用程序核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 编写和运行测试用例 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- 前后端分离中的后端接口测试工具 -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

    <!-- 接入阿里云百炼平台 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-dashscope</artifactId>
    </dependency>

    <!-- Xorbits Inference -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-xinference</artifactId>
    </dependency>

    <!--langchain4j高级功能-->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
    </dependency>

    <!-- 前后端分离中的后端接口测试工具 -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

    <!--流式输出-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-reactor</artifactId>
    </dependency>

</dependencies>

<dependencyManagement>
    <dependencies>
        <!--引入SpringBoot依赖管理清单-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--引入langchain4j依赖管理清单-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>${langchain4j.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--引入百炼依赖管理清单-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-bom</artifactId>
            <version>${langchain4j.community.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

前置基本代码配置

application.yaml配置项

配置项目主要就是服务的端口,其他的就是百炼平台的model配置与Xorbits Inference的model配置:

yaml 复制代码
server:
  port: 8999

langchain4j:
  community:
    dashscope:
      chat-model:
        api-key: ${DASH_SCOPE_API_KEY}
        #        model-name: qwen-plus
        #        model-name: qwen-plus-latest
#        model-name: qwen-plus
        model-name: qwen3-30b-a3b-instruct-2507
  # 内部自部署模型
  xInference:
    chat-model:
      # ==Xorbits Inference==
      # 非思考模型
      base-url: http://178.86.99.68:9297
      model-name: dt_gptq_qwen_int8

大家可以选择百炼,比较简单,获取下key即可配置。


基础模块代码

SpringUtil.java:Spring容器类

java 复制代码
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(String name) {
        return (T)getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

EnvironmentContext.java:环境配置项类

java 复制代码
@Component
@Data
public class EnvironmentContext {

    @Autowired
    private Environment environment;

    // dashscope
    public String getDashScopeApiKey() {
        return environment.getRequiredProperty("langchain4j.community.dashscope.chat-model.api-key");
    }

    public String getDashScopeModelName() {
        return environment.getRequiredProperty("langchain4j.community.dashscope.chat-model.model-name");
    }

    // Xorbits Inference
    public String getXInferenceBaseUrl() {
        return environment.getRequiredProperty("langchain4j.xInference.chat-model.base-url");
    }

    public String getXInferenceModelName() {
        return environment.getRequiredProperty("langchain4j.xInference.chat-model.model-name");
    }
}

ChatForm.java:表单类

java 复制代码
@Data
public class ChatForm {
    private String message;   // 用户问题
}

MyWebMvcConfig:解决跨域web问题

java 复制代码
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {

    /**
     * 解决跨域问题
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  //指定的映射地址
                .allowedHeaders("*") //允许携带的请求头
                .allowedMethods("*") //允许的请求方法
                .allowedOrigins("*");  //添加跨域请求头 Access-Control-Allow-Origin,值如:"https://domain1.com"或"*"
    }

}

方式一:AiService注入方式快速集成tools工具

介绍

对于langchain4j的springboot starter包中,给我们提供了快速构建ai智能体的实现,你可以理解它将一个大框架包含有rag检索+memeory+function tools的逻辑统一在DefaultAiServices中都给我们实现了,我们只需要去通过补充一个注解即可完成ai智能体的构建与注入。

对于在AiService中快速注入tools工具也十分简单。

快速集成步骤

对于一个ai bot,我们首先一定需要的是chat模型,这里我们手动创建对应的chat bean来完成注入对应的是ModelConfig,AiAssistant则是配置了对应的注解,来完成注入,同时,我们提供了一个控制器来来快速实现ai问答效果:

ModelConfig.java:Ai模型配置类

java 复制代码
@Configuration
public class ModelConfig {

    @Autowired
    private EnvironmentContext env;

    @Bean
    public StreamingChatModel qwenChatModel() {
//        return QwenStreamingChatModel.builder()
//                .apiKey(env.getDashScopeApiKey())
//                .modelName(env.getDashScopeModelName())
//                .build();
        return XinferenceStreamingChatModel.builder()
                .baseUrl(env.getXInferenceBaseUrl())
                .modelName(env.getXInferenceModelName())
                .logRequests(true)
                .logResponses(true)
                .build();
    }

}

CalculatorTools.java:自定义tools工具

注意:这里使用了@Component,直接会向spring容器注入一个bean实例。

java 复制代码
/**
 * @description  方式一:bean模式去创建tools,去注入到aiservice
 * @author changlu
 * @date 2025/8/16 17:36
 */
@Component
public class CalculatorTools {

    @Tool(name = "sum", value = "返回两个参数相加之和")
    double sum(
            @P(value="加数1", required = true) Double a,
            @P(value="加数2", required = true) Double b) {
        System.out.println("调用加法运算 ");
        return a + b;
    }

    @Tool(name = "squareRoot", value = "返回给定参数的平方根")
    double squareRoot(Double x) {
        System.out.println("调用平方根运算 ");
        return Math.sqrt(x);
    }

}

AiAssistant.java:ai bot代理类

在这里我们直接去完成tools的注入,来完成tools工具的快速集成

java 复制代码
@AiService(
        wiringMode = AiServiceWiringMode.EXPLICIT,
        streamingChatModel = "qwenChatModel",
        tools = "calculatorTools" //配置tools
)
public interface AiAssistant {

    Flux<String> chat(String userMessage);

}

AiBotController.java:AI助手控制器层,提供接口

java 复制代码
@Tag(name = "ai控制器")
@RestController
@RequestMapping("/ai")
public class AiBotController {

    @Autowired
    private AiAssistant aiAssistant;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        return aiAssistant.chat(chatForm.getMessage());
    }

}

测试一下

我们将项目启动:

访问网址:http://127.0.0.1:8999/doc.html#/home

测试一下问题:一加三等于多少,此时正确实现回调


方式二:手动注册tools到AiService中

介绍

在方式二中,我们将尝试自己手动去构建一个ai bot,在构建的过程中,我们手动将对应的计算器类进行实例化注册到tools中。

快速集成步骤

AiBotFactory.java:Ai Bot工厂类

java 复制代码
import com.changlu.langchain4jtools.assistant.AiAssistant;
import com.changlu.langchain4jtools.env.EnvironmentContext;
import com.changlu.langchain4jtools.tools.CalculatorTools;
import com.changlu.langchain4jtools.util.SpringUtil;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.service.AiServices;
/**
 * @description  Ai Bot工厂类
 * @author changlu
 * @date 2025/8/17 00:21
 */
public class AiBotFactory {

    // 快速构建一个Aiservice
    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);
        // 创建model
        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        // 使用langchain4j提供的代理类快速实现一个ai bot
        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                .tools(new CalculatorTools())
                .build();
        return diyAiAssistant;
    }

}

Demo01Controller.java:控制器类

java 复制代码
@Tag(name = "demo01 ai控制器")
@RestController
@RequestMapping("/demo01")
public class Demo01Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (instance == null) {
            instance = AiBotFactory.buildAiAssistant();
        }
        return instance.chat(chatForm.getMessage());
    }

}

你可以看到此时我们的ai智能助手就是通过工厂类来完成创建ai boot,最终进行chat问答。


测试一下

我们现在用demo01 ai控制器这部分接口来测试一下,回答通常正确:

原理初探

初步认识Langchain4j中@Tool & @P注解相关参数

标注到某个方法上

@Tool 注解用于标注在某个方法上有两个可选字段:

  • name(工具名称):工具的名称。如果未提供该字段,方法名会作为工具的名称。
  • value(工具描述):工具的描述信息。

根据工具的不同,即使没有任何描述,大语言模型可能也能很好地理解它(例如, add(a, b) 就很直观),但通常最好提供清晰且有意义的名称和描述。这样,大语言模型就能获得更多信息,以决定是否调用给定的工具以及如何调用。

标注到方法入参中

@P 注解有两个字段:

  • value:参数的描述信息,这是必填字段。
  • required:表示该参数是否为必需项,默认值为 true ,此为可选字段。

源码剖析

@Tool & @P注解

两个注解源码分别如下所示,通过查看源码可以看到一个是设置在方法中,一个是设置在参数上:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Tool {
    String name() default "";

    String[] value() default {""};
}
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface P {
    String value();

    boolean required() default true;
}

Function calling函数调用原理探究

原理探究:

提出问题:

1、ai大模型是怎么知道有哪些function calls?

  • 第一次发起提问的时候,就将系统信息、用户信息、function tools描述信息都发过去了。

2、中间的过程是ai主动发请求回来的吗?

  • 第一次提问之后,ai会返回一条需要执行function tools消息,本地执行function tools之后,会将历史消息及本地执行结果统一再发送给ai,如果中间涉及到多次function tools过程,则会重复发起ai请求调用,最终结束后ai得到结果会将结果值作为本次消息的最终结果返回过来。

整体提问过程如下:

核心源码位置:DefaultAiServices#build中代理类的invoke逻辑


核心构建function call request细节

入口为:

java 复制代码
AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
        .streamingChatModel(qwenStreamingChatModel)
        // 可以看到这里直接将工具类注入到tools中
        .tools(new CalculatorTools())
        .build();

可以直接点进去tools方法中去看下底层源码。

涉及到的源码方法有:ToolService、ToolSpecification

  • ToolSpecification:描述工具的元数据。名称、描述、参数schema等

  • ToolExecutionRequest:工具调用请求。工具名称、调用ID、参数字典

  • ToolExecutionResultMessage:工具执行结果。原始请求引用、执行结果文本

  • ToolServiceContext:工具执行上下文

  • 可用工具规范列表

  • 工具执行器映射表

  • **DefaultToolExecutor:**本地方法执行器

对于你自己写的一个方法,最终会将这个方法的名称、描述、参数名、参数描述去封装为一个ToolSpecification,同时会将这个ToolSpecification & 你的问题一起发给ai,ai如果识别到需要调用你的工具就会发起function tools调用,此时在langchain4j中就会将这个调用封装为一个ToolExecutionRequest。

通常每一个方法都会去这样子绑定:Map<ToolSpecification, ToolExecutor>

对应一个方法会绑定一个tool执行器,一旦来了ToolExecutionRequest定位到ToolSpecification,也就可以拿到ToolExecutor,默认如果你是本地方法调用的话,那么就会匹配一个DefaultToolExecutor,实际上本质是通过反射来执行对应方法的。

在下面方式三中,我们会带你通过手动去创建Map<ToolSpecification, ToolExecutor>来实现tools工具的集成,同时无需对写的tool类去加上注解,我们都通过采用对应手动去构建的方式去实现集成tools。


方式三:手动注册ToolSpecification集成tools

介绍

当前章节,我们将会手动去注册一个ToolSpecification以及toolExecutor,带你看下底层源码是如何去构建的,其实封装完ToolSpecification后,langchain4j底层就会拿这个ToolSpecification来发给ai,最终实现让ai进行function tools调用。

这部分我们主要去理解下底层源码如何实现tool方法的构建,这对我们后续方式四中去构建http插件打下一个基础。

快速集成步骤

CalculatorTools2.java:自定义工具类

你可以看到在这个类中我们没有@Tool也没有相应的其他参数注解,后续我们将在factory中去手动构建ToolSpecification。

java 复制代码
public class CalculatorTools2 {

    double sum(Double a, Double b) {
        System.out.println("调用加法运算 ");
        return a + b;
    }

    double squareRoot(Double x) {
        System.out.println("调用平方根运算 ");
        return Math.sqrt(x);
    }

}

AiBotFactory2.java:ai bot 工厂类

我们这里封装了两个工具方法,一个加和,一个平方根,在这章节中我们注入tools方式是手动去构建AiServices tools(Map<ToolSpecification, ToolExecutor> tools) ,自行指定一个本地工具执行器类来实现本地工具方法调用:

java 复制代码
/**
 * @description  Ai Bot工厂类
 * @author changlu
 * @date 2025/8/17 00:21
 */
public class AiBotFactory2 {

    // 快速构建一个Aiservice
    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);
        // 创建model
        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        // 使用langchain4j提供的代理类快速实现一个ai bot
        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                // 方式一:注入带@Tool注解形式
//                .tools(new CalculatorTools())
                // 方式二:手动去构建ToolSpecification
                .tools(buildDiyTools())
                .tools()
                .build();
        return diyAiAssistant;
    }

    /**
     * 模拟langchain4j底层核心封装逻辑 方法描述 & 方法执行器
     * @param
     * @return Map<ToolSpecification,ToolExecutor>
     * @author changlu
     * @createDate 2025/8/17 21:04
     */
    public static Map<ToolSpecification, ToolExecutor> buildDiyTools() {
        Map<ToolSpecification, ToolExecutor> tools = new HashMap<>();

        // 构建一个CalculatorTools2
        CalculatorTools2 calculatorTools = new CalculatorTools2();
        try {
            // 第一个方法封装 sum 两数之和
            ToolSpecification sumSpec = buildSumToolSpecification();
            Method sumMethod = CalculatorTools2.class.getDeclaredMethod("sum", Double.class, Double.class);
            tools.put(sumSpec, new DefaultToolExecutor(calculatorTools, sumMethod));

            // 第二个方法封装 平方根
            ToolSpecification sqrtSpec = buildSqrtToolSpecification();
            Method sqrtMethod = CalculatorTools2.class.getDeclaredMethod("squareRoot", Double.class);
            tools.put(sqrtSpec, new DefaultToolExecutor(calculatorTools, sqrtMethod));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
        return tools;
    }

    private static ToolSpecification buildSqrtToolSpecification() {
        // 定义参数属性
        JsonObjectSchema sqrtJsonObjectSchema = buildSqrtJsonObjectSchema();
        ToolSpecification sqrtSpec = ToolSpecification.builder()
                .name("squareRoot")
                .description("返回给定参数的平方根")
                .parameters(sqrtJsonObjectSchema)
                .build();
        return sqrtSpec;
    }

    private static JsonObjectSchema buildSqrtJsonObjectSchema() {
        // 定义的参数 & 类型
        String param1 = "arg0";
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        properties.put(param1, JsonNumberSchema.builder().description("需要计算平方根的数字").build());

        // 必要的参数
        List<String> required = new ArrayList<>();
        required.add(param1);

        JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
                .addProperties(properties)
                .required(required)
                .definitions(null)
                .build();
        return jsonObjectSchema;
    }

    private static ToolSpecification buildSumToolSpecification() {
        // 定义参数属性
        JsonObjectSchema sumJsonObjectSchema = buildSumJsonObjectSchema();
        ToolSpecification sumSpec = ToolSpecification.builder()
                .name("sum")
                .description("返回两个参数相加之和")
                .parameters(sumJsonObjectSchema)
                .build();
        return sumSpec;
    }

    /**
     * 构建方法元数据信息
     * @param
     * @return JsonObjectSchema
     * @author changlu
     * @createDate 2025/8/17 21:06
     */
    private static JsonObjectSchema buildSumJsonObjectSchema() {
        // 定义的参数 & 类型
        // 底层是根据method.getParameters()获取到的名字,所以这里直接使用arg0、arg1
        String param1 = "arg0";
        String param2 = "arg1";
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        properties.put(param1, JsonNumberSchema.builder().description("加数1").build());
        properties.put(param2, JsonNumberSchema.builder().description("加数2").build());

        // 必要的参数
        List<String> required = new ArrayList<>();
        required.add(param1);
        required.add(param2);

        JsonObjectSchema jsonObjectSchema = JsonObjectSchema.builder()
                .addProperties(properties)
                .required(required)
                .definitions(null)
                .build();
        return jsonObjectSchema;
    }



}

Demo02Controller.java:控制器类

java 复制代码
import com.changlu.langchain4jtools.assistant.AiAssistant;
import com.changlu.langchain4jtools.demo01.AiBotFactory;
import com.changlu.langchain4jtools.domain.ChatForm;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@Tag(name = "demo02 ai控制器")
@RestController
@RequestMapping("/demo02")
public class Demo02Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (instance == null) {
            instance = AiBotFactory2.buildAiAssistant();
        }
        return instance.chat(chatForm.getMessage());
    }

}

测试一下

测试一下,成功调用:

ok了如果你能看懂,后续的自定义http插件应该也可以看懂滴


方式四:自定义支持http插件完成tools工具集成

介绍

通过调研,发现coze平台的插件本质上就是基于http来实现的,所以我们这里直接去模拟写一个http tools工具来实现快速集成http请求到我们的ai助手中。

下面是调研coze的一部分文档:

插件文档:https://www.coze.cn/open/docs/guides/plugin

操作 描述
创建自定义插件 扣子提供了多种创建自定义插件的方式供你选择,相关操作步骤可参见以下文档。 基于 API 创建插件使用 IDE 创建插件通过 JSON 或 YAML 文件导入插件使用代码注册插件
使用插件 插件可以直接在智能体内使用,拓展智能体的能力边界。插件也可以作为节点添加到工作流,实现工作流的任务处理能力。详情参见使用插件

基于api创建插件:我感觉就是你可以自定义http请求在某个插件中,一个方法就是一个api,同时可自行设置传参数。【这块和aiflowy及其类似,估计也是参考实现】

基于IDE创建插件:通过代码形式构建插件。

基于JSON或YAML文件导入插件:其实本质也就是上面基于api创建插件(内部封装http请求)。

快速集成步骤

demo说明

我们单独继承了ToolExecutor接口,实现了HttpToolExecutor,该实现类能够去完成http工具的调用。

你可以理解,我们如何集成http接口作为tools工具的,就是还是本质将你接口所需要的参数去封装成ToolSpecification,然后发给ai,ai会发起function tool调用,此时就能够拿到一个ToolExecutionRequest,接着我们直接使用对应的HttpToolExecutor来完成http请求接口调用即可实现集成http接口到ai助手中。

这个也是coze的核心原理实现,在langchain4j强大的抽象封装下,我们可以很好的快速扩展并实现这样子的http插件。

下面的实现基本就是参照coze的插件模式去复刻实现的。


HttpPlugin、HttpPluginMethod、HttpToolParameter(插件三件套,插件+插件方法+插件参数类)

HttpPlugin插件类如下:

java 复制代码
@Data
@Builder
public class HttpPlugin {

    // 基础服务名称
    private String baseUrl;

    // 公共请求头
    private Map<String, String> staticHeaders;

    // 包含多个插件方法
    List<HttpPluginMethod> pluginMethods;

}

HttpPluginMethod插件方法如下:

java 复制代码
/**
 * @description  插件方法
 * @author changlu
 * @date 2025/8/19 01:06
 */
@Data
@Builder
public class HttpPluginMethod {

    // 方法名称
    private String methodName;
    // 方法描述
    private String methodDescription;

    // 请求方法类型Code
    private Integer httpMethodType;
    // 请求资源点 例如:https://baidu.com/news,其中uri就是/news
    private String uri;

    // 请求参数集合
    private List<HttpToolParameter> parameters;

}

HttpToolParameter:插件方法参数类

java 复制代码
/**
 * HTTP工具参数统一配置类
 */
public class HttpToolParameter {
    private final String methodParamName; // 参数名
    private final String methodParamDescription;     // 参数描述
    private final String mappedName;      // 映射后的真实参数名
    private final int useTypeValue;       // 参数使用类型值(QUERY/BODY/PATH/HEADER)
    private final int dataTypeValue;      // 参数数据类型值(STRING/INTEGER/NUMBER/BOOLEAN)
    private final Object defaultValue;    // 默认值
    private final boolean required;       // 是否必填

    public HttpToolParameter(String methodParamName, String mappedName,
                             int useTypeValue, int dataTypeValue,
                             Object defaultValue, boolean required,
                             String methodParamDescription) {
        this.methodParamName = methodParamName;
        this.mappedName = mappedName;
        this.useTypeValue = useTypeValue;
        this.dataTypeValue = dataTypeValue;
        this.defaultValue = defaultValue;
        this.required = required;
        this.methodParamDescription = methodParamDescription;
    }

    // Getters
    public String getMethodParamName() {
        return methodParamName;
    }

    public String getMappedName() {
        return mappedName;
    }

    public int getUseTypeValue() {
        return useTypeValue;
    }

    public int getDataTypeValue() {
        return dataTypeValue;
    }

    public Object getDefaultValue() {
        return defaultValue;
    }

    public boolean isRequired() {
        return required;
    }

    public String getMethodParamDescription() {
        return methodParamDescription;
    }

    /**
     * 转换为ToolSpecification所需的JsonSchemaElement
     */
    public JsonSchemaElement toJsonSchemaElement() {
        HttpPluginEnums.ParameterType dataType = HttpPluginEnums.ParameterType.fromValue(dataTypeValue);

        switch (dataType) {
            case INTEGER:
                return JsonIntegerSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case NUMBER:
                return JsonNumberSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case BOOLEAN:
                return JsonBooleanSchema.builder()
                        .description(methodParamDescription)
                        .build();
            case STRING:
            default:
                return JsonStringSchema.builder()
                        .description(methodParamDescription)
                        .build();
        }
    }

    /**
     * 转换为HttpToolExecutor所需的ParameterConfig
     */
    public HttpToolExecutor.ParameterConfig toParameterConfig() {
        return new HttpToolExecutor.ParameterConfig(
                mappedName,
                HttpPluginEnums.ParameterUseType.fromValue(useTypeValue),
                defaultValue,
                required
        );
    }
}

HttpPluginEnums:插件枚举类

java 复制代码
public class HttpPluginEnums {

    /**
     * HTTP 方法枚举(支持值和名称)
     */
    public enum HttpMethod {
        GET(1, "GET"),
        POST(2, "POST"),
        PUT(3, "PUT"),
        DELETE(4, "DELETE"),
        PATCH(5, "PATCH");

        private final int value;
        private final String methodName;

        HttpMethod(int value, String methodName) {
            this.value = value;
            this.methodName = methodName;
        }

        public int getValue() {
            return value;
        }

        public String getMethodName() {
            return methodName;
        }

        /**
         * 根据值获取枚举
         */
        public static HttpMethod fromValue(int value) {
            for (HttpMethod method : values()) {
                if (method.value == value) {
                    return method;
                }
            }
            throw new IllegalArgumentException("无效的HttpMethod值: " + value);
        }

        /**
         * 根据名称获取枚举(不区分大小写)
         */
        public static HttpMethod fromName(String name) {
            for (HttpMethod method : values()) {
                if (method.methodName.equalsIgnoreCase(name)) {
                    return method;
                }
            }
            throw new IllegalArgumentException("无效的HttpMethod名称: " + name);
        }
    }

    /**
     * 参数数据类型枚举
     */
    public enum ParameterType {
        STRING(1, "string"),
        INTEGER(2, "integer"),
        NUMBER(3, "number"),
        BOOLEAN(4, "boolean");

        private final int value;
        private final String name;

        ParameterType(int value, String name) {
            this.value = value;
            this.name = name;
        }

        public int getValue() {
            return value;
        }

        public String getName() {
            return name;
        }

        public static ParameterType fromValue(int value) {
            for (ParameterType type : values()) {
                if (type.value == value) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterType value: " + value);
        }

        public static ParameterType fromName(String name) {
            for (ParameterType type : values()) {
                if (type.name.equalsIgnoreCase(name)) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterType name: " + name);
        }
    }

    /**
     * 参数使用类型
     */
    public enum ParameterUseType {
        QUERY(1, "query"),    // URL查询参数
        BODY(2, "body"),     // 请求体参数
        PATH(3, "path"),     // URL路径参数
        HEADER(4, "header"); // 请求头参数

        private final int value;
        private final String name;

        ParameterUseType(int value, String name) {
            this.value = value;
            this.name = name;
        }

        public int getValue() {
            return value;
        }

        public String getName() {
            return name;
        }

        // 根据值获取枚举
        public static ParameterUseType fromValue(int value) {
            for (ParameterUseType type : values()) {
                if (type.value == value) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterUseType value: " + value);
        }

        // 根据名称获取枚举
        public static ParameterUseType fromName(String name) {
            for (ParameterUseType type : values()) {
                if (type.name.equalsIgnoreCase(name)) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Invalid ParameterUseType name: " + name);
        }
    }


}

ToolExecutionRequestUtil:需要使用到langchian4j源码包的类

java 复制代码
import dev.langchain4j.internal.Json;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description  copy from langchain4j【dev.langchain4j.service.tool.ToolExecutionRequestUtil】
 * @author changlu
 * @date 2025/8/17 22:57
 */
public class ToolExecutionRequestUtil {

    private static final Pattern TRAILING_COMMA_PATTERN = Pattern.compile(",(\\s*[}\\]])");
    private static final Pattern LEADING_TRAILING_QUOTE_PATTERN = Pattern.compile("^\"|\"$");
    private static final Pattern ESCAPED_QUOTE_PATTERN = Pattern.compile("\\\\\"");

    private ToolExecutionRequestUtil() {}

    private static final Type MAP_TYPE = new ParameterizedType() {

        @Override
        public Type[] getActualTypeArguments() {
            return new Type[] {String.class, Object.class};
        }

        @Override
        public Type getRawType() {
            return Map.class;
        }

        @Override
        public Type getOwnerType() {
            return null;
        }
    };

    /**
     * Convert arguments to map.
     *
     * @param arguments json string
     * @return map
     */
    static Map<String, Object> argumentsAsMap(String arguments) {
        if (isNullOrBlank(arguments)) {
            return Map.of();
        }

        try {
            return Json.fromJson(arguments, MAP_TYPE);
        } catch (Exception ignored) {
            String normalizedArguments = removeTrailingComma(normalizeJsonString(arguments));
            return Json.fromJson(normalizedArguments, MAP_TYPE);
        }
    }

    /**
     * Removes trailing commas before closing braces or brackets in JSON strings.
     *
     * @param json the JSON string
     * @return the corrected JSON string
     */
    static String removeTrailingComma(String json) {
        if (isNullOrEmpty(json)) {
            return json;
        }
        Matcher matcher = TRAILING_COMMA_PATTERN.matcher(json);
        return matcher.replaceAll("$1");
    }

    /**
     * Normalizes a JSON string by removing leading and trailing quotes and unescaping internal double quotes.
     *
     * @param arguments the raw JSON string
     * @return the normalized JSON string
     */
    static String normalizeJsonString(String arguments) {
        if (isNullOrEmpty(arguments)) {
            return arguments;
        }

        Matcher leadingTrailingMatcher = LEADING_TRAILING_QUOTE_PATTERN.matcher(arguments);
        String normalizedJson = leadingTrailingMatcher.replaceAll("");

        Matcher escapedQuoteMatcher = ESCAPED_QUOTE_PATTERN.matcher(normalizedJson);
        return escapedQuoteMatcher.replaceAll("\"");
    }

    /**
     * Is the given string {@code null} or empty ("")?
     *
     * @param string The string to check.
     * @return true if the string is {@code null} or empty.
     */
    public static boolean isNullOrEmpty(String string) {
        return string == null || string.isEmpty();
    }

    /**
     * Is the given string {@code null} or blank?
     *
     * @param string The string to check.
     * @return true if the string is {@code null} or blank.
     */
    public static boolean isNullOrBlank(String string) {
        return string == null || string.trim().isEmpty();
    }

    /**
     * Convert map to JSON string.
     *
     * @param map the map to convert
     * @return JSON string
     */
    public static String toJson(Map<String, Object> map) {
        return Json.toJson(map);
    }
}

HttpToolExecutor:http工具执行器(核心)

主要去实现execute的方法,你可以理解当ai发起调用的时候,会调用execute方法,将本次请求方法作为ToolExecutionRequest类来传递过来,此时我们可以根据初始的参数配置信息来完成http请求调用。

java 复制代码
/**
 * HTTP 工具执行器,支持多种参数类型:
 * - Query 参数(URL查询参数)
 * - Body 参数(JSON格式请求体)
 * - Path 参数(URL路径参数)
 * - Header 参数(请求头参数)
 * 支持参数映射和默认值配置
 */
@Slf4j
public class HttpToolExecutor implements ToolExecutor {

    private final OkHttpClient httpClient;
    private final String baseUrl;
    private final Map<String, ParameterConfig> parameterConfigs;
    private final Map<String, String> staticHeaders;
    private final HttpPluginEnums.HttpMethod method;

    /**
     * 参数配置类
     */
    public static class ParameterConfig {
        public final String mappedName;  // 映射后的参数名
        public final HttpPluginEnums.ParameterUseType type;  // 参数类型
        public final Object defaultValue; // 默认值
        public final boolean required;    // 是否必填

        public ParameterConfig(String mappedName, HttpPluginEnums.ParameterUseType type, Object defaultValue, boolean required) {
            this.mappedName = mappedName;
            this.type = type;
            this.defaultValue = defaultValue;
            this.required = required;
        }
    }


    /**
     * 构造函数
     * @param baseUrl 基础URL
     * @param method HTTP方法
     * @param staticHeaders 静态请求头(固定值)
     * @param parameterConfigs 参数配置映射(参数名 -> 参数配置)
     */
    public HttpToolExecutor(String baseUrl,
                            int methodValue, // 使用数值表示方法
                            Map<String, String> staticHeaders,
                            Map<String, ParameterConfig> parameterConfigs) {
        this.httpClient = new OkHttpClient();
        this.baseUrl = baseUrl;
        this.method = HttpPluginEnums.HttpMethod.fromValue(methodValue); // 转换为枚举
        this.staticHeaders = staticHeaders != null ? staticHeaders : new HashMap<>();
        this.parameterConfigs = parameterConfigs != null ? parameterConfigs : new HashMap<>();
    }

    @Override
    public String execute(ToolExecutionRequest toolExecutionRequest, Object memoryId) {
        long startTime = System.currentTimeMillis();

        // 将工具执行请求的参数转换为Map
        Map<String, Object> arguments = ToolExecutionRequestUtil.argumentsAsMap(toolExecutionRequest.arguments());

        // 处理参数:应用默认值并验证必填参数
        Map<String, Object> finalArguments = processArguments(arguments);

        // 构建包含路径参数的URL
        String url = buildUrlWithPathParams(finalArguments);

        Request.Builder requestBuilder = new Request.Builder().url(url);

        // 添加静态请求头【处理初始插件配置参数】
        staticHeaders.forEach(requestBuilder::addHeader);

        // 处理动态请求头参数【类型为HEADER会封装到请求头中】
        addHeaderParameters(requestBuilder, finalArguments);

        // 智能判断请求体类型并处理参数
        RequestBody requestBody = processParameters(requestBuilder, finalArguments);

        // 记录请求详情
        logRequestDetails(url, requestBuilder, requestBody, finalArguments);

        try (Response response = httpClient.newCall(requestBuilder.build()).execute()) {
            ResponseBody body = response.body();
            String responseContent = body != null ? body.string() : "Empty response";

            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;

            // 记录响应详情
            logResponseDetails(response.code(), responseContent, duration);

            if (!response.isSuccessful()) {
                return "HTTP请求失败,状态码: " + response.code();
            }
            return responseContent;
        } catch (IOException e) {
            long endTime = System.currentTimeMillis();
            log.error("HTTP请求执行失败,耗时 {} 毫秒", (endTime - startTime), e);
            return "HTTP请求失败: " + e.getMessage();
        }
    }

    /**
     * 智能处理参数并返回请求体(可能为null)
     */
    private RequestBody processParameters(Request.Builder requestBuilder, Map<String, Object> arguments) {
        // 判断是否需要使用JSON请求体
        boolean useJsonBody = shouldUseJsonBody(arguments);

        if (useJsonBody) {
            // 处理JSON请求体参数
            Map<String, Object> bodyParams = new HashMap<>();
            for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
                if (entry.getValue().type == HttpPluginEnums.ParameterUseType.BODY && arguments.containsKey(entry.getKey())) {
                    bodyParams.put(entry.getValue().mappedName, arguments.get(entry.getKey()));
                }
            }
            String json = ToolExecutionRequestUtil.toJson(bodyParams);
            RequestBody requestBody = RequestBody.create(json, MediaType.parse("application/json"));
            requestBuilder.method(method.name(), requestBody);
            return requestBody;
        } else {
            // 处理查询参数
            HttpUrl.Builder urlBuilder = HttpUrl.parse(requestBuilder.build().url().toString()).newBuilder();
            for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
                if (entry.getValue().type == HttpPluginEnums.ParameterUseType.QUERY && arguments.containsKey(entry.getKey())) {
                    urlBuilder.addQueryParameter(
                            entry.getValue().mappedName,
                            arguments.get(entry.getKey()).toString()
                    );
                }
            }
            requestBuilder.url(urlBuilder.build());
            requestBuilder.method(method.name(), null);
            return null;
        }
    }

    /**
     * 智能判断是否使用JSON请求体
     */
    private boolean shouldUseJsonBody(Map<String, Object> arguments) {
        // GET/DELETE方法强制使用查询参数
        if (method == HttpPluginEnums.HttpMethod.GET || method == HttpPluginEnums.HttpMethod.DELETE) {
            return false;
        }

        // 检查是否存在BODY类型参数
        boolean hasBodyParams = parameterConfigs.values().stream()
                .anyMatch(config -> config.type == HttpPluginEnums.ParameterUseType.BODY &&
                        (arguments.containsKey(config.mappedName) || config.defaultValue != null));

        // POST/PUT/PATCH方法且存在BODY参数时使用JSON请求体
        return hasBodyParams;
    }

    // 以下方法保持不变(与之前版本相同)
    private Map<String, Object> processArguments(Map<String, Object> providedArguments) {
        Map<String, Object> processed = new HashMap<>();
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            String paramName = entry.getKey();
            ParameterConfig config = entry.getValue();
            if (providedArguments.containsKey(paramName)) {
                processed.put(paramName, providedArguments.get(paramName));
            } else if (config.defaultValue != null) {
                processed.put(paramName, config.defaultValue);
            } else if (config.required) {
                throw new IllegalArgumentException("缺少必填参数 '" + paramName + "'");
            }
        }
        return processed;
    }

    private String buildUrlWithPathParams(Map<String, Object> arguments) {
        String url = baseUrl;
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            if (entry.getValue().type == HttpPluginEnums.ParameterUseType.PATH) {
                String paramName = entry.getKey();
                String placeholder = "{" + entry.getValue().mappedName + "}";
                if (arguments.containsKey(paramName)) {
                    url = url.replace(placeholder, arguments.get(paramName).toString());
                }
            }
        }
        return url;
    }

    private void addHeaderParameters(Request.Builder requestBuilder, Map<String, Object> arguments) {
        for (Map.Entry<String, ParameterConfig> entry : parameterConfigs.entrySet()) {
            if (entry.getValue().type == HttpPluginEnums.ParameterUseType.HEADER && arguments.containsKey(entry.getKey())) {
                requestBuilder.addHeader(
                        entry.getValue().mappedName,
                        arguments.get(entry.getKey()).toString()
                );
            }
        }
    }

    private void logRequestDetails(String url, Request.Builder requestBuilder,
                                   RequestBody requestBody, Map<String, Object> arguments) {
        log.info("=== HTTP 请求详情 ===");
        log.info("URL: {}", url);
        log.info("方法: {}", method);
        Request request = requestBuilder.build();
        log.info("请求头: {}", request.headers().toMultimap());
        Map<HttpPluginEnums.ParameterUseType, Map<String, Object>> paramsByType = arguments.entrySet().stream()
                .collect(Collectors.groupingBy(
                        e -> parameterConfigs.get(e.getKey()).type,
                        Collectors.toMap(
                                e -> parameterConfigs.get(e.getKey()).mappedName,
                                Map.Entry::getValue
                        )
                ));
        paramsByType.forEach((type, params) ->
                log.info("{} 参数: {}", type, params)
        );
        if (requestBody != null) {
            log.info("请求体: {}", requestBody.toString());
        }
        log.info("=====================");
    }

    private void logResponseDetails(int statusCode, String responseContent, long durationMs) {


        log.info("=== HTTP 响应详情 ===");
        log.info("状态码: {}", statusCode);
        log.info("耗时: {} 毫秒", durationMs);
        String truncatedResponse = responseContent.length() > 1000
                ? responseContent.substring(0, 1000) + "...[截断]"
                : responseContent;
        log.info("响应体: {}", truncatedResponse);
        log.info("=====================");
    }
}

AiBotFactory3:AiBot工厂类

在buildHttpTools中,我们去植入了一个web 检索http请求插件,并进行加载到AiServices中,实现加载了一个http插件:

java 复制代码
/**
 * @description Ai Bot factory class with enhanced HTTP tool support including default values
 */
public class AiBotFactory3 {

    public static AiAssistant buildAiAssistant() {
        EnvironmentContext env = SpringUtil.getBean(EnvironmentContext.class);

        QwenStreamingChatModel qwenStreamingChatModel = QwenStreamingChatModel.builder()
                .apiKey(env.getDashScopeApiKey())
                .modelName(env.getDashScopeModelName())
                .build();

        AiAssistant diyAiAssistant = AiServices.builder(AiAssistant.class)
                .streamingChatModel(qwenStreamingChatModel)
                .tools(buildHttpTools())
                .build();
        return diyAiAssistant;
    }

    private static Map<ToolSpecification, ToolExecutor> buildHttpTools() {
        // 定义参数配置
        List<HttpToolParameter> parameters = new ArrayList<>();
        parameters.add(new HttpToolParameter(
                "query", "q",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                null, true, "搜索关键词"));
        parameters.add(new HttpToolParameter(
                "country", "gl",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                "cn", false, "国家代码(如:'cn')"));
        parameters.add(new HttpToolParameter(
                "language", "hl",
                HttpPluginEnums.ParameterUseType.BODY.getValue(), // 使用枚举值
                HttpPluginEnums.ParameterType.STRING.getValue(),
                "zh-CN", false, "语言代码(如:'zh-CN')"));
        // 构建插件方法
        HttpPluginMethod httpPluginMethod = HttpPluginMethod.builder()
                .uri("/search")
                .methodName("searchWeb")
                .methodDescription("使用 Serper API 搜索网络")
                .httpMethodType(HttpPluginEnums.HttpMethod.POST.getValue())
                .parameters(parameters).build();


        // 定义静态请求头
        Map<String, String> staticHeaders = new HashMap<>();
        staticHeaders.put("X-API-KEY", "d420dbfcefdd0cf0261ba09f5a91dc4a35933c59");
        staticHeaders.put("Content-Type", "application/json");

        // 封装http插件,目前这里就一个插件
        HttpPlugin httpPlugin = HttpPlugin.builder()
                .baseUrl("https://google.serper.dev")
                .staticHeaders(staticHeaders)
                .pluginMethods(Arrays.asList(httpPluginMethod))
                .build();


        // -----------------
//        // 封装构建插件
//        HttpPluginMethod searchPlugin = httpPlugin.getPluginMethods().get(0);
//
//        // 转换为HttpToolExecutor需要的配置
//        Map<String, HttpToolExecutor.ParameterConfig> parameterConfigs = new HashMap<>();
//        parameters.forEach(param ->
//                parameterConfigs.put(param.getMethodParamName(), param.toParameterConfig()));
//
//        httpTools.put(
//                buildToolSpecification(searchPlugin.getMethodName(), searchPlugin.getMethodDescription(), parameters),
//                new HttpToolExecutor(
//                        httpPlugin.getBaseUrl() + searchPlugin.getUri(),
//                        searchPlugin.getHttpMethodType(),
//                        httpPlugin.getStaticHeaders(),
//                        parameterConfigs
//                )
//        );
        Map<ToolSpecification, ToolExecutor> httpPluginTools = buildHttpPluginTools(httpPlugin);
        return httpPluginTools;
    }

    public static Map<ToolSpecification, ToolExecutor> buildHttpPluginTools(HttpPlugin httpPlugin) {
        Map<ToolSpecification, ToolExecutor> res = new HashMap<>();

        String baseUrl = httpPlugin.getBaseUrl();
        Map<String, String> staticHeaders = httpPlugin.getStaticHeaders();
        List<HttpPluginMethod> pluginMethods = httpPlugin.getPluginMethods();

        for (HttpPluginMethod httpPluginMethod : pluginMethods) {
            Pair<ToolSpecification, ToolExecutor> pair = buildHttpPluginTool(baseUrl, staticHeaders, httpPluginMethod);
            res.put(pair.getFirst(), pair.getSecond());
        }
        return res;
    }

    private static Pair<ToolSpecification, ToolExecutor> buildHttpPluginTool(String baseUrl, Map<String, String> staticHeaders, HttpPluginMethod httpPluginMethod) {
        String uri = httpPluginMethod.getUri();
        Integer httpMethodType = httpPluginMethod.getHttpMethodType();
        String methodName = httpPluginMethod.getMethodName();
        String methodDescription = httpPluginMethod.getMethodDescription();
        List<HttpToolParameter> parameters = httpPluginMethod.getParameters();

        // 构建toolSpecification
        ToolSpecification toolSpecification = buildToolSpecification(methodName, methodDescription, parameters);
        // 构建HttpToolExecutor
        Map<String, HttpToolExecutor.ParameterConfig> parameterConfigs = new HashMap<>();
        parameters.forEach(param ->
                parameterConfigs.put(param.getMethodParamName(), param.toParameterConfig()));
        HttpToolExecutor httpToolExecutor = new HttpToolExecutor(
                baseUrl + uri,
                httpMethodType,
                staticHeaders,
                parameterConfigs
        );

        return new Pair<>(toolSpecification, httpToolExecutor);
    }


    private static ToolSpecification buildToolSpecification(String methodName, String methodDescription, List<HttpToolParameter> parameters) {
        Map<String, JsonSchemaElement> properties = new HashMap<>();
        List<String> required = new ArrayList<>();

        for (HttpToolParameter param : parameters) {
            properties.put(param.getMethodParamName(), param.toJsonSchemaElement());
            if (param.isRequired()) {
                required.add(param.getMethodParamName());
            }
        }

        return ToolSpecification.builder()
                .name(methodName)
                .description(methodDescription)
                .parameters(JsonObjectSchema.builder()
                        .addProperties(properties)
                        .required(required)
                        .build())
                .build();
    }
}

Demo03Controller:Ai Bot控制器类

java 复制代码
@Tag(name = "demo03 ai控制器")
@RestController
@RequestMapping("/demo03")
@Slf4j
public class Demo03Controller {

    private AiAssistant instance;

    @Operation(summary = "对话")
    @PostMapping(value = "/chat", produces = "text/event-stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        String message = chatForm.getMessage();
        if (instance == null) {
            instance = AiBotFactory3.buildAiAssistant();
        }
        return Flux.<String>create(emitter -> {  // Explicit type parameter
                    try {
                        if (StringUtils.isEmpty(message)) {
                            emitter.complete();
                            return;
                        }
                        // 特定ai调用
                        instance.chat(message)
                                .subscribe(
                                        response -> {
                                            log.debug("[LLM-Response] 收到大模型响应片段 | length: {}, content: {}", response.length(), response);
                                            emitter.next("final|CONTENT|" + response);
                                        },
                                        error -> {
                                            log.error("[LLM-Error] 大模型调用异常 |  error: {}", error.getMessage(), error);
                                            emitter.error(error);
                                        },
                                        () -> {
                                            log.info("[LLM-Complete] 大模型调用完成 ");
                                            if (!emitter.isCancelled()) {
                                                emitter.complete();
                                            }
                                        }
                                );
                    }catch (Exception e) {
                        log.error("[Chat-Error] 流式对话处理异常 | error: {}", e.getMessage(), e);
                        emitter.error(e);
                    }
                })
                .onBackpressureBuffer(128) // 设置缓冲区大小
                .doOnCancel(() -> log.warn("[Chat-Cancel] 流式对话被取消"))
                .doOnTerminate(() -> log.info("[Chat-End] 流式对话终止)"))
                .subscribeOn(Schedulers.boundedElastic());
    }

}

对接插件(web搜索):serper.dev

网址:https://serper.dev/playground

免费提供检索的额度大概是2500样子。需要去注册下获取key

mac配置环境变量如下:

shell 复制代码
vim ~/.zshrc

# 配置内容
export SERPER_KEY="xxxxxxxxx"

# 生效配置文件
source ~/.zshrc

记得重启下IDEA


测试一下

我们现在的controller已经改为了流式返回的,对应前端ui可以使用这个仓库的前端工程:

https://gitee.com/changluJava/demo-exer/tree/master/ai/langchain4j

需要将请求地址修改下:

指向本地的服务端口为:8999

启动前端服务:

shell 复制代码
npm install

npm run dev

接着启动后端服务:

我们访问网址:http://localhost:81/#/

效果如下,本地ai就会去选择使用web插件进行http请求检索相关信息,最终来实现ai回答内容:

袋鼠云一站式数据中台

本地的接口调用如下:


整理者:长路 时间:2025.8.17

资料获取

大家点赞、收藏、关注、评论啦~

精彩专栏推荐订阅:在下方专栏👇🏻

更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅

相关推荐
弹简特1 小时前
【JavaEE08-后端部分】SpringMVC03-SpringMVC第二大核心处理请求之Cookie/Session和获取header
java·spring boot·spring·java-ee
长路 ㅤ   3 小时前
langchain4j+ai模型 硅谷小智 智慧医疗实战项目案例
ai智能体·langchain4j·rag向量检索·java大模型
TimberWill13 小时前
SpringBoot整合Srping Security实现权限控制
java·spring boot·后端
q***76561 天前
工作中常用springboot启动后执行的方法
java·spring boot·后端
only-qi1 天前
Spring Boot 异步任务深度解析:从入门到避坑指南
java·spring boot·线程池·async
草履虫建模1 天前
Java面试应对思路和题库
java·jvm·spring boot·分布式·spring cloud·面试·mybatis
java1234_小锋1 天前
分享一套优质的SpringBoot4+Vue3学生信息管理系统
java·vue.js·spring boot·学生信息